infra/docs/plans/2026-06-01-wealth-dashboard-consolidation-design.md
Viktor Barzin fd0f4a0365 fix: restore tree dropped by 6d224861; land stem95su gdrive-sync (10m) [ci skip]
6d224861 came from a --no-checkout worktree whose empty index made the
commit drop every file except two. This restores 05b50d2b's full tree and
correctly adds stacks/stem95su/gdrive-sync.tf + the service-catalog stem95su
entry. Forward-only (parent=6d224861, no force-push); [ci skip] since the
live infra was never applied from the broken commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 08:45:33 +00:00

113 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Wealth Dashboard Consolidation — Design (2026-06-01)
## Goal
The `wealth` Grafana dashboard (UID `wealth`) has grown to **36 panels** with
heavy duplication. Consolidate to **~17 panels with ZERO metric loss** by
merging redundant panels, and fix the projection's empty-by-default problem.
Philosophy (user-locked): *merge duplicates, keep every metric* — no metric the
user tracks today is removed.
## Current state — 36 panels, duplication clusters
| Cluster | Panels today | Issue |
|---|---|---|
| **1. NW/contribution/growth over time** | "Net worth — total over time", "Net contribution vs market value", "Growth (market value contribution) over time" | All restate `NW = contribution + growth` |
| **2. Returns/deltas stat cards** | "12mo return/contrib/gain" (3) + "Δ 1d/7d/30d/90d" × (all/mkt) (8) = 11 cards | Same idea, many windows |
| **3. Net pay vs market gain** | "…cumulative", "…per year", "…per month" (3) | Same comparison, 3 grains |
| **4. Yearly bars** | "Yearly investment return %" + "Annual change decomposition" (2) | Same yearly data, two encodings |
| Projection row (5) | text + 3 stats + projection chart | Stats duplicate Overview; chart empty by default (shared time-range) |
## Target layout — collapsed rows
### Row: Overview (expanded by default)
- **Keep** 4 snapshot stats: Net worth · Net contribution · Growth · ROI%.
- **NEW "Returns" table** ← merges cluster 2 (11 cards). `table` panel: one row
per window (1d / 7d / 30d / 90d / 12mo), columns **Δ all £ · Δ market £ ·
return %**. Reuses the existing per-window latest-vs-N-days-ago SQL, UNION'd
into 5 rows. Preserves every value (12mo contrib = Δall Δmkt) and adds
return-% for the short windows.
### Row: Net worth over time
- **NEW merged timeseries** ← cluster 1: two lines — `net_contribution` and
`total_value` (market value) — with the **growth gap shaded** (fillBelowTo /
area between). Optionally a 3rd faint "growth" line (= total_value
net_contribution). Reuses the "Net contribution vs market value" query.
- **Keep** "Per-account stacked — total value" · "Cash vs invested (stacked)".
### Row: Returns & contributions
- **NEW yearly combo** ← cluster 4: timeseries panel, `contributions` +
`market_gain` as **bars** (drawStyle=bars via per-series override) + a
**`return_pct` line on a right Y-axis**. One query returns
`year, contributions, market_gain, return_pct` (merges the two existing
yearly queries — both already share the `yearly`/`ep` CTEs).
- **Keep** "Monthly contributions vs market gain" · "Per-account ROI %".
### Row: Income vs market
- **NEW merged "Net pay vs market gain"** ← cluster 3: one timeseries + a
**`$grain` custom variable** (`cumulative` / `yearly` / `monthly`). The rawSql
switches bucketing on `$grain`. Default `cumulative`.
### Row: Holdings — **Keep** Positions · Activity log
### Row: RSUs (META) — **Keep** vest cadence · realized PNL
### Row: Projections (rebuilt)
- **Rebuild the projection chart as a Trend panel** (`type: trend`): numeric
x-axis = **years from today** (0…`$horizon_years`), y = Low / Base / High /
Historical / "Base, no new contributions". The Trend panel renders smooth
multi-series lines on a numeric x — **independent of the dashboard time
range** — so it is ALWAYS visible (fixes empty-by-default). SQL: same FV math
as today, but emit `m.n/12.0 AS years_from_now` instead of a timestamp; format
`table`; panel `xField = years_from_now`. Carry over the dashed/dotted line
overrides + GBP unit.
- **Drop** the 3 projection-row stat cards (NW today / Historical return /
Monthly contribution) — already in Overview (return table + snapshot). **Keep**
the "How to view" text panel only if still useful (with Trend it's no longer
needed — drop it too). **Keep** the 5 template vars (rate_low/base/high,
monthly_contribution, horizon_years).
## Panel count: 36 → ~17
4 snapshot + returns table + nw-over-time + per-account + cash-vs-invested +
yearly-combo + monthly-contrib + per-account-ROI + net-pay(merged) + positions +
activity-log + meta-cadence + meta-pnl + projection-trend = **~17**.
## Merge SQL notes (validate each against live wealth-pg before deploy)
- **Returns table**: 5 `SELECT`s (one per window) UNION ALL, each computing
`Δall = nw_now nw_{ago}`, `Δmkt = Δall (contrib_now contrib_{ago})`,
`ret% = Δmkt / (nw_{ago} + 0.5·Δcontrib)·100` (Modified Dietz, the existing
formula). Window→interval: 1d/7d/30d/90d/12mo.
- **Yearly combo**: extend the "Annual change decomposition" query (already has
`contributions`, `market_gain` per year) to also emit `return_pct` (the
"Yearly investment return %" formula) — same `ep` CTE.
- **Net-pay `$grain`**: one query; `cumulative` = running sums, `yearly`/`monthly`
= period-end deltas (reuse the month-end/year-end delta pattern shipped today).
## Build / deploy / verify
1. One-off Python builder (`/tmp`, outside repo) loads `wealth.json`: removes the
merged-away panels by title, adds the new merged panels + `$grain` var,
rebuilds the projection as a Trend panel, wraps everything in collapsed rows,
assigns unique ids + clean gridPos. Clone existing panels for schema-39
fidelity where possible.
2. Validate: `json.load`; unique ids; spot-run every new/merged target's SQL
against live `wealth-pg` (the pg-sync sidecar) with default var values.
3. Deploy: `scripts/tg apply -target='module.monitoring.kubernetes_config_map.grafana_dashboards["wealth.json"]'`
(targeted — monitoring stack carries unrelated drift). `git rebase --autostash
forgejo/master` before push (shared repo).
4. Verify: ConfigMap == local file; user eyeballs each row in Grafana (esp. the
Trend projection renders without touching the time picker, and the returns
table + merged panels show the right numbers).
## Risks
- **Trend panel** is flagged experimental (since v10.0) but available in v11.2;
confirm `xField` + query `format=table` at build time.
- **Bars + line on one timeseries** (yearly combo) needs per-series `drawStyle`
overrides + a second Y-axis override — verify rendering.
- **`$grain` net-pay** SQL is the fiddliest merge; validate all 3 grains.
- Reorganizing into rows reshuffles gridPos for the whole dashboard — the
builder must lay out rows top-to-bottom without overlaps.
- Keep the contribution-correctness fixes (LOCF view, month-end deltas) intact —
the merged panels read the same `dav_corrected` view.
## Out of scope
- The `dav_corrected` view + the Fidelity growth-timing cosmetic (separate).
- No new metrics — pure consolidation.