Skip to content

Listening stats (Home "nice list" ledger), rebased onto main - #7

Merged
tsvb merged 8 commits into
mainfrom
listening-stats-rebased
Nov 7, 2026
Merged

tsvb merged 8 commits into
mainfrom
listening-stats-rebased

Conversation

@tsvb

@tsvb tsvb commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Rebases the Aug-2026 listening-stats branch onto main (e6ab835) so the Yeti redesign's Home ships with the real ledger. Lands by fast-forward, as with #5 and #6.

Commits

  • feat: keep all-time listening stats: Tim's original feature commit, replayed. The conflicts were all additive (project.yml test sources, IOSRootView menu/state, PlayerService). Main had replaced persistNow() with appDidEnterBackground(), so backgrounding now commits listening there.
  • fix: credit listening while the iOS app is backgrounded: main's 8c3c0d7 takes the 4 Hz tick off in the background, and credit was fed only from that tick, so locked-screen listening earned nothing. Adds the pure ListeningCredit.span rule: the playhead delta, capped at wall-clock time since the last sample. PlayerService settles at commit boundaries and on the existing 60 s background wake. No new wake and no background tick.
  • fix: settle listening only from a readable playhead; re-anchor on resume: findings from an independent review. A settle could fall back to the stale currentTime, which re-credited a closing track at a background queue end and could mint a play. Paused→playing now re-anchors the baseline, and a restored or adopted track anchors correctly.
  • fix: lock-screen skip seeks from the live playhead: an adjacent bug from the same tick removal. seek(by:) used the stale currentTime.
  • Docs: the four silent-failure rules for listening stats, plus what's still unverified on device.

Gate (local, Xcode 27.2, fresh DerivedData): both schemes BUILD SUCCEEDED warning-free under strict concurrency. AppleNugsTests 338 tests, 0 failures (307 on main + 25 from the feature + 6 new span tests).

Not verified on device: the populated ledger, locked-screen credit on the Duo, and the iOS reset dialog. CLAUDE.md lists these.

🤖 Generated with Claude Code

Tim and others added 8 commits November 7, 2026 15:33
…gn branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
Artists only, rolling aggregates, no event log. Every dimension left out
can only ever count forward from the day it's added — that trade was made
deliberately, along with all-time-only, Home-only, and audio-only.

ListeningCredit holds the per-track accumulator and the min(50%, 240s)
play threshold, measured against listened time rather than playhead
position so scrubbing to the end earns nothing. PlayerService feeds it
from the existing 4Hz tick but calls the store only at commit boundaries,
so ticking seconds never reach @observable state. LedgerSummary is the
pure totals-and-ranking policy; all three compile into AppleNugsTests.

The Home ledger is typographic — no cards, no tiles — and gates on
hasAnyPlay rather than on the table, so a commit doesn't invalidate the
greeting, hero, favorites strip and crate grid along with it.

The store refuses to write while a failed load is latched. These tallies
have nothing behind them to re-derive from, so a file that didn't decode
is salvaged per-element and left on disk rather than overwritten; reset
clears the latch, since a deliberate wipe is the intervention it was
waiting for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rebasing listening-stats onto main met 8c3c0d7, which takes the 4Hz tick
off in the background. Credit was fed only from that tick, so locked-screen
listening earned nothing, and the first tick back in front clamped the
whole stretch to 1s.

ListeningCredit gains span(delta:wallSeconds:duration:): playhead movement
since the last sample, capped at the real time that passed, so a scrub
forward inside a span still earns at most what elapsed. PlayerService keeps
one baseline (position + monotonic uptime) stamped by every tick and every
settle, and settles at each commit boundary, on the existing once-a-minute
background wake (no new wake, no background tick), before the tick returns
on foregrounding, at a track's end, before a gapless manual skip, and
before a seek, which then rebases the baseline. A rotation rebases to the
resume point rather than 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at's unverified

Carries the old branch's CLAUDE.md notes (the three silent-failure rules,
the deliberate scope cuts, the unverified list) into the current file
rather than replaying its conflicting Pick-up-here edits, and adds the
fourth rule this rebase introduced: backgrounded credit is settled, not
ticked. Suite count 307 -> 338.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the background-credit change found that settleListening trusted
livePosition, which falls back to the tick-fed currentTime whenever the
player's head isn't our item. Stale in the background, that pulled the
baseline down and let the next settle re-credit the gap. At a queue's end
this credited the closing track a second time (up to its full length) and
could mint a play; a wake landing inside a gapless hand-off lost a span.

Settles now read creditPosition, which is nil rather than stale when the
head doesn't match, and a nil sample leaves the baseline alone. Paused to
playing (our own resume/toggle and external flips seen by the KVO)
re-anchors the baseline without crediting, so a mid-seek settle can't
carry a skipped distance across a paused span; an external pause settles.
adoptPreload clears an unapplied resume offset before rotating, and a
restored queue anchors at its resume point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seek(by:) added the delta to the tick-fed currentTime, which has been stale
in the background since 8c3c0d7 took the tick off there. Ten minutes after
locking, the lock screen's +30s landed about 9.5 minutes back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tsvb
tsvb merged commit 11bb8d7 into main Nov 7, 2026
1 check passed
@tsvb
tsvb deleted the listening-stats-rebased branch November 7, 2026 21:25
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