Files
pad/internal/models/report_layout.go
T
xarmian eeff78118b feat(insights): per-user layout customization + persistence (TASK-1634) (#645)
* feat(insights): per-user layout customization + persistence (TASK-1634)

Let users personalize the Insights surface, persisted per-user per-workspace:
toggle which metric cards show, and remember the window + collection filter.

Backend:
- migrations 064/043: user_report_layouts (user_id, workspace_id, config JSON,
  PK(user_id,workspace_id), ON DELETE CASCADE) — dual-dialect.
- models.ReportLayout (hidden_cards/default_window/default_collections) +
  ReportCardIDs/ValidReportWindow validation.
- store.GetReportLayout / SaveReportLayout (ON CONFLICT upsert, both dialects).
- GET/PUT /workspaces/{ws}/report/layout — per-user; PUT sanitizes window +
  filters hidden_cards to the known card set. web client + TS type.

Frontend (Insights page):
- loads the saved layout, hydrates window/collections/hidden cards
- a "Customize" panel toggles each card (SvelteSet-backed); each section gated
  on !hiddenCards.has(id); Totals always shown
- debounced auto-save, gated on a per-workspace `hydrated` flag so it never
  saves during load or stomps another workspace's layout on switch

Single config per user (no named/multiple layouts — deliberate v1 scope).
Parent: PLAN-1628.

* fix(insights): save layout only on explicit user changes, not on load per Codex review (round 1)

The auto-save $effect ran once after hydration (loadLayout assigns reactive
state, then flips hydrated=true), firing a PUT /report/layout on mere page
view — which 401s on no-user/legacy-token sessions and bounces the user to
/login. Replace the effect with a scheduleSave() called only from explicit
handlers (toggleCard, selectWindow, toggleCollection, clearCollectionFilter);
hydration never saves. Also capture wsSlug at schedule time and drop the
pending save if the workspace changes mid-debounce, so A's edit can't land
on B.
2026-05-29 15:00:34 -04:00

40 lines
1.5 KiB
Go

package models
// ReportLayout is a user's personalization of the Insights surface for one
// workspace (PLAN-1628 / TASK-1634). Stored as the JSON `config` of a
// user_report_layouts row, one per (user, workspace).
type ReportLayout struct {
// HiddenCards lists the metric-card IDs the user has toggled off. Card IDs
// are the stable section identifiers in ReportCardIDs; unknown IDs are
// dropped on save. The Totals summary is always shown and not toggleable.
HiddenCards []string `json:"hidden_cards"`
// DefaultWindow is the window to restore on load (day|week|2wk|month).
// Empty means the surface default (week).
DefaultWindow string `json:"default_window"`
// DefaultCollections is the collection-slug filter to restore on load.
// Empty means all collections.
DefaultCollections []string `json:"default_collections"`
}
// ReportCardIDs is the canonical set of toggleable Insights metric cards. The
// web page gates each section on `!hidden_cards.includes(id)`; the API filters
// HiddenCards to this set on save so stored config can't drift from the UI.
var ReportCardIDs = map[string]bool{
"throughput": true,
"cycle_time": true,
"wip": true,
"completed_by_collection": true,
"status_distribution": true,
}
// ValidReportWindow reports whether w is an accepted report window (empty is
// allowed and means the surface default).
func ValidReportWindow(w string) bool {
switch w {
case "", "day", "week", "2wk", "month":
return true
default:
return false
}
}