diff options
Diffstat (limited to 'static/app.js')
| -rw-r--r-- | static/app.js | 132 |
1 files changed, 97 insertions, 35 deletions
diff --git a/static/app.js b/static/app.js index c7730e3..54d4b26 100644 --- a/static/app.js +++ b/static/app.js @@ -1,22 +1,18 @@ -// The shell: discover modules, mount each panel as a Gridstack card, poll its -// state. A module is static/modules/<name>.js exporting default {title, -// interval, mount(el), update(state, el)}. Card position and size are kept in -// this browser's localStorage. +// The shell: discover modules, sort them into tabs, mount each panel and poll +// its state. A module is static/modules/<name>.js exporting default {title, +// tab, interval, mount(el), update(state, el)}. Modules without a tab go into +// the Dashboard. Card order and size are kept in this browser's localStorage. -const gridEl = document.getElementById("grid"); +const tabBar = document.getElementById("tabs"); +const tabBody = document.getElementById("tab-body"); const refreshers = []; const LAYOUT_KEY = "deckit.layout"; +const TAB_KEY = "deckit.tab"; const DEFAULT_W = 6; const DEFAULT_H = 4; +const tabs = {}; -const grid = GridStack.init({ - column: 12, - cellHeight: 80, - margin: 8, - float: true, - handle: ".panel-drag", - resizable: { handles: "se" }, -}, gridEl); +let ready = false; function loadLayout() { try { @@ -36,7 +32,51 @@ function saveLayout() { } } -let ready = false; +function ensureTab(name) { + if (tabs[name]) return tabs[name]; + const button = document.createElement("button"); + button.textContent = name; + button.onclick = () => showTab(name); + tabBar.append(button); + const section = document.createElement("section"); + section.className = "tab-panel"; + section.dataset.tab = name; + section.hidden = true; + tabBody.append(section); + tabs[name] = { button, section }; + return tabs[name]; +} + +function showTab(name) { + for (const [n, tab] of Object.entries(tabs)) { + tab.section.hidden = n !== name; + tab.button.classList.toggle("active", n === name); + } + try { + localStorage.setItem(TAB_KEY, name); + } catch {} + if (name === "Dashboard") window.dispatchEvent(new Event("resize")); +} + +// Dashboard is the grid; it exists before the modules so Gridstack measures a +// visible container. +const dashboard = ensureTab("Dashboard"); +dashboard.section.hidden = false; +dashboard.button.classList.add("active"); + +const gridEl = document.createElement("div"); +gridEl.id = "grid"; +gridEl.className = "grid-stack"; +dashboard.section.append(gridEl); + +const grid = GridStack.init({ + column: 12, + cellHeight: 80, + margin: 8, + float: true, + handle: ".panel-drag", + resizable: { handles: "se" }, +}, gridEl); grid.on("change", saveLayout); const savedById = {}; @@ -81,33 +121,55 @@ function makeCard(name, title) { return body; } -async function mountModule(name) { - try { - const panel = (await import(`./modules/${name}.js`)).default; - const content = makeCard(name, panel.title ?? name); - panel.mount?.(content); - - const poll = async () => { - try { - const r = await fetch(`/api/${name}`); - if (r.ok) panel.update?.(await r.json(), content); - } catch { - // keep the last state; the next tick retries - } - }; - refreshers.push(poll); - await poll(); - setInterval(poll, (panel.interval ?? 1) * 1000); - } catch { - // a broken panel must not take down the rest of the dashboard - } +async function startPoll(name, panel, el) { + const poll = async () => { + try { + const r = await fetch(`/api/${name}`); + if (r.ok) panel.update?.(await r.json(), el); + } catch { + // keep the last state; the next tick retries + } + }; + refreshers.push(poll); + await poll(); + setInterval(poll, (panel.interval ?? 1) * 1000); +} + +async function mountDashboard(name, panel) { + const body = makeCard(name, panel.title ?? name); + panel.mount?.(body); + await startPoll(name, panel, body); +} + +async function mountTab(name, panel, section) { + const wrap = document.createElement("div"); + wrap.className = "tab-content"; + section.append(wrap); + panel.mount?.(wrap); + await startPoll(name, panel, wrap); } async function boot() { const r = await fetch("/api/modules"); if (!r.ok) return; - for (const { name } of await r.json()) await mountModule(name); + for (const { name } of await r.json()) { + let panel; + try { + panel = (await import(`./modules/${name}.js`)).default; + } catch { + continue; + } + const tabName = panel.tab ?? "Dashboard"; + try { + if (tabName === "Dashboard") await mountDashboard(name, panel); + else await mountTab(name, panel, ensureTab(tabName).section); + } catch { + // a broken panel must not take down the rest of the dashboard + } + } ready = true; + const saved = localStorage.getItem(TAB_KEY); + showTab(tabs[saved] ? saved : "Dashboard"); } document.addEventListener("visibilitychange", () => { |
