From 0ce735e4d77b6b90fe5191ce53f5af709567daaf Mon Sep 17 00:00:00 2001 From: Dzarlax Date: Sat, 6 Jun 2026 14:51:05 +0200 Subject: [PATCH] Clarify EnergyBank v2 cleanup state --- ENERGY_BANK.md | 16 +- SCORING.md | 26 +-- cmd/server/main.go | 9 +- ...-issue-148-energybank-v1-cleanup-docs.html | 167 ++++++++++++++++++ internal/health/energy.go | 12 +- internal/health/energy_v2.go | 11 +- internal/health/formula_invariants_test.go | 10 +- internal/health/scoring.go | 9 +- internal/storage/energy_history.go | 22 ++- internal/storage/energy_snapshot_read.go | 2 +- internal/storage/energy_v2_orchestrator.go | 13 +- internal/ui/handler.go | 8 +- internal/ui/static/charts.js | 4 +- internal/ui/templates/pages/dashboard.html | 4 +- 14 files changed, 237 insertions(+), 76 deletions(-) create mode 100644 docs/ai-plans/2026-06-06-issue-148-energybank-v1-cleanup-docs.html diff --git a/ENERGY_BANK.md b/ENERGY_BANK.md index 1031ac7..eec4ac6 100644 --- a/ENERGY_BANK.md +++ b/ENERGY_BANK.md @@ -1,6 +1,6 @@ -# EnergyBank v2 — Design +# EnergyBank v2 — Current Implementation -Status: design, not implemented. v1 (current `internal/health/energy.go`) is a stateless daily snapshot whose strain scale saturates on typical days; v2 replaces it with a Bevel-style continuous battery integrating drain and restore over time, with cross-day carryover. +Status: shipped. EnergyBank v2 stores current bank state in `energy_snapshots`, drives dashboard/report rendering when a same-day snapshot exists, and serves the hourly history chart from those rows. The old stateless daily formula in `internal/health/energy.go` remains only as a legacy fallback for fresh tenants or days before the first v2 snapshot lands. ## Model @@ -10,8 +10,8 @@ bank[t] = bank[t−1] + restore(t−1 → t) − drain(t−1 → t) State machine. Each event recomputes the bank for the affected interval and writes a snapshot. No daily reset — yesterday's residual carries through sleep restoration into today's morning capacity. -- **Live current** (dashboard hero, Telegram report): computed on read = latest stored snapshot + delta from `metric_points` since that snapshot. Always reflects "now", independent of write cadence. -- **History** (sparkline, hourly graph in `
`): reads `energy_snapshots` directly. +- **Live current** (dashboard hero, Telegram report): read from the latest same-day stored snapshot, then projected into the shared `health.EnergyBank` response shape. +- **History** (hourly graph in `
`): reads `energy_snapshots` directly. ## Storage @@ -59,7 +59,7 @@ bootstrap_tail — within first 3 days after formula_version bump (transient v2.5+ extends with `calibration_alpha_tuned`, `calibration_alert`, `data_quality_warn` — no schema change required, just new string constants in Go. -`daily_scores.energy_*` columns become a roll-up (last bucket of the day) — no longer the source of truth. +`daily_scores.energy_*` columns are legacy compatibility columns. They are no longer the source of truth and must not be dropped without an explicit reversible migration. ## Cadence — event-driven, not timer @@ -211,7 +211,7 @@ Distribution of `bank_eod` under asymptotic restore: min=3, p10=17, p25=34, medi ## Phased delivery -**v2.0 — skeleton.** Schema + event-driven recompute + sleep-only restore + UI hourly chart in hero `
` + live read with compute-on-read. Constants set to plausible starting values, calibration deferred. Old `EnergyBank` formula and verdict thresholds stay live until v2.0 is validated; AI orchestrator continues reading the daily snapshot field. +**v2.0 — shipped skeleton.** Schema + event-driven recompute + sleep-only restore + UI hourly chart in hero `
` + latest-snapshot read. Constants set to plausible starting values, calibration deferred. The old `EnergyBank` formula is retained only as a cold-start fallback. **v2.1 — calibration.** Verdict thresholds are re-derived from the user's own day-level `energy_snapshots` distribution: one latest eligible snapshot per local date, never raw intraday rows. Mature personal calibration requires 30 distinct eligible dates for the current formula (`personal_latest_formula`) or for explicitly compatible formula versions during warmup (`personal_mixed_formula_warmup`). A guarded provisional mode exists for 20-29 compatible dates (`provisional_compatible_formula_warmup`): it uses the same 180-day window, clamps Rest / Recovery / PushHard so they are not more permissive than defaults, and exposes `UsedDays`, `LatestFormulaDays`, and `CompatibleFormulaDays` so clients do not mistake it for mature calibration. β stays at 0 (the autonomic-load term is a v2.2 piece, not v2.1) — tuning it here against pre-rubric data would fit it to whatever the bank already does, defeating the validation in STRESS_MEASUREMENT.md §4.5. @@ -267,7 +267,7 @@ With one parameter and 30 observations against an external physiological signal, - **Per-user calibration tuning** at launch (everyone gets same starting α/β/quality_weight/z_threshold) — but constants live in `settings` table from day one, not as Go consts, so v2.1 can tune per-user without schema migration. β reserves the setting slot but stays at 0 effective until v2.2 ships and the §4.5 validation rubric passes; same for `z_threshold` and `stress_drain_enabled`. "Athlete profile" preset becomes a v2.1+ feature. - Workout-type detection (drain is HR/kcal driven, not exercise-class aware) - Real-time push notifications when bank drops below threshold -- Migration of `daily_scores.energy_*` rows — left in place as-is; v2 starts writing alongside; `daily_scores` becomes a derived view of v2 snapshots in v2.1 +- Migration or removal of `daily_scores.energy_*` rows — left in place as-is as compatibility columns. Any destructive cleanup needs a separate reversible migration. ## Open implementation questions @@ -277,7 +277,7 @@ These don't block v2.0 design but need resolution during build: | State | Trigger | Baseline source | UI | |---|---|---|---| - | `cold` | `n_nights < 3` overnight RHR samples in last 30d | None — `computeEnergyBank` returns `nil` | "Collecting baseline · 3 nights minimum" placeholder, no bank rendered | + | `cold` | `n_nights < 3` overnight RHR samples in last 30d | None — no v2 bank is rendered; briefing may still use the legacy fallback if its own data gate passes | "Collecting baseline · 3 nights minimum" placeholder, no bank rendered | | `warmup` | `3 ≤ n_nights < 7` OR newest sample > 14 days old | Simple mean of available samples (EMA α not calibrated for <7 points) | Bank rendered with `calibration_state: "warmup"` flag → frontend badges "calibrating · need N more nights" | | `steady` | `n_nights ≥ 7` AND newest sample ≤ 14 days old | EMA, α ≈ 0.25 (~7-day effective window) | Bank rendered normally | diff --git a/SCORING.md b/SCORING.md index 148b882..5a1cb92 100644 --- a/SCORING.md +++ b/SCORING.md @@ -416,13 +416,14 @@ the headline with section verdicts so the briefing is **internally consistent**: --- -## Energy Bank v1 (shipping — to be replaced by v2) +## Energy Bank legacy fallback -> **Status:** v1 is what currently runs in `internal/health/energy.go`. Cross-user empirical validation revealed structural problems (saturation on typical days, no multi-day carryover, formula squashing); a redesign — Energy Bank v2 — is documented in `ENERGY_BANK.md` and summarised in the next section. v1 stays here as the operational reference until v2 ships. +> **Status:** this is the compatibility fallback in `internal/health/energy.go::computeLegacyEnergyBank`. The normal dashboard/report path uses the latest v2 row from `energy_snapshots` when one exists for the requested local date. The legacy formula remains for fresh tenants, cold-start days, and older compatibility history; it is not the source of truth for current Energy Bank methodology. -Computed in `internal/health/energy.go::computeEnergyBank`, returned in -`response.energy_bank`. Where the Headline answers *"what's notable today?"*, -the Energy Bank answers *"what should you do?"* — a prescriptive verdict that +The fallback is returned in `response.energy_bank` only until the storage +briefing path replaces it with the latest v2 snapshot. Where the Headline +answers *"what's notable today?"*, Energy Bank answers *"what should you do?"* +— a prescriptive verdict that rolls capacity, observed activity load, and autonomic stress into a single plain-language action ("push_hard", "moderate", "active_recovery", "rest"). @@ -470,8 +471,8 @@ plain-language action ("push_hard", "moderate", "active_recovery", "rest"). The same active day costs more when autonomics are already taxed — this is the McEwen 1998 / 2024 PNAS allostatic-load shape [[38]](#ref-38), conservatively parameterised. Without intraday HR we cannot model - parasympathetic *refill*; the system therefore monotonically drains - through the day. Documented as `// TODO(v2)` in `energy.go`. + parasympathetic *refill*; the fallback therefore monotonically drains + through the day. v2 fixes this with cross-day state and sleep restore. ### Verdict thresholds (Plews / Vesterinen smallest-worthwhile-change band) @@ -514,13 +515,14 @@ is stronger than any single-marker green light. --- -## Energy Bank v2 (design — pending implementation) +## Energy Bank v2 (current shipped path) -> Full specification: [`ENERGY_BANK.md`](./ENERGY_BANK.md). This section captures -> the methodology and the empirically-derived design principles; the spec doc -> carries the implementation detail (schema, concurrency, rollout phases). +> Full specification and operational details: [`ENERGY_BANK.md`](./ENERGY_BANK.md). +> v2 writes `energy_snapshots`, reads the latest local-date snapshot into the +> dashboard/report `energy_bank` field, and serves hourly history from the same +> table. Legacy `daily_scores.energy_*` columns remain compatibility-only. -Three structural problems with v1 were discovered through an empirical +Three structural problems with the legacy fallback were discovered through an empirical prototype run on 90 days of real data, on **two distinct users** (one sedentary with clean data, one mixed-source with frequent sync gaps): diff --git a/cmd/server/main.go b/cmd/server/main.go index d4ae91e..9bea026 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -183,9 +183,9 @@ func main() { mux := http.NewServeMux() - // EnergyBank v2 orchestrator: runs in parallel with the v1 - // dashboard rendering. Snapshots accumulate silently for - // observation; PR8 will flip the UI over once v2 is validated. + // EnergyBank v2 orchestrator: writes snapshots consumed by the + // dashboard, reports, and history chart. Briefing rendering still + // has a legacy fallback for days before the first v2 snapshot lands. energyV2 := storage.NewEnergyV2Orchestrator() onNewData := func(db *storage.DB, dates []string) { @@ -286,8 +286,7 @@ func runSingleTenant(ctx context.Context, addr, baseURL string, trustFwdAuth boo var morningSendMu sync.Mutex maybeFireMorningReport := makeMorningTrigger(db, &morningSendMu, mgr, reg, schema, notifyDefaults) backfillDatesFn := makeBackfillDatesFn(db, schema, notifyDefaults) - // EnergyBank v2 orchestrator: same role as in multi-tenant mode — - // passive snapshot accumulation alongside the live v1 dashboard. + // EnergyBank v2 orchestrator: same role as in multi-tenant mode. energyV2 := storage.NewEnergyV2Orchestrator() onNewData := func(_ *storage.DB, dates []string) { backfillDatesFn(dates) diff --git a/docs/ai-plans/2026-06-06-issue-148-energybank-v1-cleanup-docs.html b/docs/ai-plans/2026-06-06-issue-148-energybank-v1-cleanup-docs.html new file mode 100644 index 0000000..ef7b8cc --- /dev/null +++ b/docs/ai-plans/2026-06-06-issue-148-energybank-v1-cleanup-docs.html @@ -0,0 +1,167 @@ + + + + + Implementation Plan: Issue #148 EnergyBank v1 Cleanup and Docs + + + + +
+

