diff --git a/front/pluginsCore.php b/front/pluginsCore.php index 2f7f9689..94835edd 100755 --- a/front/pluginsCore.php +++ b/front/pluginsCore.php @@ -274,6 +274,10 @@ function genericSaveData (id) { // ----------------------------------------------------------------------------- pluginDefinitions = [] +// Global counts map, populated before tabs are rendered. +// null = counts unavailable (fail-open: show all plugins) +let pluginCounts = null; + async function getData() { try { showSpinner(); @@ -282,6 +286,11 @@ async function getData() { const plugins = await fetchJson('plugins.json'); pluginDefinitions = plugins.data; + // Fetch counts BEFORE rendering tabs so we can skip empty plugins (no flicker). + // fetchPluginCounts never throws — returns null on failure (fail-open). + const prefixes = pluginDefinitions.filter(p => p.show_ui).map(p => p.unique_prefix); + pluginCounts = await fetchPluginCounts(prefixes); + generateTabs(); } catch (err) { console.error("Failed to load data", err); @@ -348,21 +357,17 @@ function postPluginGraphQL(gqlField, prefix, foreignKey, dtRequest, callback) { }); } -// Fetch badge counts for every plugin and populate sidebar + sub-tab counters. +// Fetch counts for all plugins. Returns { PREFIX: { objects, events, history } } +// or null on failure (fail-open so tabs still render). // Fast path: static JSON (~1KB) when no MAC filter is active. // Filtered path: batched GraphQL aliases when a foreignKey (MAC) is set. -async function prefetchPluginBadges() { - const mac = $("#txtMacFilter").val(); - const foreignKey = (mac && mac !== "--") ? mac : null; - - const prefixes = pluginDefinitions - .filter(p => p.show_ui) - .map(p => p.unique_prefix); - - if (prefixes.length === 0) return; +async function fetchPluginCounts(prefixes) { + if (prefixes.length === 0) return {}; try { - let counts = {}; // { PREFIX: { objects: N, events: N, history: N } } + const mac = $("#txtMacFilter").val(); + const foreignKey = (mac && mac !== "--") ? mac : null; + let counts = {}; if (!foreignKey) { // ---- FAST PATH: lightweight pre-computed JSON ---- @@ -391,7 +396,10 @@ async function prefetchPluginBadges() { headers: { "Authorization": `Bearer ${apiToken}`, "Content-Type": "application/json" }, data: JSON.stringify({ query }), }); - if (response.errors) { console.error("[plugins] badge GQL errors:", response.errors); return; } + if (response.errors) { + console.error("[plugins] badge GQL errors:", response.errors); + return null; // fail-open + } for (const p of prefixes) { counts[p] = { objects: response.data[`${p}_obj`]?.dbCount ?? 0, @@ -401,95 +409,74 @@ async function prefetchPluginBadges() { } } - // Update DOM - for (const [prefix, c] of Object.entries(counts)) { - $(`#badge_${prefix}`).text(c.objects); - $(`#objCount_${prefix}`).text(c.objects); - $(`#evtCount_${prefix}`).text(c.events); - $(`#histCount_${prefix}`).text(c.history); - } - // Zero out plugins with no rows in any table - prefixes.forEach(prefix => { - if (!counts[prefix]) { - $(`#badge_${prefix}`).text(0); - $(`#objCount_${prefix}`).text(0); - $(`#evtCount_${prefix}`).text(0); - $(`#histCount_${prefix}`).text(0); - } - }); - - // Auto-hide tabs with zero results - autoHideEmptyTabs(counts, prefixes); - + return counts; } catch (err) { - console.error('[plugins] badge prefetch failed:', err); + console.error('[plugins] fetchPluginCounts failed (fail-open):', err); + return null; } } -// --------------------------------------------------------------- -// Hide plugin tabs (left-nav + pane) where all three counts are 0. -// Within visible plugins, hide inner sub-tabs whose count is 0. -// If the active tab was hidden, activate the first visible one. -function autoHideEmptyTabs(counts, prefixes) { +// Apply pre-fetched counts to the DOM badges and hide empty tabs/sub-tabs. +function applyPluginBadges(counts, prefixes) { + // Update DOM badges + for (const [prefix, c] of Object.entries(counts)) { + $(`#badge_${prefix}`).text(c.objects); + $(`#objCount_${prefix}`).text(c.objects); + $(`#evtCount_${prefix}`).text(c.events); + $(`#histCount_${prefix}`).text(c.history); + } + // Zero out plugins with no rows in any table prefixes.forEach(prefix => { - const c = counts[prefix] || { objects: 0, events: 0, history: 0 }; - const total = c.objects + c.events + c.history; - const $li = $(`#tabs-location li:has(a[href="#${prefix}"])`); - const $pane = $(`#tabs-content-location > #${prefix}`); - - if (total === 0) { - // Hide the entire plugin tab and strip active from both nav item and pane - $li.removeClass('active').hide(); - $pane.removeClass('active').css('display', ''); - } else { - // Ensure nav item visible (in case a previous filter hid it) - $li.show(); - // Clear any inline display override so Bootstrap CSS controls pane visibility via .active - $pane.css('display', ''); - - // Hide inner sub-tabs with zero count - const subTabs = [ - { href: `#objectsTarget_${prefix}`, count: c.objects }, - { href: `#eventsTarget_${prefix}`, count: c.events }, - { href: `#historyTarget_${prefix}`, count: c.history }, - ]; - - let activeSubHidden = false; - subTabs.forEach(st => { - const $subLi = $pane.find(`ul.nav-tabs li:has(a[href="${st.href}"])`); - const $subPane = $pane.find(st.href); - if (st.count === 0) { - if ($subLi.hasClass('active')) activeSubHidden = true; - $subLi.hide(); - $subPane.removeClass('active').css('display', ''); - } else { - $subLi.show(); - $subPane.css('display', ''); - } - }); - - // If the active inner sub-tab was hidden, activate the first visible one - // via Bootstrap's tab lifecycle so shown.bs.tab fires for deferred DataTable init - if (activeSubHidden) { - const $firstVisibleSubA = $pane.find('ul.nav-tabs li:visible:first a'); - if ($firstVisibleSubA.length) { - $firstVisibleSubA.tab('show'); - } - } + if (!counts[prefix]) { + $(`#badge_${prefix}`).text(0); + $(`#objCount_${prefix}`).text(0); + $(`#evtCount_${prefix}`).text(0); + $(`#histCount_${prefix}`).text(0); } }); - // If the active left-nav tab was hidden, activate the first visible one - const $activeLi = $(`#tabs-location li.active:visible`); - if ($activeLi.length === 0) { - const $firstVisibleLi = $(`#tabs-location li:visible`).first(); - if ($firstVisibleLi.length) { - // Let Bootstrap's .tab('show') manage the active class on both - // the