/** * 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 = `
← Übersicht
${prevH ? `← ${prevH.name}` : ""} ${nextH ? `${nextH.name} →` : ""}
${h.status === "aktiv" ? "Aktiv" : "Bootcamp"}

${h.name}

${h.tagline}

${h.stats ? `
${h.stats.map(s => `${s.value} ${s.label}`).join("")}
` : ""}
`; // Quick Start Tab html += `
`; if (t.quickStart) { html += `

Quick Start

    ${t.quickStart.map((step, i) => `
  1. ${i + 1}${step}
  2. `).join("")}
`; } if (t.wellenmodus) { html += `

Wellenmodus

${t.wellenmodus.role}

→ Eintritt: ${t.wellenmodus.entry}

↪ Austritt: ${t.wellenmodus.exit}

`; } if (t.handoff) { html += `

Handoff

${Object.entries(t.handoff).map(([k, v]) => `
${k.replace("to", "→ ")}

${v}

`).join("")}
`; } html += `
`; // Architektur Tab html += `
`; if (h.architecture_link) { html += `

System-Architektur

`; } if (t.architektur && t.architektur.annotations) { html += `

Key-Komponenten

${t.architektur.annotations.map(a => `
${a.component}

${a.description}

`).join("")}
`; } html += `
`; // ADHS Tab html += `
`; 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 += `
${p.icon}

${p.label}

${t.adhsPatterns[p.key]}

`; } }); } html += `
`; // Config Tab html += `
`; if (t.config) { if (t.config.settingsKeys) { html += `

${t.config.settingsHeading || "settings.json Keys"}

${t.config.settingsKeys.map(k => `
${k.key} ${k.type} ${k.description || ""} ${k.default ? `Default: ${k.default}` : ""}
`).join("")}
`; } if (t.config.hooks) { html += `

${t.config.hooksHeading || "Hooks"}

${t.config.hooks.map(hook => `
${hook.name} ${hook.canBlock ? `blocking` : ""}

${hook.description || ""}

`).join("")}
`; } } else { html += `

Config-Details nach Bootcamp verfügbar.

`; } html += `
`; // Power-User Tab html += `
`; if (t.powerUser) { const pu = t.powerUser; // Level 2 — Config Mastery if (pu.level2) { html += `
Level 2 — Config Mastery

${pu.level2.title}

`; if (pu.level2.configHierarchy) { html += `
${pu.level2.configHierarchy.map(item => `
${item.file}

${item.role}

`).join("")}
`; } if (pu.level2.bashPermissions) { html += `

Bash Permission Patterns

${pu.level2.bashPermissions.map(p => `
${p.pattern} ${p.meaning}
`).join("")}
`; } if (pu.level2.attentionBudget) { html += `

CLAUDE.md Attention Budget

${pu.level2.attentionBudget.map(rule => `
${rule.rule}

${rule.detail}

`).join("")}
`; } html += `
`; } // Level 3 — Subagent & Cost Mastery if (pu.level3) { html += `
Level 3 — Subagent & Cost Mastery

${pu.level3.title}

`; if (pu.level3.costTable) { html += `
${pu.level3.costTable.map(r => `
${r.action} ${r.cost} ${r.tip || ""}
`).join("")}
`; } if (pu.level3.subagentPatterns) { html += `
${pu.level3.subagentPatterns.map(p => `
${p.pattern}

${p.description}

`).join("")}
`; } html += `
`; } // Level 4 — Extension System if (pu.level4) { html += `
Level 4 — Extension System

${pu.level4.title}

`; if (pu.level4.extensionTypes) { html += `
${pu.level4.extensionTypes.map(e => `
${e.type}

${e.path} — ${e.description}

`).join("")}
`; } if (pu.level4.hookDeepDive) { html += `

Hook Deep-Dive

${pu.level4.hookDeepDive.map(h => `
${h.name} ${h.blocking ? `blocking` : ""} ${h.knownBug ? `bug` : ""}

${h.detail}

${h.workaround ? `

Workaround: ${h.workaround}

` : ""}
`).join("")}
`; } if (pu.level4.activeHooks) { html += `

Samuels Live-Hooks

${pu.level4.activeHooks.map(h => `
${h.name}${h.hook}

${h.function}

`).join("")}
`; } html += `
`; } } html += `
`; // FAQ Tab (if exists) if (t.faq) { html += `

FAQ

${t.faq.map(item => `
${item.q}

${item.a}

`).join("")}
`; } html += `
`; // 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 = `HarnessModellKosten / SessionRelativBillingHinweise`; 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 = ` ${h.name} ${h.model} ${h.cost_per_session}
${h.billing} ${h.notes} `; 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 = `
${best.router_wave}

${best.name}

${best.router_reason}

Tutorial → ${best.stats?.find((s) => s.label === "Modell")?.value ?? ""}
`; }; // ───────────────────────────────────────────── // 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 = `

Ladefehler

Daten konnten nicht geladen werden.

${error.message}

`; }); window.addEventListener("hashchange", () => { if (GLOBAL_DATA) route(); });