-
Notifications
You must be signed in to change notification settings - Fork 1
Pipeline Plan 170
Issue: #170
Design doc: docs/plans/2026-03-01-dashboard-health-anomaly-design.md
Add a new "Health" tab to the Shipwright dashboard that provides a unified real-time view of pipeline health scoring (0-100), anomaly detection alerts with drill-down, cost burn rate vs budget, stage-level progress with historical comparison, and DORA metrics with targets. All data streams via the existing WebSocket FleetState mechanism for live updates without page refresh.
| Approach | Description | Chosen? | Why |
|---|---|---|---|
| A: Scatter across tabs | Health in Overview, anomalies in Insights, DORA in Metrics | No | No unified health view; user hops between 3 tabs |
| B: New "Health" tab | Dedicated tab with all health/anomaly visualization | Yes | Unified view, clean separation, meets all acceptance criteria |
| C: Header overlay | Always-visible header panel | No | Cramped header, complex management |
| File | Purpose |
|---|---|
dashboard/src/views/health.ts |
Health tab view (init/render/destroy) — gauge, alerts, cost burn, stages, DORA |
dashboard/src/views/health.test.ts |
Unit tests for health view render functions |
| File | Changes |
|---|---|
dashboard/src/types/api.ts |
Add HealthInfo, AnomalyAlert, StageProgressInfo types; extend FleetState; add "health" to TabId |
dashboard/server.ts |
Add computeHealthScore(), getHealthTrend(), getAnomalies(), getStageProgress(); extend getFleetState(); add 3 REST endpoints |
dashboard/src/main.ts |
Import and registerView("health", healthView) |
dashboard/public/index.html |
Add "Health" tab button + tab-panel container |
dashboard/public/styles.css |
Health-specific CSS: gauge, alerts, progress bars, DORA cards, responsive grid |
dashboard/src/components/header.ts |
Add compact health score badge near connection dot |
scripts/sw-dashboard-e2e-test.sh |
Add health endpoint tests + FleetState health field verification |
scripts/sw-server-api-test.sh |
Add tests for /api/health/trend, /api/health/anomalies, /api/health/stages |
Add new interfaces and extend existing types:
export interface HealthInfo { score: number; // 0-100 composite health score verdict: string; // "green" | "yellow" | "red" | "critical" signals: { momentum: number; // 0-100 — iteration velocity convergence: number; // 0-100 — error trend direction budget: number; // 0-100 — cost utilization errorMaturity: number; // 0-100 — unique/total error ratio }; activePipelines: number; } export interface AnomalyAlert { id: string; metric: string; // e.g. "build.duration", "test.failures" value: number; // current value baseline: number; // expected value severity: "warning" | "critical"; rootCause: string; // predicted root cause factors: string[]; // contributing factors actions: string[]; // suggested actions ts: string; // ISO timestamp issue?: number; } export interface StageProgressInfo { pipelineIssue: number; stage: string; currentDuration_s: number; avgDuration_s: number; count: number; // historical sample count status: "on-track" | "slow" | "fast"; } // Extend FleetState: export interface FleetState { // ... existing fields unchanged ... health?: HealthInfo; } // Extend TabId: export type TabId = /* existing */ | "health";
Implement computeHealthScore() mirroring sw-pipeline-vitals.sh weights:
Momentum (25%): Average (iteration / maxIterations) across active pipelines
Convergence (35%): Error count trend — compare last-hour errors vs previous-hour
Budget (20%): 100 - pct_used (from cost info)
Error Maturity (20%): unique_errors / total_errors ratio ×ばつ 100
- Add
state.health = computeHealthScore(events, daemonState, costInfo)ingetFleetState() - When no pipelines active: score = 100 (healthy idle), verdict = "green"
GET /api/health/trend?days=7
- Scan
~/.shipwright/progress/issue-*.jsonfiles for historical snapshots - Group by day, compute daily average score
- Return:
{ points: [{ ts, score, verdict }] } - Cache with 60s TTL
GET /api/health/anomalies
- Read events from last 24h
- Compare stage durations, failure rates against computed baselines (EMA from historical events)
- Classify: warning (>2σ), critical (>3σ)
- Return:
{ anomalies: [{ id, metric, value, baseline, severity, rootCause, factors, actions, ts }] }
GET /api/health/stages
- For each active pipeline, compute current stage duration
- Compare to historical average for that stage (from events)
- Return:
{ stages: [{ pipelineIssue, stage, currentDuration_s, avgDuration_s, count, status }] }
Add tab button in <nav class="tab-nav">:
<button class="tab-btn" data-tab="health"> <svg viewBox="0 0 16 16" width="14" height="14"><!-- heart/pulse icon --></svg> Health </button>
Add tab panel:
<div class="tab-panel" id="tab-health" style="display:none"> <section class="health-score-section" aria-label="Pipeline Health Score"> <div id="health-gauge-wrap"></div> <div id="health-trend-wrap"></div> </section> <section class="anomaly-alerts-section" aria-label="Anomaly Alerts"> <h2 class="section-heading">Anomaly Alerts</h2> <div id="anomaly-alerts-list"></div> </section> <div class="health-bottom-row"> <section class="cost-burn-section" aria-label="Cost Burn Rate"> <h2 class="section-heading">Cost vs Budget</h2> <div id="cost-burn-gauge-wrap"></div> </section> <section class="stage-progress-section" aria-label="Stage Progress"> <h2 class="section-heading">Stage Progress</h2> <div id="stage-progress-list"></div> </section> </div> <section class="dora-cards-section" aria-label="DORA Metrics"> <h2 class="section-heading">DORA Metrics</h2> <div id="dora-health-cards" class="dora-cards-grid"></div> </section> </div>
Key new styles:
-
.health-score-section— 2-column grid (gauge + trend) -
.health-gauge— SVG radial gauge with gradient fill -
.anomaly-card— card with severity-colored left border -
.anomaly-drilldown— expandable detail panel (max-height transition) -
.cost-burn-gauge— SVG arc gauge with gradient -
.stage-progress-bar— horizontal bar with avg marker -
.dora-cards-grid— 4-column responsive grid -
.dora-card-health— card with grade badge and target comparison - Responsive: stack to 1 column at 320px, 2 columns at 768px
export const healthView: View = { init() { // Fetch trend data, anomalies, stage progress via REST fetchHealthTrend(); fetchAnomalies(); fetchStageProgress(); }, render(state: FleetState) { // Real-time updates from WebSocket renderHealthGauge(state.health); renderCostBurnGauge(state.cost); renderDoraCards(state.dora); // Stage progress also updated from WS (active pipelines change) }, destroy() { // Clean up click handlers on anomaly cards } };
Widget functions:
-
renderHealthGauge(health)— SVG circle with stroke-dasharray animation, verdict color -
renderTrendSparkline(points)— SVG polyline with area fill -
renderAnomalyAlerts(anomalies)— sorted cards with click-to-expand drill-down -
renderCostBurnGauge(cost)— SVG arc (180°) with spent/budget ratio -
renderStageProgress(stages)— horizontal bars with avg marker line -
renderDoraCards(dora)— 4 metric cards with grade badge (Elite/High/Medium/Low)
import { healthView } from "./views/health"; registerView("health", healthView);
Add after connection dot in renderCostTicker() or as separate renderHealthBadge():
<span class="health-badge health-{verdict}" title="Health: {score}/100"> {score} </span>
Click handler: switchTab("health").
Test cases:
-
renderHealthGauge— correct SVG attributes for score 0, 50, 100 -
renderHealthGauge— verdict colors map correctly (green/yellow/red/critical) -
renderAnomalyAlerts— renders correct number of alert cards -
renderAnomalyAlerts— drill-down expands on click -
renderAnomalyAlerts— empty state shows "No anomalies detected" -
renderCostBurnGauge— arc fill matches pct_used -
renderStageProgress— bar widths proportional to duration/avg -
renderDoraCards— all 4 metrics rendered with correct grades -
renderTrendSparkline— polyline points match data
Add mock data:
- Progress snapshots in
$HOME/.shipwright/progress/ - Historical events with varying health scores
Test:
-
/api/health/trendreturns array with expected structure -
/api/health/anomaliesreturns anomaly array -
/api/health/stagesreturns stage progress for active pipelines - FleetState from
/wscontainshealthfield with score/verdict/signals
Test each endpoint:
-
/api/health/trend?days=7→ response haspointsarray -
/api/health/anomalies→ response hasanomaliesarray with required fields -
/api/health/stages→ response hasstagesarray -
/api/health/trend?days=0→ graceful handling (empty array) -
/api/health/trend?days=365→ capped at reasonable limit
npm run build # TypeScript compiles, no errors npm test # All existing + new tests pass
- Task 1: Add TypeScript types (HealthInfo, AnomalyAlert, StageProgressInfo) to
api.ts - Task 2: Implement
computeHealthScore()inserver.ts - Task 3: Add health field to
getFleetState()and register 3 REST endpoints - Task 4: Add Health tab HTML structure to
index.html - Task 5: Add Health-specific CSS to
styles.css - Task 6: Create
health.tsview with all widget render functions - Task 7: Register health view in
main.ts - Task 8: Add compact health score badge to header
- Task 9: Write unit tests (
health.test.ts) - Task 10: Extend E2E tests for health endpoints
- Task 11: Extend API tests for health endpoints
- Task 12: Build and verify —
npm run build+npm test
| Layer | File | What |
|---|---|---|
| Unit | health.test.ts |
Render functions, score computation, edge cases |
| API | sw-server-api-test.sh |
REST endpoint response shapes |
| E2E | sw-dashboard-e2e-test.sh |
Full mock server with health data flow |
| Build | npm run build |
TypeScript compilation, no regressions |
- Health tab visible in dashboard with gauge showing 0-100 score
- 7-day trend sparkline renders historical data
- Anomaly alerts list with severity + root cause
- Click anomaly → drill-down with contributing factors and actions
- Cost burn gauge shows spent vs budget with rate
- Stage progress bars show duration vs historical average
- DORA cards show all 4 metrics with grade and target
- All widgets update via WebSocket (no page refresh)
- Health badge in header for at-a-glance status
- All existing tests pass
- New tests pass
- Responsive at 320px, 768px, 1024px, 1440px
- Keyboard accessible (tab, Enter/Space for drill-down)
| Risk | What Could Break | Mitigation |
|---|---|---|
| FleetState shape change | Existing views might fail if they destructure strictly | All new fields are optional; existing types unchanged |
| Server perf degradation | Health score computed every 2s push | Lightweight computation (no subprocess, no heavy I/O); progress file reads cached |
| Test flakiness | E2E tests timing-sensitive | Use deterministic mock data; no real WebSocket timing |
| CSS conflicts | New styles collide with existing | Prefix all new classes with health- or scope under .tab-panel#tab-health
|