diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-09 18:12:59 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-09 18:12:59 +0100 |
| commit | 2c098221f4ed09d772df9bece5c71912f1c5e849 (patch) | |
| tree | 656c77daa1e71bee1ebfc2fbc5a5d5b3b7841757 | |
| parent | 58b6f3feda3662c2004bdfaa1e7300f14dabd931 (diff) | |
| download | deck-it-2c098221f4ed09d772df9bece5c71912f1c5e849.tar.gz deck-it-2c098221f4ed09d772df9bece5c71912f1c5e849.zip | |
feat: frontend shell and UDT-themed layout
| -rw-r--r-- | static/app.js | 39 | ||||
| -rw-r--r-- | static/deck.css | 69 | ||||
| -rw-r--r-- | static/index.html | 20 | ||||
| -rw-r--r-- | tests/test_frontend.py | 65 |
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() |
