smlflg's picture
Initial public upload from Projekte/AgentProjectMinenraeumer (part 9)
b67a1e9 verified
Raw History Blame Contribute Delete
31.7 kB
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ProjectIntakeTower verstehen</title>
<style>
:root {
--bg: #f6f7f9;
--surface: #ffffff;
--surface-2: #eef2f5;
--text: #17202a;
--muted: #627081;
--line: #d7dee7;
--cyan: #007a8a;
--green: #257a4d;
--amber: #9a6400;
--red: #b42318;
--violet: #6750a4;
--ink: #101828;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
letter-spacing: 0;
}
button, input, select { font: inherit; }
a { color: var(--cyan); text-decoration: none; }
a:hover { text-decoration: underline; }
.app {
min-height: 100vh;
display: grid;
grid-template-columns: minmax(260px, 330px) minmax(0, 1fr);
}
aside {
position: sticky;
top: 0;
height: 100vh;
overflow: auto;
border-right: 1px solid var(--line);
background: #fbfcfd;
padding: 20px;
}
main {
min-width: 0;
padding: 24px;
}
.brand {
display: grid;
gap: 6px;
margin-bottom: 18px;
}
.brand h1 {
margin: 0;
font-size: 22px;
line-height: 1.15;
letter-spacing: 0;
}
.brand p {
margin: 0;
color: var(--muted);
font-size: 13px;
line-height: 1.45;
}
.nav {
display: grid;
gap: 8px;
margin: 18px 0;
}
.nav button, .nav a, .chip, .doc-row, .gain-action {
border: 1px solid var(--line);
background: var(--surface);
color: var(--text);
border-radius: 8px;
cursor: pointer;
}
.nav button, .nav a {
text-align: left;
padding: 10px 12px;
font-size: 14px;
line-height: 1.25;
}
.nav a {
display: block;
text-decoration: none;
}
.nav button.active {
border-color: var(--cyan);
box-shadow: inset 3px 0 0 var(--cyan);
}
.mini-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin: 18px 0;
}
.metric {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
padding: 10px;
min-height: 68px;
}
.metric strong {
display: block;
font-size: 22px;
line-height: 1;
}
.metric span {
display: block;
margin-top: 6px;
color: var(--muted);
font-size: 12px;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(260px, .8fr);
gap: 18px;
align-items: stretch;
margin-bottom: 18px;
}
.panel, .gain, .reader, .doc-list, .project-table, .timeline, .visual-panel {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
}
.panel {
padding: 22px;
}
.panel h2, .section h2 {
margin: 0 0 10px;
font-size: 28px;
line-height: 1.15;
letter-spacing: 0;
}
.panel p, .section p {
color: var(--muted);
line-height: 1.6;
margin: 0;
}
.pattern {
margin-top: 18px;
padding: 12px;
border: 1px solid #c8d9df;
border-radius: 8px;
background: #eef8fa;
color: #0f5662;
font-weight: 700;
word-break: break-word;
}
.overview-gains {
display: grid;
gap: 8px;
margin-top: 16px;
border-top: 1px solid var(--line);
padding-top: 14px;
}
.overview-gain {
display: grid;
grid-template-columns: 150px minmax(0, 1fr);
gap: 10px;
font-size: 13px;
line-height: 1.45;
}
.overview-gain strong {
color: var(--ink);
}
.overview-gain span {
color: var(--muted);
}
.split-cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-top: 16px;
}
.split-card {
border: 1px solid var(--line);
border-radius: 8px;
padding: 12px;
background: #fbfcfd;
display: grid;
gap: 8px;
align-content: start;
}
.split-card strong {
font-size: 14px;
color: var(--ink);
}
.split-card span {
color: var(--muted);
font-size: 13px;
line-height: 1.45;
}
.split-card.core { border-color: #add8bf; }
.split-card.safety { border-color: #c8d9df; }
.split-card.raw { border-color: #e8cc91; }
.visual-panel {
overflow: hidden;
min-height: 260px;
display: grid;
align-content: start;
}
.visual-panel img {
width: 100%;
display: block;
aspect-ratio: 4 / 3;
object-fit: cover;
object-position: top;
border-bottom: 1px solid var(--line);
}
.visual-panel div {
padding: 14px;
color: var(--muted);
font-size: 13px;
line-height: 1.5;
}
.section {
margin: 22px 0;
}
.gains {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.gain {
padding: 16px;
display: grid;
gap: 12px;
align-content: start;
}
.gain h3 {
margin: 0;
font-size: 18px;
}
.gain p {
margin: 0;
color: var(--muted);
line-height: 1.55;
font-size: 14px;
}
.gain ul {
margin: 0;
padding-left: 18px;
color: var(--muted);
font-size: 13px;
line-height: 1.5;
}
.gain-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.gain-action {
padding: 6px 8px;
font-size: 12px;
color: var(--cyan);
}
.timeline {
padding: 14px;
display: grid;
gap: 8px;
}
.stream-note {
margin: 0 0 12px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fbfcfd;
color: var(--muted);
line-height: 1.55;
}
.digest-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
align-items: start;
}
.digest-card {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
overflow: hidden;
}
.digest-card h3 {
margin: 0;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
font-size: 16px;
background: #fbfcfd;
}
.digest-list {
display: grid;
}
.digest-row {
display: grid;
gap: 4px;
padding: 10px 14px;
border-bottom: 1px solid var(--line);
}
.digest-row:last-child {
border-bottom: 0;
}
.digest-row strong {
font-size: 13px;
line-height: 1.3;
}
.digest-row span {
color: var(--muted);
font-size: 12px;
line-height: 1.35;
overflow-wrap: anywhere;
}
.digest-card.wide {
grid-column: 1 / -1;
}
.cluster-members {
color: var(--muted);
font-size: 12px;
line-height: 1.45;
}
.step {
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
gap: 10px;
align-items: start;
}
.step-index {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--ink);
color: white;
display: grid;
place-items: center;
font-size: 12px;
font-weight: 700;
}
.step button {
width: 100%;
border: 1px solid var(--line);
background: var(--surface-2);
border-radius: 8px;
padding: 9px 10px;
text-align: left;
cursor: pointer;
color: var(--text);
}
.workspace {
display: grid;
grid-template-columns: minmax(280px, 420px) minmax(0, 1fr);
gap: 14px;
align-items: start;
}
.toolbar {
display: grid;
gap: 10px;
margin-bottom: 10px;
}
.toolbar input, .toolbar select {
width: 100%;
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px 12px;
background: var(--surface);
color: var(--text);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.chip {
padding: 6px 9px;
font-size: 12px;
}
.chip.active {
border-color: var(--cyan);
color: var(--cyan);
font-weight: 700;
}
.doc-list {
overflow: hidden;
}
.doc-list-header {
padding: 10px 12px;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 12px;
display: flex;
justify-content: space-between;
gap: 8px;
}
.doc-results {
max-height: 72vh;
overflow: auto;
}
.doc-row {
width: 100%;
display: grid;
gap: 4px;
text-align: left;
padding: 10px 12px;
border: 0;
border-bottom: 1px solid var(--line);
border-radius: 0;
}
.doc-row:hover, .doc-row.active {
background: #eef8fa;
}
.doc-row strong {
font-size: 13px;
line-height: 1.3;
}
.doc-row span {
color: var(--muted);
font-size: 11px;
line-height: 1.35;
overflow-wrap: anywhere;
}
.reader {
min-width: 0;
overflow: hidden;
}
.reader-head {
display: grid;
gap: 8px;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
}
.reader-title {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: start;
}
.reader-title h3 {
margin: 0;
font-size: 18px;
line-height: 1.25;
}
.reader-meta {
color: var(--muted);
font-size: 12px;
overflow-wrap: anywhere;
}
.reader-body {
padding: 18px;
max-height: 72vh;
overflow: auto;
line-height: 1.6;
}
.reader-body pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
margin: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-size: 13px;
}
.reader-body h1, .reader-body h2, .reader-body h3 {
line-height: 1.25;
margin: 1em 0 .4em;
}
.reader-body p {
margin: .45em 0;
}
.reader-body code {
background: #eef2f5;
padding: 1px 4px;
border-radius: 4px;
font-size: .95em;
}
.reader-body table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.reader-body th, .reader-body td {
border: 1px solid var(--line);
padding: 6px;
vertical-align: top;
}
.project-table {
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 10px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
font-size: 13px;
}
th {
color: var(--muted);
background: #fbfcfd;
position: sticky;
top: 0;
}
.badge {
display: inline-block;
padding: 2px 6px;
border-radius: 999px;
border: 1px solid var(--line);
color: var(--muted);
font-size: 11px;
margin: 0 3px 3px 0;
white-space: nowrap;
}
.badge.urgent { color: var(--red); border-color: #f2b8b5; }
.badge.keep { color: var(--green); border-color: #add8bf; }
.badge.merge { color: var(--violet); border-color: #c9bee8; }
.badge.human { color: var(--amber); border-color: #e8cc91; }
.empty {
color: var(--muted);
padding: 18px;
text-align: center;
}
@media (max-width: 980px) {
.app { grid-template-columns: 1fr; }
aside { position: static; height: auto; }
.nav { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.nav button { padding: 8px 10px; }
.mini-stats { display: none; }
.hero, .workspace, .gains, .split-cards, .digest-grid { grid-template-columns: 1fr; }
.digest-card.wide { grid-column: auto; }
main { padding: 16px; }
}
</style>
</head>
<body>
<div class="app">
<aside>
<div class="brand">
<h1>ProjectIntakeTower verstehen</h1>
<p id="generatedAt"></p>
</div>
<div class="nav">
<button data-section="overview" class="active">Ueberblick</button>
<a href="portfolio-map.html">Portfolio Map</a>
<button data-section="digest">Verdichtung</button>
<button data-section="core">Core</button>
<button data-section="safety">Safety</button>
<button data-section="recommended">Map + 4</button>
<button data-section="documents">Dokumente</button>
<button data-section="projects">Projekte & Gates</button>
</div>
<div class="mini-stats" id="miniStats"></div>
</aside>
<main>
<section class="hero" data-view="overview">
<div class="panel">
<h2>Was gestern greifbar wurde</h2>
<p id="overviewText"></p>
<div class="pattern" id="pattern"></div>
<div class="split-cards" id="workstreamCards"></div>
</div>
<div class="visual-panel">
<img id="visualAsset" alt="Project learning map">
<div>Die vorhandene Lern-Grafik bleibt als visuelle Landkarte erhalten. Die neue Website macht darunter jedes Textartefakt direkt aufrufbar.</div>
</div>
</section>
<section class="section" data-view="core">
<h2>Core: echte Projektlandkarte</h2>
<p>Das ist der Teil, der wirklich Wert erzeugt hat: Inventar, Triage, Top-10-Actions, Job-Contracts, Verifier, Visual Summary und Owner Digest.</p>
<div class="timeline" id="coreDocs"></div>
</section>
<section class="section" data-view="digest">
<h2>Verdichtung: worin der Mehrwert liegt</h2>
<p>Nicht die einzelnen <code>jobs/pending/*.json</code> sind der Hauptwert, sondern diese zusammengezogene Projektkarte.</p>
<div class="digest-grid" id="digestGrid"></div>
</section>
<section class="section" data-view="safety">
<h2>Safety: Kaskadenkontrolle</h2>
<p>Das ist nicht normale Produktarbeit, sondern Betriebsunfall-Management. Es bleibt wertvoll, aber es gehoert in eine andere mentale Schublade.</p>
<div class="timeline" id="safetyDocs"></div>
<div class="stream-note">Safety-Pattern: Chaos Power Mode -> Stop-Loss -> Containment -> Morning Resume.</div>
<div class="gains" id="gains"></div>
</section>
<section class="section" data-view="recommended">
<h2>Portfolio-Map plus 4 Stuetzen</h2>
<p>Nicht mit <code>triage/HUMAN_DECISION_REQUIRED.md</code> starten. Lies zuerst die Portfolio-Map, dann die vier Dokumente, die sie tragen.</p>
<div class="timeline" id="readingPath"></div>
</section>
<section class="section" data-view="documents">
<h2>Dokumente</h2>
<div class="workspace">
<div>
<div class="toolbar">
<input id="searchInput" type="search" placeholder="Suche in allen Textdokumenten">
<select id="groupSelect"></select>
<div class="chips" id="quickChips"></div>
</div>
<div class="doc-list">
<div class="doc-list-header"><span id="resultCount"></span><span id="activeFilter"></span></div>
<div class="doc-results" id="docResults"></div>
</div>
</div>
<article class="reader">
<div class="reader-head">
<div class="reader-title">
<h3 id="readerTitle"></h3>
<a id="readerLink" href="" target="_blank" rel="noreferrer">Original</a>
</div>
<div class="reader-meta" id="readerMeta"></div>
</div>
<div class="reader-body" id="readerBody"></div>
</article>
</div>
</section>
<section class="section" data-view="projects">
<h2>Projekte & Human Gates</h2>
<p>Die Tabelle zeigt die wichtigsten Projektknoten aus der vorhandenen Priorisierung. Externe Projektdateien werden hier nur referenziert.</p>
<div class="project-table">
<table>
<thead><tr><th>Projekt</th><th>Score</th><th>Status</th><th>Warum</th></tr></thead>
<tbody id="projectRows"></tbody>
</table>
</div>
</section>
</main>
</div>
<script src="site-data.js"></script>
<script>
const data = window.PROJECT_UNDERSTANDING_DATA;
const byPath = new Map(data.documents.map(doc => [doc.path, doc]));
let currentGroup = "all";
let currentStream = "all";
let currentQuery = "";
let activeDoc = data.documents[0];
const escapeHtml = value => String(value ?? "").replace(/[&<>"']/g, ch => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;"
}[ch]));
function compactPath(path) {
return path.replace("ProjectIntakeTower/", "PIT/");
}
function formatBytes(bytes) {
if (bytes > 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + " MB";
if (bytes > 1024) return Math.round(bytes / 1024) + " KB";
return bytes + " B";
}
function showSection(name) {
document.querySelectorAll("[data-view]").forEach(el => {
el.style.display = el.dataset.view === name ? "" : "none";
});
document.querySelectorAll(".nav button").forEach(btn => {
btn.classList.toggle("active", btn.dataset.section === name);
});
}
function renderMarkdown(content) {
const lines = String(content || "").split(/\r?\n/);
let html = "";
let inCode = false;
let code = [];
let inList = false;
let table = [];
const flushList = () => {
if (inList) { html += "</ul>"; inList = false; }
};
const flushTable = () => {
if (!table.length) return;
html += "<table><tbody>";
for (const row of table) {
const cells = row.split("|").slice(1, -1);
if (!cells.length) continue;
const isSep = cells.every(cell => /^:?-{3,}:?$/.test(cell.trim()));
if (isSep) continue;
html += "<tr>" + cells.map(cell => "<td>" + inlineMarkdown(cell.trim()) + "</td>").join("") + "</tr>";
}
html += "</tbody></table>";
table = [];
};
const flushCode = () => {
if (inCode) {
html += "<pre>" + escapeHtml(code.join("\n")) + "</pre>";
code = [];
inCode = false;
}
};
for (const line of lines) {
if (line.startsWith("```")) {
if (inCode) flushCode();
else { flushList(); flushTable(); inCode = true; code = []; }
continue;
}
if (inCode) { code.push(line); continue; }
if (/^\|.*\|$/.test(line)) {
flushList();
table.push(line);
continue;
}
flushTable();
const heading = line.match(/^(#{1,4})\s+(.*)$/);
if (heading) {
flushList();
const level = Math.min(heading[1].length + 1, 4);
html += `<h${level}>${inlineMarkdown(heading[2])}</h${level}>`;
continue;
}
const bullet = line.match(/^\s*[-*]\s+(.*)$/);
if (bullet) {
if (!inList) { html += "<ul>"; inList = true; }
html += "<li>" + inlineMarkdown(bullet[1]) + "</li>";
continue;
}
if (!line.trim()) {
flushList();
html += "";
continue;
}
flushList();
html += "<p>" + inlineMarkdown(line) + "</p>";
}
flushCode();
flushList();
flushTable();
return html;
}
function inlineMarkdown(text) {
return escapeHtml(text)
.replace(/`([^`]+)`/g, "<code>$1</code>")
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
}
function renderReader(doc) {
activeDoc = doc;
document.getElementById("readerTitle").textContent = doc.title;
document.getElementById("readerMeta").textContent =
`${doc.path} - ${doc.workstream} - ${doc.group} - ${doc.kind} - ${formatBytes(doc.size)}`;
const link = document.getElementById("readerLink");
if (doc.href) {
link.style.display = "";
link.href = doc.href;
} else {
link.style.display = "none";
}
const body = document.getElementById("readerBody");
if (doc.kind === "md" || doc.kind === "generated") {
body.innerHTML = renderMarkdown(doc.content);
} else {
body.innerHTML = "<pre>" + escapeHtml(doc.content) + "</pre>";
}
document.querySelectorAll(".doc-row").forEach(row => row.classList.toggle("active", row.dataset.path === doc.path));
history.replaceState(null, "", "#doc=" + encodeURIComponent(doc.path));
}
function scoreDoc(doc, query) {
if (!query) return doc.priority;
const q = query.toLowerCase();
const title = doc.title.toLowerCase();
const path = doc.path.toLowerCase();
const content = doc.content.toLowerCase();
let score = doc.priority;
if (title.includes(q)) score += 800;
if (path.includes(q)) score += 500;
if (content.includes(q)) score += 250;
return score;
}
function filterDocuments() {
const query = currentQuery.trim().toLowerCase();
let docs = data.documents.filter(doc =>
(currentGroup === "all" || doc.group === currentGroup) &&
(currentStream === "all" || doc.workstream === currentStream)
);
if (query) {
docs = docs.filter(doc =>
doc.title.toLowerCase().includes(query) ||
doc.path.toLowerCase().includes(query) ||
doc.content.toLowerCase().includes(query)
);
}
docs.sort((a, b) => scoreDoc(b, query) - scoreDoc(a, query) || a.path.localeCompare(b.path));
return docs;
}
function renderDocList() {
const docs = filterDocuments();
const list = document.getElementById("docResults");
document.getElementById("resultCount").textContent = `${docs.length} Treffer`;
document.getElementById("activeFilter").textContent =
`${currentStream === "all" ? "alle Streams" : currentStream} - ${currentGroup === "all" ? "alle Gruppen" : currentGroup}`;
list.innerHTML = docs.map(doc => `
<button class="doc-row ${activeDoc && activeDoc.path === doc.path ? "active" : ""}" data-path="${escapeHtml(doc.path)}">
<strong>${escapeHtml(doc.title)}</strong>
<span>${escapeHtml(compactPath(doc.path))}</span>
<span>${escapeHtml(doc.workstream)} - ${escapeHtml(doc.group)} - ${escapeHtml(doc.kind)} - ${formatBytes(doc.size)}</span>
</button>
`).join("") || '<div class="empty">Keine Treffer</div>';
list.querySelectorAll(".doc-row").forEach(row => {
row.addEventListener("click", () => {
const doc = byPath.get(row.dataset.path);
if (doc) renderReader(doc);
});
});
}
function renderStats() {
const c = data.counts;
const metrics = [
["Textdokumente", c.documents],
["Projekte", c.total_projects],
["Core", c.coreDocuments],
["Safety", c.safetyDocuments],
["Raw", c.rawDocuments],
["Urgent", c.URGENT],
];
document.getElementById("miniStats").innerHTML = metrics.map(([label, value]) =>
`<div class="metric"><strong>${escapeHtml(value ?? "-")}</strong><span>${escapeHtml(label)}</span></div>`
).join("");
}
function renderGains() {
document.getElementById("gains").innerHTML = data.gains.map(gain => `
<article class="gain">
<h3>${escapeHtml(gain.title)}</h3>
<p><strong>${escapeHtml(gain.claim)}</strong></p>
<p>${escapeHtml(gain.why)}</p>
<ul>${gain.reuseIdeas.map(item => `<li>${escapeHtml(item)}</li>`).join("")}</ul>
<div class="gain-actions">
${gain.evidencePaths.map(path => `<button class="gain-action" data-open="${escapeHtml(path)}">${escapeHtml(compactPath(path))}</button>`).join("")}
</div>
</article>
`).join("");
document.querySelectorAll(".gain-action").forEach(btn => {
btn.addEventListener("click", () => {
openDocPath(btn.dataset.open);
showSection("documents");
});
});
}
function renderWorkstreams() {
document.getElementById("workstreamCards").innerHTML = data.workstreams.map(stream => `
<article class="split-card ${escapeHtml(stream.id.toLowerCase())}">
<strong>${escapeHtml(stream.title)}</strong>
<span>${escapeHtml(stream.claim)}</span>
<span>${escapeHtml(stream.meaning)}</span>
</article>
`).join("");
}
function renderDocTimeline(targetId, paths) {
document.getElementById(targetId).innerHTML = paths.map((path, index) => {
const doc = byPath.get(path);
const title = doc ? doc.title : path;
return `<div class="step"><div class="step-index">${index + 1}</div><button data-open="${escapeHtml(path)}"><strong>${escapeHtml(title)}</strong><br><span>${escapeHtml(compactPath(path))}</span></button></div>`;
}).join("");
document.querySelectorAll(`#${targetId} button`).forEach(btn => {
btn.addEventListener("click", () => {
openDocPath(btn.dataset.open);
showSection("documents");
});
});
}
function renderReadingPath() {
renderDocTimeline("readingPath", data.recommendedReading);
renderDocTimeline("coreDocs", data.coreDocuments);
renderDocTimeline("safetyDocs", data.safetyDocuments);
}
function renderProjectDigestRows(items, mode) {
return items.map(item => {
const cats = (item.categories || []).join(", ");
const stats = `score ${item.score ?? "-"} | dirty ${item.dirty_count ?? 0} | todo ${item.todo_fixme_count ?? 0}`;
const detail = mode === "agent" && item.suggested_run
? `${item.suggested_run}${item.human_gate ? " | human gate" : ""}`
: (item.reasons || []).slice(0, 2).join(" | ");
return `<div class="digest-row">
<strong>${escapeHtml(item.name || item.path)}</strong>
<span>${escapeHtml(compactPath(item.path || ""))}</span>
<span>${escapeHtml(stats)}${cats ? " | " + escapeHtml(cats) : ""}</span>
<span>${escapeHtml(detail || "")}</span>
</div>`;
}).join("") || '<div class="empty">Keine Treffer</div>';
}
function renderClusters(clusters) {
return clusters.map(cluster => `
<div class="digest-row">
<strong>${escapeHtml(cluster.name)} (${cluster.count})</strong>
<span class="cluster-members">${cluster.members.map(m => escapeHtml(m.name || m.path)).join(", ")}</span>
</div>
`).join("") || '<div class="empty">Keine Cluster</div>';
}
function renderDigest() {
const c = data.condensation;
document.getElementById("digestGrid").innerHTML = `
<article class="digest-card">
<h3>Wirklich aktiv</h3>
<div class="digest-list">${renderProjectDigestRows(c.activeProjects)}</div>
</article>
<article class="digest-card">
<h3>Alte Energie</h3>
<div class="digest-list">${renderProjectDigestRows(c.oldEnergy)}</div>
</article>
<article class="digest-card">
<h3>Gefaehrliche dirty states</h3>
<div class="digest-list">${renderProjectDigestRows(c.dirtyDanger)}</div>
</article>
<article class="digest-card">
<h3>Gehoert zusammen</h3>
<div class="digest-list">${renderClusters(c.clusters)}</div>
</article>
<article class="digest-card wide">
<h3>Naechster echter Agentenlauf</h3>
<div class="digest-list">${renderProjectDigestRows(c.nextAgentRuns, "agent")}</div>
</article>
`;
}
function badgeClass(cat) {
const lower = String(cat).toLowerCase();
if (lower.includes("urgent")) return "urgent";
if (lower.includes("keep")) return "keep";
if (lower.includes("merge")) return "merge";
if (lower.includes("human")) return "human";
return "";
}
function renderProjects() {
document.getElementById("projectRows").innerHTML = data.projects.slice(0, 80).map(project => `
<tr>
<td><strong>${escapeHtml(project.name || project.path)}</strong><br><span class="reader-meta">${escapeHtml(project.path || "")}</span></td>
<td>${escapeHtml(project.score ?? "-")}</td>
<td>${(project.categories || []).map(cat => `<span class="badge ${badgeClass(cat)}">${escapeHtml(cat)}</span>`).join("")}</td>
<td>${(project.reasons || project.score_reasons || []).slice(0, 3).map(reason => escapeHtml(reason)).join("<br>")}</td>
</tr>
`).join("");
}
function renderFilters() {
const groups = Array.from(new Set(data.documents.map(doc => doc.group))).sort();
document.getElementById("groupSelect").innerHTML =
'<option value="all">Alle Gruppen</option>' + groups.map(group => `<option value="${escapeHtml(group)}">${escapeHtml(group)}</option>`).join("");
document.getElementById("quickChips").innerHTML = ["Core", "Safety", "Raw", "Guide"].map(stream =>
`<button class="chip" data-stream="${escapeHtml(stream)}">${escapeHtml(stream)}</button>`
).join("");
document.getElementById("groupSelect").addEventListener("change", event => {
currentGroup = event.target.value;
updateChips();
renderDocList();
});
document.querySelectorAll(".chip").forEach(chip => {
chip.addEventListener("click", () => {
currentStream = currentStream === chip.dataset.stream ? "all" : chip.dataset.stream;
updateChips();
renderDocList();
});
});
document.getElementById("searchInput").addEventListener("input", event => {
currentQuery = event.target.value;
renderDocList();
});
}
function updateChips() {
document.querySelectorAll(".chip").forEach(chip => chip.classList.toggle("active", chip.dataset.stream === currentStream));
}
function openDocPath(path) {
const doc = byPath.get(path);
if (doc) {
renderReader(doc);
renderDocList();
}
}
function init() {
document.getElementById("generatedAt").textContent = `Generiert: ${data.generatedAt}`;
document.getElementById("overviewText").textContent = data.narrativeSections.map(s => s.body).join(" ");
document.getElementById("pattern").textContent = data.pattern;
document.getElementById("visualAsset").src = data.visualAsset;
document.querySelectorAll(".nav button").forEach(btn => btn.addEventListener("click", () => showSection(btn.dataset.section)));
renderStats();
renderWorkstreams();
renderDigest();
renderGains();
renderReadingPath();
renderFilters();
renderProjects();
const hashDoc = decodeURIComponent((location.hash.match(/doc=([^&]+)/) || [])[1] || "");
const hasHashDoc = Boolean(hashDoc && byPath.has(hashDoc));
activeDoc = byPath.get(hashDoc) || byPath.get("__generated__/DREI_GEWINNE.md") || data.documents[0];
renderDocList();
renderReader(activeDoc);
showSection(hasHashDoc ? "documents" : "overview");
}
init();
</script>
</body>
</html>