Skip to content

Commit b18cf98

Browse files
committed
feat(plugins): Enhance plugin counts handling with fail-open support and improved comments
1 parent 77369c3 commit b18cf98

File tree

1 file changed

+85
-59
lines changed

1 file changed

+85
-59
lines changed

front/pluginsCore.php

Lines changed: 85 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,9 @@ function genericSaveData (id) {
274274
// -----------------------------------------------------------------------------
275275
pluginDefinitions = []
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

280281
async 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.
361364
async 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

Comments
 (0)