The ask
Add an unbounded "All Time" window to the Nightwatch Dossier, unlock-only. Free stays
at the rolling 30 days, which is settled and not reopened here.
Jeff, 2026-08-24: going forever in the history is the only way to get a true year
look-back in early January. Late December would be fine.
Why the calendar date matters
Every long window today is anchored to now, not to a calendar boundary:
app/src/main/java/com/ninelivesaudio/app/ui/dossier/NightwatchDossierViewModel.kt:58
THIRTY_DAYS -> now - 30.days
THREE_MONTHS -> now - 90.days
SIX_MONTHS -> now - 180.days
ONE_YEAR -> now - 365.days
So on Jan 3, "1 Year" covers Jan 3 of last year through today. The first days of the
year a person actually wants to review are already outside the window, and three days
of the new year are inside it. On Dec 28 the same rolling year covers essentially all
of the current calendar year, which is why late December reads fine and early January
does not. A year-in-review card that quietly drops the first week of the year is the
kind of thing one user notices and everybody else silently distrusts.
Check this before building it
DossierPeriod already has six entries, not four. Past 1 Year (which is where the
chip row runs off the right edge of the screen) there are two calendar-anchored ones:
THIS_YEAR -> Jan 1 of this year -> now
LAST_YEAR -> Jan 1 of last year -> Jan 1 of this year
LAST_YEAR is an exact prior-calendar-year look-back and it works correctly on Jan 3.
The chip row is a horizontalScroll (NightwatchDossierScreen.kt:197), so both are
reachable, just never on screen at rest. Which means the January problem may be a
discoverability bug rather than a missing window, and the fix might be a scroll hint,
a reorder, or a "Year in Review" affordance that surfaces itself in January.
Open question for Jeff: is the ask still All Time once you know Last Year exists, or
does Last Year cover the January case and All Time becomes a separate lifetime-totals
feature? Both are defensible. They are not the same work.
What All Time actually costs
Local mode: free, the data is already there. Nothing prunes LocalListeningSessions
by age. There is no retention job, no deleteOlderThan, no WorkManager cleanup. The
Dossier calls getAllSessions() and filters by window in memory
(NightwatchDossierViewModel.kt:301). An ALL_TIME entry with a cutoff of 0 is purely
additive.
Server mode: NOT forever, and this is the real work. In SERVER mode
getAllSessions() goes to Audiobookshelf instead
(ListeningSessionRepository.kt:30), and that path is hard-capped:
app/src/main/java/com/ninelivesaudio/app/data/remote/ApiService.kt:838
itemsPerPage: Int = 50
val maxPages = 20
1000 sessions, ceiling, silently. A daily listener hits that in under three years and
an "All Time" card would just stop being true with no indication. Whatever ships has to
either raise that bound, page until the server says it is done, or be honest in the UI
about how far back it actually reached. Shipping a forever window on top of a 1000-row
cap is worse than not shipping it.
Implementation notes
daysInPeriod (NightwatchDossierViewModel.kt:98) is the daily-average divisor and
every other period has a fixed length. ALL_TIME does not, so the divisor has to come
from the real span (earliest session to now) or the daily average is nonsense.
periodPhrase (:116) needs a non-"in the last X" wording, same special case
THIS_YEAR and LAST_YEAR already take.
- Gating is one line: keep it out of
FreeTier.DOSSIER_PERIODS
(FreeTier.kt:136) and effectiveDossierPeriod clamps it for free installs
automatically. The clamp is applied at query time, not just at the chip, so there is
no separate enforcement to add.
- A seventh chip pushes the row further past the right edge. Worth checking the row does
not become a place where two whole periods live invisibly.
Store listing consequence
The 2.1.0 listing copy and the slot 07 screenshot caption both describe this gate. The
current wording says "longer" spans, which stays true. Do not upgrade it to "your whole
record" or any superlative until the server-mode cap above is actually fixed, because in
server mode it would be false.
Post-2.1.0. Nothing here blocks the release or the price flip.
The ask
Add an unbounded "All Time" window to the Nightwatch Dossier, unlock-only. Free stays
at the rolling 30 days, which is settled and not reopened here.
Jeff, 2026-08-24: going forever in the history is the only way to get a true year
look-back in early January. Late December would be fine.
Why the calendar date matters
Every long window today is anchored to now, not to a calendar boundary:
app/src/main/java/com/ninelivesaudio/app/ui/dossier/NightwatchDossierViewModel.kt:58So on Jan 3, "1 Year" covers Jan 3 of last year through today. The first days of the
year a person actually wants to review are already outside the window, and three days
of the new year are inside it. On Dec 28 the same rolling year covers essentially all
of the current calendar year, which is why late December reads fine and early January
does not. A year-in-review card that quietly drops the first week of the year is the
kind of thing one user notices and everybody else silently distrusts.
Check this before building it
DossierPeriodalready has six entries, not four. Past1 Year(which is where thechip row runs off the right edge of the screen) there are two calendar-anchored ones:
LAST_YEARis an exact prior-calendar-year look-back and it works correctly on Jan 3.The chip row is a
horizontalScroll(NightwatchDossierScreen.kt:197), so both arereachable, just never on screen at rest. Which means the January problem may be a
discoverability bug rather than a missing window, and the fix might be a scroll hint,
a reorder, or a "Year in Review" affordance that surfaces itself in January.
Open question for Jeff: is the ask still All Time once you know
Last Yearexists, ordoes Last Year cover the January case and All Time becomes a separate lifetime-totals
feature? Both are defensible. They are not the same work.
What All Time actually costs
Local mode: free, the data is already there. Nothing prunes
LocalListeningSessionsby age. There is no retention job, no
deleteOlderThan, no WorkManager cleanup. TheDossier calls
getAllSessions()and filters by window in memory(
NightwatchDossierViewModel.kt:301). AnALL_TIMEentry with a cutoff of 0 is purelyadditive.
Server mode: NOT forever, and this is the real work. In
SERVERmodegetAllSessions()goes to Audiobookshelf instead(
ListeningSessionRepository.kt:30), and that path is hard-capped:app/src/main/java/com/ninelivesaudio/app/data/remote/ApiService.kt:8381000 sessions, ceiling, silently. A daily listener hits that in under three years and
an "All Time" card would just stop being true with no indication. Whatever ships has to
either raise that bound, page until the server says it is done, or be honest in the UI
about how far back it actually reached. Shipping a forever window on top of a 1000-row
cap is worse than not shipping it.
Implementation notes
daysInPeriod(NightwatchDossierViewModel.kt:98) is the daily-average divisor andevery other period has a fixed length.
ALL_TIMEdoes not, so the divisor has to comefrom the real span (earliest session to now) or the daily average is nonsense.
periodPhrase(:116) needs a non-"in the last X" wording, same special caseTHIS_YEARandLAST_YEARalready take.FreeTier.DOSSIER_PERIODS(
FreeTier.kt:136) andeffectiveDossierPeriodclamps it for free installsautomatically. The clamp is applied at query time, not just at the chip, so there is
no separate enforcement to add.
not become a place where two whole periods live invisibly.
Store listing consequence
The 2.1.0 listing copy and the slot 07 screenshot caption both describe this gate. The
current wording says "longer" spans, which stays true. Do not upgrade it to "your whole
record" or any superlative until the server-mode cap above is actually fixed, because in
server mode it would be false.
Post-2.1.0. Nothing here blocks the release or the price flip.