opencastle 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/bin/cli.mjs +69 -0
- package/dist/cli/adapters/claude-code.d.ts +22 -0
- package/dist/cli/adapters/claude-code.d.ts.map +1 -0
- package/dist/cli/adapters/claude-code.js +237 -0
- package/dist/cli/adapters/claude-code.js.map +1 -0
- package/dist/cli/adapters/cursor.d.ts +20 -0
- package/dist/cli/adapters/cursor.d.ts.map +1 -0
- package/dist/cli/adapters/cursor.js +231 -0
- package/dist/cli/adapters/cursor.js.map +1 -0
- package/dist/cli/adapters/vscode.d.ts +20 -0
- package/dist/cli/adapters/vscode.d.ts.map +1 -0
- package/dist/cli/adapters/vscode.js +132 -0
- package/dist/cli/adapters/vscode.js.map +1 -0
- package/dist/cli/copy.d.ts +14 -0
- package/dist/cli/copy.d.ts.map +1 -0
- package/dist/cli/copy.js +62 -0
- package/dist/cli/copy.js.map +1 -0
- package/dist/cli/dashboard.d.ts +3 -0
- package/dist/cli/dashboard.d.ts.map +1 -0
- package/dist/cli/dashboard.js +183 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/diff.d.ts +3 -0
- package/dist/cli/diff.d.ts.map +1 -0
- package/dist/cli/diff.js +27 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/eject.d.ts +3 -0
- package/dist/cli/eject.d.ts.map +1 -0
- package/dist/cli/eject.js +27 -0
- package/dist/cli/eject.js.map +1 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +92 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/manifest.d.ts +14 -0
- package/dist/cli/manifest.d.ts.map +1 -0
- package/dist/cli/manifest.js +34 -0
- package/dist/cli/manifest.js.map +1 -0
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -0
- package/dist/cli/mcp.js +35 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/prompt.d.ts +12 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/prompt.js +104 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/cli/run/adapters/claude-code.d.ts +16 -0
- package/dist/cli/run/adapters/claude-code.d.ts.map +1 -0
- package/dist/cli/run/adapters/claude-code.js +82 -0
- package/dist/cli/run/adapters/claude-code.js.map +1 -0
- package/dist/cli/run/adapters/copilot.d.ts +16 -0
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -0
- package/dist/cli/run/adapters/copilot.js +84 -0
- package/dist/cli/run/adapters/copilot.js.map +1 -0
- package/dist/cli/run/adapters/cursor.d.ts +16 -0
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -0
- package/dist/cli/run/adapters/cursor.js +81 -0
- package/dist/cli/run/adapters/cursor.js.map +1 -0
- package/dist/cli/run/adapters/index.d.ts +14 -0
- package/dist/cli/run/adapters/index.d.ts.map +1 -0
- package/dist/cli/run/adapters/index.js +35 -0
- package/dist/cli/run/adapters/index.js.map +1 -0
- package/dist/cli/run/executor.d.ts +15 -0
- package/dist/cli/run/executor.d.ts.map +1 -0
- package/dist/cli/run/executor.js +249 -0
- package/dist/cli/run/executor.js.map +1 -0
- package/dist/cli/run/reporter.d.ts +10 -0
- package/dist/cli/run/reporter.d.ts.map +1 -0
- package/dist/cli/run/reporter.js +112 -0
- package/dist/cli/run/reporter.js.map +1 -0
- package/dist/cli/run/schema.d.ts +28 -0
- package/dist/cli/run/schema.d.ts.map +1 -0
- package/dist/cli/run/schema.js +511 -0
- package/dist/cli/run/schema.js.map +1 -0
- package/dist/cli/run.d.ts +6 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +123 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/stack-config.d.ts +12 -0
- package/dist/cli/stack-config.d.ts.map +1 -0
- package/dist/cli/stack-config.js +146 -0
- package/dist/cli/stack-config.js.map +1 -0
- package/dist/cli/types.d.ts +169 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/update.d.ts +3 -0
- package/dist/cli/update.d.ts.map +1 -0
- package/dist/cli/update.js +50 -0
- package/dist/cli/update.js.map +1 -0
- package/package.json +48 -0
- package/src/cli/adapters/claude-code.ts +287 -0
- package/src/cli/adapters/cursor.ts +377 -0
- package/src/cli/adapters/vscode.ts +168 -0
- package/src/cli/copy.ts +79 -0
- package/src/cli/dashboard.ts +225 -0
- package/src/cli/diff.ts +44 -0
- package/src/cli/eject.ts +39 -0
- package/src/cli/init.ts +120 -0
- package/src/cli/manifest.ts +45 -0
- package/src/cli/mcp.ts +49 -0
- package/src/cli/prompt.ts +115 -0
- package/src/cli/run/adapters/claude-code.ts +95 -0
- package/src/cli/run/adapters/copilot.ts +97 -0
- package/src/cli/run/adapters/cursor.ts +94 -0
- package/src/cli/run/adapters/index.ts +40 -0
- package/src/cli/run/executor.ts +292 -0
- package/src/cli/run/reporter.ts +129 -0
- package/src/cli/run/schema.ts +595 -0
- package/src/cli/run.ts +137 -0
- package/src/cli/stack-config.ts +180 -0
- package/src/cli/types.ts +207 -0
- package/src/cli/update.ts +75 -0
- package/src/dashboard/astro.config.mjs +6 -0
- package/src/dashboard/package-lock.json +5455 -0
- package/src/dashboard/package.json +14 -0
- package/src/dashboard/public/data/delegations.ndjson +35 -0
- package/src/dashboard/public/data/panels.ndjson +13 -0
- package/src/dashboard/public/data/sessions.ndjson +50 -0
- package/src/dashboard/public/icon-192.png +0 -0
- package/src/dashboard/scripts/generate-seed-data.ts +355 -0
- package/src/dashboard/src/layouts/Layout.astro +25 -0
- package/src/dashboard/src/pages/index.astro +1070 -0
- package/src/dashboard/src/styles/dashboard.css +1078 -0
- package/src/dashboard/tsconfig.json +6 -0
- package/src/orchestrator/agent-workflows/README.md +22 -0
- package/src/orchestrator/agent-workflows/bug-fix.md +128 -0
- package/src/orchestrator/agent-workflows/data-pipeline.md +145 -0
- package/src/orchestrator/agent-workflows/database-migration.md +159 -0
- package/src/orchestrator/agent-workflows/feature-implementation.md +223 -0
- package/src/orchestrator/agent-workflows/performance-optimization.md +125 -0
- package/src/orchestrator/agent-workflows/refactoring.md +142 -0
- package/src/orchestrator/agent-workflows/schema-changes.md +164 -0
- package/src/orchestrator/agent-workflows/security-audit.md +148 -0
- package/src/orchestrator/agent-workflows/shared-delivery-phase.md +33 -0
- package/src/orchestrator/agents/api-designer.agent.md +68 -0
- package/src/orchestrator/agents/architect.agent.md +129 -0
- package/src/orchestrator/agents/content-engineer.agent.md +57 -0
- package/src/orchestrator/agents/copywriter.agent.md +95 -0
- package/src/orchestrator/agents/data-expert.agent.md +63 -0
- package/src/orchestrator/agents/database-engineer.agent.md +62 -0
- package/src/orchestrator/agents/developer.agent.md +66 -0
- package/src/orchestrator/agents/devops-expert.agent.md +57 -0
- package/src/orchestrator/agents/documentation-writer.agent.md +60 -0
- package/src/orchestrator/agents/performance-expert.agent.md +58 -0
- package/src/orchestrator/agents/release-manager.agent.md +72 -0
- package/src/orchestrator/agents/researcher.agent.md +145 -0
- package/src/orchestrator/agents/reviewer.agent.md +62 -0
- package/src/orchestrator/agents/security-expert.agent.md +64 -0
- package/src/orchestrator/agents/seo-specialist.agent.md +67 -0
- package/src/orchestrator/agents/team-lead.agent.md +644 -0
- package/src/orchestrator/agents/testing-expert.agent.md +85 -0
- package/src/orchestrator/agents/ui-ux-expert.agent.md +63 -0
- package/src/orchestrator/copilot-instructions.md +3 -0
- package/src/orchestrator/customizations/AGENT-EXPERTISE.md +325 -0
- package/src/orchestrator/customizations/AGENT-FAILURES.md +69 -0
- package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +58 -0
- package/src/orchestrator/customizations/DISPUTES.md +162 -0
- package/src/orchestrator/customizations/KNOWLEDGE-GRAPH.md +10 -0
- package/src/orchestrator/customizations/LESSONS-LEARNED.md +70 -0
- package/src/orchestrator/customizations/README.md +59 -0
- package/src/orchestrator/customizations/agents/agent-registry.md +46 -0
- package/src/orchestrator/customizations/agents/skill-matrix.md +142 -0
- package/src/orchestrator/customizations/logs/README.md +181 -0
- package/src/orchestrator/customizations/logs/delegations.ndjson +1 -0
- package/src/orchestrator/customizations/logs/panels.ndjson +1 -0
- package/src/orchestrator/customizations/logs/sessions.ndjson +1 -0
- package/src/orchestrator/customizations/project/docs-structure.md +23 -0
- package/src/orchestrator/customizations/project/tracker-config.md +45 -0
- package/src/orchestrator/customizations/project.instructions.md +64 -0
- package/src/orchestrator/customizations/stack/api-config.md +37 -0
- package/src/orchestrator/customizations/stack/cms-config.md +26 -0
- package/src/orchestrator/customizations/stack/data-pipeline-config.md +41 -0
- package/src/orchestrator/customizations/stack/database-config.md +44 -0
- package/src/orchestrator/customizations/stack/deployment-config.md +45 -0
- package/src/orchestrator/customizations/stack/testing-config.md +56 -0
- package/src/orchestrator/instructions/ai-optimization.instructions.md +143 -0
- package/src/orchestrator/instructions/general.instructions.md +194 -0
- package/src/orchestrator/mcp.json +55 -0
- package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +235 -0
- package/src/orchestrator/prompts/brainstorm.prompt.md +115 -0
- package/src/orchestrator/prompts/bug-fix.prompt.md +141 -0
- package/src/orchestrator/prompts/create-skill.prompt.md +103 -0
- package/src/orchestrator/prompts/generate-task-spec.prompt.md +154 -0
- package/src/orchestrator/prompts/implement-feature.prompt.md +124 -0
- package/src/orchestrator/prompts/metrics-report.prompt.md +142 -0
- package/src/orchestrator/prompts/quick-refinement.prompt.md +137 -0
- package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +100 -0
- package/src/orchestrator/skills/accessibility-standards/SKILL.md +164 -0
- package/src/orchestrator/skills/agent-hooks/SKILL.md +147 -0
- package/src/orchestrator/skills/agent-memory/SKILL.md +144 -0
- package/src/orchestrator/skills/api-patterns/SKILL.md +106 -0
- package/src/orchestrator/skills/browser-testing/SKILL.md +203 -0
- package/src/orchestrator/skills/code-commenting/SKILL.md +133 -0
- package/src/orchestrator/skills/contentful-cms/SKILL.md +43 -0
- package/src/orchestrator/skills/context-map/SKILL.md +135 -0
- package/src/orchestrator/skills/convex-database/SKILL.md +80 -0
- package/src/orchestrator/skills/data-engineering/SKILL.md +99 -0
- package/src/orchestrator/skills/deployment-infrastructure/SKILL.md +49 -0
- package/src/orchestrator/skills/documentation-standards/SKILL.md +85 -0
- package/src/orchestrator/skills/fast-review/SKILL.md +327 -0
- package/src/orchestrator/skills/frontend-design/SKILL.md +42 -0
- package/src/orchestrator/skills/jira-management/SKILL.md +168 -0
- package/src/orchestrator/skills/memory-merger/SKILL.md +123 -0
- package/src/orchestrator/skills/nextjs-patterns/SKILL.md +75 -0
- package/src/orchestrator/skills/nx-workspace/SKILL.md +192 -0
- package/src/orchestrator/skills/panel-majority-vote/SKILL.md +184 -0
- package/src/orchestrator/skills/panel-majority-vote/panel-report.template.md +38 -0
- package/src/orchestrator/skills/performance-optimization/SKILL.md +101 -0
- package/src/orchestrator/skills/react-development/SKILL.md +117 -0
- package/src/orchestrator/skills/sanity-cms/SKILL.md +18 -0
- package/src/orchestrator/skills/security-hardening/SKILL.md +118 -0
- package/src/orchestrator/skills/self-improvement/SKILL.md +137 -0
- package/src/orchestrator/skills/seo-patterns/SKILL.md +40 -0
- package/src/orchestrator/skills/session-checkpoints/SKILL.md +205 -0
- package/src/orchestrator/skills/slack-notifications/SKILL.md +211 -0
- package/src/orchestrator/skills/strapi-cms/SKILL.md +43 -0
- package/src/orchestrator/skills/supabase-database/SKILL.md +24 -0
- package/src/orchestrator/skills/task-management/SKILL.md +143 -0
- package/src/orchestrator/skills/team-lead-reference/SKILL.md +317 -0
- package/src/orchestrator/skills/teams-notifications/SKILL.md +249 -0
- package/src/orchestrator/skills/testing-workflow/SKILL.md +134 -0
- package/src/orchestrator/skills/validation-gates/SKILL.md +100 -0
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from '../layouts/Layout.astro';
|
|
3
|
+
import '../styles/dashboard.css';
|
|
4
|
+
|
|
5
|
+
const base = import.meta.env.BASE_URL;
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<Layout title="Observability Dashboard — OpenCastle">
|
|
9
|
+
<!-- Header -->
|
|
10
|
+
<header class="dash-header">
|
|
11
|
+
<div class="dash-header__inner">
|
|
12
|
+
<div class="dash-header__brand">
|
|
13
|
+
<img class="dash-header__icon" src={`${base}icon-192.png`} alt="OpenCastle" width="32" height="32" />
|
|
14
|
+
<h1 class="dash-header__title">Observability Dashboard</h1>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</header>
|
|
18
|
+
|
|
19
|
+
<div class="dash-layout">
|
|
20
|
+
<!-- Sidebar Navigation -->
|
|
21
|
+
<nav class="dash-sidebar" id="dash-sidebar">
|
|
22
|
+
<ul class="dash-sidebar__list">
|
|
23
|
+
<li><a class="dash-sidebar__link dash-sidebar__link--active" href="#kpi-row" data-section="kpi-row">Overview</a></li>
|
|
24
|
+
<li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section">Pipeline</a></li>
|
|
25
|
+
<li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section">Agents</a></li>
|
|
26
|
+
<li><a class="dash-sidebar__link" href="#tier-section" data-section="tier-section">Tiers</a></li>
|
|
27
|
+
<li><a class="dash-sidebar__link" href="#delegation-section" data-section="delegation-section">Delegations</a></li>
|
|
28
|
+
<li><a class="dash-sidebar__link" href="#timeline-section" data-section="timeline-section">Timeline</a></li>
|
|
29
|
+
<li><a class="dash-sidebar__link" href="#model-section" data-section="model-section">Models</a></li>
|
|
30
|
+
<li><a class="dash-sidebar__link" href="#execution-section" data-section="execution-section">Exec Log</a></li>
|
|
31
|
+
<li><a class="dash-sidebar__link" href="#panel-section" data-section="panel-section">Panels</a></li>
|
|
32
|
+
<li><a class="dash-sidebar__link" href="#sessions-section" data-section="sessions-section">Sessions</a></li>
|
|
33
|
+
</ul>
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<main class="dash-main">
|
|
37
|
+
<!-- KPI Row -->
|
|
38
|
+
<section class="kpi-row" id="kpi-row" data-nav-section>
|
|
39
|
+
<div class="kpi-card" id="kpi-sessions">
|
|
40
|
+
<span class="kpi-card__label">Total Sessions</span>
|
|
41
|
+
<span class="kpi-card__value">—</span>
|
|
42
|
+
<span class="kpi-card__sub"></span>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="kpi-card" id="kpi-success">
|
|
45
|
+
<span class="kpi-card__label">Success Rate</span>
|
|
46
|
+
<span class="kpi-card__value">—</span>
|
|
47
|
+
<span class="kpi-card__sub"></span>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="kpi-card" id="kpi-delegations">
|
|
50
|
+
<span class="kpi-card__label">Total Delegations</span>
|
|
51
|
+
<span class="kpi-card__value">—</span>
|
|
52
|
+
<span class="kpi-card__sub"></span>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="kpi-card" id="kpi-duration">
|
|
55
|
+
<span class="kpi-card__label">Avg Duration</span>
|
|
56
|
+
<span class="kpi-card__value">—</span>
|
|
57
|
+
<span class="kpi-card__sub"></span>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="kpi-card" id="kpi-retries">
|
|
60
|
+
<span class="kpi-card__label">Total Retries</span>
|
|
61
|
+
<span class="kpi-card__value">—</span>
|
|
62
|
+
<span class="kpi-card__sub"></span>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="kpi-card" id="kpi-lessons">
|
|
65
|
+
<span class="kpi-card__label">Lessons Added</span>
|
|
66
|
+
<span class="kpi-card__value">—</span>
|
|
67
|
+
<span class="kpi-card__sub"></span>
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
70
|
+
|
|
71
|
+
<!-- Pipeline View (Steroids-inspired) -->
|
|
72
|
+
<section class="chart-card" id="pipeline-section" data-nav-section>
|
|
73
|
+
<div class="chart-card__header">
|
|
74
|
+
<h2 class="chart-card__title">Task Pipeline</h2>
|
|
75
|
+
<p class="chart-card__desc">Delegation flow across execution phases</p>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="chart-card__body" id="pipeline-view">
|
|
78
|
+
<div class="loading-skeleton"></div>
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<!-- Charts Row 1 -->
|
|
83
|
+
<div class="charts-row" id="agent-section" data-nav-section>
|
|
84
|
+
<section class="chart-card">
|
|
85
|
+
<div class="chart-card__header">
|
|
86
|
+
<h2 class="chart-card__title">Sessions by Agent</h2>
|
|
87
|
+
<p class="chart-card__desc">Stacked by outcome</p>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="chart-card__body" id="agent-chart">
|
|
90
|
+
<div class="loading-skeleton"></div>
|
|
91
|
+
</div>
|
|
92
|
+
</section>
|
|
93
|
+
<section class="chart-card" id="tier-section" data-nav-section>
|
|
94
|
+
<div class="chart-card__header">
|
|
95
|
+
<h2 class="chart-card__title">Tier Distribution</h2>
|
|
96
|
+
<p class="chart-card__desc">Delegation model tiers</p>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="chart-card__body" id="tier-chart">
|
|
99
|
+
<div class="loading-skeleton"></div>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- Charts Row: Delegation Insights -->
|
|
105
|
+
<div class="charts-row" id="delegation-section" data-nav-section>
|
|
106
|
+
<section class="chart-card">
|
|
107
|
+
<div class="chart-card__header">
|
|
108
|
+
<h2 class="chart-card__title">Delegation Mechanism</h2>
|
|
109
|
+
<p class="chart-card__desc">Sub-agent vs background split</p>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="chart-card__body" id="mechanism-chart">
|
|
112
|
+
<div class="loading-skeleton"></div>
|
|
113
|
+
</div>
|
|
114
|
+
</section>
|
|
115
|
+
<section class="chart-card">
|
|
116
|
+
<div class="chart-card__header">
|
|
117
|
+
<h2 class="chart-card__title">Delegation Outcomes</h2>
|
|
118
|
+
<p class="chart-card__desc">Success rate by delegation</p>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="chart-card__body" id="delegation-outcome-chart">
|
|
121
|
+
<div class="loading-skeleton"></div>
|
|
122
|
+
</div>
|
|
123
|
+
</section>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- Charts Row 2 -->
|
|
127
|
+
<div class="charts-row" id="timeline-section" data-nav-section>
|
|
128
|
+
<section class="chart-card">
|
|
129
|
+
<div class="chart-card__header">
|
|
130
|
+
<h2 class="chart-card__title">Timeline</h2>
|
|
131
|
+
<p class="chart-card__desc">Sessions and delegations over time</p>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="chart-card__body" id="timeline-chart">
|
|
134
|
+
<div class="loading-skeleton"></div>
|
|
135
|
+
</div>
|
|
136
|
+
</section>
|
|
137
|
+
<section class="chart-card" id="model-section" data-nav-section>
|
|
138
|
+
<div class="chart-card__header">
|
|
139
|
+
<h2 class="chart-card__title">Model Usage</h2>
|
|
140
|
+
<p class="chart-card__desc">Sessions by model</p>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="chart-card__body" id="model-chart">
|
|
143
|
+
<div class="loading-skeleton"></div>
|
|
144
|
+
</div>
|
|
145
|
+
</section>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<!-- Execution Log (Duvo-inspired) -->
|
|
149
|
+
<section class="chart-card" id="execution-section" data-nav-section>
|
|
150
|
+
<div class="chart-card__header">
|
|
151
|
+
<h2 class="chart-card__title">Execution Log</h2>
|
|
152
|
+
<p class="chart-card__desc">Recent agent activity, step by step</p>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="chart-card__body" id="execution-log">
|
|
155
|
+
<div class="loading-skeleton"></div>
|
|
156
|
+
</div>
|
|
157
|
+
</section>
|
|
158
|
+
|
|
159
|
+
<!-- Panel Results -->
|
|
160
|
+
<section class="chart-card" id="panel-section" data-nav-section>
|
|
161
|
+
<div class="chart-card__header">
|
|
162
|
+
<h2 class="chart-card__title">Panel Reviews</h2>
|
|
163
|
+
<p class="chart-card__desc">Quality gate verdicts and fix items</p>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="chart-card__body" id="panel-chart">
|
|
166
|
+
<div class="loading-skeleton"></div>
|
|
167
|
+
</div>
|
|
168
|
+
</section>
|
|
169
|
+
|
|
170
|
+
<!-- Sessions Table -->
|
|
171
|
+
<section class="chart-card" id="sessions-section" data-nav-section>
|
|
172
|
+
<div class="chart-card__header">
|
|
173
|
+
<h2 class="chart-card__title">Recent Sessions</h2>
|
|
174
|
+
<p class="chart-card__desc">Last 15 sessions by timestamp</p>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="chart-card__body chart-card__body--table" id="sessions-table">
|
|
177
|
+
<div class="loading-skeleton"></div>
|
|
178
|
+
</div>
|
|
179
|
+
</section>
|
|
180
|
+
</main>
|
|
181
|
+
</div>
|
|
182
|
+
</Layout>
|
|
183
|
+
|
|
184
|
+
<script define:vars={{ base }}>
|
|
185
|
+
// ── Data Loading ──────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
async function loadNdjson(path) {
|
|
188
|
+
try {
|
|
189
|
+
const res = await fetch(path);
|
|
190
|
+
if (!res.ok) return [];
|
|
191
|
+
const text = await res.text();
|
|
192
|
+
return text
|
|
193
|
+
.trim()
|
|
194
|
+
.split('\n')
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.map((line) => JSON.parse(line));
|
|
197
|
+
} catch {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Helpers ───────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
const TIER_COLORS = {
|
|
205
|
+
premium: '#f59e0b',
|
|
206
|
+
standard: '#a78bfa',
|
|
207
|
+
utility: '#3b82f6',
|
|
208
|
+
economy: '#64748b',
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const MODEL_COLORS = {
|
|
212
|
+
'claude-opus-4-6': '#a78bfa',
|
|
213
|
+
'gpt-5-mini': '#64748b',
|
|
214
|
+
'gpt-5.3-codex': '#3b82f6',
|
|
215
|
+
'gemini-3.1-pro': '#f59e0b',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const OUTCOME_ICONS = {
|
|
219
|
+
success: '\u2713',
|
|
220
|
+
partial: '\u25CB',
|
|
221
|
+
failed: '\u2717',
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
function formatTime(ts) {
|
|
225
|
+
const d = new Date(ts);
|
|
226
|
+
return d.toLocaleDateString('en-US', {
|
|
227
|
+
month: 'short',
|
|
228
|
+
day: 'numeric',
|
|
229
|
+
hour: '2-digit',
|
|
230
|
+
minute: '2-digit',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatShortDate(dateKey) {
|
|
235
|
+
const d = new Date(dateKey + 'T00:00:00Z');
|
|
236
|
+
return d.toLocaleDateString('en-US', {
|
|
237
|
+
month: 'short',
|
|
238
|
+
day: 'numeric',
|
|
239
|
+
timeZone: 'UTC',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function escapeHtml(str) {
|
|
244
|
+
const div = document.createElement('div');
|
|
245
|
+
div.textContent = str;
|
|
246
|
+
return div.innerHTML;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── KPI Rendering ────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
function renderKpis(sessions, delegations) {
|
|
252
|
+
const total = sessions.length;
|
|
253
|
+
const successCount = sessions.filter((s) => s.outcome === 'success').length;
|
|
254
|
+
const rate = total > 0 ? Math.round((successCount / total) * 100) : 0;
|
|
255
|
+
const durSessions = sessions.filter((s) => s.duration_min != null);
|
|
256
|
+
const avgDur =
|
|
257
|
+
durSessions.length > 0
|
|
258
|
+
? Math.round(
|
|
259
|
+
durSessions.reduce((sum, s) => sum + (s.duration_min || 0), 0) /
|
|
260
|
+
durSessions.length
|
|
261
|
+
)
|
|
262
|
+
: 0;
|
|
263
|
+
const uniqueAgents = new Set(delegations.map((d) => d.agent)).size;
|
|
264
|
+
|
|
265
|
+
const kpiSessions = document.getElementById('kpi-sessions');
|
|
266
|
+
const kpiSuccess = document.getElementById('kpi-success');
|
|
267
|
+
const kpiDelegations = document.getElementById('kpi-delegations');
|
|
268
|
+
const kpiDuration = document.getElementById('kpi-duration');
|
|
269
|
+
|
|
270
|
+
if (kpiSessions) {
|
|
271
|
+
kpiSessions.querySelector('.kpi-card__value').textContent = total;
|
|
272
|
+
kpiSessions.querySelector('.kpi-card__sub').innerHTML =
|
|
273
|
+
'<span class="kpi-trend kpi-trend--up">\u2191</span> ' +
|
|
274
|
+
successCount +
|
|
275
|
+
' successful';
|
|
276
|
+
}
|
|
277
|
+
if (kpiSuccess) {
|
|
278
|
+
const trendClass =
|
|
279
|
+
rate >= 80 ? 'up' : rate >= 60 ? 'neutral' : 'down';
|
|
280
|
+
kpiSuccess.querySelector('.kpi-card__value').textContent = rate + '%';
|
|
281
|
+
kpiSuccess.querySelector('.kpi-card__sub').innerHTML =
|
|
282
|
+
'<span class="kpi-trend kpi-trend--' +
|
|
283
|
+
trendClass +
|
|
284
|
+
'">' +
|
|
285
|
+
(trendClass === 'up' ? '\u2191' : trendClass === 'down' ? '\u2193' : '\u2192') +
|
|
286
|
+
'</span> across all sessions';
|
|
287
|
+
}
|
|
288
|
+
if (kpiDelegations) {
|
|
289
|
+
kpiDelegations.querySelector('.kpi-card__value').textContent =
|
|
290
|
+
delegations.length;
|
|
291
|
+
kpiDelegations.querySelector('.kpi-card__sub').textContent =
|
|
292
|
+
uniqueAgents + ' unique agents';
|
|
293
|
+
}
|
|
294
|
+
if (kpiDuration) {
|
|
295
|
+
kpiDuration.querySelector('.kpi-card__value').textContent =
|
|
296
|
+
avgDur + 'm';
|
|
297
|
+
kpiDuration.querySelector('.kpi-card__sub').innerHTML =
|
|
298
|
+
'<span class="kpi-trend kpi-trend--neutral">\u2192</span> per session';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Retries KPI
|
|
302
|
+
const totalRetries = sessions.reduce((sum, s) => sum + (s.retries || 0), 0);
|
|
303
|
+
const kpiRetries = document.getElementById('kpi-retries');
|
|
304
|
+
if (kpiRetries) {
|
|
305
|
+
kpiRetries.querySelector('.kpi-card__value').textContent = totalRetries;
|
|
306
|
+
const retriedSessions = sessions.filter((s) => (s.retries || 0) > 0).length;
|
|
307
|
+
kpiRetries.querySelector('.kpi-card__sub').textContent =
|
|
308
|
+
retriedSessions + ' sessions with retries';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Lessons KPI
|
|
312
|
+
const totalLessons = sessions.reduce(
|
|
313
|
+
(sum, s) => sum + (s.lessons_added ? s.lessons_added.length : 0),
|
|
314
|
+
0
|
|
315
|
+
);
|
|
316
|
+
const kpiLessons = document.getElementById('kpi-lessons');
|
|
317
|
+
if (kpiLessons) {
|
|
318
|
+
kpiLessons.querySelector('.kpi-card__value').textContent = totalLessons;
|
|
319
|
+
const discoveryCount = sessions.reduce(
|
|
320
|
+
(sum, s) => sum + (s.discoveries ? s.discoveries.length : 0),
|
|
321
|
+
0
|
|
322
|
+
);
|
|
323
|
+
kpiLessons.querySelector('.kpi-card__sub').textContent =
|
|
324
|
+
discoveryCount + ' issues discovered';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Pipeline View ─────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
function renderPipeline(delegations) {
|
|
331
|
+
const el = document.getElementById('pipeline-view');
|
|
332
|
+
if (!el) return;
|
|
333
|
+
|
|
334
|
+
const phases = { 1: 0, 2: 0, 3: 0, 4: 0 };
|
|
335
|
+
delegations.forEach((d) => {
|
|
336
|
+
const p = d.phase || 1;
|
|
337
|
+
if (phases[p] !== undefined) phases[p]++;
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const stageConfig = [
|
|
341
|
+
{
|
|
342
|
+
label: 'Foundation',
|
|
343
|
+
phase: 1,
|
|
344
|
+
iconClass: 'pending',
|
|
345
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="13" y2="14"/></svg>',
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
label: 'Integration',
|
|
349
|
+
phase: 2,
|
|
350
|
+
iconClass: 'active',
|
|
351
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
label: 'Validation',
|
|
355
|
+
phase: 3,
|
|
356
|
+
iconClass: 'review',
|
|
357
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
label: 'QA Gate',
|
|
361
|
+
phase: 4,
|
|
362
|
+
iconClass: 'done',
|
|
363
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
el.innerHTML =
|
|
368
|
+
'<div class="pipeline">' +
|
|
369
|
+
stageConfig
|
|
370
|
+
.map(
|
|
371
|
+
(stage, i) =>
|
|
372
|
+
(i > 0 ? '<div class="pipeline-arrow">\u2192</div>' : '') +
|
|
373
|
+
'<div class="pipeline-stage">' +
|
|
374
|
+
'<div class="pipeline-stage__icon pipeline-stage__icon--' +
|
|
375
|
+
stage.iconClass +
|
|
376
|
+
'">' +
|
|
377
|
+
stage.icon +
|
|
378
|
+
'</div>' +
|
|
379
|
+
'<span class="pipeline-stage__count">' +
|
|
380
|
+
(phases[stage.phase] || 0) +
|
|
381
|
+
'</span>' +
|
|
382
|
+
'<span class="pipeline-stage__label">' +
|
|
383
|
+
stage.label +
|
|
384
|
+
'</span>' +
|
|
385
|
+
'</div>'
|
|
386
|
+
)
|
|
387
|
+
.join('') +
|
|
388
|
+
'</div>';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── Agent Chart ───────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
function renderAgentChart(sessions) {
|
|
394
|
+
const el = document.getElementById('agent-chart');
|
|
395
|
+
if (!el) return;
|
|
396
|
+
|
|
397
|
+
if (sessions.length === 0) {
|
|
398
|
+
el.innerHTML =
|
|
399
|
+
'<div class="empty-state"><div class="empty-state__icon">\uD83D\uDCCA</div><p class="empty-state__text">No session data available</p></div>';
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const agentMap = {};
|
|
404
|
+
sessions.forEach((s) => {
|
|
405
|
+
if (!agentMap[s.agent])
|
|
406
|
+
agentMap[s.agent] = { success: 0, partial: 0, failed: 0, total: 0 };
|
|
407
|
+
agentMap[s.agent][s.outcome] = (agentMap[s.agent][s.outcome] || 0) + 1;
|
|
408
|
+
agentMap[s.agent].total++;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const agents = Object.entries(agentMap).sort(
|
|
412
|
+
(a, b) => b[1].total - a[1].total
|
|
413
|
+
);
|
|
414
|
+
const maxTotal = Math.max(...agents.map(([, d]) => d.total));
|
|
415
|
+
|
|
416
|
+
el.innerHTML = agents
|
|
417
|
+
.map(
|
|
418
|
+
([name, data]) =>
|
|
419
|
+
'<div class="bar-row">' +
|
|
420
|
+
'<span class="bar-label">' +
|
|
421
|
+
escapeHtml(name) +
|
|
422
|
+
'</span>' +
|
|
423
|
+
'<div class="bar-track">' +
|
|
424
|
+
'<div class="bar-segment bar--success" style="width: ' +
|
|
425
|
+
((data.success / maxTotal) * 100).toFixed(1) +
|
|
426
|
+
'%"></div>' +
|
|
427
|
+
'<div class="bar-segment bar--partial" style="width: ' +
|
|
428
|
+
((data.partial / maxTotal) * 100).toFixed(1) +
|
|
429
|
+
'%"></div>' +
|
|
430
|
+
'<div class="bar-segment bar--failed" style="width: ' +
|
|
431
|
+
((data.failed / maxTotal) * 100).toFixed(1) +
|
|
432
|
+
'%"></div>' +
|
|
433
|
+
'</div>' +
|
|
434
|
+
'<span class="bar-value">' +
|
|
435
|
+
data.total +
|
|
436
|
+
'</span>' +
|
|
437
|
+
'</div>'
|
|
438
|
+
)
|
|
439
|
+
.join('');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── Tier Donut Chart ──────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
function renderTierChart(delegations) {
|
|
445
|
+
const el = document.getElementById('tier-chart');
|
|
446
|
+
if (!el) return;
|
|
447
|
+
|
|
448
|
+
if (delegations.length === 0) {
|
|
449
|
+
el.innerHTML =
|
|
450
|
+
'<div class="empty-state"><div class="empty-state__icon">\uD83D\uDFE3</div><p class="empty-state__text">No delegation data available</p></div>';
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const tierCounts = {};
|
|
455
|
+
delegations.forEach((d) => {
|
|
456
|
+
tierCounts[d.tier] = (tierCounts[d.tier] || 0) + 1;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const order = ['premium', 'standard', 'utility', 'economy'];
|
|
460
|
+
const tiers = order
|
|
461
|
+
.filter((t) => tierCounts[t])
|
|
462
|
+
.map((t) => ({ name: t, count: tierCounts[t] }));
|
|
463
|
+
|
|
464
|
+
const total = delegations.length;
|
|
465
|
+
const r = 70;
|
|
466
|
+
const circumference = 2 * Math.PI * r;
|
|
467
|
+
let cumOffset = 0;
|
|
468
|
+
|
|
469
|
+
const circles = tiers.map((t) => {
|
|
470
|
+
const pct = t.count / total;
|
|
471
|
+
const dashLen = pct * circumference;
|
|
472
|
+
const segment =
|
|
473
|
+
'<circle cx="90" cy="90" r="' +
|
|
474
|
+
r +
|
|
475
|
+
'" fill="none" ' +
|
|
476
|
+
'stroke="' +
|
|
477
|
+
(TIER_COLORS[t.name] || '#64748b') +
|
|
478
|
+
'" stroke-width="18" ' +
|
|
479
|
+
'stroke-dasharray="' +
|
|
480
|
+
dashLen.toFixed(2) +
|
|
481
|
+
' ' +
|
|
482
|
+
(circumference - dashLen).toFixed(2) +
|
|
483
|
+
'" ' +
|
|
484
|
+
'stroke-dashoffset="' +
|
|
485
|
+
(-cumOffset).toFixed(2) +
|
|
486
|
+
'" ' +
|
|
487
|
+
'transform="rotate(-90 90 90)" ' +
|
|
488
|
+
'stroke-linecap="round"/>';
|
|
489
|
+
cumOffset += dashLen;
|
|
490
|
+
return segment;
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const legend = tiers
|
|
494
|
+
.map(
|
|
495
|
+
(t) =>
|
|
496
|
+
'<div class="legend-item">' +
|
|
497
|
+
'<span class="legend-dot" style="background: ' +
|
|
498
|
+
(TIER_COLORS[t.name] || '#64748b') +
|
|
499
|
+
'"></span>' +
|
|
500
|
+
'<span class="legend-name">' +
|
|
501
|
+
t.name +
|
|
502
|
+
'</span>' +
|
|
503
|
+
'<span class="legend-count">' +
|
|
504
|
+
t.count +
|
|
505
|
+
' (' +
|
|
506
|
+
Math.round((t.count / total) * 100) +
|
|
507
|
+
'%)</span>' +
|
|
508
|
+
'</div>'
|
|
509
|
+
)
|
|
510
|
+
.join('');
|
|
511
|
+
|
|
512
|
+
el.innerHTML =
|
|
513
|
+
'<div class="donut-container">' +
|
|
514
|
+
'<div class="donut-wrap">' +
|
|
515
|
+
'<svg viewBox="0 0 180 180" class="donut-svg">' +
|
|
516
|
+
circles.join('') +
|
|
517
|
+
'</svg>' +
|
|
518
|
+
'<div class="donut-center">' +
|
|
519
|
+
'<span class="donut-total">' +
|
|
520
|
+
total +
|
|
521
|
+
'</span>' +
|
|
522
|
+
'<span class="donut-total-label">total</span>' +
|
|
523
|
+
'</div>' +
|
|
524
|
+
'</div>' +
|
|
525
|
+
'<div class="donut-legend">' +
|
|
526
|
+
legend +
|
|
527
|
+
'</div>' +
|
|
528
|
+
'</div>';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ── Mechanism Donut Chart ─────────────────────────────────
|
|
532
|
+
|
|
533
|
+
function renderMechanismChart(delegations) {
|
|
534
|
+
const el = document.getElementById('mechanism-chart');
|
|
535
|
+
if (!el) return;
|
|
536
|
+
|
|
537
|
+
if (delegations.length === 0) {
|
|
538
|
+
el.innerHTML =
|
|
539
|
+
'<div class="empty-state"><div class="empty-state__icon">\u2699\uFE0F</div><p class="empty-state__text">No delegation data available</p></div>';
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const mechCounts = {};
|
|
544
|
+
delegations.forEach((d) => {
|
|
545
|
+
const mech = d.mechanism || 'unknown';
|
|
546
|
+
mechCounts[mech] = (mechCounts[mech] || 0) + 1;
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
var MECH_COLORS = {
|
|
550
|
+
'sub-agent': '#3b82f6',
|
|
551
|
+
'background': '#a78bfa',
|
|
552
|
+
'unknown': '#64748b',
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
var MECH_LABELS = {
|
|
556
|
+
'sub-agent': 'Sub-agent (inline)',
|
|
557
|
+
'background': 'Background (worktree)',
|
|
558
|
+
'unknown': 'Unknown',
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
var mechOrder = ['sub-agent', 'background', 'unknown'];
|
|
562
|
+
var mechs = mechOrder
|
|
563
|
+
.filter(function (m) { return mechCounts[m]; })
|
|
564
|
+
.map(function (m) { return { name: m, count: mechCounts[m] }; });
|
|
565
|
+
|
|
566
|
+
var total = delegations.length;
|
|
567
|
+
var r = 70;
|
|
568
|
+
var circumference = 2 * Math.PI * r;
|
|
569
|
+
var cumOffset = 0;
|
|
570
|
+
|
|
571
|
+
var circles = mechs.map(function (m) {
|
|
572
|
+
var pct = m.count / total;
|
|
573
|
+
var dashLen = pct * circumference;
|
|
574
|
+
var segment =
|
|
575
|
+
'<circle cx="90" cy="90" r="' + r + '" fill="none" ' +
|
|
576
|
+
'stroke="' + (MECH_COLORS[m.name] || '#64748b') + '" stroke-width="18" ' +
|
|
577
|
+
'stroke-dasharray="' + dashLen.toFixed(2) + ' ' + (circumference - dashLen).toFixed(2) + '" ' +
|
|
578
|
+
'stroke-dashoffset="' + (-cumOffset).toFixed(2) + '" ' +
|
|
579
|
+
'transform="rotate(-90 90 90)" stroke-linecap="round"/>';
|
|
580
|
+
cumOffset += dashLen;
|
|
581
|
+
return segment;
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
var legend = mechs
|
|
585
|
+
.map(function (m) {
|
|
586
|
+
return '<div class="legend-item">' +
|
|
587
|
+
'<span class="legend-dot" style="background: ' + (MECH_COLORS[m.name] || '#64748b') + '"></span>' +
|
|
588
|
+
'<span class="legend-name">' + (MECH_LABELS[m.name] || m.name) + '</span>' +
|
|
589
|
+
'<span class="legend-count">' + m.count + ' (' + Math.round((m.count / total) * 100) + '%)</span>' +
|
|
590
|
+
'</div>';
|
|
591
|
+
})
|
|
592
|
+
.join('');
|
|
593
|
+
|
|
594
|
+
el.innerHTML =
|
|
595
|
+
'<div class="donut-container">' +
|
|
596
|
+
'<div class="donut-wrap">' +
|
|
597
|
+
'<svg viewBox="0 0 180 180" class="donut-svg">' +
|
|
598
|
+
circles.join('') +
|
|
599
|
+
'</svg>' +
|
|
600
|
+
'<div class="donut-center">' +
|
|
601
|
+
'<span class="donut-total">' + total + '</span>' +
|
|
602
|
+
'<span class="donut-total-label">total</span>' +
|
|
603
|
+
'</div>' +
|
|
604
|
+
'</div>' +
|
|
605
|
+
'<div class="donut-legend">' +
|
|
606
|
+
legend +
|
|
607
|
+
'</div>' +
|
|
608
|
+
'</div>';
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ── Delegation Outcome Chart ──────────────────────────────
|
|
612
|
+
|
|
613
|
+
function renderDelegationOutcomeChart(delegations) {
|
|
614
|
+
var el = document.getElementById('delegation-outcome-chart');
|
|
615
|
+
if (!el) return;
|
|
616
|
+
|
|
617
|
+
if (delegations.length === 0) {
|
|
618
|
+
el.innerHTML =
|
|
619
|
+
'<div class="empty-state"><div class="empty-state__icon">\uD83D\uDCCA</div><p class="empty-state__text">No delegation data available</p></div>';
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
var OUTCOME_COLORS = {
|
|
624
|
+
success: '#22c55e',
|
|
625
|
+
partial: '#f59e0b',
|
|
626
|
+
failed: '#ef4444',
|
|
627
|
+
redirected: '#64748b',
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
var outcomeCounts = {};
|
|
631
|
+
delegations.forEach(function (d) {
|
|
632
|
+
var outcome = d.outcome || 'unknown';
|
|
633
|
+
outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
var outcomes = Object.entries(outcomeCounts).sort(function (a, b) { return b[1] - a[1]; });
|
|
637
|
+
var maxCount = Math.max.apply(null, outcomes.map(function (o) { return o[1]; }));
|
|
638
|
+
|
|
639
|
+
el.innerHTML = outcomes
|
|
640
|
+
.map(function (entry) {
|
|
641
|
+
var name = entry[0];
|
|
642
|
+
var count = entry[1];
|
|
643
|
+
return '<div class="bar-row">' +
|
|
644
|
+
'<span class="bar-label">' + escapeHtml(name) + '</span>' +
|
|
645
|
+
'<div class="bar-track">' +
|
|
646
|
+
'<div class="bar-segment" style="width: ' + ((count / maxCount) * 100).toFixed(1) + '%; background: ' + (OUTCOME_COLORS[name] || '#64748b') + '"></div>' +
|
|
647
|
+
'</div>' +
|
|
648
|
+
'<span class="bar-value">' + count + '</span>' +
|
|
649
|
+
'</div>';
|
|
650
|
+
})
|
|
651
|
+
.join('');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ── Timeline Chart ────────────────────────────────────────
|
|
655
|
+
|
|
656
|
+
function renderTimelineChart(sessions, delegations) {
|
|
657
|
+
const el = document.getElementById('timeline-chart');
|
|
658
|
+
if (!el) return;
|
|
659
|
+
|
|
660
|
+
const dateMap = {};
|
|
661
|
+
sessions.forEach((s) => {
|
|
662
|
+
const key = s.timestamp.slice(0, 10);
|
|
663
|
+
if (!dateMap[key]) dateMap[key] = { sessions: 0, delegations: 0 };
|
|
664
|
+
dateMap[key].sessions++;
|
|
665
|
+
});
|
|
666
|
+
delegations.forEach((d) => {
|
|
667
|
+
const key = d.timestamp.slice(0, 10);
|
|
668
|
+
if (!dateMap[key]) dateMap[key] = { sessions: 0, delegations: 0 };
|
|
669
|
+
dateMap[key].delegations++;
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const dates = Object.entries(dateMap).sort(([a], [b]) =>
|
|
673
|
+
a.localeCompare(b)
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
if (dates.length === 0) {
|
|
677
|
+
el.innerHTML =
|
|
678
|
+
'<div class="empty-state"><div class="empty-state__icon">\uD83D\uDCC5</div><p class="empty-state__text">No timeline data</p></div>';
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const maxVal = Math.max(
|
|
683
|
+
...dates.map(([, d]) => Math.max(d.sessions, d.delegations))
|
|
684
|
+
);
|
|
685
|
+
const w = 500;
|
|
686
|
+
const h = 180;
|
|
687
|
+
const pad = { top: 10, right: 10, bottom: 28, left: 10 };
|
|
688
|
+
const plotW = w - pad.left - pad.right;
|
|
689
|
+
const plotH = h - pad.top - pad.bottom;
|
|
690
|
+
const groupWidth = plotW / dates.length;
|
|
691
|
+
const barWidth = Math.min(16, groupWidth * 0.35);
|
|
692
|
+
|
|
693
|
+
let rects = '';
|
|
694
|
+
let labels = '';
|
|
695
|
+
|
|
696
|
+
dates.forEach(([date, data], i) => {
|
|
697
|
+
const x = pad.left + i * groupWidth + groupWidth / 2;
|
|
698
|
+
const sH = maxVal > 0 ? (data.sessions / maxVal) * plotH : 0;
|
|
699
|
+
const dH = maxVal > 0 ? (data.delegations / maxVal) * plotH : 0;
|
|
700
|
+
|
|
701
|
+
rects +=
|
|
702
|
+
'<rect x="' +
|
|
703
|
+
(x - barWidth - 1).toFixed(1) +
|
|
704
|
+
'" y="' +
|
|
705
|
+
(pad.top + plotH - sH).toFixed(1) +
|
|
706
|
+
'" width="' +
|
|
707
|
+
barWidth.toFixed(1) +
|
|
708
|
+
'" height="' +
|
|
709
|
+
sH.toFixed(1) +
|
|
710
|
+
'" fill="#3b82f6" rx="3" opacity="0.85"/>';
|
|
711
|
+
rects +=
|
|
712
|
+
'<rect x="' +
|
|
713
|
+
(x + 1).toFixed(1) +
|
|
714
|
+
'" y="' +
|
|
715
|
+
(pad.top + plotH - dH).toFixed(1) +
|
|
716
|
+
'" width="' +
|
|
717
|
+
barWidth.toFixed(1) +
|
|
718
|
+
'" height="' +
|
|
719
|
+
dH.toFixed(1) +
|
|
720
|
+
'" fill="#a78bfa" rx="3" opacity="0.65"/>';
|
|
721
|
+
labels +=
|
|
722
|
+
'<text x="' +
|
|
723
|
+
x.toFixed(1) +
|
|
724
|
+
'" y="' +
|
|
725
|
+
(h - 6) +
|
|
726
|
+
'" text-anchor="middle" fill="#5a5a6e" font-size="10">' +
|
|
727
|
+
formatShortDate(date) +
|
|
728
|
+
'</text>';
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
el.innerHTML =
|
|
732
|
+
'<svg viewBox="0 0 ' +
|
|
733
|
+
w +
|
|
734
|
+
' ' +
|
|
735
|
+
h +
|
|
736
|
+
'" class="timeline-svg" preserveAspectRatio="xMidYMid meet">' +
|
|
737
|
+
rects +
|
|
738
|
+
labels +
|
|
739
|
+
'</svg>' +
|
|
740
|
+
'<div class="timeline-legend">' +
|
|
741
|
+
'<div class="timeline-legend__item">' +
|
|
742
|
+
'<span class="timeline-legend__dot" style="background: #3b82f6"></span>' +
|
|
743
|
+
'Sessions</div>' +
|
|
744
|
+
'<div class="timeline-legend__item">' +
|
|
745
|
+
'<span class="timeline-legend__dot" style="background: #a78bfa"></span>' +
|
|
746
|
+
'Delegations</div>' +
|
|
747
|
+
'</div>';
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ── Model Chart ───────────────────────────────────────────
|
|
751
|
+
|
|
752
|
+
function renderModelChart(sessions) {
|
|
753
|
+
const el = document.getElementById('model-chart');
|
|
754
|
+
if (!el) return;
|
|
755
|
+
|
|
756
|
+
if (sessions.length === 0) {
|
|
757
|
+
el.innerHTML =
|
|
758
|
+
'<div class="empty-state"><div class="empty-state__icon">\uD83E\uDD16</div><p class="empty-state__text">No model data</p></div>';
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const modelCounts = {};
|
|
763
|
+
sessions.forEach((s) => {
|
|
764
|
+
modelCounts[s.model] = (modelCounts[s.model] || 0) + 1;
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const models = Object.entries(modelCounts).sort((a, b) => b[1] - a[1]);
|
|
768
|
+
const maxCount = Math.max(...models.map(([, c]) => c));
|
|
769
|
+
|
|
770
|
+
el.innerHTML = models
|
|
771
|
+
.map(
|
|
772
|
+
([name, count]) =>
|
|
773
|
+
'<div class="bar-row">' +
|
|
774
|
+
'<span class="bar-label">' +
|
|
775
|
+
escapeHtml(name) +
|
|
776
|
+
'</span>' +
|
|
777
|
+
'<div class="bar-track">' +
|
|
778
|
+
'<div class="bar-segment" style="width: ' +
|
|
779
|
+
((count / maxCount) * 100).toFixed(1) +
|
|
780
|
+
'%; background: ' +
|
|
781
|
+
(MODEL_COLORS[name] || '#64748b') +
|
|
782
|
+
'"></div>' +
|
|
783
|
+
'</div>' +
|
|
784
|
+
'<span class="bar-value">' +
|
|
785
|
+
count +
|
|
786
|
+
'</span>' +
|
|
787
|
+
'</div>'
|
|
788
|
+
)
|
|
789
|
+
.join('');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ── Execution Log ─────────────────────────────────────────
|
|
793
|
+
|
|
794
|
+
function renderExecutionLog(sessions) {
|
|
795
|
+
const el = document.getElementById('execution-log');
|
|
796
|
+
if (!el) return;
|
|
797
|
+
|
|
798
|
+
const sorted = sessions
|
|
799
|
+
.slice()
|
|
800
|
+
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
|
801
|
+
.slice(0, 10);
|
|
802
|
+
|
|
803
|
+
if (sorted.length === 0) {
|
|
804
|
+
el.innerHTML =
|
|
805
|
+
'<div class="empty-state"><div class="empty-state__icon">\uD83D\uDCDD</div><p class="empty-state__text">No sessions recorded yet</p></div>';
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
el.innerHTML =
|
|
810
|
+
'<div class="exec-log">' +
|
|
811
|
+
sorted
|
|
812
|
+
.map(
|
|
813
|
+
(s, i) =>
|
|
814
|
+
'<div class="exec-step">' +
|
|
815
|
+
'<div class="exec-step__indicator">' +
|
|
816
|
+
'<div class="exec-step__dot exec-step__dot--' +
|
|
817
|
+
s.outcome +
|
|
818
|
+
'">' +
|
|
819
|
+
(OUTCOME_ICONS[s.outcome] || '') +
|
|
820
|
+
'</div>' +
|
|
821
|
+
(i < sorted.length - 1
|
|
822
|
+
? '<div class="exec-step__line"></div>'
|
|
823
|
+
: '') +
|
|
824
|
+
'</div>' +
|
|
825
|
+
'<div class="exec-step__content">' +
|
|
826
|
+
'<div class="exec-step__header">' +
|
|
827
|
+
'<span class="exec-step__agent">' +
|
|
828
|
+
escapeHtml(s.agent) +
|
|
829
|
+
'</span>' +
|
|
830
|
+
'<span class="exec-step__badge exec-step__badge--' +
|
|
831
|
+
s.outcome +
|
|
832
|
+
'">' +
|
|
833
|
+
s.outcome +
|
|
834
|
+
'</span>' +
|
|
835
|
+
'</div>' +
|
|
836
|
+
'<div class="exec-step__task">' +
|
|
837
|
+
escapeHtml(s.task) +
|
|
838
|
+
'</div>' +
|
|
839
|
+
'<div class="exec-step__meta">' +
|
|
840
|
+
'<span class="exec-step__meta-item">\uD83D\uDD52 ' +
|
|
841
|
+
formatTime(s.timestamp) +
|
|
842
|
+
'</span>' +
|
|
843
|
+
(s.duration_min != null
|
|
844
|
+
? '<span class="exec-step__meta-item">\u23F1 ' +
|
|
845
|
+
s.duration_min +
|
|
846
|
+
'm</span>'
|
|
847
|
+
: '') +
|
|
848
|
+
(s.files_changed != null
|
|
849
|
+
? '<span class="exec-step__meta-item">\uD83D\uDCC1 ' +
|
|
850
|
+
s.files_changed +
|
|
851
|
+
' files</span>'
|
|
852
|
+
: '') +
|
|
853
|
+
(s.model
|
|
854
|
+
? '<span class="exec-step__meta-item">\uD83E\uDD16 ' +
|
|
855
|
+
escapeHtml(s.model) +
|
|
856
|
+
'</span>'
|
|
857
|
+
: '') +
|
|
858
|
+
(s.retries > 0
|
|
859
|
+
? '<span class="exec-step__meta-item">\uD83D\uDD04 ' +
|
|
860
|
+
s.retries +
|
|
861
|
+
' retries</span>'
|
|
862
|
+
: '') +
|
|
863
|
+
(s.lessons_added && s.lessons_added.length > 0
|
|
864
|
+
? '<span class="exec-step__meta-item">\uD83D\uDCA1 ' +
|
|
865
|
+
s.lessons_added.length +
|
|
866
|
+
' lessons</span>'
|
|
867
|
+
: '') +
|
|
868
|
+
(s.discoveries && s.discoveries.length > 0
|
|
869
|
+
? '<span class="exec-step__meta-item">\uD83D\uDD0D ' +
|
|
870
|
+
s.discoveries.length +
|
|
871
|
+
' discoveries</span>'
|
|
872
|
+
: '') +
|
|
873
|
+
'</div>' +
|
|
874
|
+
'</div>' +
|
|
875
|
+
'</div>'
|
|
876
|
+
)
|
|
877
|
+
.join('') +
|
|
878
|
+
'</div>';
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ── Panel Chart ───────────────────────────────────────────
|
|
882
|
+
|
|
883
|
+
function renderPanelChart(panels) {
|
|
884
|
+
const el = document.getElementById('panel-chart');
|
|
885
|
+
if (!el) return;
|
|
886
|
+
|
|
887
|
+
if (panels.length === 0) {
|
|
888
|
+
el.innerHTML =
|
|
889
|
+
'<div class="empty-state"><div class="empty-state__icon">\uD83D\uDEE1\uFE0F</div><p class="empty-state__text">No panel reviews yet</p></div>';
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
el.innerHTML =
|
|
894
|
+
'<div class="panel-grid">' +
|
|
895
|
+
panels
|
|
896
|
+
.map(
|
|
897
|
+
(p) =>
|
|
898
|
+
'<div class="panel-item">' +
|
|
899
|
+
'<div class="panel-item__header">' +
|
|
900
|
+
'<span class="panel-item__key">' +
|
|
901
|
+
escapeHtml(p.panel_key) +
|
|
902
|
+
'</span>' +
|
|
903
|
+
'<span class="panel-item__verdict panel-item__verdict--' +
|
|
904
|
+
p.verdict +
|
|
905
|
+
'">' +
|
|
906
|
+
p.verdict +
|
|
907
|
+
'</span>' +
|
|
908
|
+
'</div>' +
|
|
909
|
+
'<div class="panel-item__votes">' +
|
|
910
|
+
Array.from({ length: p.pass_count })
|
|
911
|
+
.map(
|
|
912
|
+
() =>
|
|
913
|
+
'<div class="panel-item__vote panel-item__vote--pass">\u2713</div>'
|
|
914
|
+
)
|
|
915
|
+
.join('') +
|
|
916
|
+
Array.from({ length: p.block_count })
|
|
917
|
+
.map(
|
|
918
|
+
() =>
|
|
919
|
+
'<div class="panel-item__vote panel-item__vote--block">\u2717</div>'
|
|
920
|
+
)
|
|
921
|
+
.join('') +
|
|
922
|
+
'</div>' +
|
|
923
|
+
'<div class="panel-item__fixes">' +
|
|
924
|
+
(p.must_fix > 0
|
|
925
|
+
? '<strong>' + p.must_fix + ' must-fix</strong>'
|
|
926
|
+
: '') +
|
|
927
|
+
(p.must_fix > 0 && p.should_fix > 0 ? ' \u00B7 ' : '') +
|
|
928
|
+
(p.should_fix > 0 ? p.should_fix + ' should-fix' : '') +
|
|
929
|
+
(p.must_fix === 0 && p.should_fix === 0 ? 'Clean' : '') +
|
|
930
|
+
'</div>' +
|
|
931
|
+
'<div class="panel-item__meta">' +
|
|
932
|
+
'<span class="panel-item__meta-item">\uD83E\uDD16 ' + escapeHtml(p.reviewer_model || 'unknown') + '</span>' +
|
|
933
|
+
(p.attempt > 1 ? '<span class="panel-item__meta-item">\uD83D\uDD04 attempt ' + p.attempt + '</span>' : '') +
|
|
934
|
+
(p.artifacts_count ? '<span class="panel-item__meta-item">\uD83D\uDCC4 ' + p.artifacts_count + ' artifacts</span>' : '') +
|
|
935
|
+
'</div>' +
|
|
936
|
+
'</div>'
|
|
937
|
+
)
|
|
938
|
+
.join('') +
|
|
939
|
+
'</div>';
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ── Sessions Table ────────────────────────────────────────
|
|
943
|
+
|
|
944
|
+
function renderSessionsTable(sessions) {
|
|
945
|
+
const el = document.getElementById('sessions-table');
|
|
946
|
+
if (!el) return;
|
|
947
|
+
|
|
948
|
+
const sorted = sessions
|
|
949
|
+
.slice()
|
|
950
|
+
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
|
951
|
+
.slice(0, 15);
|
|
952
|
+
|
|
953
|
+
if (sorted.length === 0) {
|
|
954
|
+
el.innerHTML =
|
|
955
|
+
'<div class="empty-state"><div class="empty-state__icon">\uD83D\uDCCB</div><p class="empty-state__text">No sessions recorded</p></div>';
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
el.innerHTML =
|
|
960
|
+
'<table class="sessions-table">' +
|
|
961
|
+
'<thead><tr>' +
|
|
962
|
+
'<th>Timestamp</th>' +
|
|
963
|
+
'<th>Agent</th>' +
|
|
964
|
+
'<th>Task</th>' +
|
|
965
|
+
'<th>Outcome</th>' +
|
|
966
|
+
'<th>Duration</th>' +
|
|
967
|
+
'<th>Files</th>' +
|
|
968
|
+
'<th>Retries</th>' +
|
|
969
|
+
'<th>Issue</th>' +
|
|
970
|
+
'</tr></thead>' +
|
|
971
|
+
'<tbody>' +
|
|
972
|
+
sorted
|
|
973
|
+
.map(
|
|
974
|
+
(s) =>
|
|
975
|
+
'<tr>' +
|
|
976
|
+
'<td>' +
|
|
977
|
+
formatTime(s.timestamp) +
|
|
978
|
+
'</td>' +
|
|
979
|
+
'<td class="td-agent">' +
|
|
980
|
+
escapeHtml(s.agent) +
|
|
981
|
+
'</td>' +
|
|
982
|
+
'<td class="td-task">' +
|
|
983
|
+
escapeHtml(s.task) +
|
|
984
|
+
'</td>' +
|
|
985
|
+
'<td><span class="outcome-badge outcome-badge--' +
|
|
986
|
+
s.outcome +
|
|
987
|
+
'">' +
|
|
988
|
+
s.outcome +
|
|
989
|
+
'</span></td>' +
|
|
990
|
+
'<td class="td-num">' +
|
|
991
|
+
(s.duration_min != null ? s.duration_min + 'm' : '\u2014') +
|
|
992
|
+
'</td>' +
|
|
993
|
+
'<td class="td-num">' +
|
|
994
|
+
(s.files_changed != null ? s.files_changed : '\u2014') +
|
|
995
|
+
'</td>' +
|
|
996
|
+
'<td class="td-num">' +
|
|
997
|
+
(s.retries != null ? s.retries : '\u2014') +
|
|
998
|
+
'</td>' +
|
|
999
|
+
'<td class="td-issue">' +
|
|
1000
|
+
(s.linear_issue ? escapeHtml(s.linear_issue) : '\u2014') +
|
|
1001
|
+
'</td>' +
|
|
1002
|
+
'</tr>'
|
|
1003
|
+
)
|
|
1004
|
+
.join('') +
|
|
1005
|
+
'</tbody></table>';
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// ── Main ──────────────────────────────────────────────────
|
|
1009
|
+
|
|
1010
|
+
async function main() {
|
|
1011
|
+
const [sessions, delegations, panels] = await Promise.all([
|
|
1012
|
+
loadNdjson(base + 'data/sessions.ndjson'),
|
|
1013
|
+
loadNdjson(base + 'data/delegations.ndjson'),
|
|
1014
|
+
loadNdjson(base + 'data/panels.ndjson'),
|
|
1015
|
+
]);
|
|
1016
|
+
|
|
1017
|
+
renderKpis(sessions, delegations);
|
|
1018
|
+
renderPipeline(delegations);
|
|
1019
|
+
renderAgentChart(sessions);
|
|
1020
|
+
renderTierChart(delegations);
|
|
1021
|
+
renderMechanismChart(delegations);
|
|
1022
|
+
renderDelegationOutcomeChart(delegations);
|
|
1023
|
+
renderTimelineChart(sessions, delegations);
|
|
1024
|
+
renderModelChart(sessions);
|
|
1025
|
+
renderExecutionLog(sessions);
|
|
1026
|
+
renderPanelChart(panels);
|
|
1027
|
+
renderSessionsTable(sessions);
|
|
1028
|
+
|
|
1029
|
+
// ── Sidebar Navigation ────────────────────────────────
|
|
1030
|
+
initSidebarNav();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function initSidebarNav() {
|
|
1034
|
+
const links = document.querySelectorAll('.dash-sidebar__link');
|
|
1035
|
+
const sections = document.querySelectorAll('[data-nav-section]');
|
|
1036
|
+
|
|
1037
|
+
// Intersection observer for active state
|
|
1038
|
+
const observer = new IntersectionObserver(
|
|
1039
|
+
(entries) => {
|
|
1040
|
+
entries.forEach((entry) => {
|
|
1041
|
+
if (entry.isIntersecting) {
|
|
1042
|
+
const id = entry.target.id;
|
|
1043
|
+
links.forEach((link) => {
|
|
1044
|
+
link.classList.toggle(
|
|
1045
|
+
'dash-sidebar__link--active',
|
|
1046
|
+
link.dataset.section === id
|
|
1047
|
+
);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
},
|
|
1052
|
+
{ rootMargin: '-20% 0px -70% 0px', threshold: 0 }
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
sections.forEach((s) => observer.observe(s));
|
|
1056
|
+
|
|
1057
|
+
// Smooth scroll on click
|
|
1058
|
+
links.forEach((link) => {
|
|
1059
|
+
link.addEventListener('click', (e) => {
|
|
1060
|
+
e.preventDefault();
|
|
1061
|
+
const target = document.getElementById(link.dataset.section);
|
|
1062
|
+
if (target) {
|
|
1063
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
main();
|
|
1070
|
+
</script>
|