The spec keeps why the owner chose the section over a side panel, and why the count starts at a reader's first tick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.7 KiB
Catch up Section Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans (the owner's standing instruction: no review stops between design and execution). Steps use checkbox (
- [ ]) syntax.
Goal: Replace the nested Catch up & recorded history dropdowns with an always-open, one-game-at-a-time Catch up section on the Routines page, reachable by a button from the Checklist.
Architecture: A pure model (state/reader/catchUp.ts) turns an account into titled rows, day cells, values and a count, so every rule is tested without React. CatchUpSection.tsx draws it. ReaderApp passes a one-shot request to RoutinesPage, which scrolls and focuses.
Tech Stack: React 19, TypeScript strict, Bun test, plain CSS in src/client/redesign.css.
Spec: docs/superpowers/specs/2026-11-06-catch-up-section-design.md
Global Constraints
- No change to stored data or keys; ticks go through the existing
dayaction. - Colours are tokens plus the game hue; both themes must read.
zodstays the only runtime dependency. Puppeteer only in the scratch directory.- Commits wait for the owner to ask (
AGENTS.md), so this plan has no commit steps, and the changelog entry, which must name a commit, lands in the follow-up.
Review Focus
- A selected account retired or hidden while the page is open: fall back to the default, do not crash. Test in Task 2.
- Ticking the last unticked day: the selection must not jump to another game. Test in Task 2.
- Logged days later than today on the current clock (after a reset change): shown disabled, never lost. Test in Task 1.
- Unreadable stored values (
daysnot an array,valuenot a number): rendered as a safe label, no throw. Test in Task 1. - Two accounts on one game: chips stay distinct. Test in Task 2.
Task 1: The model
Files:
- Create:
src/client/state/reader/catchUp.ts - Test:
test/catch-up.test.tsx
Interfaces:
- Produces:
export interface CatchUpDay { key: string; ticked: boolean; today: boolean; disabled: boolean }
export interface CatchUpValue { period: string; label: string; text: string }
export interface CatchUpItem {
id: string; revisionId: string; title: string; kind: string;
/** null for number-measured and timer rows. */
days: CatchUpDay[] | null;
earlier: CatchUpDay[];
values: CatchUpValue[] | null;
behind: number;
}
export interface CatchUpGroup { id: string; title: string; note: string | null; items: CatchUpItem[] }
export interface AccountCatchUp { accountId: string; items: CatchUpItem[]; older: CatchUpGroup[]; behind: number }
export function catchUpTitle(state: ReaderState, account: Account, id: string, rows: readonly RoutineRow[]): string;
export function catchUpFor(state: ReaderState, account: Account, now: number): AccountCatchUp;
export function defaultCatchUpAccount(list: readonly AccountCatchUp[]): string | null;
-
Step 1: Write failing tests (migrated state via
migrateLegacy, astest/redesign-routines.test.tsxdoes), named and asserting:a followed battle pass is titled, not a slug, and gets no strip:catchUpTitleof afeed-cycle:id with no live row isBattle pass; a pass row is absent fromitems.an unknown id reads Removed item.dailies use the name Today's dailies uses:dailies:endfieldisDaily missions.a daily strip holds 14 days ending today: last hastoday: true, none later than today.an amount routine gets values, not days:routine:endfield:weekly-routinewith{ "2026-11-02": { value: 8 } }givesdays: null, text8 of 10 points, labelWeek of 2 Nov.a banner routine has no strip:routine:endfield:aic-quota-permits,days: null.behind counts unticked days since the first tick, not today: ticks on 3 and 5 Nov with now on 6 Nov givebehind: 1.a never-ticked routine counts nothing,a weekly counts nothing,an event with a goal counts nothing.a tick before the strip is under earlier;a tick after today is disabled.unreadable logs do not throw:days: "x"gives no ticks;value: "x"gives textunreadable amount.a logged item no longer followed lands in Older records, with recorded days only.an earlier reset timing is its own older group, with its region line.an item with nothing logged is not in older.defaultCatchUpAccount picks the first behind, else first, else null.
-
Step 2: Run
bun test test/catch-up.test.tsx. Expected: FAIL, module not found. -
Step 3: Implement.
itemscome fromroutineRowsfiltered onplacement.followedwith noplacementId. A row's timer check usesisTimerPeriodon its built-in or task period. Strip viacatchUpDays(now, row.clock.region, game, floor, 14, row.clock), withfloorthe later of the revision's effective time (null for1970) androw.window?.startor the event's start.kind: event rows (row.sourcefromseenEvents, id not a routine, task ordailies:) areEvent; otherwise Dailies, Weeklies, Monthlies or Cycles by cadence. Day and period labels parse the key as UTC withen-GB. -
Step 4: Run
bun test test/catch-up.test.tsx. Expected: PASS.
Task 2: The section
Files:
- Create:
src/client/components/redesign/CatchUpSection.tsx - Delete:
src/client/components/redesign/RoutineHistory.tsx - Modify:
src/client/components/redesign/RoutinesPage.tsx(import, bottom<details>, top-bar button),src/client/redesign.css(retire.routine-histories,.routine-history,.history-days; add.catch-up*) - Test:
test/catch-up.test.tsx
Interfaces:
-
Consumes:
catchUpFor,defaultCatchUpAccountfrom Task 1. -
Produces:
CatchUpSection({ accounts, state, now, onAction, headingRef }: { accounts: Account[]; state: ReaderState; now: number; onAction: ActionHandler; headingRef?: RefObject<HTMLHeadingElement | null> }). -
Step 1: Write failing tests:
Routines has one Catch up heading and no nested dropdowns: the old summary text is gone,<h2 ...>Catch upappears.chips name the game and follow Game order.two servers of one game have distinct chips.a day cell says weekday and date, and aria-pressed tracks the tick.the first game with something behind is chosen.pickAccount keeps a live selection, and falls back when it is gone: exportpickAccount(selected: string | null, list: readonly AccountCatchUp[]): string | null; a live selection withbehind: 0stays; a missing one returns the default.
-
Step 2: Run
bun test test/catch-up.test.tsx. Expected: FAIL. -
Step 3: Implement. Selection via
useStateinitialised once fromdefaultCatchUpAccount, resolved each render throughpickAccount. Chips reuse.focus-pillsand.focus-pillwith--hue. Ticks dispatch the samedayaction withactionMeta(). Strip is a 14-column grid, 7 columns on a phone.h2hastabIndex={-1}, andscroll-margin-top. -
Step 4: Run
bun test && bun run typecheck. Expected: all pass.
Task 3: Getting there
Files:
- Modify:
src/client/ReaderApp.tsx(~809, ~822, ~1012),src/client/components/redesign/DailyPanel.tsx(heading, newonCatchUpprop),src/client/components/redesign/RoutinesPage.tsx - Test:
test/catch-up.test.tsx
Interfaces:
-
Produces:
RoutinesPagepropscatchUpRequested?: boolean,onCatchUpShown?: () => void;DailyPanelproponCatchUp?: () => void. -
Step 1: Write failing tests:
Today's dailies offers Catch up beside Edit pins, andthe Routines top bar offers Catch up. -
Step 2: Run
bun test test/catch-up.test.tsx. Expected: FAIL. -
Step 3: Implement.
ReaderAppholdscatchUpRequestedstate, set withchangePage("routines").RoutinesPage's effect scrolls the heading into view (block: "start",behavior: "auto"under reduced motion, else"smooth"), focuses withpreventScroll, then callsonCatchUpShown, which clears the state, so later visits do not scroll. -
Step 4: Run
bun test && bun run typecheck. Expected: all pass. -
Step 5: Browser check per
AGENTS.md§ Looking at the built app, onhttp://10.20.20.2:<fresh port>: both Checklist buttons land on the section, normal navigation does not scroll, ticking toggles and persists across reload, both themes read, phone width has no horizontal scroll, no raw IDs, no console errors.
Task 4: Docs
Files: Modify docs/PRD.md (F17 and the Catch up disclosure sentence), docs/DESIGN.md (Other pages: Routines).
- Step 1: State where Catch up lives and how it is reached; remove Catch up & recorded history and the disclosure wording.
- Step 2: Run
grep -rn "recorded history\|routine-histor\|history-days\|RoutineHistory" src test docs/PRD.md docs/DESIGN.md. Expected: only unrelated Archive strings.