Download app.js from smlflg/HarnessEcosystem: direct link, hf CLI and curl.
- Browser
- Download file 26.5 kB
-
https://huggingface.co/smlflg/HarnessEcosystem/resolve/main/app.js
- Command line
-
hf download hf://smlflg/HarnessEcosystem/app.js
-
curl -L -o app.js https://huggingface.co/smlflg/HarnessEcosystem/resolve/main/app.js
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(); | |
| }); | |