Implementation Plan: Issue #148 EnergyBank v1 Cleanup and Docs

+

Approved and implemented

+ +
+

Task Summary

+

Audit remaining EnergyBank v1 code and documentation, then either remove obsolete v1 paths or explicitly document them as compatibility fallback. Update scoring docs so they describe the shipped v2 behavior rather than pending design language.

+
+ +
+

Current Behavior

+
    +
  • internal/health/energy.go::computeEnergyBank is still called by internal/health/scoring.go.
  • +
  • internal/storage/briefing.go computes the health response through the v1 path first, then overrides display bank/capacity/drain/verdict from energy_snapshots when a v2 snapshot exists.
  • +
  • internal/storage/energy_history.go::SaveEnergyBankSnapshot still writes legacy daily_scores.energy_* columns; GetEnergyHistory serves the older EOD history path.
  • +
  • internal/storage/energy_history_v2.go serves the newer hourly energy_snapshots path.
  • +
  • SCORING.md still says EnergyBank is computed in internal/health/energy.go::computeEnergyBank, while ENERGY_BANK.md includes v2 design/cutover history and says old formula stays live in older roadmap text.
  • +
+
+ +
+

Desired Behavior

+
    +
  • Every remaining v1 reference is either removed or labeled as compatibility/fallback with a current reason.
  • +
  • Docs describe the shipped v2 state machine, snapshot table, personal verdict bands, and legacy daily columns accurately.
  • +
  • No destructive DB cleanup is bundled unless separately approved and reversible.
  • +
  • Dashboard, Telegram, AI prompt, and MCP/debug paths continue to render EnergyBank correctly.
  • +
