docs: record where Catch up lives and how it counts
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 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3296784ca1
commit
bbd0267321
@@ -660,6 +660,10 @@ legend on the same baseline as "Next: …".
|
||||
rows within each reset band, history and Checklist pins follow Game order with the same A–Z fallback.
|
||||
The table has tick
|
||||
and click-to-edit amount cells. The library and create/edit drawers are shelves.
|
||||
**Catch up** sits below the bands, always open: focus-bar chips (one account at a time, a plain count,
|
||||
no heat colour), then rows with the title and type beside a 14-day strip. A ticked day is filled with
|
||||
a mix of the game's hue, an unticked one outlined, today ringed in `ink`. The strip is 14 columns, 7 at
|
||||
phone width, so a fortnight wraps into two whole weeks.
|
||||
- **Timeline.** Two looks (new and classic) chosen in Appearance. Dailies are not on the timeline.
|
||||
- **Settings.** About, Changelog, Data & sources (list options first), Region & time, Appearance
|
||||
(interface, theme, scheme, urgency colours, default view, game order, timeline design, focus bar,
|
||||
|
||||
+8
-4
@@ -265,8 +265,8 @@ bad evening and is never trusted again. An event whose end was announced already
|
||||
its checklist draws every day of the run and past days are clickable. The two that did not are the
|
||||
ones this closes: a game's **standing chore**, which had no history surface at all and is the
|
||||
most-missed thing in these games, and an event with **`endsAt: null`**, whose checklist could draw no
|
||||
run and so offered only today. Both now show the last fortnight as a strip of days, on the dailies
|
||||
section behind a "Catch up" disclosure and in the detail sheet respectively.
|
||||
run and so offered only today. Both now show the last fortnight as a strip of days, in the Routines
|
||||
page's Catch up section and in the detail sheet respectively.
|
||||
|
||||
Three rules hold it honest:
|
||||
|
||||
@@ -550,8 +550,12 @@ with the standing chore of the game it came from, because the reader thinks in g
|
||||
**F17 — Routines.**
|
||||
Routines is the library and management page for daily, weekly, monthly and cycle tasks. Follow each
|
||||
per server; pin independently to Checklist and Timeline. Built-ins offer Make it mine, copying only
|
||||
that account's logs; custom routines remain editable. Catch up & recorded history exposes logs on
|
||||
their original reset revisions. A built-in can also **reset with its game's version** (WuWa's
|
||||
that account's logs; custom routines remain editable. The page's **Catch up** section, below the
|
||||
routine lists and reached by a button in the Checklist's *Today's dailies* and *Routines* headings or
|
||||
the page's top bar, shows one game at a time, chosen on a named chip: real titles, a fourteen-day strip
|
||||
of weekday and date, and recent values for a routine counted in numbers. Its count covers only what is
|
||||
expected every day, from the reader's first tick of that item, because nothing records when they began
|
||||
following it. Ended or unfollowed items and logs on earlier reset revisions sit under *Older records*. A built-in can also **reset with its game's version** (WuWa's
|
||||
Oscillated Coral pulls, seven of each per version): it counts down to the end the version table
|
||||
records, or says *resets with next version (3.8) · expected ~23 Dec* when no end is recorded yet,
|
||||
and never resets on that estimate. It sits with the cycles and is not offered as Make it mine. A
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# 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 `day` action.
|
||||
- Colours are tokens plus the game hue; both themes must read.
|
||||
- `zod` stays 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
|
||||
|
||||
1. A selected account retired or hidden while the page is open: fall back to the default, do not crash. Test in Task 2.
|
||||
2. Ticking the last unticked day: the selection must not jump to another game. Test in Task 2.
|
||||
3. Logged days later than today on the current clock (after a reset change): shown disabled, never lost. Test in Task 1.
|
||||
4. Unreadable stored values (`days` not an array, `value` not a number): rendered as a safe label, no throw. Test in Task 1.
|
||||
5. 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:
|
||||
```ts
|
||||
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`, as `test/redesign-routines.test.tsx` does), named and asserting:
|
||||
- `a followed battle pass is titled, not a slug, and gets no strip`: `catchUpTitle` of a `feed-cycle:` id with no live row is `Battle pass`; a pass row is absent from `items`.
|
||||
- `an unknown id reads Removed item`.
|
||||
- `dailies use the name Today's dailies uses`: `dailies:endfield` is `Daily missions`.
|
||||
- `a daily strip holds 14 days ending today`: last has `today: true`, none later than today.
|
||||
- `an amount routine gets values, not days`: `routine:endfield:weekly-routine` with `{ "2026-11-02": { value: 8 } }` gives `days: null`, text `8 of 10 points`, label `Week 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 give `behind: 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 text `unreadable 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.** `items` come from `routineRows` filtered on `placement.followed` with no `placementId`. A row's timer check uses `isTimerPeriod` on its built-in or task period. Strip via `catchUpDays(now, row.clock.region, game, floor, 14, row.clock)`, with `floor` the later of the revision's effective time (null for `1970`) and `row.window?.start` or the event's start. `kind`: event rows (`row.source` from `seenEvents`, id not a routine, task or `dailies:`) are `Event`; otherwise Dailies, Weeklies, Monthlies or Cycles by cadence. Day and period labels parse the key as UTC with `en-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`, `defaultCatchUpAccount` from 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 up` appears.
|
||||
- `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`: export `pickAccount(selected: string | null, list: readonly AccountCatchUp[]): string | null`; a live selection with `behind: 0` stays; a missing one returns the default.
|
||||
|
||||
- [ ] **Step 2: Run** `bun test test/catch-up.test.tsx`. Expected: FAIL.
|
||||
|
||||
- [ ] **Step 3: Implement.** Selection via `useState` initialised once from `defaultCatchUpAccount`, resolved each render through `pickAccount`. Chips reuse `.focus-pills` and `.focus-pill` with `--hue`. Ticks dispatch the same `day` action with `actionMeta()`. Strip is a 14-column grid, 7 columns on a phone. `h2` has `tabIndex={-1}`, and `scroll-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, new `onCatchUp` prop), `src/client/components/redesign/RoutinesPage.tsx`
|
||||
- Test: `test/catch-up.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `RoutinesPage` props `catchUpRequested?: boolean`, `onCatchUpShown?: () => void`; `DailyPanel` prop `onCatchUp?: () => void`.
|
||||
|
||||
- [ ] **Step 1: Write failing tests:** `Today's dailies offers Catch up beside Edit pins`, and `the Routines top bar offers Catch up`.
|
||||
|
||||
- [ ] **Step 2: Run** `bun test test/catch-up.test.tsx`. Expected: FAIL.
|
||||
|
||||
- [ ] **Step 3: Implement.** `ReaderApp` holds `catchUpRequested` state, set with `changePage("routines")`. `RoutinesPage`'s effect scrolls the heading into view (`block: "start"`, `behavior: "auto"` under reduced motion, else `"smooth"`), focuses with `preventScroll`, then calls `onCatchUpShown`, 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, on `http://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.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Catch up section on the Routines page
|
||||
|
||||
Approved by the owner in chat on 2026-11-06. Marked **(agent's call)** where the owner approved a
|
||||
proposal rather than asking for it, or where the detail was settled after approval.
|
||||
|
||||
## Why
|
||||
|
||||
Trying to catch up on Endfield's dailies, the owner found:
|
||||
|
||||
- Catch up had drifted away from the Checklist's *Today's dailies* and *Routines* panels, so it was
|
||||
hard to find. It sat at the bottom of the Routines page.
|
||||
- Opening *Catch up & recorded history* produced seven more dropdowns with the same title, one per
|
||||
tracked game, none naming its game.
|
||||
- Inside, items were not structured: paragraphs, and slugs in place of titles.
|
||||
|
||||
Success: from the Checklist, one click lands on the section; the game is named; the thing to catch
|
||||
up on is recognisable by its title; a day is recognisable by its weekday and date.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No change to stored data, to `localStorage` keys, or to what a tick writes. The `day` action is
|
||||
unchanged.
|
||||
- No side panel. The owner rejected the drawer: the section stays on the Routines page and the
|
||||
button moves the reader to it.
|
||||
- No entering past numbers. Amounts stay read-only here, as they are today.
|
||||
- No nagging (`docs/PRODUCT.md`): counts are plain, with no heat colours.
|
||||
|
||||
## The design
|
||||
|
||||
### Where it lives
|
||||
|
||||
A section titled **Catch up** on the Routines page, below the routine bands, always open. It replaces
|
||||
the `<details>` and the per-account `<details>` inside it.
|
||||
|
||||
### Getting there
|
||||
|
||||
A **Catch up** button in the *Today's dailies* heading and the Checklist's *Routines* heading opens
|
||||
the Routines page scrolled to the section, with focus on its heading. A **Catch up** button in the
|
||||
Routines page's top bar scrolls to it. Arriving at Routines any other way does not scroll.
|
||||
|
||||
### One game at a time
|
||||
|
||||
One chip per account, in Game order, using the focus bar's chip style and the game's colour. The
|
||||
label is the game's short name plus the server suffix when a game has several accounts, so two
|
||||
servers stay distinguishable. A chip shows a count when it has one. Only the chosen account's list
|
||||
shows.
|
||||
|
||||
The chosen account starts as the first in Game order with a count, else the first. It is per-visit
|
||||
state, never stored (`AGENTS.md`, per-visit state is not a preference). It does not move when ticks
|
||||
take a count to zero: the list must not jump out from under the reader. **(agent's call)**
|
||||
|
||||
### A row
|
||||
|
||||
- The real title and its type: Dailies, Weeklies, Monthlies, Cycles, or Event.
|
||||
- **Days:** a 14-day strip, oldest first, showing weekday and date, ticked filled in the game's
|
||||
colour, unticked outlined, today ringed. A tap toggles exactly as before.
|
||||
- **Numbers:** recent values, newest first, as `Week of 2 Nov — 8 of 10 points`, four shown and the
|
||||
rest behind a disclosure. A number with nothing logged says so. **(agent's call)** Today it offers
|
||||
a day strip, whose ticks nothing reads.
|
||||
- **A routine that resets with a version or banner** has no earlier period to catch up on
|
||||
(spec 2026-10-22): no strip, values only.
|
||||
|
||||
PRD F17's three rules still hold: never a day later than today; an event's strip starts when the
|
||||
event did; the window bounds what is shown, not what is stored. So ticks older than the strip stay
|
||||
reachable behind *N earlier days* on that row. **(agent's call)**
|
||||
|
||||
### What is listed
|
||||
|
||||
The section's main list is what the Routines page itself calls followed: `routineRows` with
|
||||
`placement.followed`, minus battle passes. A pass is tracked by status against its event, not by
|
||||
days, so a strip for it wrote ticks nothing read. **(agent's call)**
|
||||
|
||||
### Older records
|
||||
|
||||
A collapsed **Older records** disclosure at the bottom of the game's list, holding only items with
|
||||
something logged: **(agent's call)**
|
||||
|
||||
- *Ended or no longer followed:* logs on the current reset timing with no live followed row. Recorded
|
||||
days only, since offering new days could claim days after an event ended. A recorded day unticked here stays offered for the rest of
|
||||
the visit, so a mis-tap can be undone.
|
||||
- *Before reset timing changed:* one group per earlier reset timing, with its region, reset and date
|
||||
line, and days as they are offered today.
|
||||
|
||||
### Titles
|
||||
|
||||
Resolved in order: `dailies:<game>` to the game's daily task name (the name *Today's dailies* uses);
|
||||
a live row with that id or placement id; the game's built-in routines; the reader's tasks; any known
|
||||
event (`contentEvents`). A `feed-cycle:` id falls back to *Battle pass*; anything else to *Removed
|
||||
item*. A raw ID is never shown. **(agent's call)**
|
||||
|
||||
The slugs had one main cause: a followed battle pass is placed under `feed-cycle:<game>:<family>`,
|
||||
which is not an ID any lookup knew.
|
||||
|
||||
### The count
|
||||
|
||||
Shown on chips and rows, and counting only what is expected every day: rows with a daily cadence,
|
||||
measured in days, with no check-in goal. Counted days are strip days before today that are unticked,
|
||||
from the first day the reader has ticked that item. **(agent's call)** Nothing records when a reader
|
||||
started following something, and fourteen unticked days on a routine followed yesterday would be
|
||||
false. A row never ticked counts nothing. Today is excluded, because today is not behind.
|
||||
|
||||
## Left to the builder
|
||||
|
||||
Class names, module boundaries, exact spacing. Colours are tokens plus the game hue, as everywhere
|
||||
else, and the section is checked in both themes, at phone width, and on a plain HTTP hostname.
|
||||
Reference in New Issue
Block a user