Skip to content

Session slots, history runs, lifetime counter, and a Study hub that stops popping in - #30

Merged
glendonjklassen merged 5 commits into
mainfrom
slots-history-counter
Aug 14, 2026
Merged

Session slots, history runs, lifetime counter, and a Study hub that stops popping in#30
glendonjklassen merged 5 commits into
mainfrom
slots-history-counter

Conversation

@glendonjklassen

Copy link
Copy Markdown
Owner

Four asks from UAT, one commit each.

1. Where you were, per SEATING

A reader's last chapter is not one thing. Somebody who studies on weekday mornings, sits in a Sunday service and goes to a Wednesday meeting has three separate places they were — and one "last chapter" serves whichever they did most recently, so arriving at church reopened Saturday night's study.

Four slots, boundaries stated in the core rather than buried in a shell: sunday-morning (before noon) · sunday-evening (from noon) · wednesday-evening (from 5pm) · other. Wednesday morning is deliberately other — the slot is for the midweek meeting, and a Wednesday morning is a weekday morning like any other.

The rule is the core's (plumbline_session_slot, engine-independent like the theme palette) and the shells pass their own local date and hour — a slot computed in UTC would put a Sunday-evening service in Monday for half the world. Weekday comes from the existing civil arithmetic, with the pre-epoch negative-modulo case tested either side of 1970.

A seating never used falls through to the plain last position — what every reader has today — and that fall-through has its own test, because it is the half that must not regress for anyone not using this.

2. History reads as runs

"Genesis 1–3", not three lines. Adjacency is in the LIST, not merely similarity: [Gen 3, John 1, Gen 2] stays three lines, because merging those two would claim the reader went 2→3 without leaving, and the order they did things in is the entire content of a history. A tap opens the run's most recent chapter.

Written twice on purpose — the shells own this list (pushHistory prepends locally; the config only reaches the engine on a debounced save, so anything core-derived would be stale the moment the reader turned a page). Both implementations are held to the same written rules: HistorySpansTest (7 cases) and e2e/history.spec.ts.

3. Lifetime Bible-read counter

On the Study hub beside the coverage bar. Seeded once by hand via a numpad (inputmode="numeric" / KeyboardType.Number), because somebody arriving with thirty years behind them should not start at nought — and earned after that. Nothing edits it; a number you can retype is a number that means nothing.

-1 is "never said", deliberately not 0: a reader who answers "none" has told us something, and if unset were 0 they'd be asked forever. Credited exactly once per finished canon — the mutation that credits on every observation reaches 1,002 and fails its test.

Not built, and worth knowing: nothing resets the reading map, so a second full pass needs the map to drop below complete and return. A "start a new pass" action is the natural follow-up.

4. The hub stops popping in

