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>
This commit is contained in:
Mortdecai
2026-09-28 16:28:36 -04:00
parent 65622501fe
commit 05fcb7f2f2
3 changed files with 43 additions and 7 deletions
+13 -5
View File
@@ -114,18 +114,26 @@ function handleLog(entry) {
const div = document.createElement('div');
div.className = 'log-entry';
const time = new Date().toLocaleTimeString();
div.innerHTML = `<span class="log-time">${time}</span>${entry}`;
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;
}
try {
// 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) {}
connect();
} catch(e) {}
})
.finally(connect);
</script>
</body>
</html>
+12
View File
@@ -109,7 +109,19 @@ class WorkbenchServer:
return web.Response(status=404, text="Not found")
return web.FileResponse(file_path)
async def log_handler(request):
# lets the page record readings, so they outlive the browser
body = await request.json()
entry = str(body.get("entry", "")).strip()
if not entry:
return web.json_response({"error": "entry required"}, status=400)
data = body.get("data") if isinstance(body.get("data"), dict) else None
append_log(request.app["project_name"], entry, data=data, workbench_dir=self.workbench_dir)
await self._broadcast_ws(request.app["project_name"], {"type": "log", "entry": entry})
return web.json_response({"ok": True})
app.router.add_get("/ws", ws_handler)
app.router.add_post("/log", log_handler)
app.router.add_get("/{path:.*}", static_handler)
app.router.add_get("/", static_handler)
+16
View File
@@ -124,3 +124,19 @@ async def test_static_blocks_path_traversal(server, tmp_workbench):
async with session.get(base + "/") as r:
assert r.status == 200
await server.workbench_stop("p")
@pytest.mark.asyncio
async def test_browser_post_log_persists_and_broadcasts(server, tmp_workbench):
from workbench.project import read_log
session, base = await _live(server)
async with session:
ws = await session.ws_connect(base + "/ws")
async with session.post(base + "/log", json={"entry": "R303 3.31M", "data": {"part": "R303"}}) as r:
assert r.status == 200
msg = await asyncio.wait_for(ws.receive_json(), 3)
assert msg == {"type": "log", "entry": "R303 3.31M"}
await ws.close()
entries = read_log("p", workbench_dir=tmp_workbench)
assert entries[-1]["entry"] == "R303 3.31M" and entries[-1]["part"] == "R303"
await server.workbench_stop("p")