From fb71048dfd988704e2de72e02ac29cc43b376d0e Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 29 May 2026 23:11:46 +0200 Subject: [PATCH] feat(diagnostic): Archiv-Modal mit Pagination fuer ARIA-Stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /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) --- diagnostic/index.html | 119 ++++++++++++++++++++++++++++++++++++++++++ diagnostic/server.js | 34 +++++++++--- 2 files changed, 146 insertions(+), 7 deletions(-) diff --git a/diagnostic/index.html b/diagnostic/index.html index a67448d..fce8737 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -357,6 +357,37 @@ + + + @@ -405,6 +436,7 @@
Idle β€” warte auf ARIA-Aktivitaet + @@ -3185,6 +3217,93 @@ _ariaMaybeScroll(); } catch (_) {} } + + // ── ARIA-Stream Archiv-Modal (Pagination) ──────────────── + let _ariaArchivePage = 1; + let _ariaArchivePagesTotal = 1; + + function openAriaStreamModal() { + const m = document.getElementById('aria-archive-modal'); + if (!m) return; + m.style.display = 'flex'; + loadAriaArchivePage(1); + } + + function closeAriaStreamModal() { + const m = document.getElementById('aria-archive-modal'); + if (m) m.style.display = 'none'; + } + + async function loadAriaArchivePage(page) { + const listEl = document.getElementById('aria-archive-list'); + const infoEl = document.getElementById('aria-arch-pageinfo'); + const totalEl = document.getElementById('aria-archive-total'); + if (!listEl) return; + const perPage = parseInt(document.getElementById('aria-archive-perpage').value, 10) || 100; + page = Math.max(1, page || 1); + listEl.innerHTML = '
Lade...
'; + try { + const r = await fetch(`/api/agent-stream?page=${page}&perPage=${perPage}`); + if (!r.ok) throw new Error('HTTP ' + r.status); + const d = await r.json(); + const events = d.lines || []; + _ariaArchivePage = d.page || page; + _ariaArchivePagesTotal = d.pagesTotal || 1; + if (totalEl) totalEl.textContent = `(${d.total || 0} Eintraege gesamt)`; + if (infoEl) infoEl.textContent = `Seite ${_ariaArchivePage} / ${_ariaArchivePagesTotal}`; + // Nav-Buttons enablen/disablen + document.getElementById('aria-arch-first').disabled = (_ariaArchivePage <= 1); + document.getElementById('aria-arch-prev').disabled = (_ariaArchivePage <= 1); + document.getElementById('aria-arch-next').disabled = (_ariaArchivePage >= _ariaArchivePagesTotal); + document.getElementById('aria-arch-last').disabled = (_ariaArchivePage >= _ariaArchivePagesTotal); + + if (!events.length) { + listEl.innerHTML = '
Keine Eintraege auf dieser Seite.
'; + return; + } + // Eintraege rendern β€” wir teilen sie in HTML-Snippets analog zu + // appendAriaStreamEvent, schreiben aber direkt in den Modal-Container. + const html = events.map(p => renderArchiveLine(p)).join(''); + listEl.innerHTML = html; + listEl.scrollTop = listEl.scrollHeight; + } catch (e) { + listEl.innerHTML = `
Fehler beim Laden: ${_ariaEsc(e.message)}
`; + } + } + + function renderArchiveLine(p) { + const t = _ariaTimePrefix(p.ts); + const kind = p.kind || ''; + const time = `[${t}]`; + if (kind === 'start') { + return `
━━━ ${t} session start (${_ariaEsc(p.model||'unknown')}) ━━━
`; + } + if (kind === 'end') { + const reason = p.reason || '?'; + const codePart = (p.code != null) ? ` code=${_ariaEsc(p.code)}` : ''; + const errPart = p.error ? ` err=${_ariaEsc(String(p.error).slice(0,120))}` : ''; + return `
━━━ ${t} session end (${_ariaEsc(reason)}${codePart}${errPart}) ━━━
`; + } + if (kind === 'text') { + return `
${time} ${_ariaEsc(p.text || '')}
`; + } + if (kind === 'thinking') { + return `
${time} πŸ’­ ${_ariaEsc(p.text || '')}
`; + } + if (kind === 'tool_use') { + const name = _ariaEsc(p.name || '?'); + const inp = _ariaEsc(p.input || ''); + const tail = p.inputTruncatedBytes ? ` ...(+${p.inputTruncatedBytes} bytes)` : ''; + return `
${time} β–Ά ${name} ${inp}${tail}
`; + } + if (kind === 'tool_result') { + const isError = p.isError === true; + const head = isError ? 'βœ— result (ERROR)' : 'βœ“ result'; + const tail = p.truncatedBytes ? ` ...(+${p.truncatedBytes} bytes)` : ''; + return `
${time} ${head}
${_ariaEsc(p.content || '')}${tail}
`; + } + return `
${time} ${_ariaEsc(kind)}: ${_ariaEsc(JSON.stringify(p).slice(0, 500))}
`; + } function ariaPanicStop() { if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return; send({ action: 'aria_panic_stop' }); diff --git a/diagnostic/server.js b/diagnostic/server.js index bec7bc3..7719b75 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -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 }));