feat: let the reader switch off daily detection
Guessing from a source's wording is right most of the time, and someone who finds it wrong often enough should be able to stop it rather than dismiss the same false positive every patch. Off, only events the reader marked themselves get a checklist. It silences the guess rather than deleting anything: overrides, ticks and streaks all survive, so switching it back on restores exactly what was there. With it off, marking an event stores an explicit yes, since there is no longer a detection for an override to agree with. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7f384eb9db
commit
41f0330e02
@@ -201,9 +201,10 @@ event's length. It adds **no schema field**, so the feed contract is untouched.
|
|||||||
claims. A source quietly moving a date must not erase a fortnight's streak that exists nowhere
|
claims. A source quietly moving a date must not erase a fortnight's streak that exists nowhere
|
||||||
else.
|
else.
|
||||||
- **Detection is a default, not a verdict.** The reader can mark any event as repeating, or unmark
|
- **Detection is a default, not a verdict.** The reader can mark any event as repeating, or unmark
|
||||||
one detection got wrong (`progress.daily`, resolved by `resolveDaily`). Store an override only
|
one detection got wrong (`progress.daily`, resolved by `resolveDaily`), and `prefs.detectDaily`
|
||||||
when it *disagrees* with detection — recording agreement would freeze today's guess and stop a
|
switches the guessing off entirely. Store an override only when it *disagrees* with detection —
|
||||||
better parser from ever reaching that event.
|
recording agreement would freeze today's guess and stop a better parser from ever reaching that
|
||||||
|
event. Neither control ever deletes a mark or a logged day, so both are reversible.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -161,7 +161,9 @@ whose end was never announced gets a day count rather than a checklist of invent
|
|||||||
|
|
||||||
That guess is only a starting point. Open any event and you can say **it repeats daily** — the
|
That guess is only a starting point. Open any event and you can say **it repeats daily** — the
|
||||||
grind whose page never prints the word still gets a checklist — or dismiss one the wording caught
|
grind whose page never prints the word still gets a checklist — or dismiss one the wording caught
|
||||||
by mistake. Anything you mark joins today's dailies at the top of the page, so ticking it off is one
|
by mistake. If you would rather it never guessed, **Spot daily events automatically** in the
|
||||||
|
settings turns detection off and leaves only what you marked yourself; it discards nothing, so your
|
||||||
|
ticks and streaks are still there if you turn it back on. Anything you mark joins today's dailies at the top of the page, so ticking it off is one
|
||||||
tap rather than a trip back into the event.
|
tap rather than a trip back into the event.
|
||||||
|
|
||||||
## Sorting
|
## Sorting
|
||||||
|
|||||||
+7
-4
@@ -199,8 +199,8 @@ Namespaced, versioned, and small. Nothing here ever goes to the server.
|
|||||||
"gacha-tracker:v1:progress" // { [eventId]: { status?, effort?, note?, at } }
|
"gacha-tracker:v1:progress" // { [eventId]: { status?, effort?, note?, at } }
|
||||||
"gacha-tracker:v1:daily" // { [id]: { days: ["2026-08-15", ...], at } }
|
"gacha-tracker:v1:daily" // { [id]: { days: ["2026-08-15", ...], at } }
|
||||||
"gacha-tracker:v1:ignored" // { [eventId]: { at } } — "stop showing me this"
|
"gacha-tracker:v1:ignored" // { [eventId]: { at } } — "stop showing me this"
|
||||||
"gacha-tracker:v1:prefs" // { region, hiddenGames[], sort, showCompleted, showIgnored,
|
"gacha-tracker:v1:prefs" // { region, hiddenGames[], sort, detectDaily, showCompleted,
|
||||||
// regionConfirmed, onboarded }
|
// showIgnored, regionConfirmed, onboarded }
|
||||||
"gacha-tracker:v1:completions" // SUPERSEDED — read once to migrate, never written
|
"gacha-tracker:v1:completions" // SUPERSEDED — read once to migrate, never written
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -257,8 +257,11 @@ Dailiness is derived from the published event — `type: "login"`, or wording li
|
|||||||
"check-in", "7-day" in the title or summary — and never from a game's habits or an event's length.
|
"check-in", "7-day" in the title or summary — and never from a game's habits or an event's length.
|
||||||
It adds no schema field, so nothing about the feed contract or the event ID changes.
|
It adds no schema field, so nothing about the feed contract or the event ID changes.
|
||||||
|
|
||||||
**The reader overrules detection.** `progress.daily` records their answer and wins outright
|
**The reader overrules detection**, per event and globally. `progress.daily` records their answer
|
||||||
(`resolveDaily`); absent means they have not said, so detection stands. An override that merely
|
for one event and wins outright (`resolveDaily`); absent means they have not said, so detection
|
||||||
|
stands. `prefs.detectDaily` switches the guessing off altogether, leaving only events they marked
|
||||||
|
themselves — it silences detection rather than deleting anything, so every mark and every logged day
|
||||||
|
survives and switching it back on restores exactly what was there. An override that merely
|
||||||
agrees with detection is **not stored** (`dailyOverride`) — freezing today's guess into their data
|
agrees with detection is **not stored** (`dailyOverride`) — freezing today's guess into their data
|
||||||
would stop a later parser improvement from ever reaching that event. This is the only field in
|
would stop a later parser improvement from ever reaching that event. This is the only field in
|
||||||
`progress` that changes what the app *shows* rather than recording what the reader did, which is
|
`progress` that changes what the app *shows* rather than recording what the reader did, which is
|
||||||
|
|||||||
+6
-2
@@ -90,7 +90,11 @@ export function App() {
|
|||||||
* the source's wording; they can overrule it either way.
|
* the source's wording; they can overrule it either way.
|
||||||
*/
|
*/
|
||||||
const repeatsDaily = (row: RowEvent): boolean =>
|
const repeatsDaily = (row: RowEvent): boolean =>
|
||||||
resolveDaily(row.event, prog.progress[row.event.id]?.daily);
|
resolveDaily(
|
||||||
|
row.event,
|
||||||
|
prog.progress[row.event.id]?.daily,
|
||||||
|
prefs.detectDaily,
|
||||||
|
);
|
||||||
|
|
||||||
/** Today's state for a repeating event, or undefined if it does not repeat. */
|
/** Today's state for a repeating event, or undefined if it does not repeat. */
|
||||||
const dailyBadge = (row: RowEvent): DailyBadge | undefined => {
|
const dailyBadge = (row: RowEvent): DailyBadge | undefined => {
|
||||||
@@ -395,7 +399,7 @@ export function App() {
|
|||||||
region={prefs.region}
|
region={prefs.region}
|
||||||
now={now}
|
now={now}
|
||||||
daily={repeatsDaily(openRow)}
|
daily={repeatsDaily(openRow)}
|
||||||
detectedDaily={isDaily(openRow.event)}
|
detectedDaily={prefs.detectDaily && isDaily(openRow.event)}
|
||||||
dailyDays={daily.daysFor(openRow.event.id)}
|
dailyDays={daily.daysFor(openRow.event.id)}
|
||||||
onDaily={prog.setDaily}
|
onDaily={prog.setDaily}
|
||||||
onToggleDay={daily.toggleDay}
|
onToggleDay={daily.toggleDay}
|
||||||
|
|||||||
@@ -86,6 +86,26 @@ export function Controls({
|
|||||||
Show events I've finished
|
Show events I've finished
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{/* Detection reads the source's wording and is wrong in both
|
||||||
|
directions. Off leaves only the events the reader marked, and
|
||||||
|
discards nothing — every mark and logged day survives, so it can
|
||||||
|
be switched back on. */}
|
||||||
|
<label className="flex cursor-pointer select-none items-start gap-2 text-xs text-muted">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={prefs.detectDaily}
|
||||||
|
onChange={(e) => onUpdate({ detectDaily: e.target.checked })}
|
||||||
|
className="mt-px size-4 accent-[var(--color-near)]"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Spot daily events automatically
|
||||||
|
<span className="mt-0.5 block max-w-xs leading-relaxed text-faint">
|
||||||
|
Off, only events you mark yourself get a checklist. Your ticks
|
||||||
|
and streaks are kept either way.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
{ignoredCount > 0 && (
|
{ignoredCount > 0 && (
|
||||||
<label className="flex cursor-pointer select-none items-center gap-2 text-xs text-muted">
|
<label className="flex cursor-pointer select-none items-center gap-2 text-xs text-muted">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ export interface Prefs {
|
|||||||
hiddenGames: GameId[];
|
hiddenGames: GameId[];
|
||||||
/** How the list is ordered. Deadline order is the default and the fallback. */
|
/** How the list is ordered. Deadline order is the default and the fallback. */
|
||||||
sort: SortMode;
|
sort: SortMode;
|
||||||
|
/**
|
||||||
|
* Whether to guess which events repeat daily from what the source printed.
|
||||||
|
* Off leaves only the ones the reader marked themselves; it never discards a
|
||||||
|
* mark or a logged day, so it is reversible.
|
||||||
|
*/
|
||||||
|
detectDaily: boolean;
|
||||||
showCompleted: boolean;
|
showCompleted: boolean;
|
||||||
/** Reveal events the reader has ignored, so they can be restored. */
|
/** Reveal events the reader has ignored, so they can be restored. */
|
||||||
showIgnored: boolean;
|
showIgnored: boolean;
|
||||||
@@ -24,6 +30,7 @@ function defaults(): Prefs {
|
|||||||
region: guessRegion(),
|
region: guessRegion(),
|
||||||
hiddenGames: [],
|
hiddenGames: [],
|
||||||
sort: "ending",
|
sort: "ending",
|
||||||
|
detectDaily: true,
|
||||||
showCompleted: true,
|
showCompleted: true,
|
||||||
showIgnored: false,
|
showIgnored: false,
|
||||||
regionConfirmed: false,
|
regionConfirmed: false,
|
||||||
|
|||||||
+8
-1
@@ -84,12 +84,19 @@ export function dailiesId(game: GameId): string {
|
|||||||
* "daily" still wants a checklist, and a banner whose blurb mentions "daily
|
* "daily" still wants a checklist, and a banner whose blurb mentions "daily
|
||||||
* login rewards" does not. The reader's own answer is the better evidence, so
|
* login rewards" does not. The reader's own answer is the better evidence, so
|
||||||
* it wins outright — `undefined` means they have not said, so detection stands.
|
* it wins outright — `undefined` means they have not said, so detection stands.
|
||||||
|
*
|
||||||
|
* `detect` is the reader's standing preference for guessing at all. Switching
|
||||||
|
* it off leaves only the events they marked themselves; it never touches a
|
||||||
|
* mark or a logged day, so switching it back on restores exactly what was
|
||||||
|
* there.
|
||||||
*/
|
*/
|
||||||
export function resolveDaily(
|
export function resolveDaily(
|
||||||
event: DailyCandidate,
|
event: DailyCandidate,
|
||||||
override: boolean | undefined,
|
override: boolean | undefined,
|
||||||
|
detect = true,
|
||||||
): boolean {
|
): boolean {
|
||||||
return override ?? isDaily(event);
|
if (override !== undefined) return override;
|
||||||
|
return detect && isDaily(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -73,6 +73,15 @@ describe("resolveDaily", () => {
|
|||||||
expect(resolveDaily(plain, true)).toBe(true);
|
expect(resolveDaily(plain, true)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("detection can be switched off entirely", () => {
|
||||||
|
// The preference only silences the guess. Nothing the reader marked is
|
||||||
|
// affected, and no logged day is touched, so it is reversible.
|
||||||
|
expect(resolveDaily(detected, undefined, false)).toBe(false);
|
||||||
|
expect(resolveDaily(detected, true, false)).toBe(true);
|
||||||
|
expect(resolveDaily(plain, true, false)).toBe(true);
|
||||||
|
expect(resolveDaily(detected, undefined, true)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("the reader can unmark a false positive", () => {
|
test("the reader can unmark a false positive", () => {
|
||||||
// A banner whose blurb happens to mention "daily login rewards" should not
|
// A banner whose blurb happens to mention "daily login rewards" should not
|
||||||
// be stuck with a twenty-box checklist the reader cannot dismiss.
|
// be stuck with a twenty-box checklist the reader cannot dismiss.
|
||||||
|
|||||||
Reference in New Issue
Block a user