Commit graph

301 commits

Author SHA1 Message Date
3ece6cb956 feat(intake-links): token resolver and daily-cap helpers
Phase 8a Task 5. resolveIntakeLink returns null for unknown tokens
(spec §3: caller gives the same response as for a revoked token).
isLinkUsable lazily resets today's counter on date rollover.
validateIntakeLinkForStep returns a three-valued LinkState so the
route can distinguish 'inactive/expired' from 'daily-cap'.
2026-09-21 01:59:28 +02:00
3b8ffb8e1f feat(intake-links): per-(token,ip) rate limiter
Phase 8a Task 4. 60 requests per 5-minute window per opaque key.
In-memory, lossy on restart, no IP persistence. Backed by the
daily cap on intake_links for coarse-grained abuse control.
2026-09-21 01:58:38 +02:00
f608931779 feat(http): best-effort client IP from headers
Phase 8a Task 3. Reads x-forwarded-for (first hop), then x-real-ip,
falls back to 'unknown'. Not used for auth or logging — pure rate-
limit keying so concurrent clients do not share one bucket.
2026-09-21 01:58:06 +02:00
36572d99f3 feat(intake): token generator and shape check
Phase 8a Task 2. 24 bytes of CSPRNG entropy, base64url-encoded to
exactly 32 URL-safe characters. isIntakeToken matches that exact
shape so the public route can fail fast on bogus input without
hitting the database or charging the rate limiter.
2026-09-21 01:57:38 +02:00
2937a305c3 chore(phase-8): progress log for 8a/8b/8c tasks
Canonical record for Phase 8 task progress. Mirrored to the Apple
Notes note 'Phase 8 — Public intake + PWA + project context' in the
DEVELOPMENT folder; the note cannot be ticked in place from this
session because Full Disk Access is not granted to the launching
app (update-note requires it).
2026-09-21 01:54:47 +02:00
c99be4d49e feat(schema): intake_links and anonymous submitter role
Phase 8a Task 1. Adds the new intake_links collection (per-token
public entry point, owner-only rules), extends cards.created_by_role
with 'anonymous', and extends activity.action with
'card_submitted_anonymously'.

Note: getOrCreateCollection is a no-op on existing collections, so
the two existing-value extensions are applied by new/extended
ensure functions (ensureCardsCreatedByRoleValues and
ensureActivityActionValues respectively). Re-runs are idempotent.
2026-09-21 01:54:11 +02:00
Ada
3e868f910a test(plan): Plan Puzzle v1 integration tests
Covers the 9 scenarios from the §6.3 plan brief, mapped to the harness's
sessionless reality:

- client propose refuses without a session and does not write fields
- rescheduleCardAction refuses a Done card with 'Card is done'
- foreign-client propose path: PocketBase read rule denies the cross-tenant
  lookup (anti-enumeration per access.ts:57-59)
- accept/dismiss/returnToScheduled refuse without a session and are no-ops
- buildActivityRows collapses same-date patches into zero rows and emits one
  row when the date actually changes

HARNESS-LIMIT: the actions that need an owner session (accept, dismiss,
returnToScheduled) cannot be driven successfully in vitest. They are
covered by the manual smoke checklist at the workspace path. The
sessionless refuse path proves the gate fires for every call; the
positive path is exercised by the live smoke run.

Test file: tests/integration/plan-puzzle.test.ts (single new file)
2026-09-19 19:58:16 +02:00
29013b5276 Merge branch 'plan-puzzle-v1-backend' into plan-puzzle-v1-frontend 2026-09-19 18:55:00 +02:00
b3aac9ba4f feat(plan): Plan Puzzle v1 backend — proposal fields, server actions, Done-column guards
Adds the three proposal fields (proposed_target_date, proposed_by,
proposed_at) to the cards collection with a partial index that backs
the 'pending proposals' query, plus two new ActivityAction values
(date_proposed, date_proposal_resolved) and column_changed for
return-to-scheduled.

