fix(soon): stop pointing at events you've finished or ignored

Two symptoms of one bug. The "next to expire" headline counted events
the reader had marked done or ignored, and the dailies strip kept a
tickable chip for a repeating event they had already finished.

showCompleted and showIgnored decide what a reader can *look at*. The
headline and the strip are *instructions*, so they answer a different
question — what is still on your plate — and both now go through one
`outstanding` lens.

Also fixes a second bug in the same line: `next` took the head of the
list, which under "doing first" sorting is whatever you're partway
through, not the soonest deadline. It reads the minimum now.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-16 20:40:46 +02:00
co-authored by Claude Opus 5
parent c3e9b9064d
commit edc6f3e4bb
7 changed files with 186 additions and 12 deletions
+23 -7
View File
@@ -16,6 +16,7 @@ import { useProgress } from "./state/useProgress.ts";
import { useDailyLog, type DailyLogMap } from "./state/useDailyLog.ts";
import { usePrefs } from "./state/usePrefs.ts";
import { compareRows, SORT_MODES, type Activity, type SortMode } from "./state/sort.ts";
import { firstToExpire, outstanding } from "./state/lens.ts";
import { clockFor, DAY, formatRemaining } from "../shared/time.ts";
import { dailySummary, isDaily, resolveDaily } from "../shared/daily.ts";
import type { GameId } from "../shared/schema.ts";
@@ -110,8 +111,10 @@ export function App() {
return { doneToday: summary.doneToday, remaining: summary.remaining };
};
const isIgnored = (id: string) => ignored.marks[id] !== undefined;
const toggleIgnored = (id: string, title: string) => {
const wasIgnored = ignored.marks[id] !== undefined;
const wasIgnored = isIgnored(id);
ignored.toggle(id);
setLastIgnored(wasIgnored ? null : { id, title });
};
@@ -153,7 +156,7 @@ export function App() {
.filter((r) => !r.clock.ended)
// Ignored events are gone from both views unless deliberately revealed
// — that is the whole point of ignoring one.
.filter((r) => prefs.showIgnored || ignored.marks[r.event.id] === undefined)
.filter((r) => prefs.showIgnored || !isIgnored(r.event.id))
.filter((r) => prefs.showCompleted || !isDone(r.event.id))
// Sorting only ever groups: both modes fall back to soonest-ending
// inside a group, so choosing one never costs the deadline order.
@@ -172,7 +175,20 @@ export function App() {
const live = visible.filter((r) => r.clock.live);
const upcoming = visible.filter((r) => r.clock.upcoming);
const next = live.find((r) => r.clock.msRemaining !== null) ?? live[0] ?? null;
/**
* What the page is telling the reader to *do*, as opposed to what it is
* letting them look at.
*
* The headline and the dailies strip are both instructions, so both drop
* events the reader has finished or ignored — being pointed at a job you
* already did is the bug whether the pointer is a countdown or a checkbox.
* `showCompleted` deliberately does not reach this: that preference says keep
* them on screen, not keep nagging me about them.
*/
const todo = outstanding(live, isDone, isIgnored);
const next = firstToExpire(todo);
const openRow = allRows.find((r) => r.event.id === openId) ?? null;
if (state.status === "loading") {
@@ -267,7 +283,7 @@ export function App() {
that expires tonight rather than next patch. */}
<Dailies
games={games.filter((g) => !prefs.hiddenGames.includes(g))}
events={live.filter(repeatsDaily).map((r) => r.event)}
events={todo.filter(repeatsDaily).map((r) => r.event)}
region={prefs.region}
now={now}
daysFor={daily.daysFor}
@@ -302,7 +318,7 @@ export function App() {
status={prog.progress[row.event.id]?.status}
effort={prog.progress[row.event.id]?.effort}
daily={dailyBadge(row)}
ignored={ignored.marks[row.event.id] !== undefined}
ignored={isIgnored(row.event.id)}
onRestore={(id) => ignored.toggle(id)}
onOpen={setOpenId}
/>
@@ -332,7 +348,7 @@ export function App() {
status={prog.progress[row.event.id]?.status}
effort={prog.progress[row.event.id]?.effort}
daily={dailyBadge(row)}
ignored={ignored.marks[row.event.id] !== undefined}
ignored={isIgnored(row.event.id)}
onRestore={(id) => ignored.toggle(id)}
onOpen={setOpenId}
/>
@@ -393,7 +409,7 @@ export function App() {
<EventDetail
row={openRow}
completed={isDone(openRow.event.id)}
ignored={ignored.marks[openRow.event.id] !== undefined}
ignored={isIgnored(openRow.event.id)}
status={prog.progress[openRow.event.id]?.status}
effort={prog.progress[openRow.event.id]?.effort}
note={prog.progress[openRow.event.id]?.note ?? ""}