+ sectionFragments['targets'] = `
${_sectionHeader('targets', t('dashboard.section.targets'), targets.length)}
${_sectionContent('targets', targetsInner)}
`;
}
+
+ // Now assemble in layout-driven order, skipping invisible
+ // sections and the perf section (which is always rendered
+ // separately at the top for chart-persistence reasons).
+ for (const section of getOrderedSections()) {
+ if (section.key === 'perf') continue;
+ if (!section.visible) continue;
+ const html = sectionFragments[section.key];
+ if (html) dynamicHtml += html;
+ }
}
// First load: build everything in one innerHTML to avoid flicker.
// Poll-interval control was moved to the transport bar (it's global,
- // not dashboard-specific) — toolbar now only keeps the tutorial
- // help button.
+ // not dashboard-specific) — toolbar now keeps the tutorial help
+ // button + the new "Customize" gear that opens the layout panel.
const isFirstLoad = !container.querySelector('.dashboard-perf-persistent');
- const toolbar = `
`;
+ const perfVisible = isSectionVisible('perf');
+ const customizeBtn = `
`;
+ const tutorialBtn = `
`;
+ const toolbar = `
${customizeBtn}${tutorialBtn}
`;
if (isFirstLoad) {
- container.innerHTML = `${toolbar}
- ${_sectionHeader('perf', t('dashboard.section.performance'), '', renderPerfModeToggle())}
- ${_sectionContent('perf', renderPerfSection())}
-
-
${dynamicHtml}
`;
- await initPerfCharts();
+ const perfBlock = perfVisible
+ ? `
+ ${_sectionHeader('perf', t('dashboard.section.performance'), '', renderPerfModeToggle())}
+ ${_sectionContent('perf', renderPerfSection())}
+
`
+ : '';
+ container.innerHTML = `${toolbar}${perfBlock}
${dynamicHtml}
`;
+ _applyGlobalLayoutAttrs();
+ if (perfVisible) await initPerfCharts();
// Event delegation for scene preset cards (attached once, works across innerHTML refreshes)
initScenePresetDelegation(container);
} else {
+ // Toggle perf visibility on subsequent renders without
+ // destroying its DOM (charts persist).
+ const existingPerf = container.querySelector('.dashboard-perf-persistent') as HTMLElement | null;
+ if (existingPerf) {
+ existingPerf.style.display = perfVisible ? '' : 'none';
+ }
const dynamic = container.querySelector('.dashboard-dynamic');
if (dynamic && dynamic.innerHTML !== dynamicHtml) {
dynamic.innerHTML = dynamicHtml;
}
+ _applyGlobalLayoutAttrs();
+ }
+ // Apply per-section density tags so CSS selectors like
+ // `.dashboard-section[data-density="dense"]` can take effect.
+ for (const s of getOrderedSections()) {
+ const el = container.querySelector(`.dashboard-section[data-section="${CSS.escape(s.key)}"]`) as HTMLElement | null;
+ if (el) el.dataset.density = s.density;
}
_lastRunningIds = runningIds;
_lastSyncClockIds = syncClocks.map(c => `${c.id}:${c.is_running}`).sort().join(',');
@@ -853,12 +940,12 @@ function renderDashboardTarget(target: any, isRunning: boolean, devicesMap: Reco