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:
@@ -357,6 +357,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</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
|
<!-- Sessions + alter Brain-Viewer entfernt — Memories laufen jetzt
|
||||||
komplett ueber den Gehirn-Tab gegen die Vector-DB im aria-brain. -->
|
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;">
|
<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>
|
<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="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">
|
<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
|
<input type="checkbox" id="live-aria-autoscroll" checked style="margin:0;"> Auto-Scroll
|
||||||
</label>
|
</label>
|
||||||
@@ -3185,6 +3217,93 @@
|
|||||||
_ariaMaybeScroll();
|
_ariaMaybeScroll();
|
||||||
} catch (_) {}
|
} 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() {
|
function ariaPanicStop() {
|
||||||
if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return;
|
if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return;
|
||||||
send({ action: 'aria_panic_stop' });
|
send({ action: 'aria_panic_stop' });
|
||||||
|
|||||||
+27
-7
@@ -1773,22 +1773,42 @@ const server = http.createServer((req, res) => {
|
|||||||
return res.end(JSON.stringify({ ok: false, error: e.message }));
|
return res.end(JSON.stringify({ ok: false, error: e.message }));
|
||||||
}
|
}
|
||||||
} else if (req.url.startsWith("/api/agent-stream") && req.method === "GET") {
|
} else if (req.url.startsWith("/api/agent-stream") && req.method === "GET") {
|
||||||
// Tail des persistierten agent_stream.jsonl. Browser-Live-View laedt das
|
// Tail / paginierter Slice des persistierten agent_stream.jsonl.
|
||||||
// beim Tab-Oeffnen damit Reload/Standby keine Events mehr wegschmeisst.
|
// 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 {
|
try {
|
||||||
const u = new URL(req.url, "http://localhost");
|
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;
|
const file = AGENT_STREAM_LOG;
|
||||||
let raw = "";
|
let raw = "";
|
||||||
try { raw = fs.readFileSync(file, "utf-8"); } catch {
|
try { raw = fs.readFileSync(file, "utf-8"); } catch {
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
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 all = raw.split("\n").filter(l => l.trim());
|
||||||
const tail = all.slice(-lines);
|
let slice, page = 1, perPage = 0, pagesTotal = 1;
|
||||||
const parsed = tail.map(l => { try { return JSON.parse(l); } catch { return { _raw: l }; } });
|
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" });
|
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) {
|
} catch (e) {
|
||||||
res.writeHead(500, { "Content-Type": "application/json" });
|
res.writeHead(500, { "Content-Type": "application/json" });
|
||||||
return res.end(JSON.stringify({ ok: false, error: e.message }));
|
return res.end(JSON.stringify({ ok: false, error: e.message }));
|
||||||
|
|||||||
Reference in New Issue
Block a user