Skip to content

feat(milestones): schema + repository + API (backend-only)#39

Merged
Musiker15 merged 1 commit into
mainfrom
feat/milestones
May 25, 2026
Merged

feat(milestones): schema + repository + API (backend-only)#39
Musiker15 merged 1 commit into
mainfrom
feat/milestones

Conversation

@Musiker15
Copy link
Copy Markdown
Member

Summary

First of the 6 sequential "deferred feature" PRs. Phase 5 originally deferred Milestones (needed by Phase 7's Burn-Down). This PR lands the backend slice so the data shape can be reviewed in isolation; UI integration follows in PR 1b.

What's in

Schema (prisma/schema.prisma)

  • Milestone model: id, boardId, enc_name (encrypted title + description envelope), startAt / endAt (server-visible dates — required for chart range + timeline filtering), archived flag
  • Card.milestoneId String? with onDelete: SetNull (deleting a milestone keeps the cards, just drops the link)
  • Indices: @@index([boardId, archived]), @@index([endAt]) on Milestone; @@index([milestoneId]) on Card

Migration

  • prisma/migrations/20260525000001_milestones/migration.sql — hand-written, mirroring exactly what prisma migrate diff would emit. Consistent with the Phase-1 pattern where the shadow DB isn't always reachable from Windows-host dev.

Repository

  • src/lib/repositories/milestones.tslistForBoard, create, update, remove, assignCard. RBAC via the existing requireBoardRead / requireBoardWrite guards. assignCard validates the milestone lives on the same board as the card.

API routes

Method Path Purpose
GET / POST /api/boards/[id]/milestones list (+?archived=1 to include archived) + create
PATCH / DELETE /api/milestones/[id] update fields / delete
PUT /api/cards/[id]/milestone assign ({milestoneId: "..."}) or unassign ({milestoneId: null})

Docs

  • CLAUDE.md §7 (schema) + §8 (endpoints) updated. Phase 7's burn-down dependency on milestones is now unblocked.

Encryption shape

enc_name is opaque to the server (XChaCha20-Poly1305 envelope, same pattern as other enc_* fields). startAt / endAt are unencrypted dates — the server needs them for chart-range queries and for the Timeline view's filter. This matches Card.dueAt / Card.startAt.

Test plan

  • pnpm typecheck clean
  • pnpm lint clean
  • pnpm test — 107/107 (no new tests; project pattern is E2E-only for repository paths)
  • CI green
  • Follow-up PR 1b adds UI + an E2E Playwright case exercising the new endpoints end-to-end

What's next

  • PR 1b — UI: milestone selector in card-drawer + management modal accessible from board settings.
  • PR 2 — Burn-Down chart (uses the dates from this PR).
  • PR 3-6 per the plan.

🤖 Generated with Claude Code

Phase-5 deferred item #1: Milestones. Backend lands first as a clean,
reviewable slice; the UI integration (selector in card-drawer +
management modal) ships as a follow-up PR so reviewers can focus on
the data shape here in isolation.

Schema (prisma/schema.prisma):
- New `Milestone` model: id, boardId, enc_name (encrypted title +
  optional description in one envelope), startAt / endAt (server-visible
  date window — needed for chart range + filtering), archived flag.
  `@@index([boardId, archived])` + `@@index([endAt])` for the typical
  list-by-board and "upcoming deadlines" queries.
- `Card.milestoneId String?` with `Card.milestone Milestone? @relation(...,
  onDelete: SetNull)` so deleting a milestone keeps the cards.
- `@@index([milestoneId])` on Card so per-milestone listing is cheap.

Migration (20260525000001_milestones/migration.sql):
- Hand-written (consistent with the Phase-1 pattern of using
  `prisma migrate diff` only when the shadow DB is reachable). Mirror
  exactly what Prisma generates: CREATE TABLE + ALTER TABLE + indices +
  FKs.

Repository (src/lib/repositories/milestones.ts):
- listForBoard (with includeArchived flag)
- create / update / remove with the existing requireBoardRead /
  requireBoardWrite RBAC guards
- assignCard — board-scoped assignment, validates the milestone lives
  on the same board as the card

API routes:
- GET / POST  /api/boards/[id]/milestones    (list + create)
- PATCH / DELETE  /api/milestones/[id]       (update + delete)
- PUT  /api/cards/[id]/milestone             (assign / unassign)

The encryption shape stays consistent with the rest of the system:
`enc_name` is opaque to the server, dates are server-visible (they
have to be for the burn-down chart range + timeline filtering).

CLAUDE.md updated:
- §7 schema gets the new `Milestone` model + Card.milestoneId
- §8 API endpoints table gets the four new routes
- Phase-7 burn-down can now be implemented (was blocked on "needs
  Milestones")

Follow-up tracked: UI integration (selector in card-drawer + Milestone
management modal). Coming as PR 1b once the backend is in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Musiker15 <info@musiker15.de>
@Musiker15 Musiker15 merged commit 364080b into main May 25, 2026
8 checks passed
@Musiker15 Musiker15 deleted the feat/milestones branch May 25, 2026 13:26
Musiker15 added a commit that referenced this pull request May 25, 2026
…d badge (#47)

Closes the UI half of #39's milestone feature. Backend (schema, repo,
API) is on main since c2a8df0364080b; this PR makes them visible.

### New files
- `milestone-types.ts` — shared `ApiMilestone`, `MilestoneMeta`,
  `MilestoneView` for board-client / drawer / manager / cache to all
  reference the same shape without circular import.
- `milestone-manager.tsx` — self-contained modal: create form,
  list of milestones with archive-toggle, edit-in-place, delete with
  confirm. Bind-to convention is "milestone:new" (same simple
  convention as templates / labels).

### board-client.tsx
- Loads milestones in the initial Promise.all with `?archived=1`
  (the manager filters client-side via the "Show archived" toggle).
- `decryptMilestone` helper next to `decryptTemplate`.
- Four new handlers: `createMilestone`, `updateMilestone`,
  `deleteMilestone`, `assignCardMilestone` — all wire through the
  routes shipped in #39.
- New "Milestones (N)" button in the board header next to the export
  buttons. Opens the manager modal.
- Mounts `<MilestoneManager>` conditionally.

### card-drawer.tsx
- `CardSnapshot` gains `milestoneId: string | null`.
- `DrawerProps` gains `milestones: MilestoneView[]` and
  `onMilestoneChange(milestoneId: string | null): Promise<void>`.
- New `<label>` block right after Due Date with a `<select>` listing
  active milestones (plus the currently-assigned one even if it is
  archived). Changing the value PUTs `/api/cards/[id]/milestone`.

### kanban-view.tsx
- `KanbanCard` gains `milestoneId: string | null`.
- `KanbanView` accepts `milestones: MilestoneView[]`.
- `SortableCard` receives an optional `milestone` prop and renders a
  small "◆ <name>" badge next to the Due-date / checklist counters
  on the card preview.

### Other plumbing
- `cards.ts` repository now returns `milestoneId` in the CardDto so
  the field flows through the public API. `Card_milestoneId_idx` from
  #39 keeps the per-milestone listing cheap.
- `board-cache.ts` (IndexedDB snapshot) adds `milestones` to the
  cached `BoardSnapshot` so offline-first reloads keep them visible.
  Snapshot version stays at 1 — defensive `?? []` on load handles
  pre-existing caches without bump.

### Out of scope (follow-ups)
- Export (`board-export.ts`) does not yet include milestones in JSON
  / Markdown exports. Trivial follow-up.
- The card-snapshot `onChange` propagation doesn't carry
  `milestoneId` because the source of truth for that field is the
  dedicated `PUT /api/cards/[id]/milestone` path, not the bulk save.

### Test plan
- [x] `pnpm typecheck` clean
- [x] `pnpm lint` clean
- [x] `pnpm test` — 107/107 (no new tests; behaviour exercised by E2E)
- [ ] CI green
- [ ] Manual: open board → "Milestones" button → create, edit,
      archive, delete. Open card → select milestone in drawer →
      reload → still assigned. Card preview shows the badge.

Closes #40.

Signed-off-by: Musiker15 <info@musiker15.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Musiker15 added a commit that referenced this pull request May 25, 2026
Closes #41. Phase-7-deferred item — landed simpler than the issue
proposed: no new schema, no new server endpoint, no ADR for a
"Done"-column flag. The existing `last column = Done` heuristic
(documented in analytics-view.tsx since Phase 7) is sufficient for
v1, and all the data the chart needs is already on the client
(cards via the board fetch, milestones via the #39 endpoint, activity
events via the existing `/api/boards/[id]/activity` poll).

### What's in
- `computeBurndown()` — pure helper that builds `BurnDownPoint[]`
  for a milestone. Scope held constant at the count of cards
  currently in the milestone (textbook burn-down assumption).
  Done = most recent `CARD_MOVED` event into a done column;
  cards currently in done with no recorded move fall back to
  their first activity timestamp or milestone start.
- `BurnDownChart` — pure SVG line chart matching the rest of the
  analytics-view's style. Two lines (ideal dashed + actual solid),
  vertical "today" marker, scope/0 baseline labels.
- Burn-Down section in `AnalyticsView`, placed after Throughput.
  Milestone selector top-right when ≥1 dated milestone exists.
  Empty states for "no milestones" and "no dated milestones".

### Wiring
- `AnalyticsView` accepts `milestones: MilestoneView[]`.
- `BoardClient` passes the `milestones` state through to the view.
- `pickedMilestoneId` defaults to null and derives the effective
  selection during render (no `useEffect` setState — avoids the
  react-hooks/set-state-in-effect lint rule).

### Why not a server endpoint
The issue suggested `GET /api/milestones/[id]/burndown`. We already
ship enough data to the client for cycle / lead / CFD analytics; the
burn-down is the same shape. Pushing the aggregation server-side
would have meant either decrypting card content (impossible) or
running an extra round-trip per milestone switch (slow). Same trade-
off the rest of `analytics-view.tsx` makes — keep it in this view.

### Why no Done-column flag yet
The existing convention (sort columns by position, last one wins)
is good enough for v1 — boards seeded by us start with
"Backlog / In progress / Done", and that's also how `doneColumnIds`
in `computeStats` already works. A per-board "this column is Done"
toggle remains a worthwhile follow-up but is independent of burn-
down — every other analytics metric would benefit from it too.

### Test plan
- [x] `pnpm typecheck` clean
- [x] `pnpm lint` clean
- [x] `pnpm test` — 107/107
- [ ] CI green
- [ ] Manual: create milestone with start = today-3d, end = today+10d,
      assign 5 cards, mark 2 done. Open Analytics → see scope=5,
      actual line dips to 3 at today, ideal slopes from 5 to 0.

Closes #41.

Signed-off-by: Musiker15 <info@musiker15.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Musiker15 added a commit that referenced this pull request May 30, 2026
Close the ADR-0005 checklist/comment clause as a *live overlay* (ADR 0020):
with a card open, peer edits to checklists/comments now arrive live (no
SSE-tick + full refetch) and concurrent structural edits merge conflict-free.

Implementation:
- New src/lib/realtime/card-collab.ts: three id-keyed Y.Maps (cl/cli/cm) on
  the card's existing Y.Doc, riding the same encrypted WS relay + binding the
  description already uses — no server/relay change. Pure view-merge builders
  (preserve in-flight edit drafts across remote merges) + a CardCollab
  controller (local-origin writes skip the local subscriber).
- card-drawer.tsx renders from the merged snapshot; each committed REST
  mutation mirrors into the overlay. View types moved into card-collab.ts.
- Comment delete = tombstone (no resurrection; authorship matters); checklist
  structure = key removal. Id-keying makes cross-peer seeding idempotent.

Authority model: REST stays the durable store + only side-effect trigger, so
RBAC, append-only audit, automation emitters (comment_added/append_checklist/
post_comment), @-mentions, notifications and the DSGVO export are untouched.
Rejected Yjs-as-authority (ADR 0020): it would strip the server of comment
authorship/membership enforcement and security-audit emission — a regression
of the zero-knowledge RBAC model. Map values are plaintext only inside the
Y.Doc (AEAD on the wire, wiped from IndexedDB on logout, exactly as the
description draft); the server still sees only opaque envelopes.

Tests: tests/unit/card-collab.test.ts (+12) incl. a two-peer sync + a
concurrent-edit-merge case. typecheck/lint/tests green (287).

Docs: ADR 0020 (new), ADR 0005 execution note, CHANGELOG, CLAUDE.md
(header, §2, §5.2, ADR table).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant