From 0b5aa121738c00530118bfebfd0c0a3dfe1d6f7a Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 20 Aug 2026 06:39:37 +0200 Subject: [PATCH] refactor: delete the section hint nothing could reach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Running now" header had a hint reading "next after this ends in …", and the commit before this one fixed a real defect in the string it produced — an unannounced end became the word "ended". Both were beside the point: the branch cannot render at all. `Section` shows `action ?? hint`. The hint needed a second live row to have anything to say, and two live rows are two visible rows, which is exactly the condition that puts the sort control in the same slot. So the action was present whenever the hint was, and won every time. What it would have said is already on the page. The headline panel's "Then" list names the deadlines behind the closest one and counts each of them down, which is the same answer with more room. So the hint slot goes, `followingDeadlineMs` goes with it as its only caller, and `Section` is left with the one control slot it actually uses. Co-Authored-By: Claude Opus 5 (1M context) --- src/client/App.tsx | 29 +++++++++++------------------ src/client/state/lens.ts | 25 ------------------------- test/lens.test.ts | 35 ----------------------------------- 3 files changed, 11 insertions(+), 78 deletions(-) diff --git a/src/client/App.tsx b/src/client/App.tsx index b569773..8b5d1e2 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -24,7 +24,6 @@ import { compareRows, SORT_MODES, type Activity, type SortMode } from "./state/s import { advanceFocus, countByGame, - followingDeadlineMs, nextToExpire, outstanding, resolveFocus, @@ -301,15 +300,6 @@ export function App() { const live = visible.filter((r) => r.clock.live); const upcoming = visible.filter((r) => r.clock.upcoming); - /** - * What falls due after the row at the top of "Running now". - * - * Read off the deadlines rather than off the list, because the list is in - * whichever order the reader chose — see `followingDeadlineMs`. Null when - * there is no second dated end, and the header then says nothing rather than - * counting down to a placeholder. - */ - const following = followingDeadlineMs(live); /** * The unstarted events the checklist actually lists. * @@ -522,11 +512,6 @@ export function App() {
1 ? (

{title}

- {action ?? (hint !== undefined &&

{hint}

)} + {action}
{legend === true && } {children} diff --git a/src/client/state/lens.ts b/src/client/state/lens.ts index 3676df6..50879ef 100644 --- a/src/client/state/lens.ts +++ b/src/client/state/lens.ts @@ -71,31 +71,6 @@ export function firstToExpire(rows: readonly T[]): T | null { return nextToExpire(rows, 1)[0] ?? null; } -/** - * How long the deadline *behind* the closest one has left, or null when there - * is not a second dated deadline to report. - * - * The "Running now" header uses this to say what falls due after the row at the - * top. It asked the list for `rows[1]` before, which was wrong twice over and - * in the two ways this module exists to prevent. - * - * The list arrives sorted by whatever mode the reader chose, so under "doing - * first" `rows[1]` is the second thing they are partway through — the same - * mistake `firstToExpire` was written to stop the headline making, one row - * further down. And an unannounced end has no time remaining at all, so the - * old `?? 0` handed `formatRemaining` a zero and the header read "next after - * this ends in ended": the `endsAt: null` rule turned into a claim that - * something expired, which is exactly backwards. - * - * So it reads the second *dated* deadline and returns null rather than a - * stand-in, and the caller omits the line when there is nothing to say. - */ -export function followingDeadlineMs( - rows: readonly T[], -): number | null { - return nextToExpire(rows, 2)[1]?.clock.msRemaining ?? null; -} - /** * The focused game, if it is still a game the reader can see. * diff --git a/test/lens.test.ts b/test/lens.test.ts index 071284f..5bea56f 100644 --- a/test/lens.test.ts +++ b/test/lens.test.ts @@ -3,7 +3,6 @@ import { advanceFocus, countByGame, firstToExpire, - followingDeadlineMs, nextToExpire, outstanding, resolveFocus, @@ -47,40 +46,6 @@ describe("outstanding", () => { }); }); -describe("followingDeadlineMs", () => { - const HOUR = 3_600_000; - - test("reads the second deadline, not the second row", () => { - // The "Running now" header says what falls due after the top row. The list - // arrives in whichever order the reader chose, so under "doing first" its - // second row is the second thing they are partway through — the mistake - // `firstToExpire` prevents one row up. - const rows = [ - row("mid-run", "genshin", 9 * 24 * HOUR), - row("next-week", "hsr", 5 * 24 * HOUR), - row("tonight", "zzz", 3 * HOUR), - ]; - expect(followingDeadlineMs(rows)).toBe(5 * 24 * HOUR); - }); - - test("an unannounced end is not a deadline of zero", () => { - // The old code took `rows[1].msRemaining ?? 0`, and `formatRemaining(0)` is - // the string "ended" — so a second row with `endsAt: null` had the header - // reading "next after this ends in ended". Null means there is nothing to - // say, and the caller says nothing. - expect(followingDeadlineMs([row("tonight", "genshin", 3 * HOUR), row("undated", "hsr", null)])) - .toBeNull(); - }); - - test("one deadline has nothing behind it", () => { - expect(followingDeadlineMs([row("only", "genshin", 3 * HOUR)])).toBeNull(); - }); - - test("an empty list is null, not a zero", () => { - expect(followingDeadlineMs([])).toBeNull(); - }); -}); - describe("firstToExpire", () => { test("takes the soonest, not the first row", () => { // The list arrives sorted by whatever mode the reader chose. Under "doing