q answers null while fetching, which renders identically to "nothing running" — so the band drew empty, then grew, shoving the card grid down a beat after it settled. It now draws a placeholder of its own shape (the row's box with no content), and the real content fades up in place.

Sized by measurement, not by eye. Two ghost rows made it worse in the other direction — the grid jumped 49px up when the real band turned out shorter. One row plus the coverage strip took the measured shift from 49px to 3px. A read that never answers can't strand it: after three seconds the band shows whatever it has. prefers-reduced-motion gets none of the animation.

Verification

13 cargo suites, clippy --workspace --all-targets clean, fmt clean, bindings regenerated (the drift guard caught the missing Kotlin declaration for the new export, which is what it's for), wasm rebuilt before the web suite ran — twice, since two commits touched the i18n catalogue. 73 Playwright specs green; Android assembleDebug + JVM unit tests green.

Each new behaviour is mutation-tested against the defect it describes: record-but-never-restore kills the slot test, merge-on-similarity kills the history rule, credit-on-every-visit inflates the counter to 1,002, and slicing the plan list to one kills the multi-plan row test.

Also caught by a guard I didn't know existed: const t = setTimeout(...) shadows the catalogue's t(), and the check named it.

The cards are static text and are on screen from the first frame; the BAND is
four queries, and `q` answers null while a fetch is in flight — which renders
identically to "nothing running". So the band drew empty, then grew, shoving
the whole card grid down the page a beat after it had settled.

A placeholder of the same shape rather than a spinner: the row's own box with
no content in it, so what the reader sees is a shape settling rather than a
thing arriving, and the real content fades up in place.

SIZED BY MEASUREMENT, not by eye. Two ghost rows made it WORSE in the other
direction — the grid jumped 49px UP when the real band turned out shorter. One
row plus the coverage strip is what the band resolves to in the common cases
(one plan running, or none and the invitation shows), and the measured shift
went from 49px to 3.

A read that never answers must not strand the skeleton, so after three seconds
the band shows whatever it has — which for a failed query is the honest empty
state.

`prefers-reduced-motion` gets none of it. The point was never the animation; it
was not moving the page.

Android takes the same treatment, keyed on its own fetch completing.

Caught by a guard I did not know existed: `const t = setTimeout(...)` shadows
the catalogue's `t()`, and the check named it.
The recents list is one entry per chapter opened, so an evening in Genesis 1, 2
and 3 filled the sheet with three lines saying almost the same thing and pushed
everything else off the bottom. A run of contiguous chapters in one book is one
thing the reader did.

THE RULE THAT MATTERS is that adjacency is in the LIST, not merely similarity:
[Gen 3, John 1, Gen 2] stays three lines. Merging those two Genesis entries
would claim the reader went 2→3 without leaving, when they went to John in
between — and the order they did things in is the entire content of a history.
The mutation that merges into any matching run instead of the current one fails
exactly that test in both shells.

A tap opens the run's MOST RECENT chapter — where the reader actually was — not
the lowest number in the span.

WRITTEN TWICE, deliberately. A view-model would normally live in the core, but
the shells own this list: `pushHistory` prepends to `config.history` locally on
every navigation and the config only reaches the engine on a debounced save, so
anything the core derived from it would be stale the moment the reader turned a
page. Since there are two implementations, the rules are written down where both
are checked against them — HistorySpansTest (7 cases) and e2e/history.spec.ts.
…y evening

A reader's last chapter is not one thing. Somebody who studies on weekday
mornings, sits in a Sunday service and goes to a Wednesday meeting has three
separate places they were, and one "last chapter" serves whichever they did
most recently — so arriving at church reopened Saturday night's study, and
Monday morning reopened the sermon passage.

Four seatings, and the boundaries are a JUDGEMENT, so they are stated in one
place (crates/core/src/session_slot.rs) rather than buried in a shell:

  sunday-morning     Sunday before noon
  sunday-evening     Sunday from noon
  wednesday-evening  Wednesday from 5pm
  other              everything else

Wednesday MORNING is deliberately `other`: the slot exists for the midweek
meeting, and a Wednesday morning is a weekday morning like any other.

THE RULE IS THE CORE'S, and the shells pass their own LOCAL date and hour. The
core has no clock and no timezone, and a slot computed in UTC would put a
Sunday-evening service in Monday for half the world. `plumbline_session_slot`
is engine-independent, like the theme palette — the drift guard caught the
missing Kotlin declaration, which is what it is for.

Weekday comes from the existing civil arithmetic. 1970-01-01 was a Thursday,
and `date_to_days` goes negative before the epoch, so the remainder is brought
back into range rather than left as C's negative modulo — tested against dates
either side.

A seating never used falls through to the plain last position, which is exactly
what every reader has today; that fall-through has its own test, because it is
the half that must not regress for anyone not using this.

The web's restore is fire-and-forget (the panes are built before the answer
lands) and is guarded by `#navigatedSinceBoot`, so a reader who taps something
in those few ms is never yanked away. Android resolves it synchronously — it is
a string comparison, not a file read.
How many times this reader has been through the whole Bible, on the Study hub
beside the coverage bar — one says how far through this pass you are, the other
how many passes there have been.

SEEDED ONCE by hand, because somebody arriving with thirty years behind them
should not start at nought: a numeric prompt (`inputmode="numeric"` on the web,
`KeyboardType.Number` on Android — both raise a phone's numpad). After that it
is EARNED. Nothing in the UI edits it; a number you can retype is a number that
means nothing.

-1 IS "NEVER SAID", deliberately not 0. A reader who answers "none" has told us
something, and if unset were 0 they would be asked again every time — which is
its own test.

CREDITED EXACTLY ONCE per finished canon. `bibleReadsCredited` marks the current
complete state as counted and is cleared if the map ever drops below full, so
the number moves on FINISHING rather than on every visit to the screen. The
mutation that credits on every observation reaches 1,002 and fails that test.

Seeding sets the flag true whatever the canon says, so a reader who is already
finished is not immediately credited with the read they have just declared; if
the canon is not in fact complete the hub reconciles the flag on sight.

NOT BUILT, and worth saying: nothing resets the reading map, so a second full
pass needs the map to drop below complete and return. A "start a new pass"
action is the follow-up that would make this count repeatedly.
A minor rather than a patch: session slots, history runs and the lifetime
counter are features, not fixes.

The release workflow gates the tag against Cargo.toml and apps/web/package.json
and fails the build if they disagree, so both move here rather than after the
tag is pushed. Cargo.lock carries the workspace version and CI builds --locked,
so it is refreshed in the same commit; package-lock.json's own version field
goes with it.
@glendonjklassen
glendonjklassen merged commit fff2239 into main Aug 14, 2026
8 checks passed
@glendonjklassen
glendonjklassen deleted the slots-history-counter branch August 14, 2026 17:16
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