New server actions under src/app/actions/:
  - client-proposal.ts: client-manager drag creates a proposal (not
    a target_date write); 404 (not 403) on cross-tenant failures and
    Done-column cards, matching the rest of /api/client/*.
  - owner-plan.ts: accept and dismiss clear the three proposal
    fields together and emit the audit rows; returnToScheduledAction
    moves a Done card back to the first working column.

Server-side Done-column guards added to:
  - src/app/actions/reschedule.ts (was UI-only)
  - src/app/api/client/cards/[id]/route.ts (was UI-only)

Both pass through the new writeCardWithAudit helper at
src/lib/cards/write-with-audit.ts — single source of truth for
'patch card + emit activity rows', replacing the inline copies in
the PATCH route and rescheduleCardAction.

Adds src/lib/roadmap/slot-date.ts (slot <-> target_date mapping)
consumed by Pixel's puzzle UI; named SlotId API rather than the
plan's numeric indices for self-documenting callers.

CLIENT_EDITABLE_CARD_FIELDS stays at priority + target_date per
the brief; proposals go through the dedicated action so they get
their own audit row type.
2026-09-19 18:35:33 +02:00
Pixel
b2f332436a feat(plan): Puzzle v1 frontend — tray + 3 month slots, keyboard listbox, proposals, toast
The Plan page replaces the read-only Bands tab with a drag-and-drop
puzzle surface. Both roles drag; owners write target_date directly via
the existing rescheduleCardAction, clients write proposed_target_date
via proposePlanMoveAction. Done-column cards are excluded; they live
on the Timeline tab with a new 'Return to scheduled' button.

UI:
- Tray (unscheduled) + 3 slot columns: This Month / Next / Month After.
  Slot headers carry count + '+N more' chip beyond 12 cards. Tray
  virtualises via scroll-window math at 50+ cards.
- HTML5 drag-and-drop matching the existing PendingTray pattern; the
  PuzzleView keeps the optimistic-state pattern from the kanban Board
  (no router.refresh until the server confirms).
- Keyboard parity: Tab focuses a card, Space opens a listbox (3 slot
  options), 1/2/3 picks a slot, Escape closes, ArrowUp/Down reorders
  within the slot (optimistic local display; no v1 server action for
  in-slot position). aria-live='polite' announces each successful move.
- prefers-reduced-motion via Tailwind motion-safe: variants — the 180ms
  settle animation is dropped for users who request it.

Proposal overlay:
- RoadmapCard shows a left-edge accent stripe + Accept/Dismiss chip
  when proposedTargetDate is set and the viewer is the owner.
- The existing RoadmapCard primitive is reused — no new card component.

Return-to-scheduled button:
- New component on Done-card rows in TimelineView. Replaces the
  drag-sideways motion that the new Done-column server guard blocks.
- Owner-only, calls returnToScheduledAction.

Toast system:
- New ui/toast.tsx: useToast() hook + top-right aria-live region, 4s
  auto-dismiss. AppShell mounts the provider so any page can call it.
- roadmap-builder.tsx no longer console.errors on a failed drag.

Server action stubs:
- client-proposal.ts and owner-plan.ts: temporary no-op stubs that
  return a non-ok response. Deleted when Lin's branch lands; the
  dynamic imports in the UI call them lazily so a missing module
  surfaces through the toast rather than crashing.

Type widening:
- src/lib/roadmap/load.ts: adds optional proposedTargetDate,
  proposedBy, proposedAt, isDone fields to the RoadmapCard interface
  so the Puzzle compiles against pre-migration data. Lin's branch
  populates and tightens these.

Files (15 changed):
- New: slot-date.ts + tests (slot↔date mapping)
- New: puzzle-keyboard.ts + tests (pure listbox state machine)
- New: toast.tsx, proposal-overlay.tsx, return-to-scheduled-button.tsx
- Rewritten: band-view.tsx now exports PuzzleView (per task brief)
- Updated: roadmap-tabs, roadmap-card, roadmap-builder, timeline-view,
  plan/page, app-shell, load.ts

Tests (38 new):
- 18 unit tests for slot-date.ts (month boundaries, year rollover,
  past/future, mid-month symmetry)
- 20 unit tests for the keyboard listbox state machine
  (nextListboxState + isCardShortcut)

UX decisions worth flagging:
- ArrowUp/Down reorders are optimistic only — the order resets to
  canonical (priority desc, title asc) on the next page load. No v1
  server action exists for in-slot position; v2 should add one.
- Done cards are excluded from the Puzzle (per spec §6.1); the
  Return-to-scheduled button lives on the Timeline tab (per task brief).
- A drop on the tray itself is a no-op in v1 — there is no
  'unschedule' action, and the spec lists only 1/2/3 for the keyboard
  listbox.
2026-09-19 18:29:32 +02:00
9ddf8571f6 fix(dashboard): truncate subject to the schema's 300-char limit
logWorkAction wrote the full polished/typed message into both `summary`
and `subject`, but `subject` exists to hold a short git commit subject
line and is capped at 300 characters at the schema level. Any note over
300 characters (trivially reachable — a 2-3 sentence polished note
routinely runs 350-450 characters) failed the create call outright, and
the generic catch surfaced only "Could not save that entry" with the
real reason discoverable only by reproducing the write directly against
PocketBase.

`subject` is only ever a fallback for when `summary` is empty
(`commit.summary || commit.subject`), and a logged entry's summary is
always populated at creation — so truncating subject costs nothing
functionally. Also logs the underlying error server-side on any future
create failure, so this class of bug doesn't require re-deriving the
cause from scratch again.

Reproduced against the live PocketBase instance with the exact message
that failed for the user (348 chars) before and after the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 14:49:39 +02:00
990de29683 docs(plan): Phase 7 — logged work entries implementation plan
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 13:47:59 +02:00
b96f38a259 fix(test): tighten log-work assertions, repair summarise.test.ts for source exclusion, add owner-only UI check
Critical fixes to test suite:

1. Fix time bomb in summarise.test.ts: Task 4 changed summaryStatus filter to exclude source="manual",
   so the assertion would fail once any manual entry exists. Updated test to count only non-manual
   commits and added coverage proving manual entries are excluded.

2. Tighten log-work metrics assertions from toBeGreaterThanOrEqual to exact equality (toBe).
   The old assertions would miss if manual entries were incorrectly fed into session clustering.
   Use a fresh baseline recompute to catch regression of Task 2's manual exclusion fix.

3. Add recomputeMetrics call in afterAll to restore cache after test cleanup,
   preventing inflated dashboard metrics on the demo project.

4. Fix fixture link_source from "none" to "manual" to match actual logWorkAction behavior
   (Task 6 review fix).

5. Add UI coverage: "Log work" button is owner-only, verifying the security model.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 13:44:34 +02:00
f45dfa16a0 test(dashboard): logWorkAction auth and manual-minutes recompute
- Tests that logWorkAction refuses requests without a valid owner session
- Tests that recomputeMetrics correctly adds manual entry minutes to estimatedMinutes
- Both tests verify the integration between log-work action and metrics computation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 13:30:37 +02:00
71c808ed05 fix(dashboard): prevent polish/submit race, narrow cards payload, tighten form validation — Task 8
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 13:27:21 +02:00
2f55ca0114 feat(dashboard): Log work button and form
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 13:19:02 +02:00
123be346a3 refactor(dashboard): shared LoggedTag component, fix dead toggle and colliding a11y labels
- Create shared LoggedTag component to prevent style drift across three render paths
- Replace all three inline spans with single LoggedTag component import
- Fix dead "Show technical detail" toggle: only render when card has git commits
- Fix colliding screen-reader labels in unassigned tray by using commit subject instead of generic text
- Ensures manual entries stay visually consistent and semantically correct everywhere
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 13:16:07 +02:00
73f0f6e655 feat(dashboard): tag logged entries, hide fabricated raw detail in all three render paths
- Add "Logged" tag to manual entries in recent-activity feed
- Add "Logged" tag to manual entries in card commit history
- Hide raw technical detail block for manual entries (no diff exists)
- Replace short_sha code block with "Logged" tag in unassigned linking tray
- Fix screen-reader label to say "this logged entry" instead of "manual"
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 13:08:41 +02:00
dba1fd1763 fix(dashboard): close review findings in logWorkAction/polishNoteAction — Task 6
- Consolidate requireOwnerPb to ensure getAdminPb unreachable without auth check
- Both actions now never throw; all failures return { ok: false, message }
- Verify cardId belongs to projectId before creating commit
- link_source always "manual" (never "none") to prevent auto-attachment
- Gracefully handle recomputeMetrics failures (entry saved, only KPIs stale)
- Revalidate card page + unassigned tray in addition to dashboard
- Fix hours validation message to clarify 0 is not valid

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 13:06:16 +02:00
8c860f2d50 fix(auth): verify sessions against PocketBase instead of trusting the cookie
getCurrentUser() only decoded the pb_auth cookie locally and trusted its
embedded role/client/active fields directly — the PocketBase JS SDK holds
no secret to check a JWT signature against, so a completely forged cookie
(garbage signature, role: "owner", any email) was accepted as a real
session. Verified exploitable end-to-end: a raw Cookie header against the
live app, no browser and no password, rendered the owner's home page.

This mattered most for actions writing through the superuser connection
(log-work.ts, summarise.ts, ingest.ts, and critically users.ts for account
creation) — those never route the caller's token through PocketBase's own
rule engine at all, so the app-level role check was the ONLY gate, and it
was reading an unverified claim. Actions using the caller's own token
(cards.ts, projects.ts) still made the wrong authorization DECISION from
the forged role, even though PocketBase's rule engine would have rejected
the actual data write.

Fixed by having getCurrentUser() call authRefresh(), which asks PocketBase
to confirm the token is genuine; a forged or tampered token is refused
there and treated as signed-out. Wrapped in React's cache() so the extra
round-trip happens once per request, not once per call site (several pages
call this more than once). Re-fetching the live record also means a
deactivated account (`active: false`) stops working on its very next
request instead of only being blocked at a new login.

Regression test builds the exact forged cookie used to verify this,
confirmed red against the vulnerable code and green against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 12:47:18 +02:00
a54a2f2875 feat(dashboard): logWorkAction and polishNoteAction
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 11:04:32 +02:00
1aaf31a4c5 refactor(ai): extract testable polish prompt, guard against empty input
- Extract prompt text into buildPolishPrompt() in polish-prompt.ts (no server-only),
  matching the pattern used by batch.ts/buildBatchPrompt and narrative.ts/buildNarrativePrompt
  so the prompt itself is unit testable.
- Add MAX_NOTE_LENGTH guard to truncate excessively long notes before polishing.
- Add early-return guard against empty input (non-retryable error).
- Run getProvider() and getSystemPrompt() concurrently via Promise.all().
- Document that polishNote() throws (foreground user-initiated action).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 11:02:16 +02:00
3c8edeb946 feat(ai): single-shot note polishing for logged work entries
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 10:51:49 +02:00
5283a455fa fix(ai): exclude manual entries from card narratives, clarify summary panel wording
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 10:50:12 +02:00
08cf5d5ce7 fix(ai): never queue a logged entry for AI re-summarization
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 10:42:30 +02:00
a5b1c9ea2f fix(dashboard): match methodology's logged-hours figure to the KPI tile's rounding
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 10:41:10 +02:00
5d772ef672 feat(metrics): recompute reads manual entries, methodology states them
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 10:34:25 +02:00
47d19f9f38 fix(metrics): kpi caption honesty, manual-entry safety, extra coverage — Task 2 review findings
- Fix KPI row "Hours invested" caption to honestly show both session-derived and manually logged hours
- Add JSDoc warnings about manualMinutes being a discriminator (do not pass 0 for git commits)
- Extract isManual predicate to single source of truth, avoid triple derivation
- Add tests for mixed session+manual in same month and manual-only shape

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 10:32:40 +02:00
284385ca2b feat(metrics): fold manually logged minutes into project totals
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 10:25:04 +02:00
4d1e6cf412 fix(commits): set source on ingest/backfill writes, tidy Task 1 review findings
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 10:22:26 +02:00
65b09170af feat(commits): add source and manual_minutes fields
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 10:14:51 +02:00
1cb05f4689 docs(spec): Phase 7 — logged work entries
Design for letting the owner record off-repo work as a commits row
tagged source=manual, reusing every existing commit render path
(recent work, card history, activity/monthly aggregation) instead of
a parallel system, with a declared-hours figure added to the total
rather than session-clustered, and a small "Logged" tag distinguishing
it from a real commit everywhere it appears.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 08:10:55 +02:00
2d87cd505b chore(config): update .gitignore and enhance Next.js configuration
Added .superpowers directory to .gitignore to prevent tracking of specific Pocketbase data. Updated Next.js configuration to enable standalone output for optimized Docker image builds, reducing the need for the full node_modules tree.
2026-09-19 07:52:09 +02:00
616a6674c8 chore(plan): tighten timeline row geometry and label font
Uncommitted tweaks from the dashboard review pass: ROW_HEIGHT 36→40
gives row content a touch more headroom for two-line titles, BAR_HEIGHT
22→18 lets priority-coloured stripes feel deliberate rather than heavy,
MIN_WIDTH 0.06→0.10 widens the unscheduled-on-the-end fallback so a
card with only a target_date reads as a clear mark. Percent column
widened to fit three-digit figures.
2026-09-17 14:58:35 +02:00
4bc59316ce fix(dashboard): BarList duplicate key when items contain an 'other' row
parseCommitMessage emits type 'other' for unparseable subjects, so
metrics.byType can legitimately contain a row whose key is 'other'.
The fold-into-'other' code in BarList used the same string for the
synthesised bucket — two <li key="other"> siblings, React warns:

  Encountered two children with the same key, 'other'.

Reserve a key '__fold__' that cannot collide with raw input and display
'other' only at render time. The visible label stays the same.
2026-09-17 14:58:31 +02:00
4ce9b76874 feat(dashboard): add Refresh commits button, separate from Recompute
The single Recompute button conflated two ideas: re-aggregating the cached
metrics (instant, costs nothing) and pulling new commits from Forgejo
(slow, hits the external API). A user who clicked Recompute expecting
Recent Work to update was waiting on a metric that only depends on what
is already in the database.

Split the two actions:

* Refresh commits — wraps ingestProject(stopAfterKnownPages: 1), the
  same path as the hourly cron, scoped to one project. Snappy because
  it stops once a full page is already known.
* Recompute — unchanged behaviour. Re-aggregates the cached commits
  into the project_metrics row.

Both actions revalidate the dashboard, so Recent Work, head-line KPIs
and the 'Updated just now' indicator all stay consistent within a
single click.
2026-09-17 14:58:01 +02:00
d909a8bb34 feat(plan): redo the timeline as a proper Gantt chart
Replaces the row-packed bar layout with the slide-style Gantt the user
asked for: rows grouped by client, full date-range bars (start → target),
priority-coloured fills, a today marker, percent indicators, and
quarter/month header ticks. Single-day cards (no start_date set, which is
the entire existing dataset) get a MIN_WIDTH floor so they read as
substantial bars instead of zero-pixel dots.

Tray entry point and drop-zone wiring are unchanged: an empty tray
shows a quiet 'Every schedulable card is already on the timeline'
placeholder; dropping a block from the tray still calls the same
rescheduleCardAction.

When cards start carrying start_date as well as target_date the bars
will widen to the full date range automatically — the layout supports
real duration without further code.
2026-09-17 12:35:52 +02:00
0d4b62b0b4 fix(plan): pass ALL cards to RoadmapBuilder so dated bars survive
The Timeline tab was filtering cards to !targetDate before handing
them to RoadmapBuilder. The builder then computed its dated subset
from that pre-filtered list, so the timeline always received zero
dated cards regardless of what was in the database — the timeline
always rendered the empty-state hint even when the user had cards
dated weeks ago.

The bands view (Now/Next/Later) still does its own per-view filter
above the builder, so the builder receives the full set. Pass cards
through unfiltered and let the builder split them internally — the
tray gets the !targetDate subset, the timeline gets the rest.
2026-09-17 12:31:59 +02:00
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