Commit graph

163 commits

Author SHA1 Message Date
2a57d29279 chore(dev): add dev:webpack script as a Turbopack fallback
The chunk.reason.enqueueModel crash tracked in vercel/next.js#92362 is
Turbopack-specific — the React flight client in react-server-dom-turbopack
exposes an enqueueModel method that react-server-dom-webpack does not. Run

> client-board@0.1.0 dev:webpack
> next dev --webpack

▲ Next.js 16.2.6 (webpack)
- Local:         http://localhost:3000
- Network:       http://192.168.1.162:3000
- Environments: .env.local
✓ Ready in 554ms
[?25h while the upstream issue is open if the overlay
keeps catching the crash in spite of the ThemeScript / hydration fixes.
2026-09-17 12:24:40 +02:00
17a4df5f80 feat(plan): timeline builder — drag unscheduled blocks onto the timeline
Replaces the static Timeline tab with a builder layout: an
Unscheduled tray on the left (grouped Inbox/Now/Next/Later) and the
existing timeline on the right. Drag a block from the tray onto the
timeline at the date you want it scheduled; drag a bar horizontally to
reschedule a card already on the timeline. Both paths route through
the same rescheduleCardAction so the activity log only ever shows one
date_changed entry.

TimelineView gains a drop zone (custom MIME so tray drops do not
collide with other text drags) and a vertical drop indicator that
follows the pointer while dragging. The empty-state hint replaces
the previous early return so users with only undated cards still see a
target to drag onto. The dated-bar drag-to-reschedule behaviour is
unchanged.
2026-09-17 12:23:32 +02:00
02adcb43ac fix(board): keep optimistic state authoritative so a single drop always lands
The previous handler called router.refresh() after every successful move
and the Board effect then adopted the new initialCards prop. Because
router.refresh() schedules the server re-render asynchronously, the new
initialCards could land AFTER setSaving(false) had already cleared busy.
The effect then ran with !busy and setCards(initialCards) reverted the
optimistic move — the card visibly snapped back to its origin column
on every single drag, even though the database had the move. The user
had to drag the card a second time to land it in the destination.

Trust the optimistic state for the lifetime of the Board instance.
The first mount still adopts the server snapshot via the useState
initializer. Concurrent moves from other clients appear on the next
page visit, which is acceptable for an owner-only kanban.

The useRefreshOnFocus hook still keeps a focus listener registered
while not busy, so a real cross-tab refresh still fires.
2026-09-17 12:15:34 +02:00
e7110c44ba fix(theme): no <script> in RSC, OS preference via CSS media query
The earlier 'use next/script beforeInteractive' attempt still emitted a
<script> tag inside React's renderer and therefore still tripped React
19's 'Encountered a script tag' warning on every page that used the root
layout, which is one of the documented triggers of the
chunk.reason.enqueueModel flight-client crash in
vercel/next.js#92362.

ThemeScript now returns null. The first paint picks the OS preference via
a @media (prefers-color-scheme: dark) block in globals.css so no flash
occurs for users on the default. An explicit stored choice is applied
synchronously by ThemeSync (a small client component) on mount — a
trade-off the shadcn dark-mode guide accepts too.

Also fixes the .light class selector the new media-query rule introduces.
2026-09-17 12:06:25 +02:00
a4774783a9 fix(dashboard): single template-literal child for <title> to stop SVG hydration mismatch
React 19's HTML serializer drops mixed text/expression children inside
<title> elements when emitting SVG markup, so the server rendered
<title></title> while the client rendered <title>2026-05-24: 0 commits</title>.
That mismatch was one of the documented triggers of the
chunk.reason.enqueueModel flight-client crash in
vercel/next.js#92362.

Pre-formatting the tooltip into a single string before passing it as the
<title>'s only child keeps server and client output identical.
2026-09-17 12:06:20 +02:00
950c3fb8ee fix(theme): use next/script beforeInteractive — <script> in RSC was breaking the flight client
Next.js 16 + React 19 reject <script> tags rendered from React
components: 'Encountered a script tag while rendering React component.
Scripts inside React components are never executed when rendering on the
client.' The error fires during RSC rendering for every page that uses
the root layout, which corrupts Turbopack's flight stream and surfaces
as 'chunk.reason.enqueueModel is not a function' on every navigation
(vercel/next.js#92362).

The dev-overlay recommendation to use <template> is misleading — that
won't execute a script. The supported App Router pattern is
<Script strategy="beforeInteractive"> from next/script, which inlines
the script in <head> before hydration. Theme behaviour is unchanged.
2026-09-17 11:22:40 +02:00
5c26f39f2c fix(nav): avoid Kanban flyout state update racing with RSC navigation
A click outside the Kanban sidebar flyout called setOpen(false) even when
the click was on a Link, so the deferred state update landed during the
Link navigation's RSC stream and corrupted Turbopack's flight client
(vercel/next.js#92362).

Skip the close when the click target is inside an <a> and flush the close
synchronously from the Link's own onClick so the state update completes
before navigation, not during RSC processing.
2026-09-17 11:15:32 +02:00
dc45063508 test(nav): prototype visibility and client scoping on the new home page 2026-09-17 10:42:52 +02:00
2cb745779a feat(nav): sidebar Kanban flyout, grouped by client 2026-09-17 10:42:50 +02:00
140703e0eb feat(projects): kind on create/admin, home page grouped by client 2026-09-17 10:42:48 +02:00
5dae0946eb feat(projects): add kind field (project | prototype) 2026-09-17 10:42:46 +02:00
5c986e6449 docs(spec): Phase 6 — client-grouped navigation and project kind
Design for grouping projects by client on the home page and in a new
sidebar Kanban flyout tree, making the dashboard the project landing
page with the board one click away, and a prototype/project kind flag
so unsolicited client-facing prototypes can live alongside committed
projects with the same permissions, just tagged differently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 10:14:31 +02:00
6c41f4c662 fix(ai): raise max_tokens — inline reasoning was eating the whole budget
MiniMax M2.x reasons inside <think> tags in the normal content field, and
that reasoning is billed against max_tokens. At 4096 the heavier batches
spent the entire budget thinking, were truncated before writing a single
answer, and stripThinking correctly removed the unclosed block — leaving an
empty response.

The signature was distinctive: batches returned 25/25 or 0/25, never
anything partial. 193 of 779 commits went unsummarised with no error
anywhere. At 16384 the same 193 completed 8/8 batches with zero failures.

The interview's question call had the same bug at 1024, where it would have
returned an empty question and silently ended the interview on the first
turn. Now 8192.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 09:19:34 +02:00
226f8576f2 fix(board): pin the DndContext id to stop a hydration mismatch
dnd-kit derives the id of its accessibility description element from an
internal counter that starts fresh on the server and is already advanced on
the client. Every sortable card rendered aria-describedby="DndDescribedBy-0"
server-side and "DndDescribedBy-3" client-side, and React reported a
hydration mismatch across the whole board.

A stable id makes both sides agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 09:08:02 +02:00
8888acb2c6 fix(tests): stop the settings test destroying the configured API key
The test overwrote ai_api_key with a marker and then restored it to ""
rather than to its previous value, so every run of the integration suite
silently wiped the AI provider key. Symptom appears much later and looks
nothing like a test problem: AI features quietly stop working.

Captures the existing value first and restores it in a finally block, so a
failed assertion cannot leave the marker in place either. Deletes the
record instead when the test created it.

The suite runs against the shared instance — there is no separate test
database — so anything it touches, it must put back exactly.

feat(board): give the owner both a quick box and Plan with AI

The Phase 4 spec made the intake wizard client-facing only, reasoning that
the developer creates cards directly. That was wrong about how he wants to
work. The quick box stays the default for lines he already understands;
Plan with AI runs the same interview for work that is genuinely unformed.
Hidden entirely when no provider is configured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 08:13:24 +02:00
7059105d78 fix(board): stop every successful drag from snapping back
The effect that syncs server state re-ran whenever `busy` flipped, which it
does at the end of every drag. It then called setCards(initialCards) with
the snapshot it already had — the server had not re-rendered — discarding
the optimistic move. The card visibly returned to its original column while
the database held the correct value.

Tracks the last adopted snapshot in a ref so the effect only acts on a
genuinely new one, and calls router.refresh() after a successful move so a
new snapshot actually arrives.

Also hardens the timeline drag: the pointerup handler compared against
`dragging` state, which updates asynchronously, so it could read the
pre-drag value and return early with the drag never committing and no error
anywhere. Reads a ref instead, and clears on pointercancel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 08:08:10 +02:00
6afd419dd3 test(roadmap): reschedule auditing and role scoping 2026-09-17 07:19:54 +02:00
9a5f6ca3f7 feat(roadmap): planning page with bands, timeline and stats 2026-09-17 07:19:00 +02:00
ecf5f0c173 feat(roadmap): reschedule from the timeline, always audited 2026-09-17 07:18:20 +02:00
bed7af848e feat(roadmap): Gantt-style timeline with today marker and tracks 2026-09-17 07:17:58 +02:00
bc8cc9f776 feat(roadmap): Now/Next/Later view 2026-09-17 07:17:35 +02:00
6e231374e2 feat(roadmap): loader scoped by the caller's own read rules 2026-09-17 07:17:18 +02:00
b164f3273c feat(roadmap): checklist-derived completion percentage 2026-09-17 07:16:57 +02:00
1c24687dae feat(roadmap): time axis, month ticks and greedy row packing 2026-09-17 07:16:47 +02:00
0454abb853 feat(roadmap): Now/Next/Later derived from board order and dates 2026-09-17 07:16:24 +02:00
d88dc91d7d docs: Phase 5 spec and plan for the planning and roadmap view
Two views rather than one chart, because the data forced it: measured on
the live database, 1 of 5 live cards has a target date and none has both
dates. target_date is client-owned and optional; start_date is only set on
Inbox acceptance. A conventional Gantt would render almost empty.

So Now/Next/Later is the default and works with zero dates, and the
timeline earns its place as clients set dates. From Dave's Gantt reference
it takes the quarter header, the Today marker, project grouping with the
client's colour, and the per-row completion percentage — which maps onto
checklist progress, the one honest source for it. The faint full-width
track is load-bearing rather than decorative: it makes a sparse chart read
as unscheduled instead of broken.

Drag-to-reschedule always writes an activity row naming the owner, because
target_date belongs to the client. A developer silently moving a date the
client chose is the failure the activity log exists to prevent.

Completes the plan set for phases 1 through 5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 07:14:47 +02:00
8a2fb46788 test(intake): server-decided fields and project scoping 2026-09-16 18:07:16 +02:00
5a9d1cb97f feat(card): show the intake brief and transcript 2026-09-16 18:05:42 +02:00
d5cf51c944 feat(intake): wizard on the board with a plain-box fallback 2026-09-16 18:05:20 +02:00
58be48a4b0 feat(intake): brief confirmation before submission 2026-09-16 18:04:53 +02:00
04e78aef6d feat(api): intake route creating an Inbox card from a brief 2026-09-16 18:04:44 +02:00
05ce52a6bf feat(schema): card brief, transcript and size band 2026-09-16 18:04:22 +02:00
0c25a3e6a1 test(intake): cap enforcement, brief whitelist, project scoping 2026-09-16 17:56:37 +02:00
f05a2a2ed2 feat(intake): interview action and client-facing wizard 2026-09-16 17:56:06 +02:00
a74f3e79b5 feat(intake): stateless interview service 2026-09-16 17:55:36 +02:00
bac4694ab1 feat(intake): whitelist brief parsing that degrades to raw answers 2026-09-16 17:55:14 +02:00
1c6e739b16 feat(intake): question and brief prompts 2026-09-16 17:54:54 +02:00
04f38ee114 feat(intake): size band derived from decomposition and known areas 2026-09-16 17:54:39 +02:00
95c4c6c8fa feat(intake): question cap and transcript sanitisation 2026-09-16 17:54:27 +02:00
0c4bbd951f docs(plan): Phase 4a and 4b for the client intake wizard
4a builds the stateless interview. Every constraint that matters is
enforced in code rather than requested in the prompt: the seven-question
cap, the answer length, and what fields a brief may contain. parseBrief is
a whitelist, not a parser — a brief asserting priority or accepted loses
those at the boundary and they never reach a caller that could act on them.

4b adds a separate intake route rather than extending Plan 1a's client
card route. That route's tests were written against a two-field body;
widening the shape it accepts would leave them passing while proving less.
Both routes set column, priority, accepted and created_by_role themselves,
and both are separately proven to.

The property both plans are written to: the worst a hostile brief can
achieve is a strangely-worded card in the Inbox.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:46:54 +02:00
d928e7fd9e docs(spec): Phase 4 design for the client intake wizard
A guided interview that produces a written brief with sub-tasks, landing
in the Inbox as a draft through Plan 1c's existing accept flow.

Corrects an over-promise made during the Phase 1 discussion: the wizard
was described as grounding date proposals in measured per-card velocity,
and that data does not exist — Plan 2c deferred per-card effort because
session padding does not sum correctly across cards. This phase therefore
produces no time estimate at all, only a size band.

Names the intake as the highest-risk surface in the product: it is the
only place where text written by someone other than the developer enters
a prompt whose output creates records. The security property is that the
worst a hostile brief achieves is a strangely-worded Inbox card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:37:57 +02:00
de8ae83dcb test(narrative): no invention, and raw-subject fallback 2026-09-16 16:26:29 +02:00
ee65f2238d feat(admin): manual narrative generation 2026-09-16 16:25:58 +02:00
4a278fa56b feat(dashboard): recent work in plain English 2026-09-16 16:25:39 +02:00
8711c36833 feat(card): progress narrative and humanised commit history 2026-09-16 16:25:19 +02:00
30446153c8 feat(ai): card narratives regenerated when the commit set changes 2026-09-16 16:24:38 +02:00
7ca2798561 feat(schema): card narrative fields 2026-09-16 16:24:18 +02:00
5cecc87010 feat(ai): narrative prompt and set-based staleness 2026-09-16 16:23:32 +02:00
aea000f654 test(ai): merge exclusion, graceful degradation, summary persistence 2026-09-16 16:18:40 +02:00
cff4f9c7fd feat(admin): summary progress and manual generation 2026-09-16 16:16:56 +02:00