Skip to content

Connect in the app: Scan a QR code first and say where the QR is; Find on LibriVox follows a fetch past the page - #297

Merged
KodeStar merged 17 commits into
mainfrom
feat/new-user-b
Nov 14, 2026
Merged

KodeStar merged 17 commits into
mainfrom
feat/new-user-b

Conversation

@KodeStar

Copy link
Copy Markdown
Owner

Closes #264, #293 (except its "not verified live" items, covered below).

Connect (#264)

A new user's app asked for a username and password, and nothing said the QR was the easy way or where to find it.

  • In the app, Scan a QR code is the one primary, full-width button at the top, with "Or enter the server address" under it.
  • The lead and the scanner hint say where the QR is: "If you run the server, it's in the admin console under People > your name > Pair a device. If you were invited, it's on your invite page."
  • The sign-in step has "No password? Scan a QR code instead", and the invite-code hint says no password is needed. All six locales; STYLEGUIDE.md has the pattern.
  • A scan from a reconnect's sign-in step keeps the reconnect, so a reset server re-pairs the same person with no duplicate. The reconnect target is handed to the scanner in memory, never through a route param, and is re-checked against the QR's address. The web connect layout is unchanged.

Find on LibriVox (#293)

  • A root-mounted watcher follows a fetch from the moment Add answers, so leaving the page (or closing the work) no longer stops the Library refreshing when it ends.
  • A lapsed Bridge heartbeat keeps the page with a notice and holds Add and Cancel back. The page re-reads /server every 60 s while away.
  • A failed /server read shows Retry instead of a skeleton.
  • A link without ?connection= uses the default connection (Find shows too). A link naming a library that server doesn't have shows "This link doesn't point to anything here." once the library list has settled, instead of adding to the first books library.
  • Cancel sits under the status (shared JobStatusLayout with Generate). Unused params are gone, and directory search keeps its results when a re-read fails.

Review and verification

  • Review: /simplify, then two /code-review high --fix passes on the whole branch, then one fix round. Gate green after npm ci: tsc, lint, prettier, 7785 tests.
  • Verified live in the web player against a scratch server and the real Bridge (KodeStar/audiosilo-bridge fix/librivox-fetch-followups):
    • the Library refreshed when a fetch ended after leaving Find;
    • the bad-link state for a missing library;
    • Cancel's placement;
    • the web connect and sign-in steps pixel-identical to main at 1440 and 390 px, apart from the new code hint.
  • Native: a Pixel 6a run is in progress; results will be added here.

Pairs with KodeStar/audiosilo-server (feat/new-user-b) and KodeStar/audiosilo-docs (docs/new-user-b, to follow).

A new user moving from Audiobookshelf found the app asking for a
username and password: the address field and Continue were the main
action, Scan a QR code sat below as an outline button, and nothing said
where the QR is. The scanner's hint pointed at the server's connect
page, which asks for an invite code first.

In the app, Scan a QR code is now the screen's one primary (full width,
lg), above the address field under "Or enter the server address" (its
Continue secondary). The lead and the scanner's hint say where the QR
is: the admin console under People > your name > Pair a device, or the
invite page. The sign-in step offers "No password? Scan a QR code
instead" in the app, and the invite code's hint says no password is
needed. The web keeps its layout. All six locales use the console's own
words for People and Pair a device; STYLEGUIDE.md records the pattern.

Part of #264.
…e Bridge lapses

Leaving Find on LibriVox (or closing the work) before a fetch ended
stopped following it, so the Library showed the new book only after a
resume or its stale time. A root listener, LibrivoxFetchWatcher, now
holds every fetch a row follows (in memory) and is its one poller; when
one ends it asks the works again and, for a done one, reads the
library's lists again and the works once more after 2 s on a module
timer, so the recheck survives the row unmounting too. Rows read the
job without polling. A watch stops on a 404, a refusal or a sign-out.

The rest of the L6 review's follow-ups:
- a /server read again while the Bridge's heartbeat has lapsed keeps
  the open page, its search and followed fetches, says the Bridge is
  away and holds Add back (notAdmin and metadataOff still close it);
- a failed /server read with nothing kept says so with Retry instead
  of leaving the skeleton up;
- a link without ?connection= is the default connection's (useCid),
  here and in Find shows, instead of "Only an admin can add";
- Cancel sits under the fetch's status, where Add was, instead of
  floating 560 px into a full-width row; the status shares
  JobStatusLayout with Generate's (same look there);
- librivoxSearch drops its unused lang/limit, parseFindLibrivoxParams
  its unused connectionId;
- the podcast directory search keeps its results when reading the
  same search again fails, like Find on LibriVox.

Part of #293.
…ult connection

The watcher's own suite and the search's each mock the other half, so
nothing failed if RootListeners stopped rendering it: the root services
test now asserts it under RootListeners and the web's RootServices.
Find shows gets the link-without-?connection= case Find on LibriVox
has. CONVENTIONS drops the search's removed lang/limit and says what a
lapsed heartbeat does to the watcher: /server's bridge flag is true only
while the Bridge is online, so its reads pause and the watch is kept.

Part of #293.
JobStatusLayout had an actionBelow mode with one caller, which rendered the
action exactly where children already go. The fetch status now passes its
Cancel as children, so action only ever means the title's end. The tone
union is exported once (JobStatusTone) instead of being spelled in the
layout and in the fetch's looks, and lineLines and bridgeAway are required:
every caller passes them, and an optional bridgeAway could be forgotten.

Part of #293.
The watcher dropped a watch on its own list of statuses (400, 401, 403)
while the poll it mirrors stops on isSignedOutError and isNotInLibraryError.
A change to one would have left a watch mounted with a poll that never
asks again. The watcher now uses the same two predicates, with a 404 kept
as an end (the works are asked again) rather than a refusal.

Also drops the test-only reset helper (the tests set the store, as they do
the session), and corrects the poll interval's comment: the watcher asks it
active now, not the focused page.

Part of #293.
A queued fetch reached the watcher only through the row's state and an
effect. Add asks the metadata service before it queues, so a page left in
that window got its answer with the row gone: the job ran unwatched and its
end never read the Library again, which is the case the watcher is for. The
row now hands the job over as the answer arrives.

A work opened again shows its last answer first, which can still name a
fetch that has ended. The row handed it back and the watcher ran its end a
second time (the works and up to six lists read again). A job the row
already reads as ended is no longer handed over.

Part of #293.
Find on LibriVox now stays open through a lapsed heartbeat, but only Add
was held back. Cancel is gated on the bridge capability, which the server
drops with the heartbeat, so it was an enabled button that sent nothing and
said nothing (the row swallows a never-sent cancel, which was right when the
page closed with the flag). It is disabled with Add until the Bridge is back.

Part of #293.
The sign-in step's Scan a QR code instead opened the scanner with nothing
of the reconnect it was part of, so the pairing that followed ran as a new
sign-in. For a server that was reset that matters twice: reconnecting
another person's connection (Household's Sign in again) left their dead
identity flagged and put the new connection in the profile of whoever was
listening, and a code naming another of the connection's addresses stored
that address, so the dead identity was never matched and stayed beside a
duplicate. A code or a password typed on the same step did neither.

