feat: let the reader mark an event as repeating daily

Dailiness was read off the source's wording alone, which is wrong in both
directions: a grind that resets every day but whose page never prints
"daily" got no checklist, and a banner whose blurb mentions "daily login
rewards" got one nobody could dismiss.

The reader's answer now wins. The control sits exactly where the
checklist goes — the one place the answer visibly matters — so marking an
event and ticking today off are the same gesture in the same place.

An override is stored only when it disagrees with detection. Recording
agreement would freeze today's guess into the reader's own data, so a
later parser improvement could never reach that event.

Marked events also join today's dailies at the top of the page, beside
the per-game chores: at 23:50 a login campaign and a commission run are
the same job, and ticking one should not mean opening a sheet to find its
checklist.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 22:14:37 +02:00
co-authored by Claude Opus 5
parent 6177633abc
commit 7f384eb9db
9 changed files with 293 additions and 69 deletions
+13 -2
View File
@@ -17,7 +17,7 @@ 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 { clockFor, DAY, formatRemaining } from "../shared/time.ts";
import { dailySummary, isDaily } from "../shared/daily.ts";
import { dailySummary, isDaily, resolveDaily } from "../shared/daily.ts";
import type { GameId } from "../shared/schema.ts";
type View = "soon" | "calendar";
@@ -85,9 +85,16 @@ export function App() {
return daily.daysFor(id).length > 0 ? "doing" : "idle";
};
/**
* Whether an event repeats, the reader's own answer included. Detection reads
* the source's wording; they can overrule it either way.
*/
const repeatsDaily = (row: RowEvent): boolean =>
resolveDaily(row.event, prog.progress[row.event.id]?.daily);
/** Today's state for a repeating event, or undefined if it does not repeat. */
const dailyBadge = (row: RowEvent): DailyBadge | undefined => {
if (!isDaily(row.event)) return undefined;
if (!repeatsDaily(row)) return undefined;
const summary = dailySummary({
startsMs: row.clock.startsMs,
endsMs: row.clock.endsMs,
@@ -255,6 +262,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)}
region={prefs.region}
now={now}
daysFor={daily.daysFor}
@@ -386,7 +394,10 @@ export function App() {
note={prog.progress[openRow.event.id]?.note ?? ""}
region={prefs.region}
now={now}
daily={repeatsDaily(openRow)}
detectedDaily={isDaily(openRow.event)}
dailyDays={daily.daysFor(openRow.event.id)}
onDaily={prog.setDaily}
onToggleDay={daily.toggleDay}
onStatus={prog.setStatus}
onEffort={prog.setEffort}