aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--static/app.js39
-rw-r--r--static/deck.css69
-rw-r--r--static/index.html20
-rw-r--r--tests/test_frontend.py65
4 files changed, 191 insertions, 2 deletions
diff --git a/static/app.js b/static/app.js
new file mode 100644
index 0000000..7a9ff40
--- /dev/null
+++ b/static/app.js
@@ -0,0 +1,39 @@
+// The shell: discover modules, mount each panel, poll its state.
+// A module is static/modules/<name>.js exporting default {title, interval,
+// mount(el), update(state, el)}. A missing or broken panel is skipped.
+
+const grid = document.getElementById("grid");
+
+async function mountModule(name) {
+ let panel;
+ try {
+ panel = (await import(`./modules/${name}.js`)).default;
+ } catch {
+ return;
+ }
+ 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() {
+ const r = await fetch("/api/modules");
+ if (!r.ok) return;
+ for (const { name } of await r.json()) await mountModule(name);
+}
+
+boot();
diff --git a/static/deck.css b/static/deck.css
new file mode 100644
index 0000000..9bbd806
--- /dev/null
+++ b/static/deck.css
@@ -0,0 +1,69 @@
+* { box-sizing: border-box; margin: 0; }
+
+html, body { height: 100%; }
+
+body {
+ background: var(--udt-base);
+ color: var(--udt-text);
+ font: 400 14px/20px "Noto Sans", system-ui, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ user-select: none;
+ overflow: hidden;
+}
+
+#grid {
+ height: 100dvh;
+ padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right))
+ max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
+ display: grid;
+ gap: 12px;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ grid-auto-rows: minmax(0, auto);
+ align-content: start;
+ overflow-y: auto;
+}
+
+.panel {
+ background: var(--udt-surface0);
+ border: 1px solid var(--udt-surface1);
+ border-radius: 16px;
+ padding: 16px;
+ min-height: 0;
+ min-width: 0;
+}
+
+.panel h2 {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--udt-subtext0);
+ margin-bottom: 12px;
+}
+
+.num {
+ font-family: "Inconsolata Nerd Font Mono", ui-monospace, monospace;
+ font-variant-numeric: tabular-nums;
+}
+
+.muted { color: var(--udt-overlay0); }
+
+button {
+ font: inherit;
+ color: inherit;
+ background: var(--udt-surface1);
+ border: 1px solid var(--udt-surface2);
+ border-radius: 10px;
+ padding: 10px 12px;
+ cursor: pointer;
+ touch-action: manipulation;
+}
+
+button:active { background: var(--udt-surface2); }
+
+button.on { background: var(--udt-accent); color: var(--udt-crust); border-color: var(--udt-accent); }
+
+.good { color: var(--udt-green); }
+.bad { color: var(--udt-red); }
+.warn { color: var(--udt-yellow); }
diff --git a/static/index.html b/static/index.html
index 4afa18f..2c5bdfa 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1,3 +1,19 @@
<!doctype html>
-<html lang="en"><head><meta charset="utf-8"><title>deck-it</title></head>
-<body><p>deck-it</p></body></html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no,viewport-fit=cover">
+<meta name="apple-mobile-web-app-capable" content="yes">
+<meta name="mobile-web-app-capable" content="yes">
+<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+<meta name="apple-mobile-web-app-title" content="deck-it">
+<meta name="theme-color" content="#24273a">
+<link rel="stylesheet" href="theme.css">
+<link rel="stylesheet" href="deck.css">
+<title>deck-it</title>
+</head>
+<body>
+<main id="grid"></main>
+<script type="module" src="app.js"></script>
+</body>
+</html>
diff --git a/tests/test_frontend.py b/tests/test_frontend.py
new file mode 100644
index 0000000..d90bf48
--- /dev/null
+++ b/tests/test_frontend.py
@@ -0,0 +1,65 @@
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+import http.client
+import os
+import socket
+import tempfile
+import threading
+import unittest
+
+import deckit.module as mod
+import deckit.server as srv
+
+
+class UnixConnection(http.client.HTTPConnection):
+ def __init__(self, path):
+ super().__init__("localhost")
+ self.path = path
+
+ def connect(self):
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.sock.connect(self.path)
+
+
+class Frontend(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.dir = tempfile.TemporaryDirectory()
+ cls.sock = os.path.join(cls.dir.name, "http.sock")
+ srv.TOKEN = "t"
+ srv.REGISTRY = mod.Registry([])
+ cls.server = srv.UnixHTTPServer(cls.sock, srv.Handler)
+ threading.Thread(target=cls.server.serve_forever, daemon=True).start()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.server.shutdown()
+ cls.server.server_close()
+ cls.dir.cleanup()
+
+ def get(self, path):
+ conn = UnixConnection(self.sock)
+ conn.request("GET", path, headers={"Host": "127.0.0.1:8765"})
+ r = conn.getresponse()
+ data = r.read()
+ conn.close()
+ return r.status, data
+
+ def test_page_references_assets(self):
+ status, body = self.get("/")
+ self.assertEqual(status, 200)
+ text = body.decode()
+ self.assertIn("app.js", text)
+ self.assertIn("deck.css", text)
+
+ def test_css_uses_udt_vars(self):
+ status, body = self.get("/deck.css")
+ self.assertEqual(status, 200)
+ self.assertIn(b"--udt-", body)
+
+
+if __name__ == "__main__":
+ unittest.main()