smlflg's picture
Initial public upload from Projekte/HarnessEcosystem
5134cea verified
Raw History Blame Contribute Delete
26.5 kB
/**
* HarnessEcosystem β€” app.js
* SPA with Hash Routing + Tutorial Pages
*/
let GLOBAL_DATA = null;
const create = (tag, className, text) => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
};
const load = async () => {
const response = await fetch("./data/ecosystem.json");
if (!response.ok) throw new Error("ecosystem.json konnte nicht geladen werden");
return response.json();
};
// ─────────────────────────────────────────────
// HASH ROUTING
// ─────────────────────────────────────────────
const route = () => {
const hash = location.hash.slice(1);
if (!hash || hash === "home") {
showMain();
} else {
showTutorial(hash);
}
updateNavHighlight();
};
const showMain = () => {
document.querySelector("#mainPage").classList.remove("hidden");
document.querySelector("#tutorialPage").classList.add("hidden");
window.scrollTo(0, 0);
};
const showTutorial = (id) => {
if (!GLOBAL_DATA) return;
const harness = GLOBAL_DATA.harnesses.find((h) => h.id === id);
if (!harness) { showMain(); return; }
document.querySelector("#mainPage").classList.add("hidden");
document.querySelector("#tutorialPage").classList.remove("hidden");
document.querySelector("#tutorialPage").innerHTML = renderTutorialHTML(harness, GLOBAL_DATA);
window.scrollTo(0, 0);
};
const updateNavHighlight = () => {
const hash = location.hash.slice(1) || "home";
document.querySelectorAll(".nav-link").forEach((link) => {
link.classList.toggle("active", link.dataset.nav === hash);
});
};
// ─────────────────────────────────────────────
// TUTORIAL PAGE RENDERER
// ─────────────────────────────────────────────
const renderTutorialHTML = (h, data) => {
const t = h.tutorial || {};
const harnessIndex = data.harnesses.findIndex((x) => x.id === h.id);
const prevH = data.harnesses[harnessIndex - 1];
const nextH = data.harnesses[harnessIndex + 1];
let html = `
<div class="tutorial-page">
<!-- Back + Nav -->
<div class="tutorial-topbar">
<a href="#home" class="tutorial-back">← Übersicht</a>
<div class="tutorial-nav">
${prevH ? `<a href="#${prevH.id}" class="tutorial-prev">← ${prevH.name}</a>` : ""}
${nextH ? `<a href="#${nextH.id}" class="tutorial-next">${nextH.name} β†’</a>` : ""}
</div>
</div>
<!-- Hero -->
<header class="tutorial-hero">
<div class="tutorial-hero-badge color-${h.color}">${h.status === "aktiv" ? "Aktiv" : "Bootcamp"}</div>
<h1 class="tutorial-title">${h.name}</h1>
<p class="tutorial-tagline">${h.tagline}</p>
${h.stats ? `
<div class="tutorial-stats">
${h.stats.map(s => `<span class="t-stat"><strong>${s.value}</strong> ${s.label}</span>`).join("")}
</div>` : ""}
</header>
<!-- Tabs -->
<div class="tutorial-tabs">
<button class="t-tab active" data-tab="quickstart">Quick Start</button>
<button class="t-tab" data-tab="architektur">Architektur</button>
<button class="t-tab" data-tab="adhs">ADHS-Patterns</button>
<button class="t-tab" data-tab="config">Config</button>
<button class="t-tab" data-tab="poweruser">Power-User ↑</button>
</div>
<!-- Tab Content -->
<div class="tutorial-content">
`;
// Quick Start Tab
html += `<div class="t-tabcontent active" id="tab-quickstart">`;
if (t.quickStart) {
html += `
<div class="t-section">
<h2>Quick Start</h2>
<ol class="t-steps">
${t.quickStart.map((step, i) => `<li><span class="t-step-num">${i + 1}</span><span class="t-step-text">${step}</span></li>`).join("")}
</ol>
</div>
`;
}
if (t.wellenmodus) {
html += `
<div class="t-section">
<h3>Wellenmodus</h3>
<div class="t-welleneinsatz">
<div class="t-welleneinsatz-badge">${t.wellenmodus.role}</div>
<div class="t-welleneinsatz-meta">
<p><strong>β†’ Eintritt:</strong> ${t.wellenmodus.entry}</p>
<p><strong>β†ͺ Austritt:</strong> ${t.wellenmodus.exit}</p>
</div>
</div>
</div>
`;
}
if (t.handoff) {
html += `
<div class="t-section">
<h3>Handoff</h3>
<div class="t-handoff">
${Object.entries(t.handoff).map(([k, v]) => `
<div class="t-handoff-card">
<span class="t-handoff-target">${k.replace("to", "β†’ ")}</span>
<p>${v}</p>
</div>
`).join("")}
</div>
</div>
`;
}
html += `</div>`;
// Architektur Tab
html += `<div class="t-tabcontent" id="tab-architektur">`;
if (h.architecture_link) {
html += `
<div class="t-section">
<h2>System-Architektur</h2>
<div class="t-diagramm">
<iframe src="${h.architecture_link}" class="t-iframe"></iframe>
</div>
</div>
`;
}
if (t.architektur && t.architektur.annotations) {
html += `
<div class="t-section">
<h3>Key-Komponenten</h3>
<div class="t-annotations">
${t.architektur.annotations.map(a => `
<div class="t-annotation">
<span class="t-ann-badge">${a.component}</span>
<p>${a.description}</p>
</div>
`).join("")}
</div>
</div>
`;
}
html += `</div>`;
// ADHS Tab
html += `<div class="t-tabcontent" id="tab-adhs">`;
if (t.adhsPatterns) {
const patterns = [
{ key: "bodyDoubling", label: "Body Doubling", icon: "πŸ‘₯" },
{ key: "dopaminTrigger", label: "Dopamin-Trigger", icon: "⚑" },
{ key: "antiParalysis", label: "Anti-Paralysis", icon: "🚫" },
{ key: "sessionRhythm", label: "Session-Rhythm", icon: "⏱" },
];
patterns.forEach(p => {
if (t.adhsPatterns[p.key]) {
html += `
<div class="t-section t-adhs-card">
<div class="t-adhs-header">
<span class="t-adhs-icon">${p.icon}</span>
<h3>${p.label}</h3>
</div>
<p>${t.adhsPatterns[p.key]}</p>
</div>
`;
}
});
}
html += `</div>`;
// Config Tab
html += `<div class="t-tabcontent" id="tab-config">`;
if (t.config) {
if (t.config.settingsKeys) {
html += `
<div class="t-section">
<h2>${t.config.settingsHeading || "settings.json Keys"}</h2>
<div class="t-configtable">
${t.config.settingsKeys.map(k => `
<div class="t-configrow">
<code class="t-configkey">${k.key}</code>
<span class="t-configtype">${k.type}</span>
<span class="t-configdesc">${k.description || ""}</span>
${k.default ? `<span class="t-configdefault">Default: ${k.default}</span>` : ""}
</div>
`).join("")}
</div>
</div>
`;
}
if (t.config.hooks) {
html += `
<div class="t-section">
<h3>${t.config.hooksHeading || "Hooks"}</h3>
<div class="t-hooks">
${t.config.hooks.map(hook => `
<div class="t-hook">
<div class="t-hook-header">
<code>${hook.name}</code>
${hook.canBlock ? `<span class="t-hook-badge">blocking</span>` : ""}
</div>
<p>${hook.description || ""}</p>
</div>
`).join("")}
</div>
</div>
`;
}
} else {
html += `<p class="t-muted">Config-Details nach Bootcamp verfΓΌgbar.</p>`;
}
html += `</div>`;
// Power-User Tab
html += `<div class="t-tabcontent" id="tab-poweruser">`;
if (t.powerUser) {
const pu = t.powerUser;
// Level 2 β€” Config Mastery
if (pu.level2) {
html += `<div class="t-section">
<div class="t-level-badge">Level 2 β€” Config Mastery</div>
<h2>${pu.level2.title}</h2>`;
if (pu.level2.configHierarchy) {
html += `<div class="t-annotations">
${pu.level2.configHierarchy.map(item => `
<div class="t-annotation">
<span class="t-ann-badge">${item.file}</span>
<p>${item.role}</p>
</div>`).join("")}
</div>`;
}
if (pu.level2.bashPermissions) {
html += `<div class="t-section">
<h3>Bash Permission Patterns</h3>
<div class="t-configtable">
${pu.level2.bashPermissions.map(p => `
<div class="t-configrow">
<code class="t-configkey">${p.pattern}</code>
<span class="t-configdesc">${p.meaning}</span>
</div>`).join("")}
</div>
</div>`;
}
if (pu.level2.attentionBudget) {
html += `<div class="t-section">
<h3>CLAUDE.md Attention Budget</h3>
<div class="t-annotations">
${pu.level2.attentionBudget.map(rule => `
<div class="t-annotation">
<span class="t-ann-badge">${rule.rule}</span>
<p>${rule.detail}</p>
</div>`).join("")}
</div>
</div>`;
}
html += `</div>`;
}
// Level 3 β€” Subagent & Cost Mastery
if (pu.level3) {
html += `<div class="t-section">
<div class="t-level-badge level3">Level 3 β€” Subagent & Cost Mastery</div>
<h2>${pu.level3.title}</h2>`;
if (pu.level3.costTable) {
html += `<div class="t-configtable">
${pu.level3.costTable.map(r => `
<div class="t-configrow">
<code class="t-configkey">${r.action}</code>
<span class="t-configtype">${r.cost}</span>
<span class="t-configdesc">${r.tip || ""}</span>
</div>`).join("")}
</div>`;
}
if (pu.level3.subagentPatterns) {
html += `<div class="t-hooks">
${pu.level3.subagentPatterns.map(p => `
<div class="t-hook">
<div class="t-hook-header"><code>${p.pattern}</code></div>
<p>${p.description}</p>
</div>`).join("")}
</div>`;
}
html += `</div>`;
}
// Level 4 β€” Extension System
if (pu.level4) {
html += `<div class="t-section">
<div class="t-level-badge level4">Level 4 β€” Extension System</div>
<h2>${pu.level4.title}</h2>`;
if (pu.level4.extensionTypes) {
html += `<div class="t-annotations">
${pu.level4.extensionTypes.map(e => `
<div class="t-annotation">
<span class="t-ann-badge">${e.type}</span>
<p>${e.path} β€” ${e.description}</p>
</div>`).join("")}
</div>`;
}
if (pu.level4.hookDeepDive) {
html += `<div class="t-section">
<h3>Hook Deep-Dive</h3>
<div class="t-hooks">
${pu.level4.hookDeepDive.map(h => `
<div class="t-hook">
<div class="t-hook-header">
<code>${h.name}</code>
${h.blocking ? `<span class="t-hook-badge">blocking</span>` : ""}
${h.knownBug ? `<span class="t-hook-badge" style="background:var(--red);color:#000">bug</span>` : ""}
</div>
<p>${h.detail}</p>
${h.workaround ? `<p><strong>Workaround:</strong> ${h.workaround}</p>` : ""}
</div>`).join("")}
</div>
</div>`;
}
if (pu.level4.activeHooks) {
html += `<div class="t-section">
<h3>Samuels Live-Hooks</h3>
<div class="t-hooks">
${pu.level4.activeHooks.map(h => `
<div class="t-hook">
<div class="t-hook-header"><code>${h.name}</code><span class="t-hook-badge">${h.hook}</span></div>
<p>${h.function}</p>
</div>`).join("")}
</div>
</div>`;
}
html += `</div>`;
}
}
html += `</div>`;
// FAQ Tab (if exists)
if (t.faq) {
html += `
<div class="t-section">
<h2>FAQ</h2>
<div class="t-faq">
${t.faq.map(item => `
<details class="t-faq-item">
<summary>${item.q}</summary>
<p>${item.a}</p>
</details>
`).join("")}
</div>
</div>
`;
}
html += `</div></div>`; // close content + page
return html;
};
// ─────────────────────────────────────────────
// INIT TABS (event delegation)
// ─────────────────────────────────────────────
const initTabs = () => {
document.querySelector("#tutorialPage").addEventListener("click", (e) => {
if (e.target.classList.contains("t-tab")) {
const tabName = e.target.dataset.tab;
document.querySelectorAll(".t-tab").forEach(t => t.classList.remove("active"));
document.querySelectorAll(".t-tabcontent").forEach(c => c.classList.remove("active"));
e.target.classList.add("active");
document.querySelector(`#tab-${tabName}`)?.classList.add("active");
}
});
};
// ─────────────────────────────────────────────
// EXISTING RENDER FUNCTIONS
// ─────────────────────────────────────────────
const renderHero = (data) => {
const { hero } = data;
document.querySelector("#heroTitle").textContent = hero.title;
document.querySelector("#heroSubtitle").textContent = hero.subtitle;
const panel = document.querySelector("#heroPanel");
hero.stats.forEach(({ label, value }) => {
const row = create("div", "summary-stat");
row.append(create("span", "", label));
row.append(create("strong", "", value));
panel.append(row);
});
};
const renderStyleFit = (data) => {
const { style_fit } = data;
document.querySelector("#styleFitKicker").textContent = style_fit.kicker;
document.querySelector("#styleFitHeading").textContent = style_fit.heading;
const table = document.querySelector("#matrixTable");
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
headerRow.append(document.createElement("th"));
style_fit.harnesses.forEach((h) => {
const th = document.createElement("th");
th.textContent = h;
headerRow.append(th);
});
thead.append(headerRow);
table.append(thead);
const tbody = document.createElement("tbody");
style_fit.task_types.forEach((task, rowIndex) => {
const tr = document.createElement("tr");
const tdTask = document.createElement("td");
tdTask.textContent = task;
tr.append(tdTask);
style_fit.matrix[rowIndex].forEach((rating) => {
const td = document.createElement("td");
const badge = create("span", `fit-badge fit-${rating}`, rating);
td.append(badge);
tr.append(td);
});
tbody.append(tr);
});
table.append(tbody);
const host = document.querySelector("#styleFitRecommendations");
style_fit.recommendations.forEach(({ harness, for: forText, color }) => {
const card = create("article", "card");
card.append(create("div", "label", harness));
const bar = create("div", `harness-color-bar color-${color}`);
bar.style.height = "3px";
bar.style.width = "32px";
bar.style.borderRadius = "999px";
bar.style.marginBottom = "10px";
card.append(bar);
card.append(create("p", "supporting", forText));
host.append(card);
});
};
const renderHarnesses = (data) => {
const host = document.querySelector("#harnessCards");
data.harnesses.forEach((harness) => {
const card = create("article", "harness-card");
const body = create("div", "harness-body");
const bar = create("div", `harness-color-bar color-${harness.color}`);
body.append(bar);
const content = create("div", "harness-content");
const header = create("div", "harness-header");
const titleBlock = create("div");
titleBlock.append(create("div", "label", "Harness"));
titleBlock.append(create("h3", "", harness.name));
header.append(titleBlock);
const statusClass = harness.status === "aktiv" ? "status-aktiv" : "status-pending";
header.append(create("span", `harness-status ${statusClass}`, harness.status === "aktiv" ? "Aktiv" : "Bootcamp pending"));
content.append(header);
content.append(create("p", "supporting", harness.tagline));
const details = create("div", "harness-details");
if (harness.data_complete) {
if (harness.stats) {
const statsRow = create("div", "harness-stats-row");
harness.stats.forEach(({ label, value }) => {
const stat = create("div", "harness-stat");
stat.append(create("span", "stat-label", label));
stat.append(create("span", "stat-value", value));
statsRow.append(stat);
});
details.append(statsRow);
}
if (harness.strengths) {
const strengthsBlock = create("div", "harness-strengths");
strengthsBlock.append(create("h4", "", "Staerken"));
const list = create("ul", "bullet-list");
harness.strengths.forEach((s) => list.append(create("li", "", s)));
strengthsBlock.append(list);
details.append(strengthsBlock);
}
if (harness.weaknesses) {
const weaknessBlock = create("div", "harness-weaknesses");
weaknessBlock.append(create("h4", "", "Schwaechen"));
const list = create("ul", "bullet-list");
harness.weaknesses.forEach((w) => list.append(create("li", "", w)));
weaknessBlock.append(list);
details.append(weaknessBlock);
}
if (harness.architecture_link) {
const link = document.createElement("a");
link.className = "arch-link";
link.href = `#${harness.id}`;
link.textContent = "Tutorial ansehen β†’";
link.addEventListener("click", (e) => {
e.stopPropagation();
location.hash = harness.id;
});
details.append(link);
}
} else {
details.append(create("p", "placeholder-note", harness.placeholder));
}
content.append(details);
content.append(create("p", "harness-expand-hint", "Klicken zum Aufklappen"));
body.append(content);
card.append(body);
card.addEventListener("click", () => card.classList.toggle("expanded"));
host.append(card);
});
};
const renderDiagrams = (data) => {
const host = document.querySelector("#diagrams");
data.architecture_diagrams.forEach((diagram, index) => {
const card = document.createElement("a");
card.className = "diagram-card";
card.href = diagram.link;
card.target = "_blank";
card.append(create("div", "label", `Diagramm ${index + 1}`));
card.append(create("h3", "", diagram.name));
card.append(create("p", "supporting", diagram.description));
host.append(card);
});
};
const renderLaws = (data) => {
const host = document.querySelector("#lawCards");
data.laws.forEach((law) => {
const card = create("article", `law-card law-${law.color}`);
const numEl = create("div", "law-number", String(law.number));
card.append(numEl);
const content = create("div", "law-content");
content.append(create("h3", "", law.title));
content.append(create("p", "supporting", law.text));
const beweis = create("p", "law-beweis", law.beweis);
content.append(beweis);
card.append(content);
host.append(card);
});
};
const renderAntiPatterns = (data) => {
const host = document.querySelector("#antiPatternCards");
data.anti_patterns.forEach((ap) => {
const card = create("article", "antipattern-card");
card.append(create("span", `severity severity-${ap.severity}`, ap.severity.toUpperCase()));
card.append(create("h3", "", ap.name));
card.append(create("p", "supporting", ap.description));
const folge = create("p", "antipattern-folge", ap.folge);
card.append(folge);
host.append(card);
});
};
const renderWaveMode = (data) => {
const { wave_mode } = data;
document.querySelector("#waveKicker").textContent = wave_mode.kicker;
document.querySelector("#waveHeading").textContent = wave_mode.heading;
document.querySelector("#waveDescription").textContent = wave_mode.description;
const phasesHost = document.querySelector("#wavePhases");
wave_mode.phases.forEach((phase, index) => {
const phaseEl = create("div", "wave-phase");
const header = create("div", "wave-phase-header");
const num = create("div", `wave-phase-number phase-${phase.color}`, String(index + 1));
header.append(num);
header.append(create("h3", "", phase.name));
phaseEl.append(header);
phaseEl.append(create("p", "supporting", phase.description));
const steps = create("ul", "wave-steps");
phase.steps.forEach((step) => steps.append(create("li", "", step)));
phaseEl.append(steps);
if (index < wave_mode.phases.length - 1) {
const arrow = create("div", "wave-arrow", ">");
phaseEl.append(arrow);
}
phasesHost.append(phaseEl);
});
const checkpointsHost = document.querySelector("#waveCheckpoints");
wave_mode.checkpoints.forEach((cp) => {
const item = create("div", "wave-checkpoint");
item.append(create("div", "checkpoint-dot"));
item.append(create("span", "", cp));
checkpointsHost.append(item);
});
};
const renderCosts = (data) => {
const { costs } = data;
document.querySelector("#costsKicker").textContent = costs.kicker;
document.querySelector("#costsHeading").textContent = costs.heading;
const table = document.querySelector("#costsTable");
const thead = document.createElement("thead");
thead.innerHTML = `<tr><th>Harness</th><th>Modell</th><th>Kosten / Session</th><th>Relativ</th><th>Billing</th><th>Hinweise</th></tr>`;
table.append(thead);
const tbody = document.createElement("tbody");
costs.harnesses.forEach((h) => {
const costBarClass = h.cost_relative >= 80 ? "high" : h.cost_relative >= 40 ? "mid" : "low";
const barWidth = h.cost_relative > 0 ? h.cost_relative : 15;
const row = document.createElement("tr");
row.innerHTML = `
<td>${h.name}</td>
<td class="muted">${h.model}</td>
<td>${h.cost_per_session}</td>
<td><div class="cost-bar-wrap"><div class="cost-bar ${costBarClass}" style="width: ${barWidth}%;"></div></div></td>
<td class="muted">${h.billing}</td>
<td class="muted">${h.notes}</td>
`;
tbody.append(row);
});
table.append(tbody);
};
// ─────────────────────────────────────────────
// DECISION ROUTER
// ─────────────────────────────────────────────
const renderDecisionRouter = (data) => {
const QUICK_TAGS = [
"Architektur planen", "Feature bauen", "Bug fixen", "Tests schreiben",
"Refactoring", "Research", "Code Review", "Neues Projekt", "Debugging",
];
const tagsHost = document.querySelector("#routerTags");
QUICK_TAGS.forEach((tag) => {
const btn = create("button", "router-tag", tag);
btn.addEventListener("click", () => {
document.querySelector("#routerInput").value = tag;
showRouterResult(tag.toLowerCase(), data);
});
tagsHost.append(btn);
});
document.querySelector("#routerSubmit").addEventListener("click", () => {
const val = document.querySelector("#routerInput").value.trim().toLowerCase();
if (val) showRouterResult(val, data);
});
document.querySelector("#routerInput").addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
const val = e.target.value.trim().toLowerCase();
if (val) showRouterResult(val, data);
}
});
};
const showRouterResult = (input, data) => {
const candidates = data.harnesses.filter((h) => h.router_keywords?.length);
let best = candidates[0];
let bestScore = 0;
candidates.forEach((h) => {
const score = h.router_keywords.filter((kw) => input.includes(kw)).length;
if (score > bestScore) { bestScore = score; best = h; }
});
const resultEl = document.querySelector("#routerResult");
resultEl.classList.remove("hidden");
resultEl.scrollIntoView({ behavior: "smooth", block: "nearest" });
resultEl.innerHTML = `
<div class="router-card border-${best.color}">
<span class="router-wave-badge">${best.router_wave}</span>
<h3 class="router-harness-name">${best.name}</h3>
<p class="router-reason">${best.router_reason}</p>
<div class="router-actions">
<a href="#${best.id}" class="router-link">Tutorial β†’</a>
<span class="router-cost">${best.stats?.find((s) => s.label === "Modell")?.value ?? ""}</span>
</div>
</div>
`;
};
// ─────────────────────────────────────────────
// MAIN INIT
// ─────────────────────────────────────────────
load()
.then((data) => {
GLOBAL_DATA = data;
renderHero(data);
renderStyleFit(data);
renderHarnesses(data);
renderDiagrams(data);
renderLaws(data);
renderAntiPatterns(data);
renderWaveMode(data);
renderCosts(data);
renderDecisionRouter(data);
route();
initTabs();
})
.catch((error) => {
document.body.innerHTML = `
<main class="page-shell">
<article class="panel">
<p class="eyebrow">Ladefehler</p>
<h1>Daten konnten nicht geladen werden.</h1>
<p class="lede">${error.message}</p>
</article>
</main>`;
});
window.addEventListener("hashchange", () => {
if (GLOBAL_DATA) route();
});