Calendar v2 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Ship spacious, crash-safe v2 calendar views (day/week/month) alongside the legacy ones, with the appointments-pagination data bug fixed, in one PR with a query-flag rollback.
Architecture: New v2 files (day-view-v2.tsx, week-view-v2.tsx, month-view-v2.tsx) live next to the legacy ones; schedular-view-filteration.tsx picks v2 by default and falls back to legacy on ?calendar=legacy. Shared primitives (EventCard, OverflowPill, UtilizationBar, UtilizationRing, HourHeatStrip, NowLine) and a useDayIndex hook keep per-render work O(1) per cell. Server limit cap bumped to 2000; client requests limit=2000 and recursively splits the range if it ever returns full.
Tech Stack: React 19, TypeScript, Tailwind, framer-motion (already in use), vitest (existing test runner), Hono + Drizzle on the server, Zod for query validation.
Spec: docs/superpowers/specs/2026-05-21-calendar-v2-design.md
Task 1: Bump server appointments query cap
Files:-
Modify:
server/src/routes/appointments.ts:64-71 -
Test:
server/src/routes/__tests__/appointments-limit.test.ts(new) - Step 1: Write the failing test
server/src/routes/__tests__/appointments-limit.test.ts:
- Step 2: Run test, confirm it does NOT fail (it’s a mirror — the test ensures we have a regression catch if the schema drifts).
cd server && pnpm vitest run src/routes/__tests__/appointments-limit.test.ts
Expected: PASS — this test exists to lock the contract; we now update the real schema to match.
- Step 3: Update the real schema
server/src/routes/appointments.ts:64-71. Replace:
- Step 4: Run the server tests in this file’s neighborhood
cd server && pnpm vitest run src/routes/__tests__/appointments
Expected: PASS (no other appointment tests should regress).
- Step 5: Commit
Task 2: Add localDateStr + addDaysToDateStr + midpointDate helpers in shared lib
Files:
-
Create:
ui/src/lib/date-range.ts -
Test:
ui/src/lib/date-range.test.ts - Step 1: Write the failing test
ui/src/lib/date-range.test.ts:
- Step 2: Run test to verify it fails
cd ui && pnpm vitest run src/lib/date-range.test.ts
Expected: FAIL — module not found.
- Step 3: Write the implementation
ui/src/lib/date-range.ts:
- Step 4: Run test to verify it passes
cd ui && pnpm vitest run src/lib/date-range.test.ts
Expected: PASS — all 5 cases green.
- Step 5: Commit
Task 3: Update getAppointments to pass limit=2000 + recursive cap-overflow safety net
Files:
-
Modify:
ui/src/lib/serverComm.ts:1996-2002 -
Test:
ui/src/lib/getAppointments.test.ts(new) - Step 1: Write the failing test
ui/src/lib/getAppointments.test.ts:
- Step 2: Run test to verify it fails
cd ui && pnpm vitest run src/lib/getAppointments.test.ts
Expected: FAIL — at least the “passes limit=2000” assertion fails since the URL currently has no limit param.
- Step 3: Update
getAppointments
ui/src/lib/serverComm.ts:1996-2002. Replace:
getAppointments import block: at the top of serverComm.ts make sure midpointDateStr and addDaysToDateStr from ./date-range are imported.
- Step 4: Run test to verify it passes
cd ui && pnpm vitest run src/lib/getAppointments.test.ts
Expected: PASS — all 4 cases green.
- Step 5: Sanity-check the wider type check
cd ui && pnpm tsc --noEmit
Expected: no new type errors introduced by the edit.
- Step 6: Commit
Task 4: Add useDayIndex hook
Files:
-
Create:
ui/src/hooks/use-day-index.ts -
Test:
ui/src/hooks/use-day-index.test.tsx - Step 1: Write the failing test
ui/src/hooks/use-day-index.test.tsx:
- Step 2: Run test, confirm failure
cd ui && pnpm vitest run src/hooks/use-day-index.test.tsx
Expected: FAIL — module not found.
- Step 3: Write the implementation
ui/src/hooks/use-day-index.ts:
- Step 4: Run test to verify it passes
cd ui && pnpm vitest run src/hooks/use-day-index.test.tsx
Expected: PASS — all 5 cases green.
- Step 5: Commit
Task 5: Add status-styling constants
Files:-
Create:
ui/src/components/schedule/_components/v2/status-style.ts -
Test:
ui/src/components/schedule/_components/v2/status-style.test.ts - Step 1: Write the failing test
ui/src/components/schedule/_components/v2/status-style.test.ts:
- Step 2: Run test, confirm failure
cd ui && pnpm vitest run src/components/schedule/_components/v2/status-style.test.ts
Expected: FAIL — module not found.
- Step 3: Write the implementation
ui/src/components/schedule/_components/v2/status-style.ts:
- Step 4: Run test to verify it passes
cd ui && pnpm vitest run src/components/schedule/_components/v2/status-style.test.ts
Expected: PASS — all 3 cases green.
- Step 5: Commit
Task 6: EventCard primitive
Files:
-
Create:
ui/src/components/schedule/_components/v2/EventCard.tsx -
Test:
ui/src/components/schedule/_components/v2/EventCard.test.tsx - Step 1: Write the failing test
ui/src/components/schedule/_components/v2/EventCard.test.tsx:
- Step 2: Run test, confirm failure
cd ui && pnpm vitest run src/components/schedule/_components/v2/EventCard.test.tsx
Expected: FAIL — module not found.
- Step 3: Write the implementation
ui/src/components/schedule/_components/v2/EventCard.tsx:
- Step 4: Run test to verify it passes
cd ui && pnpm vitest run src/components/schedule/_components/v2/EventCard.test.tsx
Expected: PASS.
- Step 5: Commit
Task 7: UtilizationBar and UtilizationRing primitives
Files:
-
Create:
ui/src/components/schedule/_components/v2/Utilization.tsx -
Test:
ui/src/components/schedule/_components/v2/Utilization.test.tsx - Step 1: Write the failing test
ui/src/components/schedule/_components/v2/Utilization.test.tsx:
- Step 2: Run test, confirm failure
cd ui && pnpm vitest run src/components/schedule/_components/v2/Utilization.test.tsx
Expected: FAIL — module not found.
- Step 3: Write the implementation
ui/src/components/schedule/_components/v2/Utilization.tsx:
- Step 4: Run test to verify it passes
cd ui && pnpm vitest run src/components/schedule/_components/v2/Utilization.test.tsx
Expected: PASS.
- Step 5: Commit
Task 8: HourHeatStrip primitive
Files:
-
Create:
ui/src/components/schedule/_components/v2/HourHeatStrip.tsx -
Test:
ui/src/components/schedule/_components/v2/HourHeatStrip.test.tsx - Step 1: Write the failing test
ui/src/components/schedule/_components/v2/HourHeatStrip.test.tsx:
- Step 2: Run test, confirm failure
cd ui && pnpm vitest run src/components/schedule/_components/v2/HourHeatStrip.test.tsx
Expected: FAIL — module not found.
- Step 3: Write the implementation
ui/src/components/schedule/_components/v2/HourHeatStrip.tsx:
- Step 4: Run test to verify it passes
cd ui && pnpm vitest run src/components/schedule/_components/v2/HourHeatStrip.test.tsx
Expected: PASS.
- Step 5: Commit
Task 9: OverflowPill primitive (with popover)
Files:
-
Create:
ui/src/components/schedule/_components/v2/OverflowPill.tsx -
Test:
ui/src/components/schedule/_components/v2/OverflowPill.test.tsx - Step 1: Write the failing test
ui/src/components/schedule/_components/v2/OverflowPill.test.tsx:
- Step 2: Run test, confirm failure
cd ui && pnpm vitest run src/components/schedule/_components/v2/OverflowPill.test.tsx
Expected: FAIL — module not found.
- Step 3: Write the implementation
ui/src/components/schedule/_components/v2/OverflowPill.tsx:
- Step 4: Verify shadcn
popovercomponent exists
ls ui/src/components/ui/popover.tsx
Expected: file exists. (If it doesn’t, skip ahead to Task 9b below.)
- Step 4b (only if popover.tsx is missing): scaffold shadcn popover
cd ui && npx shadcn@latest add popover --yes
Verify: ui/src/components/ui/popover.tsx now exists.
- Step 5: Run test to verify it passes
cd ui && pnpm vitest run src/components/schedule/_components/v2/OverflowPill.test.tsx
Expected: PASS.
- Step 6: Commit
Task 10: NowLine primitive
Files:
-
Create:
ui/src/components/schedule/_components/v2/NowLine.tsx -
Test:
ui/src/components/schedule/_components/v2/NowLine.test.tsx - Step 1: Write the failing test
ui/src/components/schedule/_components/v2/NowLine.test.tsx:
- Step 2: Run test, confirm failure
cd ui && pnpm vitest run src/components/schedule/_components/v2/NowLine.test.tsx
Expected: FAIL — module not found.
- Step 3: Write the implementation
ui/src/components/schedule/_components/v2/NowLine.tsx:
- Step 4: Run test to verify it passes
cd ui && pnpm vitest run src/components/schedule/_components/v2/NowLine.test.tsx
Expected: PASS.
- Step 5: Commit
Task 11: useOperatingMinutes helper hook
Files:
-
Create:
ui/src/hooks/use-operating-minutes.ts -
Test:
ui/src/hooks/use-operating-minutes.test.ts - Step 1: Write the failing test
ui/src/hooks/use-operating-minutes.test.ts:
- Step 2: Run test, confirm failure
cd ui && pnpm vitest run src/hooks/use-operating-minutes.test.ts
Expected: FAIL — module not found.
- Step 3: Write the implementation
ui/src/hooks/use-operating-minutes.ts:
- Step 4: Run test to verify it passes
cd ui && pnpm vitest run src/hooks/use-operating-minutes.test.ts
Expected: PASS.
- Step 5: Commit
Task 12: useDayLayout hook (overlap clustering, pure)
Files:
-
Create:
ui/src/hooks/use-day-layout.ts -
Test:
ui/src/hooks/use-day-layout.test.ts - Step 1: Write the failing test
ui/src/hooks/use-day-layout.test.ts:
- Step 2: Run test, confirm failure
cd ui && pnpm vitest run src/hooks/use-day-layout.test.ts
Expected: FAIL — module not found.
- Step 3: Write the implementation
ui/src/hooks/use-day-layout.ts:
- Step 4: Run test to verify it passes
cd ui && pnpm vitest run src/hooks/use-day-layout.test.ts
Expected: PASS.
- Step 5: Commit
Task 13: Day v2 — skeleton with hour rail + zoom
Files:-
Create:
ui/src/components/schedule/_components/view/day/day-view-v2.tsx - Step 1: Create the skeleton file
ui/src/components/schedule/_components/view/day/day-view-v2.tsx:
- Step 2: Compile-check
cd ui && pnpm tsc --noEmit
Expected: no errors. If useScheduler() as any triggers a lint warning, that’s intentional — the legacy provider doesn’t export state on its type yet. (Task 17 narrows this.)
- Step 3: Smoke-render in the existing app
ui/src/components/schedule/_components/view/schedular-view-filteration.tsx: temporarily swap import DailyView from './day/daily-view'; to import DailyView from './day/day-view-v2'; and reload the dev server. Switch to Day view in the app, confirm appointments render. Revert the import before committing this task — the wiring is done in Task 17.
- Step 4: Commit
Task 14: Day v2 — empty state + dense-day compact fallback
Files:-
Modify:
ui/src/components/schedule/_components/view/day/day-view-v2.tsx - Step 1: Add empty-state + dense-day rendering
day-view-v2.tsx, find the <div className="absolute inset-y-0 right-0"> containing the day column. Right after the <NowLine ...> line, add a dense-mode short-circuit:
Jump to next booking button calls (props as any).onDateChange?.(...) — change the signature of the component to destructure onDateChange (already in Props) and call it directly:
- Step 2: Add dense-day compact variant
layout.cards.map(...) so it passes variant={isDense ? 'compact' : 'normal'}:
- Step 3: Compile-check
cd ui && pnpm tsc --noEmit
Expected: no errors.
- Step 4: Commit
Task 15: Week v2
Files:-
Create:
ui/src/components/schedule/_components/view/week/week-view-v2.tsx - Step 1: Create the file
ui/src/components/schedule/_components/view/week/week-view-v2.tsx:
- Step 2: Compile-check
cd ui && pnpm tsc --noEmit
Expected: no errors.
-
Step 3: Smoke-render (temporarily swap import in
schedular-view-filteration.tsx, verify, revert before committing). - Step 4: Commit
Task 16: Month v2
Files:-
Create:
ui/src/components/schedule/_components/view/month/month-view-v2.tsx - Step 1: Create the file
ui/src/components/schedule/_components/view/month/month-view-v2.tsx:
- Step 2: Compile-check
cd ui && pnpm tsc --noEmit
Expected: no errors.
- Step 3: Smoke-render (temporarily swap import, verify, revert).
- Step 4: Commit
Task 17: Wire v2 via schedular-view-filteration.tsx with legacy escape hatch
Files:
-
Modify:
ui/src/components/schedule/_components/view/schedular-view-filteration.tsx - Step 1: Add the v2 imports
ui/src/components/schedule/_components/view/schedular-view-filteration.tsx. After the existing imports for DailyView, MonthView, WeeklyView, add:
- Step 2: Detect the legacy escape-hatch query flag at mount
SchedulerViewFilteration component body (near where state is declared), add:
useMemo to the React imports if not already imported.
- Step 3: Swap each view’s render
<DailyView ...> JSX (inside the 'day' TabsContent, both the timeline branch — not the doctor swimlane branch) with:
<WeeklyView ...> JSX with:
<MonthView ...> JSX with:
- Step 4: Compile-check
cd ui && pnpm tsc --noEmit
Expected: no errors.
- Step 5: Manual QA
cd ui && pnpm dev) and verify against the canonical test tenant ssh & Associates (clinic id b6d3a3f3-…):
- Day view loads, appointments render, hour zoom buttons work.
- Week view loads, day headers show utilization bars + counts.
- Month view loads, heat strips visible on days with appointments.
?calendar=legacyreverts to the old views.- Navigate to a week that crosses a month boundary — confirm both views render appointments on both halves of the week.
- Step 6: Commit
Task 18: Narrow the state access on useScheduler() (type safety)
Files:
-
Modify:
ui/src/providers/schedular-provider.tsx -
Modify:
ui/src/types/index.ts(ifSchedulerContextTypelives there) -
Step 1: Check where
SchedulerContextTypeis defined
grep -n "SchedulerContextType" ui/src/types/index.ts ui/src/providers/schedular-provider.tsx | head -10
Expected: at least one definition in ui/src/types/index.ts.
- Step 2: Expose
stateon the context type
SchedulerContextType, add a field:
ui/src/providers/schedular-provider.tsx, the value provided to SchedulerContext.Provider already includes state indirectly — find the JSX <SchedulerContext.Provider value={...}> and ensure the object contains state. If it’s not there, add it:
- Step 3: Drop the
as anycasts in v2 views
day-view-v2.tsx, week-view-v2.tsx, and month-view-v2.tsx, change:
- Step 4: Compile-check
cd ui && pnpm tsc --noEmit
Expected: no errors.
- Step 5: Run all v2 tests
cd ui && pnpm vitest run src/components/schedule/_components/v2 src/hooks/use-day-index.test.tsx src/hooks/use-day-layout.test.ts src/hooks/use-operating-minutes.test.ts src/lib/date-range.test.ts src/lib/getAppointments.test.ts
Expected: all green.
- Step 6: Commit
Task 19: Release notes + APP_VERSION bump
Files:-
Modify:
ui/src/components/pages/sign-in.tsx(APP_VERSION) -
Create:
docs/releases/v1.11.0-calendar-v2.md - Step 1: Bump APP_VERSION
ui/src/components/pages/sign-in.tsx (it’ll be near the bottom of the sign-in form footer) and bump from v1.10.0 to v1.11.0.
- Step 2: Write the release note
docs/releases/v1.11.0-calendar-v2.md:
- Step 3: Compile-check
cd ui && pnpm tsc --noEmit
Expected: no errors.
- Step 4: Commit
Task 20: Final smoke matrix + deploy
Files:- No code changes — verification and deploy.
- Step 1: Run the full test suite
cd ui && pnpm vitest run
Expected: all green (no v2 regressions).
Run: cd server && pnpm vitest run
Expected: all green.
- Step 2: Manual smoke matrix on the test tenant
- Day with 0 events — empty state renders with the two CTAs.
- Day with a handful of events — cards render with correct status ribbons and times.
- Day with 30+ events — overflow pills appear on hours with 4+ overlapping events.
- Day with 100+ events — compact card variant kicks in; performance is smooth.
- Week view of the current week — utilization bars on each day header.
- Week view straddling a month boundary — both halves show their events.
- Month view of the current month — heat strips visible, today’s cell ringed.
- Clicking a month cell jumps to Day for that date.
?calendar=legacy— old views render.
- Step 3: Deploy via the canonical workflow
odontox-commit-deploy skill. It runs the safety checks, builds the bundle, deploys to Cloudflare Pages, and force-promotes the CF canonical.
- Step 4: Post-deploy verification
https://go.odontox.io/appointments in an incognito window, sign in, and re-run smoke matrix items 1–8 against the live deploy.
- Step 5: Final commit (if any cleanup arose during smoke)
Self-review
Spec coverage:- Data layer fix (cap + client) — Tasks 1, 3.
useDayIndex— Task 4.- Shared primitives (
EventCard,OverflowPill,UtilizationBar,UtilizationRing,HourHeatStrip,NowLine) — Tasks 5–10. - Helpers (
useOperatingMinutes,useDayLayout) — Tasks 11–12. - Day v2 (zoom, ribbon, dense fallback, empty state, overflow) — Tasks 13–14. By-doctor swim-lane mode is deferred — added as an explicit follow-up below since the existing
DoctorDayViewalready covers the case and a v2 swim-lane is a larger isolated piece of work. - Week v2 (utilization headers, day-header click-through, closed-day wash, overflow) — Task 15.
- Month v2 (heat strip, utilization ring, status dots, hover-to-peek omitted — see follow-up) — Task 16.
- View selector with legacy flag — Task 17.
- Type cleanup — Task 18.
- Release notes — Task 19.
- Smoke matrix + deploy — Task 20.
- Day v2 by-doctor swim-lane mode (existing
DoctorDayViewremains available via the day-mode toggle for now). - Month v2 day-peek hover popover (cells already drill into Day on click).
- Mobile parity polish on v2 views (mobile path still routes through current mobile-specific code paths).
LaidOutCard.totalColumns, OverflowGroup.band.{startHour,endHour}, and DayIndex.{byDate,byDateByHour} names match across use-day-layout.ts, use-day-index.ts, and the three view files. EventCardProps.{event, variant, width, onClick} names match between definition (Task 6) and call sites (Tasks 13, 15, 16).
Execution Handoff
Plan complete and saved todocs/superpowers/plans/2026-05-21-calendar-v2.md. Two execution options:
- Subagent-Driven (recommended) — I dispatch a fresh subagent per task, review between tasks, fast iteration.
- Inline Execution — Execute tasks in this session using executing-plans, batch execution with checkpoints.

