Files
workbench-server/src/workbench/scaffold.html
T
Mortdecai 05fcb7f2f2 feat: POST /log from the page + load state.json on page open
- Browser can record entries (e.g. meter readings) into session.md/jsonl;
  broadcast to other open clients. Log feed renders as text, not HTML.
- Page loads state.json on open instead of only localStorage, so a new
  device (or one that missed the WS push) shows current content.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:28:36 -04:00

140 lines
3.7 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}}</title>
<style>
:root {
--bg: #0a0f0c;
--panel-bg: #111a15;
--border: #2a3a2e;
--text: #aaccaa;
--text-dim: #557755;
--accent: #33ff66;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: monospace;
font-size: 14px;
height: 100vh;
display: flex;
flex-direction: column;
}
#content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
#log-feed {
border-top: 1px solid var(--border);
padding: 8px 12px;
max-height: 200px;
overflow-y: auto;
}
#log-feed h3 {
color: var(--text-dim);
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
margin-bottom: 4px;
}
.log-entry {
font-size: 12px;
color: var(--text-dim);
padding: 2px 0;
}
.log-entry .log-time {
color: var(--accent);
margin-right: 8px;
}
#status {
background: var(--panel-bg);
border-top: 1px solid var(--border);
padding: 4px 12px;
font-size: 11px;
color: var(--text-dim);
}
#status.connected { color: var(--accent); }
</style>
</head>
<body>
<div id="content">
<h1 style="color: var(--accent); font-size: 18px;">{{TITLE}}</h1>
<p style="color: var(--text-dim); margin-top: 8px;">{{DESCRIPTION}}</p>
<p style="color: var(--text-dim); margin-top: 16px;">Waiting for content...</p>
</div>
<div id="log-feed">
<h3>Session Log</h3>
<div id="log-entries"></div>
</div>
<div id="status">Connecting...</div>
<script>
const WS_URL = `ws://${location.host}/ws`;
let ws;
function connect() {
ws = new WebSocket(WS_URL);
ws.onopen = () => {
document.getElementById('status').textContent = 'Connected';
document.getElementById('status').className = 'connected';
};
ws.onclose = () => {
document.getElementById('status').textContent = 'Disconnected — reconnecting...';
document.getElementById('status').className = '';
setTimeout(connect, 2000);
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'state') handleState(msg.state);
if (msg.type === 'log') handleLog(msg.entry);
};
}
function handleState(state) {
if (state.template) {
document.getElementById('content').innerHTML = state.template;
}
if (state.styles) {
let el = document.getElementById('dynamic-styles');
if (!el) { el = document.createElement('style'); el.id = 'dynamic-styles'; document.head.appendChild(el); }
el.textContent = state.styles;
}
if (state.script) {
try { new Function(state.script)(); } catch(e) { console.error('Script error:', e); }
}
window.__workbench_state = state;
try { localStorage.setItem('workbench-state', JSON.stringify(state)); } catch(e) {}
}
function handleLog(entry) {
const div = document.createElement('div');
div.className = 'log-entry';
const time = new Date().toLocaleTimeString();
const t = document.createElement('span');
t.className = 'log-time';
t.textContent = time;
div.append(t, entry); // text, not HTML — browsers can POST entries now
const feed = document.getElementById('log-entries');
feed.appendChild(div);
feed.scrollTop = feed.scrollHeight;
}
// state.json on disk is the source of truth; localStorage only covers the server being unreachable
fetch('state.json', {cache: 'no-store'})
.then(r => r.json())
.then(s => { if (Object.keys(s).length) handleState(s); else throw 0; })
.catch(() => {
try {
const saved = JSON.parse(localStorage.getItem('workbench-state'));
if (saved) handleState(saved);
} catch(e) {}
})
.finally(connect);
</script>
</body>
</html>