Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions docs/decisions/0003-report-export-routing-and-page-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
status: 'accepted'
date: 2026-06-08
decision-makers: santosral
consulted:
informed:
---

# Nested per-report download endpoints and separate per-report page routes

## Context and Problem Statement

The user profile report adds a second report to the admin **Generate Report** area (`/admin/reports`) alongside the existing quiz report, plus a second download endpoint. Two structural choices follow: how to shape the download routes, and how to present the two reports. How should each be done without over-engineering for two reports?

## Decision Drivers

- Single-purpose, easy-to-read handlers and page loads.
- A clear "report exports" family in the route tree.
- Server-side pagination kept working per report.
- Pages and API endpoints kept in separate route trees.
- No speculative abstraction for what is currently two reports (rule of three).

## Considered Options

Download routing:

- Nested per-report endpoints — `admin/api/download/quiz` and `admin/api/download/user-profile`
- A single endpoint with a `?type=` query-param discriminator
- A shared config/registry the endpoints derive from

Page presentation:

- Separate per-report page routes with a shared layout nav — `admin/reports/quiz` and `admin/reports/user-profile`
- Inline tabs driven by a `?tab=quiz|user-profile` query param on one page
- A reusable `Tabs` component

## Decision Outcome

Chosen options: "Nested per-report download endpoints" and "Separate per-report page routes".

Each download is its own single-purpose handler under `admin/api/download/{quiz,user-profile}`, keeping the export family obvious in the tree and keeping downloads — which are API endpoints that stream a file — in the API route tree, separate from the page routes.

Each report preview is its own page route under `admin/(protected)/reports/{quiz,user-profile}`. A shared `+layout.svelte` renders the "Generate Report" header and the report nav (two links, active by pathname), and `/admin/reports` redirects to the quiz report (the default). Because each page's load runs only its own queries, server-side pagination keeps working per report without tab-branching or a discriminated-union page payload.

No reusable `Tabs` component is introduced: the nav is two links in a layout, with no ARIA/keyboard widget complexity, and two reports do not meet the rule of three.

### Consequences

- Good, because each page load and each download handler stays single-purpose.
- Good, because per-page loading keeps server-side pagination working for both reports, with no discriminated-union payload or tab branching.
- Good, because pages and API endpoints remain in cleanly separated route trees.
- Good, because no abstraction is built ahead of need.
- Bad, because there is minor duplication between the two page routes, the two endpoints, and the two nav links; acceptable at two, to be revisited at three.

### Confirmation

Spec B has separate page routes whose loads each query only their own dataset; the new `download/user-profile` endpoint exists as a sibling of `download/quiz`; a shared reports layout renders the nav and `/admin/reports` redirects to the default; reviewers confirm no shared discriminator/registry and no reusable `Tabs` component were introduced.

## Pros and Cons of the Options

### Nested per-report endpoints

- Good, because each handler is single-purpose and discoverable, and downloads stay in the API tree.
- Bad, because a little setup is repeated per endpoint.

### Single endpoint with `?type=` discriminator

- Good, because one file.
- Bad, because it becomes a multi-purpose branchy handler.

### Shared config/registry

- Good, because exports become data.
- Bad, because it is over-engineering for two reports (YAGNI).

### Separate per-report page routes

- Good, because each page is single-purpose, its load has no tab branching or union payload, and pages stay separate from API endpoints.
- Bad, because a little setup, and the nav, is repeated per page.

### Inline query-param tabs

- Good, because there is only one page route.
- Bad, because the load branches per tab and the page carries a discriminated-union payload.

### Reusable `Tabs` component

- Good, because it would centralize tab behavior.
- Bad, because two simple reports do not justify it (rule of three).
129 changes: 129 additions & 0 deletions docs/decisions/0004-card-grid-report-index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
status: 'accepted'
date: 2026-06-10
decision-makers: santosral
consulted:
informed:
---