The step now tells the scanner which connection it re-pairs. The scanner
keeps that only for a code of one of the connection's own addresses (any
other server is a new connection, as before): an invite code's sign-in step
gets it as its reconnect, and a pairing code's connect screen takes it from
memory by the code's token, never from a route param a link could set. The
pairing then uses the connection's own proxy headers, as a typed reconnect
does.

Part of #264.
An invite link's code signs in as the step opens. The scan link stayed
live meanwhile: the camera opened, the first sign-in finished under it, and
a code scanned then started a second sign-in for another session.

Part of #264.
The page now stays open when the Bridge's heartbeat lapses, but nothing
read /server again on an open page (its answer is fresh for five minutes
and has no interval). A Bridge that came back seconds later left Add and
Cancel disabled and the followed fetches' reads skipped until the app was
left and opened again. While away, the page asks every minute with the app
in the foreground.

Part of #293.
The watcher drops a watch whose read is refused, and the row no longer
polls. A token revoked mid-download (a 401) dropped the watch; once signed
in again the job was read once, still running, and the row showed that
progress for ever, with nothing to notice the end. The row now hands the
fetch over with each answer that reads it open, which changes nothing while
it is watched.

The row and the watcher also share one reading of a job's answer (gone, the
job, ended) instead of a copy each: the row decides what to hand over by
the rule the watcher ends on.

Part of #293.
parseFindShowsParams still returned a connectionId nothing read, while
the page takes its connection from its scope and falls back to the default
one. The twin LibriVox parser had already lost the field. The two pages'
comments now name the pages that share the fallback (book and series), not
every content page: the browse and account pages do not fall back.

Part of #293.
The sign-in step told the scanner which connection it re-pairs through a
route param, and a link opens that route too (audiosilo://connect/scan), so
a link could name a connection to re-pair: on a server that was reset, the
connection retired and the profile the new sign-in lands in. The connect
screen already refused to take it from its own params for that reason.

Both scan buttons now open the scanner through openScanner, which holds the
reconnect in memory for the scanner to take once; opening it without one
clears what an earlier opener left. The connect screen checks the held
reconnect against the code's address again as it pairs (the token alone
matched before, so the same token at another address kept it), and the
flow's layout drops whatever nothing took when the flow is left.

