Skip to content

feat(report): the weekly report, plus four fixes found by using it - #9

Merged
dspv merged 2 commits into
masterfrom
feat/weekly-report
Sep 1, 2026
Merged

feat(report): the weekly report, plus four fixes found by using it#9
dspv merged 2 commits into
masterfrom
feat/weekly-report

Conversation

@dspv

@dspv dspv commented Sep 1, 2026

Copy link
Copy Markdown
Owner

The third paid feature, and four defects found by looking at the screen rather than at the tests.

The weekly report

What moved this week, sent to a Telegram bot the user owns. Three decisions, and two of them are about restraint.

"Usual" is a baseline, not last week. The premium page promises "the repository that cost 3× its usual week", and comparing two weeks gives a ratio rather than a finding — $2 to $6 is three times and is nothing. A repository is named only when it clears both an absolute floor in dollars and twice the median of the four preceding weeks. The median rather than the mean, because one runaway week must not become the baseline that hides the next one.

The floor started at $3 and the tests caught it immediately: $2 → $6 clears $3, which is the exact finding the floor exists to suppress. It is $10.

Most weeks the answer is that nothing moved, and the message says so — a report that lists nothing reads as broken, and one that cries wolf is one people stop opening. A week the machine was off is reported as such rather than as a collapse in spending.

The schedule is a comparison, not a countdown. A ticker set for Monday 09:00 fires for nobody whose laptop is shut at the weekend; macOS does not replay missed ticks. The daemon asks hourly whether the ISO week of the last report is behind the current one, so a machine opened on Wednesday gets Monday's report on Wednesday, labelled with the week it covers. The marker lives in meta rather than memory, or a restart would send a second copy of a message somebody already read.

The bot token is stored, and the Gemini key still is not. ADR-024 argues the difference: a key attached to a billing account can spend real money, while a bot token drives a bot made for this one purpose. It is the API's first write-only field — accepted by PUT, never returned by GET, which reports report_bot_set instead. A test fails if the token appears in a settings response.

Four fixes

Saving one setting overwrote the others. "Set plan keeps resetting" was not a save that failed: the hook PUT the whole Settings object from a module-level cache, so any control could write back a field it had read minutes earlier. Two tabs were enough. usePlan now takes a Partial and sends only what changed — which is what the server always assumed.

The activity feed showed a fortnight of history as live activity. Two causes at once: it seeded with events(id, 0, 60), which pages forward from a session's first event, and LastEvents ordered by rowid rather than by time. Those agree for a session captured live and diverge for one whose transcript was re-read, because a backfill inserts old events with new rowids. On a session with fifteen thousand events the "newest sixty" were two weeks old — every row equally stale, which is why it also read as "I cannot even scroll".

The spawn dialog cut off the permission label. "Accept edits · asks before com…" truncated the consequence, which is the one thing that label exists to state. 620px wide, shorter labels.

The premium chip was two controls disguised as one. The label went straight to Stripe's checkout while a chevron opened the dialog — one border, one colour, no seam — so clicking the word "premium" to find out what premium is took you to a card form. It now reads "Get premium $30/yr", opens the dialog, and once bought names the plan with the expiry on hover.

Verification

make check green. 478 UI tests, 57 files. Every fix has a test that fails against the old code — checked by reverting each one. The report's judgement is tested as a pure function: a real mover is found, a $2→$6 week is not, steady spend is not, one wild week does not become the baseline, an empty week is not a collapse.

Checked against a copy of the real database: before the ordering fix the fourth session in the feed returned events 14 days old; after it, all four return events from the last 24 hours.

https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF

dspv added 2 commits September 1, 2026 21:34
The third paid feature: what moved this week, to a Telegram bot the user
owns. Four decisions shape it, and three of them are about restraint.

**"Usual" is a baseline, not last week.** The premium page promises "the
repository that cost 3× its usual week", and comparing two weeks gives a
ratio rather than a finding — $2 to $6 is three times and is nothing. So a
repository is named only when it clears both an absolute floor in dollars
and twice the median of the four preceding weeks. The median rather than
the mean, because one runaway week must not become the baseline that hides
the next one. The floor started at $3 and the tests caught it immediately:
$2 to $6 clears $3, which is the exact finding the floor exists to
suppress. It is $10.

Most weeks the answer is that nothing moved, and the message says so. A
report that lists nothing reads as broken; a report that cries wolf is one
people stop opening. A week the machine was off is reported as such rather
than as a collapse in spending — the reader cannot click into a message to
check it, which is why this is stricter than the dashboard.

**The schedule is a comparison, not a countdown.** A ticker anchored to
Monday 09:00 fires for nobody whose laptop is shut at the weekend: macOS
does not replay missed ticks. The daemon asks hourly whether the ISO week
of the last report is behind the current one, so a machine opened on
Wednesday gets Monday's report on Wednesday, labelled with the week it
covers. The marker lives in `meta` rather than memory, or a restart would
send a second copy of a message somebody already read — the bug
cap.Guard.firedOn has, tolerable for a cap and not for a phone.

**The bot token is stored, and the Gemini key still is not.** ADR-024
argues the difference: a key attached to a billing account can spend real
money, while a bot token drives a bot made for this one purpose. The
deciding argument is what the alternative costs — editing a launchd plist
to turn on a feature sold as two minutes of setup is a feature nobody
finishes. It is the API's first write-only field: accepted by PUT, never
returned by GET, which instead reports that one is set. A test fails if
the token appears in a settings response.

**A send that fails is on the screen.** A weekly message that stops
arriving is invisible — an absence looks exactly like a quiet week — so
the last outcome is on the panel, in Telegram's own words, because "chat
not found" and "bot was blocked by the user" are both things only the user
can fix.

Claude-Session: https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF
"Set plan keeps resetting" was not a save that failed. The settings hook
PUT the whole Settings object, assembled from a module-level cache, so any
control could write back a field it knew nothing about with a value it had
read minutes earlier. Two tabs were enough: change the plan in one, click
"check for updates" in the other, and the second restated the plan from its
own stale copy. The server has always treated PUT as a patch — absent
fields are left alone — so the fix is to stop pretending otherwise on the
client. usePlan now takes a Partial and sends only what changed.

Also fixes the activity feed, which was showing a fortnight of history as
what just happened. Two causes, one on each side. The feed seeded with
`events(id, 0, 60)`, which pages FORWARD from a session's first event —
the same defect the session timeline had. And LastEvents ordered by rowid
rather than by time: those agree for a session captured live and diverge
for one whose transcript was re-read, because a backfill inserts old
events with new rowids. On a session with fifteen thousand events the
"newest sixty" were two weeks old, every row equally stale, which is why
it also read as "I cannot even scroll". EventsBefore's cursor moves to the
timestamp for the same reason: an id cursor under a time ordering skips
rows.

The spawn dialog goes from 520 to 620 wide and the permission labels get
shorter. "Accept edits · asks before com…" cut off the consequence, which
is the one thing that label exists to state.

And the premium chip is one control instead of two disguised as one. The
label went straight to Stripe's checkout while a chevron beside it opened
the dialog, in one border, one colour, with no seam — so clicking the word
"premium" to find out what premium IS took you to a card form. It now
reads "Get premium $30/yr", opens the dialog, and once bought it names the
plan (yearly or lifetime) with the expiry on hover, plus a renew flag
inside the grace period.

Claude-Session: https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF
@dspv
dspv merged commit 89f0e7e into master Sep 1, 2026
9 checks passed
@dspv
dspv deleted the feat/weekly-report branch September 1, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant