Module system: discovery, route mounting, client loader, clock module
Milestone 2. Server scans modules/*/module.json, serves module assets at /modules/<id>/, mounts optional server routes (exact + trailing-/* prefix patterns) under /api/modules/<id>/ with per-module config from config/modules.json, and lists enabled manifests at GET /api/modules. Shell dynamically imports each module's client.js and hands it the moduleApi (config, instanceSettings, scoped fetch, auto-reconnecting sse, setStatus). Clock module proves the contract end to end; password-typed config fields never leave the server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
/* Clock — the reference ProdDash module. Client-only: no server entry.
|
||||
Everything a module may do is shown here: render inside root, read
|
||||
instanceSettings, report status, clean up in stop(). */
|
||||
|
||||
export default function create({ root, moduleApi }) {
|
||||
let timer = null;
|
||||
let timeEl = null;
|
||||
let dateEl = null;
|
||||
let resizeObserver = null;
|
||||
|
||||
function settings() {
|
||||
return moduleApi.instanceSettings; // shell merges schema defaults for us
|
||||
}
|
||||
|
||||
function renderFrame() {
|
||||
root.innerHTML = '';
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'clock-wrap';
|
||||
timeEl = document.createElement('div');
|
||||
timeEl.className = 'clock-time';
|
||||
dateEl = document.createElement('div');
|
||||
dateEl.className = 'clock-date';
|
||||
wrap.append(timeEl, dateEl);
|
||||
root.appendChild(wrap);
|
||||
}
|
||||
|
||||
function tick() {
|
||||
const { format, showSeconds, showDate } = settings();
|
||||
const now = new Date();
|
||||
timeEl.textContent = now.toLocaleTimeString([], {
|
||||
hour12: format === '12h',
|
||||
hour: format === '12h' ? 'numeric' : '2-digit',
|
||||
minute: '2-digit',
|
||||
...(showSeconds ? { second: '2-digit' } : {}),
|
||||
});
|
||||
dateEl.hidden = !showDate;
|
||||
if (showDate) {
|
||||
dateEl.textContent = now.toLocaleDateString([], {
|
||||
weekday: 'long', month: 'long', day: 'numeric',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Scale the digits to the tile (CSS alone can't see the tile's box). */
|
||||
function fit() {
|
||||
const { showSeconds, showDate } = settings();
|
||||
const chars = (showSeconds ? 8 : 5) + 1;
|
||||
const size = Math.min((root.clientWidth * 1.7) / chars, root.clientHeight * (showDate ? 0.42 : 0.55));
|
||||
timeEl.style.fontSize = Math.max(12, Math.floor(size)) + 'px';
|
||||
dateEl.style.fontSize = Math.max(10, Math.floor(size * 0.28)) + 'px';
|
||||
}
|
||||
|
||||
return {
|
||||
start() {
|
||||
renderFrame();
|
||||
tick();
|
||||
fit();
|
||||
timer = setInterval(tick, 250);
|
||||
resizeObserver = new ResizeObserver(fit);
|
||||
resizeObserver.observe(root);
|
||||
moduleApi.setStatus('ok', 'Ticking');
|
||||
},
|
||||
stop() {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = null;
|
||||
root.innerHTML = '';
|
||||
},
|
||||
onResize() {
|
||||
fit();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "clock",
|
||||
"name": "Clock",
|
||||
"version": "1.0.0",
|
||||
"description": "Big booth clock — the smallest possible ProdDash module",
|
||||
"client": "client.js",
|
||||
"style": "style.css",
|
||||
"minSize": { "w": 2, "h": 1 },
|
||||
"defaultSize": { "w": 3, "h": 2 },
|
||||
"instanceSchema": {
|
||||
"format": {
|
||||
"type": "select",
|
||||
"label": "Format",
|
||||
"default": "24h",
|
||||
"options": [
|
||||
{ "value": "24h", "label": "24-hour" },
|
||||
{ "value": "12h", "label": "12-hour" }
|
||||
]
|
||||
},
|
||||
"showSeconds": { "type": "boolean", "label": "Show seconds", "default": true },
|
||||
"showDate": { "type": "boolean", "label": "Show date", "default": false }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/* Clock module — scoped under its tile via [data-module="clock"], styled
|
||||
with the shell's CSS variables only. */
|
||||
|
||||
[data-module="clock"] .clock-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2%;
|
||||
}
|
||||
|
||||
[data-module="clock"] .clock-time {
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
[data-module="clock"] .clock-date {
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
Reference in New Issue
Block a user