feat(milestones): schema + repository + API (backend-only)#39
Merged
Conversation
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>
This was referenced May 25, 2026
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 c2a8df0 → 364080b; 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>
5 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
Milestonemodel:id,boardId,enc_name(encrypted title + description envelope),startAt/endAt(server-visible dates — required for chart range + timeline filtering),archivedflagCard.milestoneId String?withonDelete: SetNull(deleting a milestone keeps the cards, just drops the link)@@index([boardId, archived]),@@index([endAt])on Milestone;@@index([milestoneId])on CardMigration
prisma/migrations/20260525000001_milestones/migration.sql— hand-written, mirroring exactly whatprisma migrate diffwould emit. Consistent with the Phase-1 pattern where the shadow DB isn't always reachable from Windows-host dev.Repository
src/lib/repositories/milestones.ts—listForBoard,create,update,remove,assignCard. RBAC via the existingrequireBoardRead/requireBoardWriteguards.assignCardvalidates the milestone lives on the same board as the card.API routes
GET/POST/api/boards/[id]/milestones?archived=1to include archived) + createPATCH/DELETE/api/milestones/[id]PUT/api/cards/[id]/milestone{milestoneId: "..."}) or unassign ({milestoneId: null})Docs
Encryption shape
enc_nameis opaque to the server (XChaCha20-Poly1305 envelope, same pattern as otherenc_*fields).startAt/endAtare unencrypted dates — the server needs them for chart-range queries and for the Timeline view's filter. This matchesCard.dueAt/Card.startAt.Test plan
pnpm typecheckcleanpnpm lintcleanpnpm test— 107/107 (no new tests; project pattern is E2E-only for repository paths)What's next
🤖 Generated with Claude Code