# Card-grid report index with per-page back navigation

## Context and Problem Statement

[ADR-0003](./0003-report-export-routing-and-page-routes.md) presents the two
reports in the admin **Generate Report** area as a shared layout nav — two links,
active by pathname — with `/admin/reports` redirecting to the default report. That
ADR deliberately deferred anything richer until a third report appeared ("minor
duplication … acceptable at two, to be revisited at three").

We now want the report-selection UI to stay readable as more reports are added,
rather than grow a horizontal nav strip that crowds or overflows. This revisits
only the **page-presentation** half of ADR-0003; its download-routing decision
(nested per-report endpoints) is unchanged and carries forward.

## Decision Drivers

- A selection UI whose readability does not degrade as the report count grows.
- No added complexity for the two reports that exist today.
- Self-contained, explicit report pages that are easy to read in isolation.
- Consistency with the codebase's preference for small duplication over
speculative abstraction at low counts (the rule-of-three stance of ADR-0003).
- The admin sidebar already links to the reports area, so that entry point should
land somewhere useful rather than bounce through a redirect.

## Considered Options

- A card-grid landing page with per-page back navigation
- Keep the shared layout tab nav and default-report redirect (status quo, ADR-0003)
- A scrollable / overflowing horizontal tab strip
- A single dropdown selector for the active report
- A left sidebar list of reports

## Decision Outcome

Chosen option: "A card-grid landing page with per-page back navigation."

The reports area entry point becomes a landing page that presents each report as a
card (title and a one-line description); selecting a card opens that report's own
page. The default-report redirect is removed — the landing page is the
destination — and the shared tab nav is dropped, since the grid is now the
switcher. Each report page carries a back affordance to the landing page.

The set of reports shown on the landing page is inline presentational data
co-located with the landing page, not a shared registry the endpoints or pages
derive behaviour from: each report page and each download endpoint stays
independent, exactly as in ADR-0003. The back affordance likewise lives on each
report page rather than in shared chrome, keeping every page readable top to
bottom and avoiding a layout that has to special-case its landing route.

A card grid of two cards is no more complex than two nav links, and an inline
list of two entries is the same shape as the nav array it replaces, so this is a
lateral presentation choice — not abstraction ahead of need — whose layout simply
does not crowd as the report count rises.

### Consequences

- Good, because the landing grid stays readable as reports are added, where a
horizontal nav strip would crowd or overflow.
- Good, because the change adds no complexity at the current count of two: a grid
of two cards and a two-entry list match the prior two-link nav.
- Good, because each report page is self-contained — its own content and its own
back affordance — with no shared layout branching on which child is the landing
route.
- Good, because the sidebar entry now lands on a useful overview instead of an
immediate redirect.
- Bad, because the back affordance is repeated per report page and the landing
list gains one entry per report — small duplication, accepted at low counts and
to be revisited (a shared back-link element, or promoting the list to its own
module) when a third-plus report makes it nag.
- Bad, because reaching a report now passes through the landing page rather than a
direct default — one extra step, traded for a clearer overview.

### Confirmation

The reports landing route renders one card per report and no longer redirects;
each report page renders a back affordance to the landing route and the shared tab
nav is gone; reviewers confirm the report list is inline presentational data (no
shared registry) and that the nested per-report download endpoints from ADR-0003
are untouched.

## Pros and Cons of the Options

### Card-grid landing page with per-page back navigation

- Good, because a grid scales to more reports without the layout crowding.
- Good, because it is equal in complexity to the prior nav at two reports.
- Good, because pages stay explicit and self-contained.
- Bad, because the back affordance and a list entry are repeated per report.

### Keep the shared layout tab nav and redirect (status quo)

- Good, because it is already built and is minimal for two reports.
- Bad, because a horizontal nav strip crowds and eventually overflows as reports
are added — the very thing this decision sets out to avoid.

### Scrollable / overflowing horizontal tab strip

- Good, because it is a small change from the status quo.
- Bad, because horizontal scrolling hides reports off-screen and reads poorly;
it postpones rather than solves the crowding.

### Single dropdown selector

- Good, because it is compact and scales to many reports.
- Bad, because it hides the list behind a control and shows no description, making
reports less discoverable than cards.

### Left sidebar list

- Good, because a vertical list scales cleanly.
- Bad, because it adds a second persistent nav column inside a page that already
sits next to the admin sidebar, competing for the same affordance.

## More Information

Supersedes the page-presentation decision of
[ADR-0003](./0003-report-export-routing-and-page-routes.md); that ADR's
download-routing decision remains accepted. The chosen outcome is realised in the
[User Profile Report spec](../superpowers/specs/2026-06-08-user-profile-report-design.md),
which states the presentation as built and links back here for the rationale.
3 changes: 2 additions & 1 deletion docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ the ADR; the spec consumes its outcome.

- [0001 — Stream report exports end-to-end with ExcelJS via a generic helper](./0001-stream-report-exports-with-exceljs.md) — accepted
- [0002 — Read streaming exports with keyset cursor pagination on the primary key](./0002-keyset-cursor-pagination-on-primary-key.md) — accepted
- [0003 — Nested per-report download routes and inline query-param tabs](./0003-report-export-routing-and-tabs.md) — accepted
- [0003 — Nested per-report download endpoints and separate per-report page routes](./0003-report-export-routing-and-page-routes.md) — accepted
- [0004 — Card-grid report index with per-page back navigation](./0004-card-grid-report-index.md) — accepted
4 changes: 2 additions & 2 deletions docs/superpowers/plans/2026-06-08-streaming-report-exports.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

**Tech Stack:** SvelteKit (adapter-node, Node 24), Svelte 5, Prisma (pg adapter), `exceljs`, Vitest. Package manager: `pnpm`.

**Scope:** This is **PR1 of 2** (Spec A — [streaming foundation + quiz migration](../specs/2026-06-08-streaming-report-exports-design.md)). The onboarding report (Spec B) is a separate plan, stacked on this branch, written after PR1 lands. Decisions: [ADR-0001](../../decisions/0001-stream-report-exports-with-exceljs.md) (streaming via generic helper), [ADR-0002](../../decisions/0002-keyset-cursor-pagination-on-primary-key.md) (keyset cursor on primary key).
**Scope:** This is **PR1 of 2** (Spec A — [streaming foundation + quiz migration](../specs/2026-06-08-streaming-report-exports-design.md)). The user profile report (Spec B) is a separate plan, stacked on this branch, written after PR1 lands. Decisions: [ADR-0001](../../decisions/0001-stream-report-exports-with-exceljs.md) (streaming via generic helper), [ADR-0002](../../decisions/0002-keyset-cursor-pagination-on-primary-key.md) (keyset cursor on primary key).

**Conventions (from CLAUDE.md + project memory):**

Expand Down Expand Up @@ -88,7 +88,7 @@ git commit -m "chore: add exceljs dependency"

## Task 2: Shared pure utilities (`sanitizeSpreadsheetCell`, `formatTimestamp`)

Two pure helpers co-located in `reports/helpers.ts`, both shared by the quiz export and the onboarding report (Spec B): `sanitizeSpreadsheetCell` neutralizes CSV/formula injection by prefixing a leading formula-trigger character with `'`; `formatTimestamp` formats a `Date` as a `DDMMYYYYHHmmss` filename prefix.
Two pure helpers co-located in `reports/helpers.ts`, both shared by the quiz export and the user profile report (Spec B): `sanitizeSpreadsheetCell` neutralizes CSV/formula injection by prefixing a leading formula-trigger character with `'`; `formatTimestamp` formats a `Date` as a `DDMMYYYYHHmmss` filename prefix.

**Files:**

Expand Down
Loading
Loading