| const formatHours = (value) => `${Number(value).toFixed(1)} h`; |
|
|
| 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/analysis.json"); |
| if (!response.ok) throw new Error("analysis.json konnte nicht geladen werden"); |
| return response.json(); |
| }; |
|
|
| const renderHero = (data) => { |
| document.querySelector("#headline").textContent = data.executive_summary.headline; |
| const panel = document.querySelector("#heroPanel"); |
| const stats = [ |
| ["Best Estimate", formatHours(data.work_estimate.best_estimate_hours)], |
| ["Aktive Projekte", String(data.work_estimate.active_project_count)], |
| ["Top Empfehlung", data.rankings.top_recommendation.name], |
| ["Analysefenster", `${data.meta.window_start} bis ${data.meta.window_end}`], |
| ]; |
| stats.forEach(([label, value]) => { |
| const row = create("div", "summary-stat"); |
| row.append(create("span", "", label)); |
| row.append(create("strong", "", value)); |
| panel.append(row); |
| }); |
| }; |
|
|
| const renderFindings = (data) => { |
| const host = document.querySelector("#findingCards"); |
| data.executive_summary.key_findings.forEach((finding, index) => { |
| const card = create("article", "card"); |
| card.append(create("div", "label", `Antwort ${index + 1}`)); |
| card.append(create("p", "supporting", finding)); |
| host.append(card); |
| }); |
| }; |
|
|
| const renderWork = (data) => { |
| const host = document.querySelector("#workCards"); |
| const cards = [ |
| ["Best Estimate", formatHours(data.work_estimate.best_estimate_hours), "Zentralwert aus Session-Bloecken, Commit-Spans und Artefakt-Bonus."], |
| ["Untergrenze", formatHours(data.work_estimate.lower_bound_hours), "Konservative Sicht: nur ein Teil der Session-Zeit zaehlt als aktive Build-Zeit."], |
| ["Obergrenze", formatHours(data.work_estimate.upper_bound_hours), "Erweitert um stille Vor-/Nacharbeit, verworfene Versuche und uncommitted Arbeit."], |
| ]; |
| cards.forEach(([label, metric, support]) => { |
| const card = create("article", "card"); |
| card.append(create("div", "label", label)); |
| card.append(create("p", "metric", metric)); |
| card.append(create("p", "supporting", support)); |
| host.append(card); |
| }); |
|
|
| const methodList = document.querySelector("#methodList"); |
| data.work_estimate.method.forEach((line) => methodList.append(create("li", "", line))); |
| const uncertaintyList = document.querySelector("#uncertaintyList"); |
| data.work_estimate.uncertainties.forEach((line) => uncertaintyList.append(create("li", "", line))); |
| }; |
|
|
| const renderWorkflow = (data) => { |
| const workflow = data.workflow_patterns; |
| const narrative = document.querySelector("#workflowNarrative"); |
| [workflow.startup_pattern, workflow.iteration_pattern, workflow.parallel_vs_serial.evidence].forEach((line) => { |
| narrative.append(create("p", "", line)); |
| }); |
|
|
| const total = workflow.prototyping_vs_refinement.prototype_signals + workflow.prototyping_vs_refinement.refinement_signals; |
| const protoPct = total ? (workflow.prototyping_vs_refinement.prototype_signals / total) * 100 : 0; |
| const refinePct = 100 - protoPct; |
| const chart = document.querySelector("#ratioChart"); |
| [ |
| ["Prototyping", protoPct, "proto"], |
| ["Refinement", refinePct, "refine"], |
| ].forEach(([label, pct, klass]) => { |
| const row = create("div", "mini-row"); |
| row.append(create("div", "", label)); |
| const track = create("div", "track"); |
| const fill = create("span"); |
| fill.className = klass; |
| fill.style.width = `${pct}%`; |
| track.append(fill); |
| row.append(track); |
| row.append(create("div", "muted", `${pct.toFixed(0)}%`)); |
| chart.append(row); |
| }); |
| document.querySelector("#ratioNote").textContent = workflow.prototyping_vs_refinement.interpretation; |
|
|
| const cards = [ |
| ["Seriell vs. Parallel", workflow.parallel_vs_serial.label], |
| ["Eval-Loops", workflow.evaluation_loops.interpretation], |
| ["Bevorzugte Projektarten", workflow.preferred_project_types.join(" • ")], |
| ["Meta-Systeme", workflow.meta_systems.join(" • ")], |
| ]; |
| const host = document.querySelector("#workflowCards"); |
| cards.forEach(([label, support]) => { |
| const card = create("article", "card"); |
| card.append(create("div", "label", label)); |
| card.append(create("p", "supporting", support)); |
| host.append(card); |
| }); |
| }; |
|
|
| const renderClusters = (data) => { |
| const grid = document.querySelector("#clusterGrid"); |
| data.project_clusters.forEach((cluster) => { |
| const card = create("article", "cluster-card"); |
| card.append(create("div", "label", `${cluster.count} Projekte`)); |
| card.append(create("h3", "", cluster.name)); |
| card.append(create("p", "supporting", cluster.summary)); |
| const pills = create("div", "pill-row"); |
| cluster.projects.forEach((project) => { |
| pills.append(create("span", "pill", `${project.name} · ${project.commit_count}`)); |
| }); |
| card.append(pills); |
| grid.append(card); |
| }); |
|
|
| const tableHost = document.querySelector("#projectTable"); |
| const table = create("table"); |
| table.innerHTML = ` |
| <thead> |
| <tr> |
| <th>Projekt</th> |
| <th>Cluster</th> |
| <th>Commits</th> |
| <th>Sessions</th> |
| <th>Status</th> |
| </tr> |
| </thead> |
| <tbody></tbody> |
| `; |
| const tbody = table.querySelector("tbody"); |
| data.projects.slice(0, 12).forEach((project) => { |
| const row = document.createElement("tr"); |
| row.innerHTML = ` |
| <td>${project.name}</td> |
| <td>${project.cluster}</td> |
| <td>${project.commit_count}</td> |
| <td>${project.session_count}</td> |
| <td>${project.status}</td> |
| `; |
| tbody.append(row); |
| }); |
| tableHost.append(table); |
| }; |
|
|
| const renderTimeline = (data) => { |
| const host = document.querySelector("#timeline"); |
| const grid = create("div", "timeline-grid"); |
| data.timeline.forEach((day) => { |
| const cell = create("article", `day-cell level-${day.intensity}`); |
| const date = new Date(day.date); |
| const label = date.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit" }); |
| const header = create("div", "day-head"); |
| header.append(create("span", "", label)); |
| header.append(create("span", "", `${day.projects.length} Projekte`)); |
| cell.append(header); |
| const projects = create("div", "day-projects"); |
| if (day.projects.length === 0) { |
| projects.append(create("span", "muted", "ruhiger Tag")); |
| } else { |
| day.projects.slice(0, 3).forEach((project) => { |
| projects.append(create("span", "", `${project.name} · ${project.session_count} Session`)); |
| }); |
| } |
| cell.append(projects); |
| grid.append(cell); |
| }); |
| host.append(grid); |
| }; |
|
|
| const renderRanking = (data) => { |
| const top = data.rankings.top_recommendation; |
| const main = document.querySelector("#topRecommendation"); |
| main.append(create("div", "badge", "Top Empfehlung")); |
| main.append(create("h3", "", top.name)); |
| main.append(create("p", "metric", `${top.score.toFixed(2)} / 5`)); |
| main.append(create("p", "supporting", top.summary)); |
|
|
| const stack = create("div", "mini-stack"); |
| Object.entries(top.scores).forEach(([label, value]) => { |
| const row = create("div", "mini-row"); |
| row.append(create("div", "", label.replace("_", " "))); |
| const track = create("div", "track"); |
| const fill = create("span"); |
| fill.style.width = `${(value / 5) * 100}%`; |
| track.append(fill); |
| row.append(track); |
| row.append(create("div", "muted", value.toFixed(1))); |
| stack.append(row); |
| }); |
| main.append(stack); |
|
|
| const topThree = document.querySelector("#topThree"); |
| data.rankings.top_three.forEach((project, index) => { |
| const item = create("div", "rank-item"); |
| item.innerHTML = `<strong>#${index + 1} ${project.name}</strong><p class="supporting">${project.summary}</p>`; |
| topThree.append(item); |
| }); |
|
|
| const rankingTable = document.querySelector("#rankingTable"); |
| const table = create("table"); |
| table.innerHTML = ` |
| <thead> |
| <tr> |
| <th>Projekt</th> |
| <th>Score</th> |
| <th>Commits</th> |
| <th>Kernaussage</th> |
| </tr> |
| </thead> |
| <tbody></tbody> |
| `; |
| const tbody = table.querySelector("tbody"); |
| data.rankings.shortlist.forEach((project) => { |
| const row = document.createElement("tr"); |
| row.innerHTML = ` |
| <td>${project.name}</td> |
| <td>${project.score.toFixed(2)}</td> |
| <td>${project.commit_count}</td> |
| <td>${project.summary}</td> |
| `; |
| tbody.append(row); |
| }); |
| rankingTable.append(table); |
| }; |
|
|
| const renderEvidence = (data) => { |
| const host = document.querySelector("#evidenceGrid"); |
| data.evidence.forEach((entry) => { |
| const card = create("article", "evidence-card"); |
| card.append(create("div", "label", entry.project)); |
| card.append(create("p", "supporting", `${entry.commits} Commits im Fenster · letzter Commit ${entry.last_commit ?? "n/a"}`)); |
| const pills = create("div", "pill-row"); |
| Object.entries(entry.artifacts).forEach(([key, value]) => pills.append(create("span", "pill", `${key}: ${value}`))); |
| card.append(pills); |
| const list = create("ul", "bullet-list"); |
| entry.sample_messages.forEach((message) => list.append(create("li", "", message))); |
| card.append(list); |
| host.append(card); |
| }); |
| }; |
|
|
| load() |
| .then((data) => { |
| renderHero(data); |
| renderFindings(data); |
| renderWork(data); |
| renderWorkflow(data); |
| renderClusters(data); |
| renderTimeline(data); |
| renderRanking(data); |
| renderEvidence(data); |
| }) |
| .catch((error) => { |
| document.body.innerHTML = `<main class="page-shell"><article class="panel"><h1>Ladefehler</h1><p>${error.message}</p></article></main>`; |
| }); |
|
|