diff --git a/AGENTS.md b/AGENTS.md index a6c832a..6387d8a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -112,6 +112,7 @@ src/ingest/ html.ts, dates.ts (sixteen formats), merge.ts, sanitize.ts, ro adapters/ index.ts — SOURCES registry binding url+game+parser, and the sanitize seam src/client/ React app, service worker, manifest state/ progress, daily log, ignores, prefs, sort — all localStorage + gameOrder.ts — the reader's game order, and the A–Z fallback; pure useCustom.ts — the reader's own games and events (PRD F13) lens.ts — who sees which rows (focus, outstanding, next-to-expire); pure zoom.ts — the timeline's scale ladder; pure @@ -119,7 +120,7 @@ src/client/ React app, service worker, manifest theme.ts — dark or light, and what a game hue reads as on each scripts/ build-feed.ts, build-static.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches) serve.ts static server + /api/health -test/ 772 tests +test/ 831 tests fixtures// raw HTML + .expected.json per source — pinned, kept forever snapshots/ current page per source, rewritten by refresh — see its README ``` @@ -878,6 +879,40 @@ to an open page). Four things hold it up: and treating that as "has been offered nothing" switches off every game they already read. Seeding records what is on their screen and changes nothing else. Lanes they invented (`mygame:`) are recorded but never hidden. +- **The order games appear in is the reader's, and absent means unplaced.** Nothing used to decide + it: the focus bar, the settings list, the timeline's lanes and the dailies strip all rendered + `App`'s `games`, which is whichever game happened to hold the first event row. `orderGames` + (`src/client/state/gameOrder.ts`) is now the one rule, and every surface goes through it — sorted + on the **name** and never the `LaneId`, because the id is not what is printed, and through + `localeCompare`, because `<` files hololive Dreams after every capitalised game. An absent + `prefs.gameOrder` means *the reader has never placed a game*, not an empty order, so they get the + alphabetical rule — the `knownGames` distinction again, and the same trap. Two properties are + load-bearing rather than incidental. The result is **always a permutation of the lanes it was + given**: a game dropped there is indistinguishable on screen from one the reader switched off, and + switching it back on would not bring it back. And a lane it does not name **trails** the ones it + does, which is what keeps a game we add later out of the middle of a hand-made order, and what + keeps a retired source's slot warm rather than pruning it. `games` itself is left in feed order on + purpose: `adoptNewLanes` diffs it and `knownGames` is seeded from it, so ordering it at source + would let a display preference reach the code that hides a reader's games. +- **Reordering is a settings-only affordance.** The focus bar and the dailies strip are the fastest + tap targets in the app, and a drag target on top of a tick target costs somebody a streak the first + time it misfires — so the live surfaces are never draggable. Both a handle and ↑↓ buttons ship, + because touch fires no drag events at all: the arrows are the mechanism and the drag is the pointer + fast path, and being ordinary buttons is what makes the whole thing reachable by keyboard and + screen reader without a second implementation of the same interaction. +- **A game's dailies stay together, and grouping never re-sorts inside a group.** `dailyGroups` + (`Dailies.tsx`) emits a game's standing chore followed by that game's repeating events, in the + order those arrived; it replaced `[...chores, ...repeating]`, which put a game's chore and its own + login event at opposite ends of the strip. Collapsed, the grouping is **adjacency only** — no + per-game headings, because that strip is the part of the page answerable in ten seconds and a + heading per game makes it the tallest block on it, pushing "next to expire" down the page. +- **A day already gone is still tickable, and the window bounds display and never data.** People play + at midnight and tick at breakfast, so `catchUpDays` (`src/shared/daily.ts`) offers the last + `CATCH_UP_DAYS` game-days for a standing chore and for an event whose end was never announced — + the two cases `dailyDays` cannot answer. It never returns a day past today: a tick claims you did + it, and a future pip is a control for a claim that cannot be true, so it is absent rather than + disabled. A tick older than the window stays logged and keeps counting toward the streak — nothing + prunes the log against a window, here or anywhere else. - **Which view opens is the reader's answer.** `prefs.view` is asked once on the first run (PRD F8) and written by the tabs from then on. It was component state, which meant a reader who preferred the timeline was put back on the list by every reload, with nothing to blame but the app diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index 0ae2016..5d33883 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -217,7 +217,8 @@ Namespaced, versioned, and small. Nothing here ever goes to the server. "gacha-tracker:v1:progress" // { [eventId]: { status?, effort?, note?, at } } "gacha-tracker:v1:daily" // { [id]: { days: ["2026-08-15", ...], at } } "gacha-tracker:v1:ignored" // { [eventId]: { at } } — "stop showing me this" -"gacha-tracker:v1:prefs" // { region, hiddenGames[], knownGames[]?, focusGame, sort, view, +"gacha-tracker:v1:prefs" // { region, hiddenGames[], knownGames[]?, gameOrder[]?, + // focusGame, sort, view, // timelineDayWidth, timelineGroup, showUpcoming, // timelineSplitUpcoming, detectDaily, showCompleted, // showIgnored, theme, regionConfirmed, onboarded } @@ -250,6 +251,19 @@ Namespaced, versioned, and small. Nothing here ever goes to the server. // knownGames is every lane the reader has been offered. Absent // means unrecorded, not "offered nothing" — see PRD F8; a lane // missing from it is new to them and arrives switched off. + // gameOrder is the order the reader put their games in, and it + // governs every surface that lists a game: the focus bar, the + // dailies strip, the settings list and the timeline's lanes. + // Absent means they have never placed one — NOT an empty order + // — and `orderGames` then sorts alphabetically by name, which + // is what the first-run picker does. The same distinction + // knownGames draws, for the same reason: every install + // predating the field is in that state. A lane it does not + // name trails the ones it does, so a game we add later never + // lands in the middle of a hand-made order, and a lane that + // has left the feed keeps its slot rather than being pruned — + // a source that comes back returns where the reader put it. + // Reset writes the field away rather than storing []. PRD F16. // theme is "dark" | "light" | "system", defaulting to dark — see // PRD F15. It is read by the app *and* by a pre-paint script in // index.html, which is the only thing outside the client bundle @@ -296,6 +310,19 @@ server time, so a player finishing at 02:00 is still on the previous day's daili computed against the reader's chosen region (`RESET_HOUR_LOCAL`, `dayKey`). Keys sort lexicographically, which is what "how many days are left" and streak counting rely on. +**A day already gone is still writable, and `catchUpDays` decides how far back.** People play at +midnight and tick at breakfast; a log that only accepts today goes wrong on its first bad evening. +`dailyDays` already answers this for an event whose end was published, because the whole run is +known — the two cases it cannot answer are a `dailies:` chore, which has no start or end +because it is a routine, and an event with `endsAt: null`, where it returns null. `catchUpDays` +answers both with the last `CATCH_UP_DAYS` (14) game-days, clipped at an event's start where there is +one and **never extending past today**: a tick claims you did it, and nobody can have done tomorrow. + +That window bounds **display and never storage**. A tick older than it stays in `daily`, keeps +counting toward `streakOf` and `dailySummary`, and is simply not drawn — the same rule as a tick +outside the window the feed now claims. Nothing prunes the log against either window, because a +fortnight's streak exists on that device and nowhere else. + **Not every game has a server per region.** `GameMeta.resetOffsets` records the regions where a game's server clock differs from `REGION_RESET_UTC_OFFSET`. Endfield is the case this exists for: it has two server groups rather than three, and Europe is served off the Americas machine on a fixed diff --git a/docs/PRD.md b/docs/PRD.md index 17abc94..b1cc3c1 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -248,6 +248,26 @@ The heuristic assumes about an hour of play a day and says so. It never hides or it adds a flag the reader can ignore. **An event with no recorded effort never gets a warning**, because inferring an estimate in order to warn about it would be fabricating their input. +**A day you did but never ticked can be recorded.** People play at midnight and tick at breakfast, or +forget for a week and come back — a log that only accepts today is a log that goes wrong on its first +bad evening and is never trusted again. An event whose end was announced already allowed this, since +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. + +Three rules hold it honest: + +- **Never a day later than today.** A tick is a claim that you did it, and tomorrow is not something + anyone can have done, so a future day is absent rather than shown and disabled. +- **An event's strip starts when the event did**, capped at the same fortnight. A standing login + campaign that opened in March gets fourteen days, not a hundred and eighty — and never a day before + it was claimable. +- **The window bounds what is shown and never what is stored.** A tick from five weeks ago stays + logged, keeps counting toward the streak and the totals, and is simply off-screen. Nothing removes a + day the reader did not remove themselves. + **F13 — Your own games and your own events.** No feasible adapter set covers everyone. Fourteen games were named in the first release thread and the reader with the largest collection asked for exactly one thing — *"can you add a custom game @@ -375,6 +395,30 @@ Switching is instant, costs nothing and saves nothing: no reload, and nothing ma ticked is touched. And it survives the load it is chosen on — the shell sets the theme before first paint, so a reader on light is never shown a dark page while the bundle downloads. +**F16 — Your own game order.** +Nothing used to decide the order games appear in. The focus bar, the settings list, the timeline's +lanes and the dailies strip all rendered whichever game happened to hold the first event row — which +is arbitrary, and shifts as events come and go. Two answers, and the reader gets both: + +- **A rule, for everybody.** Absent any choice of theirs, games are alphabetical by name, which is + what the first-run picker does (F8). Absent means *they have never placed a game*, not that they + have no order — so every existing reader gets the rule rather than an empty list, the same + distinction `knownGames` draws. +- **An order, for anyone who wants one.** Games can be dragged, or nudged with arrows, into any order + in settings. Every surface that lists a game follows it, so the game they actually play is first + everywhere at once. + +**Reordering lives in settings and nowhere else.** The focus bar and the dailies strip are the fastest +tap targets in the app — the strip is the part of the page answerable in ten seconds — and a drag +target on top of a tick target costs somebody a streak the first time it misfires. Both a drag handle +and arrow buttons ship, because touch fires no drag events at all and a drag-only list is unreachable +on a phone and by keyboard alike. + +A game added later is not slotted into an order the reader made by hand: it trails everything they +placed. It also arrives switched off (F8), so settings is where they meet it, which is where they +would move it anyway. **A game's dailies stay together** — a repeating event the reader marked sits +with the standing chore of the game it came from, because the reader thinks in games and not in kinds. + ## Out of scope for v1 Accounts and sync; push notifications; in-game resource or pull tracking; native mobile apps (the diff --git a/docs/superpowers/specs/2026-08-20-game-order-and-daily-catch-up-design.md b/docs/superpowers/specs/2026-08-20-game-order-and-daily-catch-up-design.md new file mode 100644 index 0000000..55d70e8 --- /dev/null +++ b/docs/superpowers/specs/2026-08-20-game-order-and-daily-catch-up-design.md @@ -0,0 +1,278 @@ +# Game order, grouped dailies, and catching up a missed day + +**Date:** 2026-08-20 +**Status:** implemented, 2026-08-20 + +Three changes that share one primitive. Read `AGENTS.md` first — every rule cited below is already +written down there, and the citations are load-bearing rather than decorative. + +## Why + +Three complaints, one root: + +1. **Nothing decides the order games appear in.** The focus bar, the settings chips and the timeline + lanes all render `games` from `App.tsx`, which is + `[...new Set([...allRows.map(r => r.event.game), ...custom.lanes])]` — *whichever game happened to + hold the first event row*. That is arbitrary, it shifts as events come and go, and it is the same + problem already fixed on the first-run picker (`docs/PRD.md` F8). +2. **A game's dailies are scattered.** `Dailies.tsx` builds `[...chores, ...repeating]`, so Genshin's + standing chore and Genshin's login event are separated by every other game on the page. The reader + thinks in games; the strip thinks in kinds. +3. **A day you did but did not tick cannot be recorded.** Three surfaces, and only one of them works: + + | Surface | Backfill today? | + |---|---| + | Event with an announced end, in the detail sheet | **Yes** — every day in `dailyDays` is a pip button, only future days disabled | + | Event with `endsAt: null` | **No** — `days === null` renders no pips at all, so only today is reachable | + | A game's standing chore (`dailies:`) | **No, and there is no history UI at all** — one `TickChip` wired to today | + + The third is the one the code itself calls "the most-missed thing in every one of these games". + +The store is already capable: `toggleDay(id, day)` takes any day key and `streakOf` reads whatever is +logged. This is UI reach, not a data-model change — **nothing here touches an event ID, a `dailies:` +key or a `dayKey` format.** + +## Decisions taken + +| Question | Answer | +|---|---| +| Whose order is it? | The reader's, stored in `prefs.gameOrder` | +| Where is it edited? | Settings only. The live surfaces are never draggable | +| How? | A drag handle **and** ↑↓ buttons, both always visible | +| Default before they touch it | Alphabetical by display name | +| A game added later | Lands after everything hand-placed; not auto-appended to the order | +| Which surfaces obey it | Focus bar, Today's dailies, settings games list, timeline lanes | +| Chore catch-up window | 14 days | +| Undated-event catch-up window | Its start day → today, capped at 14 days back | + +The live surfaces stay drag-free because the dailies strip is the fastest tap target on the page — +the code calls it the part "answerable in ten seconds" — and a drag target on top of a tick target +costs a streak when it misfires. `AGENTS.md` § Conventions ("a list row is one target") is the same +worry. + +## 1. `src/client/state/gameOrder.ts` — new, pure, no React + +```ts +export function orderGames( + lanes: readonly LaneId[], + stored: readonly LaneId[] | undefined, + nameOf: (id: LaneId) => string, +): LaneId[] + +/** Move one entry. Out-of-range indices are a no-op. */ +export function moveGame(order: readonly LaneId[], from: number, to: number): LaneId[] +``` + +`orderGames`, in priority order: + +1. **`stored` absent → every lane alphabetical by `nameOf`, via `localeCompare`.** Absent means *the + reader has never placed a game*, exactly as an absent `knownGames` means unrecorded rather than + "offered nothing" (`usePrefs.ts` § `adoptNewLanes`). Every existing install is in this state on + day one, and this default is what the first-run picker already does. +2. **`stored` present → the lanes it names, in its order, filtered to lanes that exist; then every + unnamed lane, alphabetical, appended.** +3. **The result is always a permutation of `lanes`.** Never drops one, never invents one, never + duplicates one even if `stored` does. This is the safety property: a game missing from the focus + bar or from settings is indistinguishable from a game the reader switched off, and their fix for + that — switch it back on — would do nothing. +4. **Total.** An unknown lane still gets a name (`metaFor` answers "Unknown game"), so a `mygame:` + lane, or an import carrying an event whose game did not come with it, sorts rather than throws. +5. **A retired game keeps its slot.** Filter on output; never prune `stored`. A source that goes away + and comes back returns to where the reader put it — `AGENTS.md` § "Retiring a game, a source or a + page must never cost the reader a row they typed", applied to a preference. + +`localeCompare`, not `<`: `hololive Dreams` is the one lowercase name in `games.ts` and a code-point +sort files it after every capitalised game. Sort on the **name**, never the `LaneId` — `hsr` is +Honkai: Star Rail, `nikke` is Goddess of Victory: Nikke. + +**A reorder is always applied to the list as displayed, and the whole result is stored.** Both halves +matter, and an earlier draft of this spec had a separate `seedOrder` helper for the second one before +noticing the first: the indices a drag or an arrow produces are positions *on screen*, so applying +them to a stored order naming only some lanes would move the wrong game — and storing only the moved +id would leave rule 2 reading "that game, then everything else alphabetically", which is not what +dragging one row one notch means. Writing back what the reader is looking at gets both right, and +needs no helper. + +### `prefs.gameOrder` + +```ts +/** The reader's own game order. Absent means they have never placed one — see orderGames. */ +gameOrder?: LaneId[]; +``` + +Optional and **absent from `defaults()`**, deliberately: a stored `[]` would mean "an order that +names nothing", which is indistinguishable from absent under rule 2 but invites a future reader of +the code to treat it as meaningful. "Reset to A–Z" writes `gameOrder: undefined`, which +`JSON.stringify` drops. + +No new key space and no migration — one more field in the single `prefs` blob, like `timelineGroup`. +It rides the export for free (`exportProgress` already writes `prefs`). + +**Known asymmetry, not fixed here:** `importProgress` restores progress, daily, ignored and custom +but **never prefs**, so an imported file does not restore a game order. That is pre-existing and +applies equally to region, theme and view. Do not fix it as a side effect of this work; it deserves +its own decision about merge semantics. + +## 2. The surfaces follow the order + +`App.tsx` computes it once, next to `games`: + +```ts +const ordered = useMemo( + () => orderGames(games, prefs.gameOrder, (id) => gameMeta(id).name), + [games, prefs.gameOrder, gameMeta], +); +const enabled = useMemo( + () => ordered.filter((g) => !prefs.hiddenGames.includes(g)), + [ordered, prefs.hiddenGames], +); +``` + +**`games` itself does not change.** It stays the lane-identity list that `adoptNewLanes` diffs and +`knownGames` is seeded from. Reordering it at source would entangle a display preference with the +logic that decides which of a reader's games get switched off — a reordering bug would become a +*game silently hidden* bug. + +Then: focus bar gets `enabled` (already does — it just becomes ordered), `Dailies` gets ordered games, +`Controls` gets `ordered`, and `timelineLanes` gains lane ordering: + +```ts +timelineLanes(rows, mode, split, order?: readonly LaneId[]) +``` + +Applied to the **lanes**, never to the rows inside one — `lanes.ts` already states that grouping is +not a licence to re-sort within a group. A lane whose game is absent from `order` sorts after the +placed ones, keeping the function total. The merged `"ending"` mode is untouched: it is deliberately +not per-game. + +`Welcome.tsx` drops its local comparator and calls `orderGames(available, undefined, nameOf)` — the +picker precedes any stored order, so it always gets the alphabetical rule. One rule, not a fifth copy. + +## 3. The settings reorder editor + +The Games chip row in `Controls.tsx` becomes one row per game: + +``` +∷ ● Genshin Impact 3 of 14 ↑ ↓ [on] +``` + +- Drag handle (`draggable` + `dragover`/`drop`, no dependency — the only runtime dep is `zod`) **and** + ↑↓ buttons, both always visible. **Touch does not fire drag events**, so ↑↓ is the real mechanism + and drag is the desktop fast path. Say so in the UI copy. +- ↑↓ are ordinary buttons, so keyboard and screen-reader support come free: + `aria-label="Move Genshin Impact up"`, and the row states its position so it is audible. +- A reorder writes `moveGame(games, from, to)` — the list as displayed — as the whole new + `gameOrder`. +- "Reset to A–Z" writes `gameOrder: undefined`. +- **The row is not itself a target.** The handle, two arrows and the on/off toggle are four explicit + controls and nothing else is clickable, which is why this does not breach "a list row is one + target" — that rule exists because a full-bleed row target plus an inner control is a mis-tap. + +## 4. Dailies: grouping, and catching up + +### Grouping + +Replace `[...chores, ...repeating]` with a pure exported function in the component module — the +pattern `Timeline.tsx` already uses for `boardWindow` / `splitAt` / `startMarkers`, so it is tested +directly with no rendering: + +```ts +export function dailyGroups( + games: readonly LaneId[], // already in the reader's order + events: readonly DisplayEvent[], + now: number, + region: Region, +): DailyGroup[] // { game, chore | null, events } +``` + +For each game in order: its standing chore first, then that game's repeating events **in the order +they arrived**. Grouping is not a licence to re-sort inside a group. A `mygame:` lane contributes +events but no chore (`isCustomGameId` — there is no routine we could name on a reader's behalf). + +**Collapsed, grouping is adjacency only — no per-game headings.** Chips stay one wrapping row; a +game's chore and its events simply become neighbours, and they already share a hue. Headings would +make the ten-second strip the tallest block on the page, above "next to expire", which is the answer +the reader came for. Headings appear only in the expanded state, where there is vertical room. + +The `2/4` counter, `soonest` and the mixed-reset line are computed over the same items and are +unaffected: grouping only reorders. Per-game reset clocks stay per-game — Endfield's European day can +still be yesterday's (`Dailies.tsx:46-48`). + +### `catchUpDays` — `src/shared/daily.ts`, beside `dailyDays` + +```ts +export const CATCH_UP_DAYS = 14; + +export function catchUpDays( + now: number, + region: Region, + game: LaneId | undefined, + notBefore: number | null, +): string[] +``` + +Day keys oldest → today inclusive: at most `CATCH_UP_DAYS`, never earlier than `notBefore`'s day, +**never past today**. Pure, clock as an argument, like everything else in that module. + +- A **chore** passes `notBefore: null` — no start exists — and gets the last 14 days. +- An **undated event** passes its `startsMs`, so the strip begins at its real start, capped at 14 back. + That cap is what stops a login campaign that opened in March from rendering 180 pips. + +### The two strips + +- `Dailies` gains a `Catch up ▾` disclosure: `useState(false)`, **per-visit, never stored** — the same + argument the repo already makes for "show all N" (`AGENTS.md` § "Truncating a list is not + re-sorting it"): it is something a reader does while reading, not a statement about how the app + should work. Expanded, each group renders its game heading and a pip row per item. +- `DailyChecklist`'s `days === null` branch gains the same strip, replacing a sentence that currently + offers nothing to click. Dated events keep the full `dailyDays` strip they already have. +- `DayPip` moves out of `DailyChecklist.tsx` into `src/client/components/DayPip.tsx` so both surfaces + share one pip instead of growing two that drift. + +### Four guard rails + +1. **Future days are never rendered** in a catch-up strip — not rendered-and-disabled. A tick claims + you did it. The dated checklist keeps its dimmed future pips: there the run's length is published + and the strip is a forecast, whereas catch-up is explicitly about the past. +2. **The window bounds display, never data.** A tick from five weeks ago stays logged, keeps counting + in `streakOf` and in the `logged` total, and is simply off-screen. `toggleDay` only ever touches the + exact day tapped — `AGENTS.md`: "a tick is never removed except by the reader". +3. **Every call passes `game`.** Anything reading or writing a tick must, or it writes under one clock + and reads under another. Chores pass their game id, events pass `event.game`. Endfield's European + reset is 09:00 UTC, so this is not theoretical. +4. **Catch-up cannot resurrect a finished event.** Expanded rows come from the same items the collapsed + strip uses, which `App` already filters through `outstanding` — an event the reader marked done has + no line left to tick. True by construction; pinned by a test anyway. + +The completion **fireworks** fire on `complete > was.complete` at equal `total`. Backfilling yesterday +leaves today's count alone, so no burst — correct. Ticking today from inside the expanded strip does +burst, also correct. + +## Testing + +| File | Pins | +|---|---| +| `test/game-order.test.ts` | `orderGames` is a permutation in every case (absent / present / retired / unknown / duplicate-bearing `stored`); alphabetical by name and not by id; `localeCompare` and the lowercase name; unplaced lanes trail placed ones; `moveGame` records the whole displayed list, never loses an entry, and is a no-op off either end | +| `test/daily.test.ts` | `catchUpDays`: the 14-day cap, the `notBefore` clip, never past today, a per-game reset offset (Endfield europe UTC-5) | +| `test/daily.test.ts` | A tick older than the window survives and still counts toward the streak | +| `test/views.test.tsx` | `dailyGroups` keeps a chore adjacent to its game's events, preserves within-game order, and yields no row for a done event; `Welcome` still orders alphabetically through the shared rule | +| `test/controls.test.tsx` | The reorder rows render both affordances; ↑ on the first row is inert | +| `test/prefs.test.ts` | `gameOrder` absent survives a load; reset writes it away | + +Tests must not need build output, and must run offline. + +## Docs to update in the same change + +- `docs/PRD.md` — the game order as reader-facing behaviour (F4/F8 neighbourhood), and catch-up under + the dailies feature. +- `docs/DATA-MODEL.md` — `prefs.gameOrder`, and that absent means unplaced. +- `AGENTS.md` § Conventions — the ordering rule, and that catch-up bounds display and never data. + +## Commit split + +Four commits, each typechecking and passing tests on its own, per `AGENTS.md` § Conventions: + +1. `gameOrder` module + `prefs.gameOrder` + tests. Nothing renders differently. +2. The four surfaces follow the order; `Welcome.tsx` converted to the shared rule. +3. The settings reorder editor. +4. Dailies grouping + `catchUpDays` + `DayPip` extraction + both strips.