+
+ +
+

Assumptions And Unknowns

+
    +
  • Assumption: do not drop daily_scores.energy_* in this PR; keep or deprecate them unless a separate migration is approved.
  • +
  • Assumption: computeEnergyBank may still be needed as a bootstrap/fallback when no v2 snapshot exists.
  • +
  • Unknown: whether the legacy sparkline path is still used by any client after the v2 hourly chart.
  • +
  • Unknown: whether production has enough v2 stability to remove fallback behavior entirely; issue says at least 7 production days without material issues.
  • +
+
+ +
+

Files Likely To Change

+
    +
  • internal/health/energy.go and internal/health/scoring.go - only if v1 fallback can be safely removed or renamed as legacy.
  • +
  • internal/storage/briefing.go - clarify or simplify v1-to-v2 override comments and persistence behavior.
  • +
  • internal/storage/energy_history.go, internal/storage/energy_history_v2.go, and related UI handlers - audit whether legacy EOD history is still needed.
  • +
  • SCORING.md, ENERGY_BANK.md, README.md, AGENTS.md, CLAUDE.md - update current methodology and compatibility notes.
  • +
  • internal/health/*energy*_test.go and storage energy tests - update tests only where they pin obsolete v1 behavior.
  • +
+
+ +
+

Implementation Steps

+
    +
  1. Run git grep computeEnergyBank, git grep SaveEnergyBankSnapshot, and git grep energy_ to build a final removal/deprecation list.
  2. +
  3. Trace dashboard, Telegram report, AI prompt, MCP, and chart reads to identify which paths still depend on legacy daily columns.
  4. +
  5. Decide between two safe paths: docs-only deprecation of v1 fallback, or code removal where no live callsite remains.
  6. +
  7. Update methodology docs first so reviewers can check intended current behavior.
  8. +
  9. Apply the smallest code cleanup that matches the audit; avoid schema drops.
  10. +
  11. Run EnergyBank-focused tests and full Go verification.
  12. +
+
+ +
+

Impact

+
    +
  • Data: no destructive migration planned; legacy columns remain unless separately approved.
  • +
  • API/UX: EnergyBank JSON shape should remain compatible.
  • +
  • Docs: methodology docs become the main output if code still needs v1 fallback.
  • +
  • Deployment: normal app deploy only if production code changes; docs-only changes need no deploy for runtime behavior.
  • +
+
+ +
+

Test Plan

+
    +
  • go test ./internal/health -run "Test.*Energy|Test.*Verdict" -count=1
  • +
  • go test ./internal/storage -run "Test.*Energy" -count=1
  • +
  • go test ./internal/ui -run "Test.*Energy|Test.*Dashboard" -count=1 if UI/chart handlers change.
  • +
  • go test ./...
  • +
  • go vet ./...
  • +
  • Manual QA if deployed: confirm dashboard hero EnergyBank, hourly chart, and Telegram morning report still render.
  • +
+
+ +
+

Risks And Edge Cases

+
    +
  • Removing v1 too early can break cold-start tenants or days with no v2 snapshot.
  • +
  • Docs can become inconsistent if AGENTS.md and CLAUDE.md are not kept in lockstep.
  • +
  • Legacy daily columns may still support old chart or AI history paths; audit before removal.
  • +
  • Changing EnergyBank behavior under a cleanup issue would be out of scope.
  • +
+
+ +
+

Rollback Plan

+

For docs/test cleanup, revert the PR. For any code removal, restore the legacy fallback callsite and tests. No database rollback is expected because schema drops are excluded from this plan.

+
+ +
+

Open Questions

+
    +
  • Should computeEnergyBank remain as a named legacy fallback until all tenants have v2 snapshots?
  • +
  • Is the old EOD GetEnergyHistory path still needed by any frontend or mobile client?
  • +
  • Do we want a separate future issue for dropping daily_scores.energy_* columns after compatibility expires?
  • +
+
+ +
+

Approval Gate

+

Approved by the user on 2026-06-06 before implementation.

+
+ +
+

Implementation Result

+
    +
  • Renamed computeEnergyBank to computeLegacyEnergyBank to make the remaining v1 behavior explicitly compatibility-only.
  • +
  • Kept the legacy fallback behavior unchanged for fresh tenants and days before a v2 snapshot exists.
  • +
  • Updated storage, server, UI, and chart comments that still described v2 as passive, pending, or waiting for a future UI flip.
  • +
  • Updated SCORING.md and ENERGY_BANK.md so v2 is documented as the current shipped path and daily_scores.energy_* columns are documented as legacy compatibility columns.
  • +
  • No schema drops or destructive database cleanup were included.
  • +
+
+ +
+

Verification Run

+
    +
  • gofmt -w on changed Go files.
  • +
  • go test ./internal/health -run "Test.*Energy|Test.*Verdict" -count=1
  • +
  • go test ./internal/storage -run "Test.*Energy" -count=1
  • +
  • go test ./internal/ui -run "Test.*Energy|Test.*Dashboard" -count=1
  • +
  • go test ./...
  • +
  • go vet ./...
  • +
  • git diff --check
  • +
+
+ +
+

Known Limitations And Follow-Up

+
    +
  • The legacy fallback is intentionally still present; removing it would need a separate compatibility decision.
  • +
  • daily_scores.energy_* columns remain in place and should only be removed by a future explicit migration.
  • +
  • The day-level history endpoint remains available for compatibility and AI verdict-history context.
  • +
+
+
+ + diff --git a/internal/health/energy.go b/internal/health/energy.go index 6d083dc..6918c34 100644 --- a/internal/health/energy.go +++ b/internal/health/energy.go @@ -5,7 +5,7 @@ import ( "math" ) -// Energy Bank — Bevel-inspired prescriptive metric. +// Legacy Energy Bank — stateless daily fallback. // // Capacity is the user's "energy budget" at the start of the day, drawn from // the same readiness signal that already aggregates sleep + autonomic markers. @@ -54,16 +54,18 @@ const ( currentPushHardMin = 60 // need this and HRV green to push hard ) -// computeEnergyBank produces the day's prescriptive verdict. Returns nil when -// the underlying readiness/sleep data is too thin — same gate as the readiness -// score itself, keeps the briefing JSON clean for new accounts. +// computeLegacyEnergyBank produces the v1 daily prescriptive verdict used only +// as a compatibility fallback when no v2 energy_snapshots row exists for the +// requested day. Returns nil when the underlying readiness/sleep data is too +// thin — same gate as the readiness score itself, keeping the briefing JSON +// clean for new accounts. // // `readinessScore` is the score already computed by computeReadiness — it // already blends sleep + HRV + RHR with the U-curve penalty, so we re-use it // as morning capacity rather than re-deriving an analogous number here. // // `headline` is consulted for the stress-coherence rule and may be nil. -func computeEnergyBank(d RawMetrics, readinessScore int, headline *HeadlineSignal, ls LangStrings) *EnergyBank { +func computeLegacyEnergyBank(d RawMetrics, readinessScore int, headline *HeadlineSignal, ls LangStrings) *EnergyBank { if len(d.HRV) < minBaseline+2 || len(d.Sleep) < minBaseline+2 { return nil } diff --git a/internal/health/energy_v2.go b/internal/health/energy_v2.go index 084b41a..907c899 100644 --- a/internal/health/energy_v2.go +++ b/internal/health/energy_v2.go @@ -2,12 +2,10 @@ package health // EnergyBank v2 — pure-math kernel. // -// This file holds the formulas only; orchestration (14-day forward -// iteration, snapshot persistence, missing-data imputation) lands in -// later PRs. v1 (computeEnergyBank, scoreEnergyBank) stays in use and -// untouched until PR8 flips the dashboard over. Until then, every -// symbol below is dead code by design — the unit-test suite is the only -// caller. +// This file holds the formulas only. Storage owns the 14-day forward +// iteration, missing-data imputation, snapshot persistence, and read-side +// projection from energy_snapshots. The legacy daily formula remains in +// energy.go only as a cold-start fallback. // // Design rationale and validation: ENERGY_BANK.md. @@ -142,4 +140,3 @@ func ClampSignedBank(bank float64) float64 { } return bank } - diff --git a/internal/health/formula_invariants_test.go b/internal/health/formula_invariants_test.go index 000c088..8b50921 100644 --- a/internal/health/formula_invariants_test.go +++ b/internal/health/formula_invariants_test.go @@ -133,17 +133,17 @@ func TestComputeAlerts_RequiresBaselineVariance(t *testing.T) { } } -func TestComputeEnergyBank_MissingInputsReturnNil(t *testing.T) { +func TestComputeLegacyEnergyBank_MissingInputsReturnNil(t *testing.T) { ls := GetStrings("en") - if got := computeEnergyBank(RawMetrics{}, 70, nil, ls); got != nil { - t.Fatalf("computeEnergyBank without baseline data = %+v, want nil", got) + if got := computeLegacyEnergyBank(RawMetrics{}, 70, nil, ls); got != nil { + t.Fatalf("computeLegacyEnergyBank without baseline data = %+v, want nil", got) } d := RawMetrics{ HRV: repeatFloat(40, minBaseline+2), Sleep: append([]float64{0}, repeatFloat(7.5, minBaseline+1)...), } - if got := computeEnergyBank(d, 70, nil, ls); got != nil { - t.Fatalf("computeEnergyBank with zero sleep today = %+v, want nil", got) + if got := computeLegacyEnergyBank(d, 70, nil, ls); got != nil { + t.Fatalf("computeLegacyEnergyBank with zero sleep today = %+v, want nil", got) } } diff --git a/internal/health/scoring.go b/internal/health/scoring.go index 1153274..b631dc3 100644 --- a/internal/health/scoring.go +++ b/internal/health/scoring.go @@ -67,10 +67,11 @@ func ComputeBriefing(d RawMetrics, lang string) *BriefingResponse { resp.Overall = overallStatus(resp.Sections) // re-derive after coherence updateReadinessServing(resp) - // Energy Bank runs *after* the coherence pass so a stress-capped readiness - // flows in as morning capacity, and so the verdict can downgrade - // `push_hard` when a stress headline is present. - resp.EnergyBank = computeEnergyBank(d, resp.ReadinessScore, headline, ls) + // Legacy EnergyBank runs after the coherence pass so it is a reasonable + // fallback for fresh tenants and days where the v2 snapshot has not been + // written yet. The storage briefing path replaces this with the latest + // v2 energy_snapshots row when one exists. + resp.EnergyBank = computeLegacyEnergyBank(d, resp.ReadinessScore, headline, ls) return resp } diff --git a/internal/storage/energy_history.go b/internal/storage/energy_history.go index f70d70e..f1c4bb1 100644 --- a/internal/storage/energy_history.go +++ b/internal/storage/energy_history.go @@ -7,11 +7,9 @@ import ( "health-receiver/internal/health" ) -// EnergyHistoryPoint is one day of the EnergyBank trend chart. Capacity is -// what the user started the day with; CurrentEOD is what was left at the -// last snapshot (typically end-of-day after all sync ticks); Drain is the -// cumulative cost; Verdict is the prescriptive action that fell out of -// the formula. All four come straight from health.EnergyBank. +// EnergyHistoryPoint is one legacy day-level EnergyBank point from +// daily_scores.energy_*. The v2 hourly chart reads energy_snapshots instead; +// these rows stay as a compatibility history/verdict source for old data. type EnergyHistoryPoint struct { Date string `json:"date"` Capacity int `json:"capacity"` @@ -20,9 +18,9 @@ type EnergyHistoryPoint struct { Verdict string `json:"verdict"` } -// SaveEnergyBankSnapshot upserts the EOD snapshot of the day's EnergyBank -// into daily_scores. Best-effort — errors are logged but not returned, and -// callers should never block briefing rendering on this. +// SaveEnergyBankSnapshot upserts a legacy EOD EnergyBank snapshot into +// daily_scores.energy_*. Best-effort — errors are logged but not returned, +// and callers should never block briefing rendering on this. // // Called from the briefing path on every render so the latest in-memory // EnergyBank for `date` is the value that lands. By construction the @@ -51,10 +49,10 @@ func (s *DB) SaveEnergyBankSnapshot(date string, eb *health.EnergyBank) { } } -// GetEnergyHistory returns the most recent `days` EOD snapshots in -// ascending date order. Rows without an energy_verdict (snapshot never -// taken) are skipped — the sparkline shouldn't render a flat line for -// pre-persistence days. +// GetEnergyHistory returns the most recent `days` legacy EOD snapshots in +// ascending date order. Rows without an energy_verdict are skipped. Current +// dashboard hourly history should use GetEnergyHistoryV2; this path remains +// for day-level compatibility and AI verdict-history context. func (s *DB) GetEnergyHistory(days int) ([]EnergyHistoryPoint, error) { if days <= 0 { days = 14 diff --git a/internal/storage/energy_snapshot_read.go b/internal/storage/energy_snapshot_read.go index dff56dd..da36c69 100644 --- a/internal/storage/energy_snapshot_read.go +++ b/internal/storage/energy_snapshot_read.go @@ -26,7 +26,7 @@ type LatestEnergySnapshot struct { // error when no row exists for that date — a fresh tenant or a // recompute that hasn't run yet for today should not surface as a // hard error to the briefing path; the caller must fall through to -// the v1 EnergyBank instead. +// the legacy EnergyBank instead. func (s *DB) GetLatestEnergySnapshotForDate(ctx context.Context, dateStr string) (*LatestEnergySnapshot, error) { row := s.pool.QueryRow(ctx, ` SELECT bank, drain_delta, restore_delta, formula_version, flags diff --git a/internal/storage/energy_v2_orchestrator.go b/internal/storage/energy_v2_orchestrator.go index f163f79..9be88e3 100644 --- a/internal/storage/energy_v2_orchestrator.go +++ b/internal/storage/energy_v2_orchestrator.go @@ -12,10 +12,9 @@ import ( // serialises recomputes for that tenant and coalesces ingest bursts // into one immediate compute + at most one follow-up. // -// In v2.0 the orchestrator runs in parallel with the v1 EnergyBank -// rendered on the dashboard — the snapshots accumulate silently for -// observation, no UI consumes them yet. PR8 will flip the dashboard -// over once the v2 numbers have been validated against live data. +// The dashboard and reports now consume the latest v2 energy_snapshots row +// when present. The legacy daily EnergyBank remains only as the briefing +// fallback for fresh tenants or days before the first v2 snapshot lands. // // The orchestrator owns no resources beyond the per-tenant // TenantRecompute structs; the lifecycle ctx for those workers is @@ -64,10 +63,8 @@ func (o *EnergyV2Orchestrator) recompute(ctx context.Context, db *DB, schema, tz log.Printf("[ENERGY_V2] schema=%s upsert error: %v", schema, err) return } - // One log line per recompute lets us observe v2 behaviour in - // production without UI changes — the v1 dashboard is unaffected - // in this PR. Useful for spotting regressions early before PR8 - // flips rendering. + // One log line per recompute lets us observe v2 behaviour in production + // without depending on request-path rendering. log.Printf("[ENERGY_V2] schema=%s bank=%d display=%d state=%s flags=%v alpha=%.4f drain=%d restore=%d", schema, res.Bank, res.Display, res.State, res.Flags, res.AlphaUsed, res.TodayDrain, res.TodayRestore) } diff --git a/internal/ui/handler.go b/internal/ui/handler.go index 2a33d46..4f9854f 100644 --- a/internal/ui/handler.go +++ b/internal/ui/handler.go @@ -1190,12 +1190,10 @@ func (h *Handler) readinessHistory(w http.ResponseWriter, r *http.Request) { // energyHistory serves the EnergyBank trend chart in two modes: // -// ?granularity=day (default) — legacy v1 behaviour, reads EOD +// ?granularity=day (default) — legacy day-level behaviour, reads EOD // snapshots from daily_scores. Kept for -// backward compatibility with existing -// dashboard sparkline callers; will be -// retired in PR8 once the UI flips to -// v2. +// backward compatibility and AI verdict +// history context. // ?granularity=hour — v2 behaviour, reads 5-min buckets // from energy_snapshots over the last // ?hours= hours. diff --git a/internal/ui/static/charts.js b/internal/ui/static/charts.js index 44cd087..4aedc74 100644 --- a/internal/ui/static/charts.js +++ b/internal/ui/static/charts.js @@ -189,8 +189,8 @@ function loadEnergySparkline(canvasId) { // // Verdict colouring intentionally NOT applied here: the v2 endpoint // doesn't expose per-bucket verdict (it's a derived property of the -// iteration, not a stored column). PR8 will reconcile the colour -// story when it flips the dashboard's source of truth from v1 to v2. +// iteration, not a stored column). The current hero verdict comes from +// the freshest same-day snapshot, while the chart stays value-only. var energyHourlyChart = null; function loadEnergyHourlyChart(canvasId) { // Guard before fetch: when the server renders the dashboard for a diff --git a/internal/ui/templates/pages/dashboard.html b/internal/ui/templates/pages/dashboard.html index cefbf53..99deb8e 100644 --- a/internal/ui/templates/pages/dashboard.html +++ b/internal/ui/templates/pages/dashboard.html @@ -105,8 +105,8 @@ + legacy day-level history endpoint remains available for + compatibility, but this chart reads energy_snapshots. -->