Part of #264.
A scanned code kept its reconnect for any of the connection's addresses,
the home address among them. Another server can answer at the same private
address on another network (a friend's house on the same subnet). Its code
scanned from the reconnect's sign-in step read as this server reset: the
connection was retired and the other server's session stored at its public
address. A sign-in typed on that step never goes to a home address that has
not answered as this server.

The reconnect is now kept for the address the connection was paired at or
its away address only. A code naming a home address it was not paired at is
a new connection, as one scanned from the first step is.

Part of #264.
The mock's selector built a new object at every call for a job not asked
or not read yet, so React logged that getSnapshot should be cached and a
watch on an id with no seeded read ran to the update depth limit.

Part of #293.
A Find on LibriVox link without ?connection= reads its library id on the
default server, and when that server had no such books library the page
silently put Add on the server's first books library. It now shows the
broken-link sentence instead, so a fetch only ever goes to a library the
link or the picker named. Find shows never picked another library, but it
gets the same refusal rather than a page whose every follow fails. While
the libraries load nothing is refused. The "add a books library first"
line is gone: a server with none now refuses the link.

Part of #293.
The libraries are cached at startup, so a library added since was refused
with the broken-link sentence until the background read landed. Both Find
pages now refuse only when the list is not being read again; meanwhile
LibriVox holds Add back. The LibriVox refusal also returns before the form
and the results are built.

Part of #293.
@KodeStar

Copy link
Copy Markdown
Owner Author

Device run (release APK of this branch, installed as app.audiosilo.dev), all five checks PASS:

  1. First run (Pixel 6a): Scan a QR code is the full-width primary button above the fold, with "Or enter the server address" under it.
  2. Scanner hint (Pixel 6a and emulator): the text fits inside its box.
  3. Sign-in step:
    • On the Pixel, "No password? Scan a QR code instead" is shown.
    • On the emulator, scanning the console's Pair a device QR paired ("Your library is ready").
    • Back from there lands on the used sign-in step. That is a known polish item, tracked in Connect and Find on LibriVox polish #299.
  4. Reconnect (emulator): revoke the token, then from the reconnect's sign-in step use Scan instead. It signs back in as the same admin, with one new token and one profile.
  5. Reset server at the same address (emulator, new server_id): it re-pairs as admin with one token, one profile and no duplicate.

The Pixel's camera showed only black in this setup, so the actual scans in checks 3 to 5 ran on a Pixel 9 Pro emulator, with the real QR shown on the emulator's virtual scene. That emulator ran the same APK. A real scan through a phone camera still needs someone to point a phone at a code.

No errors in logcat or the console.

@KodeStar
KodeStar merged commit 8a8987c into main Nov 14, 2026
1 check passed
@KodeStar
KodeStar deleted the feat/new-user-b branch November 14, 2026 10:21
KodeStar added a commit to KodeStar/audiosilo-docs that referenced this pull request Nov 14, 2026
…vites on a phone, and LibriVox fetch changes (#306)

Closes #278, #304. Docs for KodeStar/audiosilo-server#384,
KodeStar/audiosilo-frontend#297 and KodeStar/audiosilo-bridge#70.

## User Guide
- **New page "Move over from Audiobookshelf"**
(`getting-started/coming-from-audiobookshelf.md`), linked from first-run
step 5, the sidebar and Features. It covers:
- Audiobookshelf's `metadata.json`, which is off by default ("Store
metadata with item"; Audiobookshelf writes a book's file only when it
saves a change to it);
  - the import, Match automatically, and what unmatched means;
- adding a missing book, layout differences, and running both side by
side.
- **Pairing:**
- `connecting.md` leads with: install the app, tap Scan a QR code, and
where the QR is for an admin and for someone invited.
- The camera claim is fixed in `first-run.md`, `connecting.md` and
`account.md`: the phone's camera opens the web player, or for an invite
the invite page.
  - New troubleshooting entry: "Scanning the QR code opened a web page".
- **Invites** (`admin/users-and-invites.md`): the 7-day default, Share,
the optional password, the three notices, the phone invite page and the
store links.
- **Matching** (`health.md`, `books.md`): the community database has
gaps; link to "add a missing book".
- **LibriVox:**
  - a fetch is followed after you leave the page;
  - the Bridge-away notice and the bad-link page;
  - the matched, kept and unmatched notifications;
- sections are kept through an archive.org outage, and abandoned
downloads are cleared after 7 days.
- **Proxy headers** (`remote-access.md`): a set goes to the one https
address it was entered for. A plain-HTTP home address never gets them.

## Developer Docs
- `bridge/librivox.md`: `adopted` (false only when this run placed the
folder), the outage rule, meta refusals failing fast, a parked fetch
freeing the lane slot, the sweep, the server id in the state, and the
fold by `adopted`.
- `api/reference.md`: the redeem payload's `admin`; `links`
`ios`/`android`/`apk`; librivox.fetch `title` and `adopted`;
`librivox_match`.
- Also updated: the contract mirror and the data model, auth, web UI and
frontend pages.

## Screenshots
- Regenerated against the PR branches:
  - `admin/invite.png`
  - `setup/abs.png`
  - `setup/phone.png`
  - `server/connect-page.png`
- the new `server/phone-connect-invite.png`, taken in an iPhone context
  - `web-player/connect*.png`
  - `web-player/find-librivox.png`, now taken in capture-web (#304)
- The capture-web path ran for real through the shared helpers.
- The native app's connect screen is not in the pipeline (web only).

## Review
/simplify plus one `/code-review high --fix`; the console strings match
server commit 0f7420f. Gate: `npm run build` green (links and anchors
checked); old anchors kept.
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.

Find on LibriVox: follow fetches past the page, and polish Connect screen: make Scan a QR code the primary action and say where the QR is

1 participant