feat(diag): Memory-Druckansicht — Strg+P → als PDF

Neuer Button "📄 Drucken / PDF" im Gehirn-Tab oeffnet eine sauber
formatierte Print-View in neuem Tab. Druck-CSS optimiert (page-break-
inside:avoid pro Entry, schwarze Borders fuer Print, Action-Bar wird
versteckt). Aktueller Type+Pinned-Filter wird respektiert.

Browser-eigenes "Als PDF speichern" greift dann — kein Tool noetig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 15:06:45 +02:00
parent 517bc7ca8e
commit 39eec25828
+115
View File
@@ -812,6 +812,7 @@
<h2 style="margin:0;">Memories <button class="info-btn" onclick="showInfo('memories')" title="Hot vs. Cold — wie funktioniert das Gedaechtnis?"></button></h2>
<div>
<button class="btn secondary" onclick="resetBrainFilters();loadBrainMemoryList()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
<button class="btn secondary" onclick="printBrainMemory()" style="padding:4px 10px;font-size:11px;" title="Druckbare Ansicht öffnen — dort dann Strg+P → Als PDF speichern">📄 Drucken / PDF</button>
<button class="btn" onclick="openMemoryModal()" style="padding:4px 10px;font-size:11px;">+ Neu</button>
</div>
</div>
@@ -3583,6 +3584,120 @@
el.innerHTML = (html + extra) || '(Keine bekannten Typen gefunden)';
}
async function printBrainMemory() {
// Aktuellen Filter respektieren, damit Stefan z.B. "nur pinned" drucken kann.
const typeFilter = document.getElementById('brain-filter-type')?.value || '';
const pinnedFilter = document.getElementById('brain-filter-pinned')?.value || 'all';
try {
const params = new URLSearchParams({ limit: '2000' });
if (typeFilter) params.set('type', typeFilter);
const r = await fetch('/api/brain/memory/list?' + params.toString());
if (!r.ok) throw new Error('HTTP ' + r.status);
let items = await r.json();
if (pinnedFilter === 'pinned') items = items.filter(m => m.pinned);
else if (pinnedFilter === 'cold') items = items.filter(m => !m.pinned);
// Items nach Type gruppieren, Reihenfolge aus BRAIN_TYPE_ORDER
const byType = {};
items.forEach(m => { (byType[m.type] = byType[m.type] || []).push(m); });
const knownTypes = BRAIN_TYPE_ORDER.filter(t => byType[t]);
const unknownTypes = Object.keys(byType).filter(t => !BRAIN_TYPE_ORDER.includes(t));
const allTypes = [...knownTypes, ...unknownTypes];
const filterDesc = [
typeFilter ? `Typ: ${BRAIN_TYPE_LABELS[typeFilter] || typeFilter}` : 'alle Typen',
pinnedFilter === 'pinned' ? 'nur pinned' : (pinnedFilter === 'cold' ? 'nur cold' : 'pinned + cold'),
].join(' · ');
const printedAt = new Date().toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' });
const escapeForHtml = (s) => String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const renderItem = (m) => {
const pin = m.pinned ? '📌 ' : '';
const cat = m.category ? `<span class="cat">[${escapeForHtml(m.category)}]</span>` : '';
const tags = (m.tags || []).length
? `<div class="tags">${m.tags.map(t => `<span class="tag">${escapeForHtml(t)}</span>`).join(' ')}</div>`
: '';
return `
<div class="entry">
<div class="entry-title">${pin}<strong>${escapeForHtml(m.title || '(ohne Titel)')}</strong> ${cat}</div>
<div class="entry-content">${escapeForHtml(m.content || '')}</div>
${tags}
</div>`;
};
const sections = allTypes.map(t => {
const label = BRAIN_TYPE_LABELS[t] || t;
const fixed = BRAIN_TYPE_INFO[t]?.fixed ? '<span class="fixed-marker">FEST im System-Prompt</span>' : '';
const entries = byType[t].map(renderItem).join('');
return `
<section class="type-section">
<h2>${escapeForHtml(label)} <span class="count">(${byType[t].length})</span> ${fixed}</h2>
${entries}
</section>`;
}).join('');
const html = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>ARIA Gehirn — Druckansicht (${printedAt})</title>
<style>
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; color: #111; background: #fff; padding: 24px; max-width: 920px; margin: 0 auto; line-height: 1.45; }
header { border-bottom: 2px solid #0096FF; padding-bottom: 10px; margin-bottom: 18px; display: flex; justify-content: space-between; align-items: baseline; gap: 16px; flex-wrap: wrap; }
header h1 { font-size: 22px; margin: 0; color: #0096FF; }
header .meta { font-size: 11px; color: #666; }
.summary { font-size: 12px; color: #444; margin-bottom: 18px; }
.type-section { margin-bottom: 22px; page-break-inside: auto; }
.type-section h2 { font-size: 15px; color: #0096FF; border-bottom: 1px solid #0096FF44; padding-bottom: 4px; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; page-break-after: avoid; }
.type-section h2 .count { color: #888; font-weight: normal; font-size: 12px; margin-left: 6px; }
.fixed-marker { background: #0096FF; color: #fff; font-size: 9px; padding: 2px 6px; border-radius: 3px; vertical-align: middle; margin-left: 8px; letter-spacing: 0.4px; }
.entry { padding: 8px 0; border-bottom: 1px solid #eee; page-break-inside: avoid; }
.entry:last-child { border-bottom: none; }
.entry-title { font-size: 13px; margin-bottom: 4px; }
.entry-title .cat { color: #888; font-size: 10px; font-weight: normal; margin-left: 6px; }
.entry-content { font-size: 12px; color: #222; white-space: pre-wrap; word-wrap: break-word; }
.tags { margin-top: 4px; }
.tag { display: inline-block; background: #f0f0f5; color: #555; font-size: 9px; padding: 1px 5px; border-radius: 8px; margin-right: 3px; }
.actions { position: fixed; top: 12px; right: 12px; }
.actions button { background: #0096FF; color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; }
.empty { color: #888; font-style: italic; padding: 20px 0; }
@media print {
.actions { display: none; }
body { padding: 0; max-width: none; }
header { border-bottom-color: #000; }
.type-section h2 { color: #000; border-bottom-color: #000; }
.type-section h2 { page-break-after: avoid; }
.entry { page-break-inside: avoid; }
.fixed-marker { background: #000; }
}
</style>
</head>
<body>
<div class="actions"><button onclick="window.print()">🖨️ Drucken / als PDF</button></div>
<header>
<h1>ARIA Gehirn — Druckansicht</h1>
<div class="meta">${escapeForHtml(printedAt)}</div>
</header>
<div class="summary">Filter: ${escapeForHtml(filterDesc)} · ${items.length} Eintrag${items.length === 1 ? '' : 'e'}</div>
${sections || '<div class="empty">Keine Eintraege fuer diesen Filter.</div>'}
</body>
</html>`;
const win = window.open('', '_blank');
if (!win) {
alert('Popup blockiert — bitte Popups für Diagnostic erlauben und nochmal klicken.');
return;
}
win.document.open();
win.document.write(html);
win.document.close();
} catch (e) {
alert('Druckansicht konnte nicht geladen werden: ' + e.message);
}
}
function _updateCategoryDatalist(items) {
const dl = document.getElementById('memory-category-suggestions');
if (!dl) return;