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 yhatformatted per metric, subtitleBand lo – hi · method: <method>.insufficient_data/ query error / no-forecast → the honest Unavailable state carrying the server'sreason(never a number, never a fabricated projection). - Active early-warnings — count, tone by max severity (critical→critical, any→warning, none→good); honest
0on 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_data → ChartPanel 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.ts—PREDICT_METRICS+PredictMetric;ForecastParams/ForecastHistoryPoint/ForecastPoint/ForecastStatus/ForecastResponse;AnomalyMethod/AnomaliesParams/Anomaly/AnomaliesResponse;EarlyWarningsParams/WarningEvidence/EarlyWarning/EarlyWarningsResponse;getForecast/getAnomalies/getEarlyWarningson the existingrequest()pattern. Shapes re-verified againstahu-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 pinsPREDICT_METRICSto the three non-percentile metrics.src/views/PredictView.tsx(+.test.tsx, 9 tests) — the view; test mirrorsSLAView.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—/forecastroute, noRequireRolewrapper (executive-level).
Judgment calls
- Forecast band as two boundary lines, not a shaded area.
@carbon/chartsLineChart(via ourLineTimeSerieswrapper) 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:yhatas its own line andlo/hias two faint boundary lines, all colour-distinct with a legend. Readable and honest. - Forecast lines anchor to the last observed point.
forecastChartPointsprepends 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. insufficient_datashows the reason and draws nothing — even when the response still carries a lone history point. The panelemptypredicate isinsufficient || no points, so a refused forecast is never dressed up as a one-dot line; the tile mirrors this with Unavailable + reason.- 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.
- Banner subtitle is a plain string. Carbon's
InlineNotification.subtitleis typedstring(notReactNode) in@carbon/react1.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). tsc -bis the real typecheck. The roottsconfig.jsonis a references solution file (files: []), sotsc --noEmittype-checks nothing; the actual gate isnpm run build'stsc -b, which caught the subtitle type (fixed pre-commit).- Reuses the
executive__*layout classes (plus apredictroot class) — pure layout, no new view-specific SCSS. - 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 foriqr; 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.