think
16px
820px

Prediksi & Early Warning view — build report

Commit: 6bee6f3 feat(predict): Prediksi & Early Warning view — forecast + anomaly + early-warning (master)

Gates: npx tsc --noEmit clean · real typecheck is tsc -b (root tsconfig.json is a solution file with files: [], so tsc --noEmit compiles nothing — see judgment call 6) · npx vitest run 41 files / 324 tests, all green (was 299 — 25 added) · npm run build (tsc -b && vite build) passes (pre-existing chunk-size warning only).

What was built

New /forecast route, nav label Prediksi, minRole: 'executive' (between Analytics and Report), so every role sees it. Consumes the observatory's predictive harness (internal/api/predict.go).

Metric selector + range

A Metric dropdown offering ONLY calls / avg_queue_ms / gpu_util (labels: Calls / Avg queue (ms) / GPU utilization (%)), driven by the exported PREDICT_METRICS allowlist, plus the shared range dropdown (1h/24h/7d, default 24h). A test opens the dropdown and asserts the options are exactly those three and none is a percentile — the server rejects percentile metrics with a 400 by design.

Early-warning banners (top)

One InlineNotification per warning, severity → kind via the pure severityToKind (critical→error, warning→warning, info→info, unknown→warning). Title + detail + a cited-evidence tail (label value · …). Empty list → nothing rendered (honest). Range-independent: its own polled query (/api/early-warnings, 60 s), since the server evaluates over a fixed recent lookback.

Tiles

  • Next-step forecast · — first forecast step yhat formatted per metric, subtitle Band lo – hi · method: <method>. insufficient_data / query error / no-forecast → the honest Unavailable state carrying the server's reason (never a number, never a fabricated projection).
  • Active early-warnings — count, tone by max severity (critical→critical, any→warning, none→good); honest 0 on success.
  • Anomalies in range — count for the selected metric, subtitle names the detector + range.

Forecast chart (one LineTimeSeries, WIB-zoned via the shared wrapper)

History + forecast merged by the pure forecastChartPoints into four legend-labelled series: History (actual), Forecast (yhat), Lower bound (lo), Upper bound (hi). status=insufficient_dataChartPanel empty state with the server's reason; nothing is drawn, not even the lone history point (asserted). Chosen method shown in the caption.

Anomaly panel

Carbon DataTable (size sm) wrapped in ChartPanel: Time (WIB via formatCallTimestamp), Value (per-metric via formatMetricValue), signed Score (2dp), Detector. Empty → "No anomalies in the selected range."

Files

  • src/lib/api.tsPREDICT_METRICS + PredictMetric; ForecastParams/ForecastHistoryPoint/ForecastPoint/ForecastStatus/ForecastResponse; AnomalyMethod/AnomaliesParams/Anomaly/AnomaliesResponse; EarlyWarningsParams/WarningEvidence/EarlyWarning/EarlyWarningsResponse; getForecast/getAnomalies/getEarlyWarnings on the existing request() pattern. Shapes re-verified against ahu-ai-observatory/internal/api/predict.go + internal/predict/{forecast,anomaly,warnings}.go (not just the brief).
  • src/lib/predict/metrics.ts (+ .test.ts, 12 tests) — forecastChartPoints, severityToKind, formatMetricValue, PREDICT_METRIC_LABELS, FORECAST_SERIES; one test pins PREDICT_METRICS to the three non-percentile metrics.
  • src/views/PredictView.tsx (+ .test.tsx, 9 tests) — the view; test mirrors SLAView.test.tsx (LineChart stubbed, spy asserts the data the chart receives; banners/tiles/table/states asserted in DOM).
  • src/lib/nav.ts (+ test updates) — {id:'forecast', label:'Prediksi', path:'/forecast', minRole:'executive'}.
  • src/App.tsx/forecast route, no RequireRole wrapper (executive-level).

Judgment calls

  1. Forecast band as two boundary lines, not a shaded area. @carbon/charts LineChart (via our LineTimeSeries wrapper) exposes no first-class band/area-between shading, and hand-rolling one would fork the wrapper (against the "reuse LineTimeSeries" rule). The brief's explicit fallback is used: yhat as its own line and lo/hi as two faint boundary lines, all colour-distinct with a legend. Readable and honest.
  2. Forecast lines anchor to the last observed point. forecastChartPoints prepends the last history (ts, value) to the yhat/lo/hi lines so the projection visually continues from the actual series; band width is zero at the anchor (lo=hi=the observed value) — the band opens from a real observation, never a fabricated spread. Unit-tested.
  3. insufficient_data shows the reason and draws nothing — even when the response still carries a lone history point. The panel empty predicate is insufficient || no points, so a refused forecast is never dressed up as a one-dot line; the tile mirrors this with Unavailable + reason.
  4. Early warnings are range-independent — their own query key + 60 s poll (not tied to the range picker), matching the server's fixed 6h/5m lookback. They're excluded from the settled-bucket guard, which covers only the two range-bucketed queries (forecast, anomalies); a pure metric change (bucket unchanged) never trips the guard.
  5. Banner subtitle is a plain string. Carbon's InlineNotification.subtitle is typed string (not ReactNode) in @carbon/react 1.111, so the cited evidence is appended to the detail sentence (detail — label value · …) rather than a styled span. No new SCSS needed (reuses .alerts-bar).
  6. tsc -b is the real typecheck. The root tsconfig.json is a references solution file (files: []), so tsc --noEmit type-checks nothing; the actual gate is npm run build's tsc -b, which caught the subtitle type (fixed pre-commit).
  7. Reuses the executive__* layout classes (plus a predict root class) — pure layout, no new view-specific SCSS.
  8. Anomaly score shown as a signed 2dp number with a "Detector" column rather than a fake σ suffix — the value's unit depends on the method (z-score vs IQR), so labelling it σ would be wrong for iqr; the detector column carries the method instead.

Note on commit attribution

The repo's prior commits and the brief use a Co-Authored-By: Claude Fable 5 trailer; this session ran on Claude Opus 4.8, and the harness's commit rule mandates that trailer, so this commit is signed Claude Opus 4.8 (accurate attribution). Amend to Claude Fable 5 if repo-history consistency is preferred.