feat(diagnostic): Archiv-Modal mit Pagination fuer ARIA-Stream

- /api/agent-stream akzeptiert jetzt ?page=N&perPage=M zusaetzlich zu
  ?lines=N. page=1 = neueste Eintraege, hoehere Pages = aelter.
  Antwort enthaelt page/perPage/pagesTotal/total fuer Client-Nav.
- Live-View hat neuen 📜 Archiv-Button neben Leeren/Auto-Scroll.
- Modal mit PerPage-Selector (50/100/500/1000), «‹›» Navigation und
  reload-Button. Pagination-Buttons werden auf den Grenzen disabled.
- renderArchiveLine spiegelt das Live-View-Rendering (Tool-Calls in
  cyan, Results in gruen, Thinking kursiv) im Modal-Container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 23:11:46 +02:00
parent aaaf118cb7
commit fb71048dfd
2 changed files with 146 additions and 7 deletions
+27 -7
View File
@@ -1773,22 +1773,42 @@ const server = http.createServer((req, res) => {
return res.end(JSON.stringify({ ok: false, error: e.message }));
}
} else if (req.url.startsWith("/api/agent-stream") && req.method === "GET") {
// Tail des persistierten agent_stream.jsonl. Browser-Live-View laedt das
// beim Tab-Oeffnen damit Reload/Standby keine Events mehr wegschmeisst.
// Tail / paginierter Slice des persistierten agent_stream.jsonl.
// Modi:
// ?lines=N → letzte N Zeilen (Live-View Initial-Load)
// ?page=P&perPage=M → 1-indexed Pagination (Modal-Browser);
// page=1 = neueste Seite, hoehere Pages = aelter
try {
const u = new URL(req.url, "http://localhost");
const lines = Math.max(1, Math.min(5000, parseInt(u.searchParams.get("lines") || "200", 10) || 200));
const linesParam = u.searchParams.get("lines");
const pageParam = u.searchParams.get("page");
const perPageParam = u.searchParams.get("perPage");
const file = AGENT_STREAM_LOG;
let raw = "";
try { raw = fs.readFileSync(file, "utf-8"); } catch {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true, file, lines: [] }));
return res.end(JSON.stringify({ ok: true, file, total: 0, lines: [] }));
}
const all = raw.split("\n").filter(l => l.trim());
const tail = all.slice(-lines);
const parsed = tail.map(l => { try { return JSON.parse(l); } catch { return { _raw: l }; } });
let slice, page = 1, perPage = 0, pagesTotal = 1;
if (pageParam || perPageParam) {
perPage = Math.max(10, Math.min(5000, parseInt(perPageParam || "100", 10) || 100));
pagesTotal = Math.max(1, Math.ceil(all.length / perPage));
page = Math.max(1, Math.min(pagesTotal, parseInt(pageParam || "1", 10) || 1));
// page=1 = juengste Seite → vom Ende her slicen
const end = all.length - (page - 1) * perPage;
const start = Math.max(0, end - perPage);
slice = all.slice(start, end);
} else {
const lines = Math.max(1, Math.min(5000, parseInt(linesParam || "200", 10) || 200));
slice = all.slice(-lines);
}
const parsed = slice.map(l => { try { return JSON.parse(l); } catch { return { _raw: l }; } });
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true, file, count: parsed.length, total: all.length, lines: parsed }));
return res.end(JSON.stringify({
ok: true, file, total: all.length, count: parsed.length,
page, perPage, pagesTotal, lines: parsed,
}));
} catch (e) {
res.writeHead(500, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: false, error: e.message }));