The design spec said an unset token gets a 401 like a missing/wrong one;
the implementation (and the plan's own Definition of Done) has always
treated unset/misconfigured as 500, matching CRON_SECRET's own behavior.
Also extends the .env.example comment to spell out that this same value
must be set in Coolify's environment too, not just .env.local — this app
has no local PocketBase, so the token is checked against process.env on
whichever environment actually receives the request.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
no-double-accept, Inbox-never-accepts, and 404s
Adds the whole-branch-review test gaps: a single parametrised test looping
over all 6 (method, path, body) combinations asserting 401 for missing and
wrong tokens plus no-write-occurred for the 3 write routes; a move-route
case-insensitivity assertion (real column name uppercased); a negative test
that moving an already-accepted card again does not re-log card_accepted;
a negative test that moving a card into Inbox never sets accepted: true;
and 404 checks for a nonexistent card id across GET/POST/POST/PATCH.
Each new describe block creates and cleans up its own dedicated fixture
card(s), since the existing cardId fixtures from earlier describes in this
file are already deleted by their own afterAll by the time these run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every /api/owner/* and /api/client/* route wraps its body in try/catch;
none of the six /api/agent/* routes did, so an unexpected failure (e.g. a
PocketBase validation error partway through the subtasks loop) surfaced as
Next's default non-JSON 500 instead of the jsonError shape this surface
otherwise guarantees everywhere else. A caller retrying after an opaque
500 mid-loop could also create duplicate subtasks for ones that already
succeeded.
Follows the exact pattern already used by
src/app/api/owner/cards/[id]/move/route.ts: requireAgentToken and the
initial body-parse/validation stay outside the try (they touch no
PocketBase and already return correctly-shaped errors); everything after
is wrapped, with a route-specific console.error prefix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every other comment path in this codebase (the owner's own addOwnerComment,
and the client manager's POST /api/client/cards/:id/comments) writes a
comment_added row so the card's activity feed shows "commented" in its
timeline. The agent comment route was the one path silently invisible to
that audit trail — exactly where an audit trail matters most, since it's a
machine writing on the owner's behalf.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.env.example ships AGENT_API_TOKEN=replace-me, and unlike PB_SUPERUSER_PASSWORD's
own replace-me placeholder, an unchanged agent token fails open: any caller
sending "Bearer replace-me" would get full owner-equivalent access. Treat a
secret that is unset, literally "replace-me", or shorter than 32 characters
as not configured, matching the existing 500 behavior for an unset token.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.