feat(dashboardAPI): resolve Grafana folder by name (fixes stale folderUid 400s)

A pinned folderUid goes stale whenever Grafana is rebuilt — the same-named
folder returns with a fresh uid and every dashboard upsert then 400s
"folder not found", silently dropping all generated dashboards.

Add a folderTitle config field: when set, resolveFolderUid() looks the folder
up by name (GET /api/folders), creates it if absent (POST /api/folders),
caches the uid for the process, and falls back to the configured folderUid on
any failure (never worse than the pinned behavior). The emit handlers
(registerChild/regenerateDashboard/emitDashboardsFor) are now async and await
the resolution. folderUid retained as an explicit override/fallback.

Locked by slice48-folder-resolve-by-name; existing emit tests made async.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-27 21:02:38 +02:00
parent 5533293647
commit 5d651b59ef
8 changed files with 217 additions and 27 deletions

View File

@@ -32,14 +32,14 @@ test('recordChild caches child source by id; subsequent ones replace by id', ()
assert.equal(api.cachedChildSources().length, 2);
});
test('regenerate-dashboard with no cached children is a no-op (no msgs emitted)', () => {
test('regenerate-dashboard with no cached children is a no-op (no msgs emitted)', async () => {
const api = new DashboardApi({});
const sends = [];
handlers.regenerateDashboard(api, { topic: 'regenerate-dashboard', payload: {} }, makeCtx(sends));
await handlers.regenerateDashboard(api, { topic: 'regenerate-dashboard', payload: {} }, makeCtx(sends));
assert.equal(sends.length, 0);
});
test('regenerate-dashboard re-emits for each cached child, bypassing diff', () => {
test('regenerate-dashboard re-emits for each cached child, bypassing diff', async () => {
const api = new DashboardApi({});
// Pre-populate cache as if two children had registered.
api.recordChild(makeChildPayload('m-1'));
@@ -50,18 +50,18 @@ test('regenerate-dashboard re-emits for each cached child, bypassing diff', () =
api.lastFlowsStartedDiff = { added: [], changed: [], removed: [], rewired: [] };
const sends = [];
handlers.regenerateDashboard(api, { topic: 'regenerate-dashboard', payload: {} }, makeCtx(sends));
await handlers.regenerateDashboard(api, { topic: 'regenerate-dashboard', payload: {} }, makeCtx(sends));
// Each child yields at least one dashboard message (the root for the child's view).
assert.ok(sends.length >= 2, `expected ≥2 emitted msgs, got ${sends.length}`);
// Every emitted msg carries trigger: 'manual' in meta.
for (const m of sends) assert.equal(m.meta?.trigger, 'manual');
});
test('child.register stamps trigger: child.register in emitted msg meta', () => {
test('child.register stamps trigger: child.register in emitted msg meta', async () => {
const api = new DashboardApi({});
api.lastFlowsStartedDiff = null; // cold-start → always regen
const sends = [];
handlers.registerChild(api, { topic: 'child.register', payload: makeChildPayload('m-3') }, makeCtx(sends));
await handlers.registerChild(api, { topic: 'child.register', payload: makeChildPayload('m-3') }, makeCtx(sends));
assert.ok(sends.length >= 1);
for (const m of sends) assert.equal(m.meta?.trigger, 'child.register');
});