aboutsummaryrefslogtreecommitdiffstats
path: root/static
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-09 18:26:00 +0100
committerDanilo M. <danix@danix.xyz>2026-11-09 18:26:00 +0100
commit46a164e797c0fd77ee67e2bae9bec5c02ad1de2d (patch)
tree566bb56c30b85a37f15b96a529ff252d69acb867 /static
parent7f99167f0b6a3c83bf77e20bc5e03942b901fb71 (diff)
downloaddeck-it-46a164e797c0fd77ee67e2bae9bec5c02ad1de2d.tar.gz
deck-it-46a164e797c0fd77ee67e2bae9bec5c02ad1de2d.zip
fix: add stats, music and windows panels
Diffstat (limited to 'static')
-rw-r--r--static/app.js47
-rw-r--r--static/deck.css5
-rw-r--r--static/modules/music.js48
-rw-r--r--static/modules/obs.js2
-rw-r--r--static/modules/stats.js49
-rw-r--r--static/modules/windows.js65
6 files changed, 195 insertions, 21 deletions
diff --git a/static/app.js b/static/app.js
index 7a9ff40..ed19c60 100644
--- a/static/app.js
+++ b/static/app.js
@@ -3,31 +3,34 @@
// mount(el), update(state, el)}. A missing or broken panel is skipped.
const grid = document.getElementById("grid");
+const refreshers = [];
async function mountModule(name) {
- let panel;
try {
- panel = (await import(`./modules/${name}.js`)).default;
+ const panel = (await import(`./modules/${name}.js`)).default;
+ const el = document.createElement("section");
+ el.className = "panel";
+ el.dataset.module = name;
+ const title = document.createElement("h2");
+ title.textContent = panel.title ?? name;
+ el.append(title);
+ grid.append(el);
+ panel.mount?.(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);
} catch {
- return;
+ // a broken panel must not take down the rest of the dashboard
}
- const el = document.createElement("section");
- el.className = "panel";
- el.dataset.module = name;
- el.innerHTML = `<h2>${panel.title ?? name}</h2>`;
- grid.append(el);
- panel.mount?.(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
- }
- };
- await poll();
- setInterval(poll, (panel.interval ?? 1) * 1000);
}
async function boot() {
@@ -36,4 +39,8 @@ async function boot() {
for (const { name } of await r.json()) await mountModule(name);
}
+document.addEventListener("visibilitychange", () => {
+ if (document.visibilityState === "visible") refreshers.forEach((fn) => fn());
+});
+
boot();
diff --git a/static/deck.css b/static/deck.css
index abc4566..afe9379 100644
--- a/static/deck.css
+++ b/static/deck.css
@@ -79,6 +79,11 @@ select {
.sbb .id { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.stats .row, .windows .row { margin-bottom: 6px; }
+.now b { display: block; font-size: 16px; }
+.wt { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
+.windows .row.on { color: var(--udt-accent); }
+
.llm .meter { height: 6px; background: var(--udt-surface1); border-radius: 3px; overflow: hidden; margin: 2px 0 8px; }
.llm .meter > i { display: block; height: 100%; background: var(--udt-accent); }
diff --git a/static/modules/music.js b/static/modules/music.js
new file mode 100644
index 0000000..de9df7f
--- /dev/null
+++ b/static/modules/music.js
@@ -0,0 +1,48 @@
+async function post(action) {
+ try {
+ await fetch(`/api/music/${action}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: "{}",
+ });
+ } catch {
+ // the next poll shows the real state
+ }
+}
+
+function button(label, action) {
+ const b = document.createElement("button");
+ b.textContent = label;
+ b.onclick = () => post(action);
+ return b;
+}
+
+export default {
+ title: "Music",
+ interval: 2,
+
+ mount(el) {
+ const box = document.createElement("div");
+ box.className = "music";
+ box.innerHTML = `
+ <div class="now"><b class="title">—</b><span class="artist muted"></span></div>
+ <div class="row controls"></div>
+ <div class="row vol"></div>`;
+ box.querySelector(".controls").append(
+ button("⏮", "prev"), button("⏯", "play"), button("⏭", "next"));
+ box.querySelector(".vol").append(
+ button("Vol−", "voldown"), button("Mute", "mute"), button("Vol+", "volup"));
+ el.append(box);
+ },
+
+ update(st, el) {
+ const box = el.querySelector(".music");
+ box.querySelector(".title").textContent = st.title || "Nothing playing";
+ box.querySelector(".artist").textContent = st.artist || "";
+ box.querySelector(".controls button:nth-child(2)").textContent =
+ st.status === "Playing" ? "⏸" : "▶";
+ const mute = box.querySelector(".vol button:nth-child(2)");
+ mute.classList.toggle("on", Boolean(st.volume?.muted));
+ mute.textContent = st.volume?.muted ? "Unmute" : "Mute";
+ },
+};
diff --git a/static/modules/obs.js b/static/modules/obs.js
index c6a3ebc..9c5c34c 100644
--- a/static/modules/obs.js
+++ b/static/modules/obs.js
@@ -40,7 +40,7 @@ export default {
const sources = el.querySelector(".obs-sources");
if (!state.connected) {
- controls.textContent = "OBS offline";
+ controls.textContent = state.error ? `OBS offline: ${state.error}` : "OBS offline";
scenes.textContent = audio.textContent = sources.textContent = "";
return;
}
diff --git a/static/modules/stats.js b/static/modules/stats.js
new file mode 100644
index 0000000..b29b4ae
--- /dev/null
+++ b/static/modules/stats.js
@@ -0,0 +1,49 @@
+const gb = (kb) => (kb / 1048576).toFixed(1);
+const rate = (b) => (b >= 1048576 ? `${(b / 1048576).toFixed(1)} MB/s` : `${Math.round(b / 1024)} KB/s`);
+const uptime = (s) => {
+ const d = Math.floor(s / 86400);
+ const h = Math.floor((s % 86400) / 3600);
+ const m = Math.floor((s % 3600) / 60);
+ return d ? `${d}d ${h}h` : `${h}h ${m}m`;
+};
+const gpu = (g) => {
+ if (!g) return "—";
+ if (g.asleep) return `${g.name} (asleep)`;
+ return `${g.name} ${g.util}%${g.temp != null ? ` · ${g.temp}°C` : ""}`;
+};
+
+function row(label, value) {
+ const div = document.createElement("div");
+ div.className = "row";
+ const name = document.createElement("span");
+ name.className = "muted";
+ name.textContent = label;
+ const val = document.createElement("span");
+ val.className = "num";
+ val.textContent = value;
+ div.append(name, val);
+ return div;
+}
+
+export default {
+ title: "System",
+ interval: 1,
+
+ mount(el) {
+ const box = document.createElement("div");
+ box.className = "stats";
+ el.append(box);
+ },
+
+ update(st, el) {
+ const box = el.querySelector(".stats");
+ box.textContent = "";
+ box.append(
+ row("CPU", `${st.cpu}%${st.cpuTemp != null ? ` · ${st.cpuTemp}°C` : ""}`),
+ row("Memory", `${gb(st.memUsed)} / ${gb(st.memTotal)} GiB`),
+ row("GPU", gpu(st.gpu)),
+ row("Network", `↓ ${rate(st.net.down)} ↑ ${rate(st.net.up)}`),
+ row("Uptime", uptime(st.uptime)),
+ );
+ },
+};
diff --git a/static/modules/windows.js b/static/modules/windows.js
new file mode 100644
index 0000000..7a2eeb8
--- /dev/null
+++ b/static/modules/windows.js
@@ -0,0 +1,65 @@
+async function post(action, body = {}) {
+ try {
+ await fetch(`/api/windows/${action}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+ } catch {
+ // the next poll shows the real state
+ }
+}
+
+function button(label, onclick) {
+ const b = document.createElement("button");
+ b.textContent = label;
+ b.onclick = onclick;
+ return b;
+}
+
+export default {
+ title: "Windows",
+ interval: 1,
+
+ mount(el) {
+ const box = document.createElement("div");
+ box.className = "windows";
+ box.innerHTML = `
+ <div class="row ws"></div>
+ <div class="winlist"></div>
+ <div class="row launches"></div>`;
+ el.append(box);
+ },
+
+ update(st, el) {
+ const box = el.querySelector(".windows");
+
+ const ws = box.querySelector(".ws");
+ ws.textContent = "";
+ for (const id of st.workspaces ?? []) {
+ const b = button(String(id), () => post("workspace", { n: id }));
+ b.classList.toggle("on", (st.windows ?? []).some((w) => w.workspace === id));
+ ws.append(b);
+ }
+
+ const list = box.querySelector(".winlist");
+ list.textContent = "";
+ for (const w of (st.windows ?? []).slice(0, 12)) {
+ const row = document.createElement("div");
+ row.className = "row";
+ row.classList.toggle("on", Boolean(w.active));
+ const title = document.createElement("span");
+ title.className = "wt";
+ title.textContent = w.title || w.class;
+ title.onclick = () => post("focus", { address: w.address });
+ row.append(title, button("×", () => post("close", { address: w.address })));
+ list.append(row);
+ }
+
+ const launches = box.querySelector(".launches");
+ launches.textContent = "";
+ for (const key of st.launches ?? []) {
+ launches.append(button(key, () => post("launch", { key })));
+ }
+ },
+};