Implements the route to fetch all projects across all clients the owner has.
No status filtering—agents drafting a plan need to see paused/completed projects.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Design for a bearer-token-authenticated REST API letting an external
AI coding agent read every project/card the owner has and write back
exactly three things: a comment, subtasks, or a column move. Follows
the same auth pattern as the existing CRON_SECRET-protected route.
Agent-authored comments are prefixed so they're never mistaken for
something the owner typed, mirroring Phase 7's "Logged" tag principle.
MCP explicitly deferred to a later, thinner follow-up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Added `NEXT_PUBLIC_APP_URL` to `.env.example` for app URL configuration.
- Updated `README.md` to document the new `NEXT_PUBLIC_APP_URL` variable.
- Improved CSS to support light and dark color schemes.
- Refactored intake link actions to utilize the new app URL and handle token generation more efficiently.
- Enhanced public intake route to validate work area selection.
- Updated `IntakeLinksPanel` to allow copying of generated links.
- Improved error handling and user experience in `BriefPreviewPublic` and `IntakePublic` components.
- Refined theme synchronization and toggle components for better user preference management.
Any signed-in user — owner or client_manager — can now edit
dates directly on the Gantt. Three interaction shapes:
- Drag the bar body to move it (both start and target shift
by the same number of days, snapped to whole UTC days).
- Drag the right edge to resize (target only).
- Double-click any card or subtask bar to open a modal with
Start + End date inputs, ESC to close, Enter to save.
Both card and subtask rows ship the same affordances. Cards
go through the existing rescheduleCardAction so the
activity-log row keeps recording who moved the date;
subtasks go through a new setSubtaskDatesAction (no audit
row — matches the existing subtask-toggle pattern).
Components touched:
- GanttBar gains canEdit / onMove / onResize / onEditRequest.
- GanttDateEditor is the inline two-input modal.
- ProjectSection forwards four callbacks (card move/resize,
subtask move/resize) plus onEditRequest.
- GanttPlanView holds the editor's open state and computes
one shared timeline range across sections.
- GanttClient wraps the view and bridges to the actions.
- /plan/page.tsx renders GanttClient.
Spec §6 amended to move drag from out-of-scope into the
delivered surface.
482/482 unit tests pass (was 471, +11 from bar-math).
Integration plan-gantt tests still pass.
Build clean.
Pure helpers for the Gantt's drag/double-click math:
- fractionToDate(fraction, range): what ISO date a fractional
position in the chart represents, snapped to whole UTC days.
- shiftDatesByFraction: move a bar's start and target by the
same delta, clamped to the visible range, with start dropped
when clamping would invert or collapse the span.
- resizeEndToFraction: what the new target date becomes when
the user drags a right edge.
- moveCardByFraction: alias that produces the action's input
shape.
11 unit tests cover the corners (no dates set, point bar,
clamping past the right edge, inversion guard).
rescheduleCardAction also drops its owner-only gate. Any
signed-in user — owner or client manager — can move dates
now. PocketBase's existing row-level rules govern which cards
a given user can write (a client manager still only writes
their own client's cards); the activity-log row is unchanged
and attributes the move to the actual user either way.
Adds setSubtaskDatesAction so the same drag/double-click
gestures work on subtask rows. No audit log on subtasks —
matches the existing pattern (subtask toggles don't log).
When a subtask has start_date or target_date set, the card
detail's checklist row now shows a small '5 Mar → 19 Mar' label
next to the title. Mirrors the Gantt's view of the same data
without duplicating the chart on the card page.
Build clean.
Two tests against the live database and the seeded project:
- loadPlan returns a section containing the seeded project.
- GanttPlanView renders the project name and at least one
card title to HTML.
vitest integration config now picks up .test.tsx too.
The Puzzle + Timeline tabs of Phase 5 are gone. /plan now
renders one ProjectSection per active project. The old
RoadmapStats strip is removed — its only honest count was
'unscheduled', which the section footer already states
per project.
Build clean.
Composes one ProjectSection per section with a single shared
timeline range. Renders the 'Nothing scheduled yet' empty
state when no section has any dated cards or subtasks.
One project's Gantt section: header with brand-colour top border,
label column with indented subtask labels, timeline track with
month labels + W1..W4 ticks + today marker, and per-row bars
reusing GanttBar. Flat row array drives both columns so labels
and bars cannot drift apart. Footer counts undated cards and
subtasks.
Single horizontal bar — same component renders both card bars
(full colour) and subtask bars (same colour, optionally
strikethrough when done). Uses the same TimelineRange as the
header so the bar and the month ticks above it can never
disagree. Keyboard-focusable with a brand-coloured focus ring.
New loader returns the sectioned shape the Gantt view needs:
- buildPlanSections (pure): groups cards by project, sorts by
start_date asc / title, splits subtasks into dated vs undated.
Five unit tests cover the grouping and sorting rules.
- loadPlan (server-only): one PocketBase trip per collection,
delegates to buildPlanSections.
471/471 unit tests pass (was 466, +5 new).
Three new optional fields, idempotent ensure helpers:
- cards.assignee: free-text label rendered above the card bar
on the new /plan Gantt. Empty by default.
- subtasks.start_date: ISO date or empty. Lets subtasks appear
on the Gantt under their parent card with their own duration.
- subtasks.target_date: same, for the bar end.
Live migration confirmed idempotent — first run added the
fields, second run reported both already present.
Nine tasks: schema adds, loadPlan + pure grouping helper,
GanttBar / ProjectSection / GanttPlanView components, page
wire-up, integration test, and full verification. Drag and
inline subtask editing stay out of scope.
Redesign the planning page as a project-grouped Gantt:
parent rows are cards (start_date..target_date), indented
children are subtasks (gaining optional start_date and target_date).
No new collections. cards gains an optional assignee text field.
Replaces the Phase 5 Puzzle + Timeline tabs at /plan.
Out of scope: drag-to-reschedule, subtask dates edited on the
card page, connector lines, status/priority filters.
Phase 8 (public anonymous intake + PWA + per-project context) and
Phase 9 (per-client request links + project picker) are now on
main, alongside the Plan Puzzle v1 work that the sibling thread
left in the working tree.
The branch plan-puzzle-v1-tests had accumulated changes from a
sibling thread (Plan Puzzle v1 backend + frontend) that hadn't
been committed yet, plus a new src/lib/git/repo-url helper used by
the Forgejo ingest path. Pick them all up so the working tree is
clean and the branch reflects everything since its fork.
Files (Plan Puzzle):
src/app/actions/client-proposal.ts
src/app/actions/owner-plan.ts
src/app/actions/reschedule.ts
src/app/actions/reschedule-helpers.ts (+ test)
src/components/admin/project-form.tsx
src/components/roadmap/roadmap-builder.tsx
src/components/roadmap/timeline-view.tsx
src/lib/cards/write-with-audit.ts (+ test)
src/lib/roadmap/timeline.ts (+ test)
New helper:
src/lib/git/repo-url.ts (+ test)
Verified: npm run build clean, 466/466 unit tests pass.
The project admin page used to show only the legacy per-project
intake links (Phase 8 shape). After Phase 9 minting moved the
default to per-client links, owners navigating from a project no
longer saw its request link there — only the BackgroundMdEditor and
other panels. They'd find the link on the client admin page but the
project page suddenly looked empty of intake context.
Fix: query links where link.client == this project's client (the
Phase 9 shape) and render them through the existing panel in mode='client'.
The Phase 8 panel stays too — both shapes coexist during the
migration window. Same data, two views.
The request-links panel now has a trash button at the far right of
each row. Click it, the link is gone — cascadeDelete on
intake_link_submissions.link removes the daily counters in the same
transaction, and revalidatePath re-renders the panel without the row.
Hard delete, no confirmation: matches the existing Pause/Resume
pattern (no dialog there either). The icon is a Trash2 from
lucide-react; the title attribute is the only label, by design
owner-only surface.
Phase 9 shape: action takes a scope so revalidation hits the
client admin page the panel is mounted on.
Phase 9c. 12 tests covering:
- picker page (multi-project list, single-project redirect,
zero-projects fallback)
- wizard URL (project-on-client 200, cross-client 404,
show_in_intake=false 404)
- submission flow (card lands on chosen project's inbox,
cross-client refused, hostile body fields ignored,
per-(link, project) cap isolates one project from another)
- Phase 8 backwards compat (per-project link redirects to the
wizard; the existing Phase 8 submission route still accepts
Phase 8 links)
Three bridges from the plan:
1. PocketBase stores date as 'YYYY-MM-DD 00:00:00.000Z'; the
filter must use that form. Passing 'YYYY-MM-DD' silently
matches zero rows, even though the create succeeds.
2. PocketBase's getFirstListItem returns 404 for a row just
written by the same admin client; getList works. The helper
uses getList with perPage=1.
3. PocketBase's read-after-write latency is significant.
isLinkProjectUsable retries with exponential backoff for
up to ~2.5s before giving up; bumpDailyCapForProject's
upsert pattern uses a fallback read+update path if the
create hits the composite unique index from a peer's
concurrent write.
Phase 9b Task 6. intakeStepPublicAction takes optional projectId;
branches on link.client — Phase 9 path runs isLinkProjectUsable
(per-(link, project) cap) and resolveProjectForClient; Phase 8 path
falls through to validateIntakeLinkForStep unchanged. The wizard
component threads projectId from the URL into the action.
POST /api/public/intake verifies the URL's projectId belongs to the
link's client and is opted in; 404 if not, 429 if the
per-(link, project) cap is reached, 201 on success. On success the
bump goes to bumpDailyCapForProject (Phase 9) or the legacy
bumpSubmissionCount (Phase 8).
Phase 8 backwards compat: a per-project link (link.client empty)
goes through the unchanged Phase 8 path; body without projectId
still works on the Phase 8 route.
Phase 9b Task 5. Verifies the URL's projectId belongs to the link's
client AND is opted in AND is active. Phase 8 URLs (with link.project
set, link.client empty) ignore the URL projectId and use the link's
own — so the Phase 8 wizard URL pattern still works.
Phase 9b Task 4. The /intake/[token] route is now the picker.
Single-project links redirect straight to the wizard (no extra
click). Zero-projects links show a static 'nothing here yet' page
with no info leakage. Phase 8 backwards compat: a Phase 8 link
(with link.project set, link.client empty) redirects into the
wizard URL using that projectId, so the Phase 8 UX is unchanged.
Phase 9b Task 3. listPickerProjects reads the public-eligible
projects on a client; resolveProjectForClient checks that a given
projectId belongs to the client and is opted in. Both fail closed
on any DB read error.