MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-342

6.3.1 Dashboard + widget data model, services, and CRUD API (workspace-scoped, access private|workspace, TOTAL widget-type registry, 20-widget cap)

Done
Description

Estimate: 30m · Depends on: 6.2.1

The dashboard substrate. Pure backend — no UI.

Migrationdashboard (id, workspaceId, name, ownerId, access: 'private' | 'workspace', layout: 'one' | 'two' | 'three' columns, timestamps) and dashboard_widget (id, dashboardId, type, column, position — the shipped base-62 fractional index (lib/workItems/positioning.ts vocabulary) so drag-reorder is one-field, config Json, plus a NULLABLE savedFilterId FK (onDelete: SetNull — a deleted filter STALES the widget, never deletes it) and a nullable projectId FK (onDelete: Cascade)). Both FKs modelled as @relations on BOTH sides (the FK-drift rule). Indexes: [workspaceId], [dashboardId, column, position].

lib/dashboards/widgetRegistry.ts — the TOTAL per-widget-type registry (mistake #29): each type (filter_results, distribution, created_vs_resolved) maps to a config Zod schema (validating the data source — exactly one of savedFilterId/projectId — plus per-type settings: page size ≤50 / statistic-type id / period + days-back + cumulative), a data-source resolver, and the renderer/editor kinds (the UI contract for 6.3.3/6.3.5). Unknown type or malformed config → typed 422; the enumeration test fails on any registry gap.

dashboardsService (4-layer; one method = one transaction): dashboard CRUD (create = any workspace member; rename/relayout/access-change/delete = owner-only — typed 403 otherwise), widget add/update/remove/move (validated through the registry; the 20-widget cap → typed 422; move takes column + neighbour ids and computes the fractional index server-side, the board precedent), and the reads — listDashboards (private-to-me + workspace-shared, bounded) and getDashboard (widgets ordered by column/position; access enforced: private + not owner → 404-shaped denial). Routes are HTTP-only under app/api/dashboards/; the finding-#26 workspaceId gate covers every route. Fill the reserved 6.2.1 dependents line: extend the saved-filter delete-dependents enumeration with the widget FK join (the Cloud-style warning now counts "N dashboard widgets" alongside subscriptions — the line 6.2.2's warning design reserved); the delete itself stays SetNull (the widget goes STALE, the verified Cloud gadget behaviour — it never cascades).

Acceptance criteria

  • The migration applies cleanly (re-run: no drift); both FKs are two-sided @relations; deleting a saved filter nulls savedFilterId (stale, not gone); deleting a project cascades its project-sourced widgets; the 6.2.1 delete-dependents read counts the filter's widgets (asserted: the warning DTO names N widgets before a delete that stales them).
  • The registry is total over the three types (enumeration test: every type has schema × resolver × renderer-kind × editor-kind); unknown types/configs → 422; the data-source XOR is enforced.
  • CRUD honours the permission matrix (member create / owner-only mutate / private invisible to non-owners — each asserted both service- and route-level); the widget cap holds; move produces stable fractional orderings under concurrent-ish sequences (the 3.2 test pattern).
  • Reads are bounded (list ≤ a sane page; get loads one dashboard's ≤20 widgets); empty-input guards on the new repo methods (coverage gate); pnpm test:coverage ≥90%.

Context refs

  • 6.2.1 (the saved_filter table the FK references + the delete-dependents enumeration this extends — its "6.3 widgets join in by FK later" line)
  • lib/workItems/positioning.ts + the 3.2 move-service pattern (fractional index, server-computed)
  • motir-core/CLAUDE.md (4-layer, required-tx, FK/migration rules); finding #26 (workspace gate); lock-before-read-derived-update (the move tx)
  • The verified Jira dashboard facts in the Story 6.3 description (ownership/sharing/layout/cap)
Status
Done
Type
Sub-task