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-341

6.3 Dashboards & reports

Done
Description

The reporting layer: configurable dashboards — named, shareable grids of widgets backed by a saved filter (6.2) or the project — and the project Reports hub hosting the two built-in analysis reports (created-vs-resolved, status distribution). Charts reuse the Epic-4 viz (the 4.6.2 token-aware SVG primitives, grown by a donut + a two-series difference/area form) — no charting library (the recorded 4.6 decision).

Where it sits relative to the mirror (verified, and the deviations recorded). Jira dashboards are site-level: any user creates them, an owner shares them via viewer/editor audiences, gadgets configure a project OR a saved filter as their data source, edit mode offers column layouts + drag-rearrange, and filter-displaying gadgets paginate at a hard 50/page. Ours map the "site" to the WORKSPACE (the shell's outer boundary — rung 2) at the shipped /dashboard route (the 1.1.2 smoke landing this story replaces): multiple named dashboards, an access of private | workspace, owner-only edit. Saved filters are PROJECT-contained (the 6.2 recorded deviation), so every widget data source is project-scoped and a dashboard aggregates cross-project WIDGET-BY-WIDGET — each gadget names its own project or filter, exactly the verified gadget pattern. The richer audience matrix (user/group/role/public, editor lists), the default/system dashboard, starring, wallboard mode, and per-gadget auto-refresh are documented extensions the shape grows into.

The widget-type registry (the load-bearing piece). Three widget types ship — filter-results (a paginated issue table, the verified 50/page cap), distribution (donut by a statistic type — the finite-value fields the 6.1 registry already enumerates as enum-ish: kind, status, priority, assignee, reporter, sprint, label, component, select/user custom fields), and created-vs-resolved (period bucket + days-back window + cumulative toggle — the verified Jira config). Each registers in a TOTAL per-widget-type registry (mistake #29): config schema + validation, data-source resolution, renderer, and config-editor kind — an unknown widget type or malformed config is a typed 422, never a silent pass-through. Every widget names its data source as { savedFilterId } | { projectId } (the verified gadget pattern); a dashboard caps at 20 widgets (the DC default as our sanity bound).

"Resolved" semantics (recorded deviation). Jira's created-vs-resolved counts the Resolution FIELD being set; we have no resolution field, so "resolved" = a transition into a done-category status (workflowsService.getTerminalStatusKeys) — the SAME predicate the burndown (4.6.3), velocity (4.6.4), and rollups (4.3.3) resolve, so every report agrees on "done". The resolved series derives from the 1.4.6 revision trail via ONE bounded grouped query windowed by the days-back config (the 4.6.3 pattern — never an all-revisions load + JS reduce); the created series buckets createdAt. A reopened issue (a transition back OUT of done inside the window) subtracts — the series count NET resolutions per bucket.

Viewer-scoped permissions (the 6.4 seam). A workspace-shared dashboard renders for EVERY workspace member, but each widget read enforces the 6.4 project-access gate per VIEWER, not per owner: a widget over a private project (or a saved filter scoped to one) the viewer can't access renders the designed no-access widget state — it never leaks counts, rows, or chart shapes (the mirror behaviour: Jira gadgets show only what the viewer can see). Dashboard CRUD itself: create = any member; edit/delete = owner.

Bounded + complete (finding #57 + the real-product states). Every widget read is a bounded aggregate or a paginated page (filter-results rides the 2.5.12 pagination read at ≤50/page; distribution is a GROUP-BY with counts; created-vs-resolved is day/week/month buckets over a capped window — never per-issue loads); per-widget loading / error / empty / no-access / stale-referent states (a widget whose saved filter was deleted renders the designed stale state, never a crash) are designed + asserted; the dashboards list, the empty dashboard, and the zero-data chart states are all specified.

Out of scope (documented extension slots, each justified): the Jira audience matrix + editor lists (the access enum grows); default/system dashboard + starring + the wallboard/slideshow (presentation-layer extensions); per-gadget auto-refresh (needs a polling story); more gadget types (Assigned to Me, Activity Stream, Two Dimensional Statistics, Average Age, Resolution Time — registry additions); version overlays on created-vs-resolved (no version entity in the schema); column config on filter-results (fixed sensible columns ship; a picker is additive); cross-workspace dashboards (no such scope exists — rung 2).

Verification

  • Pull the Story branch, pnpm install, pnpm prisma migrate dev (the 6.3.1 dashboard tables apply cleanly; re-run reports "No difference detected"), pnpm db:seed, pnpm dev.
  • pnpm test:coverage — Vitest (real Postgres) over the widget registry, the aggregation reads (bucket/cumulative/group-by matrices), and the permission gates ≥90% per-file branch/fn/line.
  • Dashboards flow: sign in as zhuyue@motir.co / !QAZ1qaz → /dashboard (matching design/reports/dashboard.mock.html) → create "Team overview", access Workspace → add a filter-results widget over a 6.2 saved filter (rows paginate at the designed page size, ≤50), a distribution donut (Statistic type: Status — counts + percentages in the legend), and a created-vs-resolved chart (Weekly, last 90 days; toggle Cumulative and watch the series re-shape) → switch the layout between 1/2/3 columns and drag widgets between columns → reload: layout + positions persist.
  • Sharing + permissions: as bophilips@motir.co, the Workspace dashboard is visible but not editable (no edit affordances); a Private dashboard is invisible. Point a widget at a 6.4-private project bophilips isn't a member of → bophilips sees the no-access widget state, zhuyue sees the data (per-viewer gating).
  • Stale referent: delete the saved filter behind a widget → the 6.2 delete warning names the dependent widget ("1 dashboard widget" — the enumeration 6.3.1 extends); confirm → the widget renders the inherited "filter missing" card with the reconfigure affordance (never a crash).
  • Reports hub: /reports (the stub page this story replaces) lists the agile group (links to the shipped burndown / velocity / sprint-report surfaces) + the analysis group. Open Created vs Resolved → scope = project or saved filter, period + days-back + cumulative controls, the difference/area chart with the red/green semantics; resolve an issue (transition into a done-category status) and re-run → the resolved series ticks up in today's bucket; reopen it → the net count drops back. Open Status distribution → the donut + legend track the statistic-type picker (status, priority, assignee, …).
  • Both charts match the chart visual language (axes, gridlines, visible text legend, --el-chart-* tokens only — finding #35: colour never the sole signal); dark mode parity holds.
  • pnpm test:e2e --grep dashboards — Playwright over the real stack: the create-dashboard → add-widgets → drag → reload journey + the reports journey.
  • a11y check: the dashboard grid (widget chrome, add-gadget picker, config panels) and both report pages pass the strict axe sweep; charts carry their visually-hidden data tables (the 4.6.1 a11y pattern); fully keyboard-operable.