mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
docs(dashboard-layout): document merge semantics for SB-8 audit gap
The dashboard layout resolver in src/lib/dashboard-layout.ts is append-only on read: new widget ids added to DASHBOARD_WIDGET_IDS automatically appear for every existing user on next GET, with visible / tileVisible / order taken from DEFAULT_DASHBOARD_LAYOUT. No database migration is ever required when introducing a new widget. Documents the merge contract so a future widget-introduction does not ship a one-shot SQL migration that would clobber every user's customisation. PUT preservation rules captured: chartOverlayPrefs is preserved-on-omit, comparisonBaseline is not. Investigation-only — no code change.
This commit is contained in:
1 parent
99a801f993
commit
6c2eadb342
1 file changed
+86
@@ -0,0 +1,86 @@
|
||||
# SB-8 — Dashboard tile-visibility merge semantics
|
||||
|
||||
**Owner:** W-IOS-COORD (v1.4.41)
|
||||
**Route:** `src/app/api/dashboard/widgets/route.ts`
|
||||
**Resolver:** `src/lib/dashboard-layout.ts` (`resolveDashboardLayout` + `DEFAULT_DASHBOARD_LAYOUT`)
|
||||
**Status:** investigation-only — no code change. Documents the contract a
|
||||
future widget-introduction migration must honour.
|
||||
|
||||
## How the layout is stored
|
||||
|
||||
`User.dashboardWidgetsJson` (Prisma `Json?`, default `null`). A null /
|
||||
missing value means "no user customisation, use defaults". A populated
|
||||
value is a `DashboardLayout` blob shaped by the Zod schema in the PUT
|
||||
route. The resolver in `src/lib/dashboard-layout.ts` is the single
|
||||
read-side merge point.
|
||||
|
||||
## Merge semantics on read (`resolveDashboardLayout`)
|
||||
|
||||
1. **Null/empty input** → return `DEFAULT_DASHBOARD_LAYOUT` verbatim.
|
||||
2. **Known widget ids** → keep the user's `visible` / `tileVisible` /
|
||||
`order` exactly as stored.
|
||||
3. **Unknown widget ids** (stale entries from a removed widget) → dropped
|
||||
silently.
|
||||
4. **New widgets present in `DASHBOARD_WIDGET_IDS` but absent from the
|
||||
user's blob** → appended at the end of `widgets[]` with the default
|
||||
layout's `visible`, `tileVisible`, and `order` values. This is the
|
||||
key behaviour any new-widget migration must rely on. The resolver
|
||||
does this on every read; no DB migration is required when a new
|
||||
widget id ships.
|
||||
5. **`tileVisible` omitted** → mirrors `visible` for back-compat with
|
||||
pre-v1.4.15 saved layouts.
|
||||
6. **Unknown `comparisonBaseline` / `chartOverlayPrefs` keys** → clamped
|
||||
to defaults so a stale client cannot poison the dashboard.
|
||||
|
||||
## Merge semantics on write (`PUT /api/dashboard/widgets`)
|
||||
|
||||
1. The Zod schema requires the **full** `widgets[]` array — the route
|
||||
does NOT support partial updates. Clients must read the current
|
||||
layout, mutate, and PUT the result.
|
||||
2. `chartOverlayPrefs` is preserved automatically when omitted: the
|
||||
route re-reads the persisted blob and re-attaches the existing
|
||||
prefs before writing (see lines 137–151).
|
||||
3. `comparisonBaseline` is **not** preserved-on-omit — an omitted
|
||||
field clamps to the default. Clients that want to keep an existing
|
||||
baseline must round-trip the value.
|
||||
4. `DELETE` resets to defaults by writing `Prisma.JsonNull` (not
|
||||
`null` — Prisma's typed null sentinel).
|
||||
|
||||
## Future-migration guidance
|
||||
|
||||
Adding a new widget id is **migration-free** when these rules hold:
|
||||
|
||||
- Add the id to `DASHBOARD_WIDGET_IDS`.
|
||||
- Add a default entry to `DEFAULT_DASHBOARD_LAYOUT.widgets[]` with the
|
||||
desired `visible`, `tileVisible`, and a unique `order`.
|
||||
- That's it. Every existing user gets the new widget appended on next
|
||||
GET. No `prisma migrate` step is needed for the layout blob.
|
||||
|
||||
Removing a widget id is also migration-free:
|
||||
|
||||
- Drop the id from `DASHBOARD_WIDGET_IDS` and from
|
||||
`DEFAULT_DASHBOARD_LAYOUT`.
|
||||
- The resolver drops the orphan entry on next read. A subsequent PUT
|
||||
rewrites the blob without it.
|
||||
|
||||
**Anti-pattern (do NOT do):** a "one-shot migration" that rewrites every
|
||||
`User.dashboardWidgetsJson` to overlay a new tile-visibility default
|
||||
will clobber every user's customisation. The resolver's append-only
|
||||
merge is intentionally designed so we never need such a migration.
|
||||
|
||||
## Open audit gap closed
|
||||
|
||||
The original audit note flagged "what happens when a future migration
|
||||
introduces a new tile and we want it visible by default for existing
|
||||
users". Answer: do nothing — the resolver handles it. If a tile must
|
||||
be visible-by-default for existing users, set `visible: true` in
|
||||
`DEFAULT_DASHBOARD_LAYOUT`. The user's saved blob never has the new
|
||||
id, so the resolver's append step uses the default.
|
||||
|
||||
## References
|
||||
|
||||
- `src/app/api/dashboard/widgets/route.ts` — PUT schema + chart-overlay
|
||||
preservation logic.
|
||||
- `src/lib/dashboard-layout.ts` — `resolveDashboardLayout`,
|
||||
`serializeDashboardLayout`, `DEFAULT_DASHBOARD_LAYOUT`.
|
||||
- `src/lib/__tests__/dashboard-layout.test.ts` — resolver coverage.
|
||||
Reference in new issue
Block a user