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
+119
View File
@@ -357,6 +357,37 @@
</div>
</div>
<!-- ARIA-Stream Archiv-Modal: paginierter Browser fuer den
persistierten agent_stream.jsonl. Page 1 = juengste Eintraege. -->
<div id="aria-archive-modal" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.75);z-index:1100;align-items:center;justify-content:center;padding:24px;" onclick="if(event.target===this) closeAriaStreamModal();">
<div style="background:#0D0D1A;border:1px solid #1E1E2E;border-radius:12px;width:100%;max-width:1100px;height:85vh;display:flex;flex-direction:column;">
<div style="display:flex;align-items:center;padding:12px 14px;border-bottom:1px solid #1E1E2E;gap:8px;flex-wrap:wrap;">
<h2 style="margin:0;color:#FFD60A;font-size:15px;flex:1;">📜 ARIA-Stream Archiv <span id="aria-archive-total" style="color:#8888AA;font-weight:normal;"></span></h2>
<label style="color:#8888AA;font-size:11px;">Pro Seite:</label>
<select id="aria-archive-perpage" onchange="loadAriaArchivePage(1)" style="background:#1A1A2E;color:#E0E0F0;border:1px solid #1E1E2E;border-radius:4px;padding:3px 6px;font-size:11px;">
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="500">500</option>
<option value="1000">1000</option>
</select>
<button class="btn secondary" onclick="loadAriaArchivePage(_ariaArchivePage)" style="padding:3px 10px;font-size:11px;" title="Aktuelle Seite neu laden"></button>
<button class="btn secondary" onclick="closeAriaStreamModal()" style="padding:3px 12px;font-size:11px;">Schliessen</button>
</div>
<div style="display:flex;align-items:center;gap:6px;padding:8px 14px;border-bottom:1px solid #1E1E2E;flex-wrap:wrap;">
<button class="btn secondary" onclick="loadAriaArchivePage(1)" id="aria-arch-first" style="padding:3px 8px;font-size:11px;" title="Juengste Seite">«</button>
<button class="btn secondary" onclick="loadAriaArchivePage(_ariaArchivePage-1)" id="aria-arch-prev" style="padding:3px 8px;font-size:11px;" title="Eine Seite juenger"></button>
<span id="aria-arch-pageinfo" style="color:#8888AA;font-size:11px;min-width:140px;text-align:center;">Seite ? / ?</span>
<button class="btn secondary" onclick="loadAriaArchivePage(_ariaArchivePage+1)" id="aria-arch-next" style="padding:3px 8px;font-size:11px;" title="Eine Seite aelter"></button>
<button class="btn secondary" onclick="loadAriaArchivePage(_ariaArchivePagesTotal)" id="aria-arch-last" style="padding:3px 8px;font-size:11px;" title="Aelteste Seite">»</button>
<span style="flex:1;"></span>
<span style="color:#555570;font-size:10px;">Seite 1 = neueste · höhere Pages = älter</span>
</div>
<div id="aria-archive-list" style="flex:1;overflow-y:auto;background:#040408;font-family:'Courier New',monospace;font-size:11px;line-height:1.4;color:#C0C0D0;padding:6px 12px;">
<div style="color:#555570;font-style:italic;padding:20px;text-align:center;">Lade...</div>
</div>
</div>
</div>
<!-- Sessions + alter Brain-Viewer entfernt — Memories laufen jetzt
komplett ueber den Gehirn-Tab gegen die Vector-DB im aria-brain. -->
@@ -405,6 +436,7 @@
<div id="live-aria-bar" style="display:flex;gap:6px;align-items:center;padding:4px 4px 6px;flex-shrink:0;">
<span id="live-aria-status" style="font-size:11px;color:#8888AA;flex:1;">Idle — warte auf ARIA-Aktivitaet</span>
<button class="btn" onclick="clearAriaLive()" style="padding:4px 12px;font-size:11px;" title="Live-Mitschrift leeren">Leeren</button>
<button class="btn" onclick="openAriaStreamModal()" style="padding:4px 12px;font-size:11px;" title="Komplettes Archiv durchblaettern">📜 Archiv</button>
<label style="font-size:11px;color:#8888AA;display:flex;align-items:center;gap:4px;cursor:pointer;" title="Bei jeder neuen Zeile ans Ende scrollen">
<input type="checkbox" id="live-aria-autoscroll" checked style="margin:0;"> Auto-Scroll
</label>
@@ -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 = '<div style="color:#555570;font-style:italic;padding:20px;text-align:center;">Lade...</div>';
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 = '<div style="color:#555570;font-style:italic;padding:20px;text-align:center;">Keine Eintraege auf dieser Seite.</div>';
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 = `<div style="color:#FF6B6B;padding:20px;">Fehler beim Laden: ${_ariaEsc(e.message)}</div>`;
}
}
function renderArchiveLine(p) {
const t = _ariaTimePrefix(p.ts);
const kind = p.kind || '';
const time = `<span style="color:#777799;">[${t}]</span>`;
if (kind === 'start') {
return `<div style="color:#444460;">━━━ ${t} session start (${_ariaEsc(p.model||'unknown')}) ━━━</div>`;
}
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 `<div style="color:#444460;">━━━ ${t} session end (${_ariaEsc(reason)}${codePart}${errPart}) ━━━</div>`;
}
if (kind === 'text') {
return `<div style="color:#D0D0E0;white-space:pre-wrap;word-break:break-word;">${time} ${_ariaEsc(p.text || '')}</div>`;
}
if (kind === 'thinking') {
return `<div style="color:#888866;font-style:italic;white-space:pre-wrap;word-break:break-word;">${time} 💭 ${_ariaEsc(p.text || '')}</div>`;
}
if (kind === 'tool_use') {
const name = _ariaEsc(p.name || '?');
const inp = _ariaEsc(p.input || '');
const tail = p.inputTruncatedBytes ? `<span style="color:#777799;"> ...(+${p.inputTruncatedBytes} bytes)</span>` : '';
return `<div style="color:#C0C0D0;white-space:pre-wrap;word-break:break-word;">${time} <span style="color:#0096FF;">▶ ${name}</span> <span style="color:#8888AA;">${inp}${tail}</span></div>`;
}
if (kind === 'tool_result') {
const isError = p.isError === true;
const head = isError ? '<span style="color:#FF6B6B;">✗ result (ERROR)</span>' : '<span style="color:#34C759;">✓ result</span>';
const tail = p.truncatedBytes ? `<span style="color:#777799;"> ...(+${p.truncatedBytes} bytes)</span>` : '';
return `<div style="color:#9090A0;">${time} ${head}<div style="white-space:pre-wrap;padding-left:14px;border-left:2px solid #2A2A3E;margin-top:2px;">${_ariaEsc(p.content || '')}${tail}</div></div>`;
}
return `<div style="color:#AAAACC;">${time} <span>${_ariaEsc(kind)}: ${_ariaEsc(JSON.stringify(p).slice(0, 500))}</span></div>`;
}
function ariaPanicStop() {
if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return;
send({ action: 'aria_panic_stop' });
+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 }));