@@ -274,8 +274,9 @@ function genericSaveData (id) {
274274// -----------------------------------------------------------------------------
275275pluginDefinitions = []
276276
277- // Global counts map, populated before tabs are rendered
278- let pluginCounts = {};
277+ // Global counts map, populated before tabs are rendered.
278+ // null = counts unavailable (fail-open: show all plugins)
279+ let pluginCounts = null;
279280
280281async function getData() {
281282 try {
@@ -285,7 +286,8 @@ function genericSaveData (id) {
285286 const plugins = await fetchJson('plugins.json');
286287 pluginDefinitions = plugins.data;
287288
288- // Fetch counts BEFORE rendering tabs so we can skip empty plugins (no flicker)
289+ // Fetch counts BEFORE rendering tabs so we can skip empty plugins (no flicker).
290+ // fetchPluginCounts never throws — returns null on failure (fail-open).
289291 const prefixes = pluginDefinitions.filter(p => p.show_ui).map(p => p.unique_prefix);
290292 pluginCounts = await fetchPluginCounts(prefixes);
291293
@@ -355,54 +357,63 @@ function postPluginGraphQL(gqlField, prefix, foreignKey, dtRequest, callback) {
355357 });
356358}
357359
358- // Fetch counts for all plugins. Returns { PREFIX: { objects, events, history } }.
360+ // Fetch counts for all plugins. Returns { PREFIX: { objects, events, history } }
361+ // or null on failure (fail-open so tabs still render).
359362// Fast path: static JSON (~1KB) when no MAC filter is active.
360363// Filtered path: batched GraphQL aliases when a foreignKey (MAC) is set.
361364async function fetchPluginCounts(prefixes) {
362365 if (prefixes.length === 0) return {};
363366
364- const mac = $("#txtMacFilter").val();
365- const foreignKey = (mac && mac !== "--") ? mac : null;
366- let counts = {};
367-
368- if (!foreignKey) {
369- // ---- FAST PATH: lightweight pre-computed JSON ----
370- const stats = await fetchJson('table_plugins_stats.json');
371- for (const row of stats.data) {
372- const p = row.tableName; // 'objects' | 'events' | 'history'
373- const plugin = row.plugin;
374- if (!counts[plugin]) counts[plugin] = { objects: 0, events: 0, history: 0 };
375- counts[plugin][p] = row.cnt;
376- }
377- } else {
378- // ---- FILTERED PATH: GraphQL with foreignKey ----
379- const apiToken = getSetting("API_TOKEN");
380- const apiBase = getApiBase();
381- const fkOpt = `, foreignKey: "${foreignKey}"`;
382- const fragments = prefixes.map(p => [
383- `${p}_obj: pluginsObjects(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
384- `${p}_evt: pluginsEvents(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
385- `${p}_hist: pluginsHistory(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
386- ].join('\n ')).join('\n ');
387-
388- const query = `query BadgeCounts {\n ${fragments}\n }`;
389- const response = await $.ajax({
390- method: "POST",
391- url: `${apiBase}/graphql`,
392- headers: { "Authorization": `Bearer ${apiToken}`, "Content-Type": "application/json" },
393- data: JSON.stringify({ query }),
394- });
395- if (response.errors) { console.error("[plugins] badge GQL errors:", response.errors); return counts; }
396- for (const p of prefixes) {
397- counts[p] = {
398- objects: response.data[`${p}_obj`]?.dbCount ?? 0,
399- events: response.data[`${p}_evt`]?.dbCount ?? 0,
400- history: response.data[`${p}_hist`]?.dbCount ?? 0,
401- };
367+ try {
368+ const mac = $("#txtMacFilter").val();
369+ const foreignKey = (mac && mac !== "--") ? mac : null;
370+ let counts = {};
371+
372+ if (!foreignKey) {
373+ // ---- FAST PATH: lightweight pre-computed JSON ----
374+ const stats = await fetchJson('table_plugins_stats.json');
375+ for (const row of stats.data) {
376+ const p = row.tableName; // 'objects' | 'events' | 'history'
377+ const plugin = row.plugin;
378+ if (!counts[plugin]) counts[plugin] = { objects: 0, events: 0, history: 0 };
379+ counts[plugin][p] = row.cnt;
380+ }
381+ } else {
382+ // ---- FILTERED PATH: GraphQL with foreignKey ----
383+ const apiToken = getSetting("API_TOKEN");
384+ const apiBase = getApiBase();
385+ const fkOpt = `, foreignKey: "${foreignKey}"`;
386+ const fragments = prefixes.map(p => [
387+ `${p}_obj: pluginsObjects(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
388+ `${p}_evt: pluginsEvents(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
389+ `${p}_hist: pluginsHistory(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
390+ ].join('\n ')).join('\n ');
391+
392+ const query = `query BadgeCounts {\n ${fragments}\n }`;
393+ const response = await $.ajax({
394+ method: "POST",
395+ url: `${apiBase}/graphql`,
396+ headers: { "Authorization": `Bearer ${apiToken}`, "Content-Type": "application/json" },
397+ data: JSON.stringify({ query }),
398+ });
399+ if (response.errors) {
400+ console.error("[plugins] badge GQL errors:", response.errors);
401+ return null; // fail-open
402+ }
403+ for (const p of prefixes) {
404+ counts[p] = {
405+ objects: response.data[`${p}_obj`]?.dbCount ?? 0,
406+ events: response.data[`${p}_evt`]?.dbCount ?? 0,
407+ history: response.data[`${p}_hist`]?.dbCount ?? 0,
408+ };
409+ }
402410 }
403- }
404411
405- return counts;
412+ return counts;
413+ } catch (err) {
414+ console.error('[plugins] fetchPluginCounts failed (fail-open):', err);
415+ return null;
416+ }
406417}
407418
408419// Apply pre-fetched counts to the DOM badges and hide empty tabs/sub-tabs.
@@ -478,17 +489,20 @@ function generateTabs() {
478489
479490 let assignActive = true;
480491
481- // Build list of visible plugins (skip plugins with 0 total count)
492+ // When counts are available, skip plugins with 0 total count (no flicker).
493+ // When counts are null (fetch failed), show all show_ui plugins (fail-open).
494+ const countsAvailable = pluginCounts !== null;
482495 const visiblePlugins = pluginDefinitions.filter(pluginObj => {
483496 if (!pluginObj.show_ui) return false;
497+ if (!countsAvailable) return true; // fail-open: show all
484498 const c = pluginCounts[pluginObj.unique_prefix] || { objects: 0, events: 0, history: 0 };
485499 return (c.objects + c.events + c.history) > 0;
486500 });
487501
488- // Create tab DOM for visible plugins only — no flicker
502+ // Create tab DOM for visible plugins only
489503 visiblePlugins.forEach(pluginObj => {
490504 const prefix = pluginObj.unique_prefix;
491- const c = pluginCounts[prefix] || { objects: 0, events: 0, history: 0 };
505+ const c = countsAvailable ? ( pluginCounts[prefix] || { objects: 0, events: 0, history: 0 }) : null ;
492506 createTabContent(pluginObj, assignActive, c);
493507 createTabHeader(pluginObj, assignActive, c);
494508 assignActive = false;
@@ -519,9 +533,11 @@ function generateTabs() {
519533 tabContainer: '#tabs-location'
520534 });
521535
522- // Apply badge counts to the DOM and hide empty inner sub-tabs
523- const prefixes = visiblePlugins.map(p => p.unique_prefix);
524- applyPluginBadges(pluginCounts, prefixes);
536+ // Apply badge counts to the DOM and hide empty inner sub-tabs (only if counts loaded)
537+ if (countsAvailable) {
538+ const prefixes = visiblePlugins.map(p => p.unique_prefix);
539+ applyPluginBadges(pluginCounts, prefixes);
540+ }
525541
526542 hideSpinner()
527543}
@@ -664,17 +680,27 @@ className: colDef.css_classes || '',
664680 });
665681 }
666682
667- // Initialize the Objects table immediately (it is the active/visible sub-tab).
668- // Defer Events and History tables until their sub-tab is first shown.
683+ // Initialize the DataTable for whichever inner sub-tab is currently active
684+ // (may not be Objects if autoHideEmptyTabs switched it).
685+ // Defer the remaining sub-tabs until their shown.bs.tab fires.
669686 const [objCfg, evtCfg, histCfg] = tableConfigs;
670- buildDT(objCfg.tableId, objCfg.gqlField, objCfg.countId, objCfg.badgeId);
671-
672- $(`a[ href=" #eventsTarget_${prefix}"]`).one('shown.bs.tab', function() {
673- buildDT(evtCfg.tableId, evtCfg.gqlField, evtCfg.countId, evtCfg.badgeId);
674- }) ;
687+ const allCfgs = [
688+ { cfg: objCfg, href: `#objectsTarget_${prefix}` },
689+ { cfg: evtCfg, href: ` #eventsTarget_${prefix}` },
690+ { cfg: histCfg, href: `#historyTarget_${prefix}` },
691+ ] ;
675692
676- $(`a[href="#historyTarget_${prefix}"]`).one('shown.bs.tab', function() {
677- buildDT(histCfg.tableId, histCfg.gqlField, histCfg.countId, histCfg.badgeId);
693+ allCfgs.forEach(({ cfg, href }) => {
694+ const $subPane = $(href);
695+ if ($subPane.hasClass('active') && $subPane.is(':visible')) {
696+ // This sub-tab is the currently active one — initialize immediately
697+ buildDT(cfg.tableId, cfg.gqlField, cfg.countId, cfg.badgeId);
698+ } else if ($subPane.closest('.tab-pane').length) {
699+ // Defer until shown
700+ $(`a[href="${href}"]`).one('shown.bs.tab', function() {
701+ buildDT(cfg.tableId, cfg.gqlField, cfg.countId, cfg.badgeId);
702+ });
703+ }
678704 });
679705}
680706
0 commit comments