Skip to content

Matching: Match automatically where people look, Add it for books not found, ASINs the database doesn't know; invite and Bridge job polish - #390

Merged
KodeStar merged 24 commits into
mainfrom
feat/matching-surfaces
Nov 14, 2026
Merged

KodeStar merged 24 commits into
mainfrom
feat/matching-surfaces

Conversation

@KodeStar

@KodeStar KodeStar commented Nov 14, 2026 •

Copy link
Copy Markdown
Owner

A new user moving from Audiobookshelf could not find bulk matching, hit a dead end when a book was not in the community database, and had no way to see books whose own ASIN the database does not know. This puts Match automatically where people look, gives "not found" a next step, and folds in the invite and Bridge-job polish left from batch B.

Closes #370. Part of #386 (everything except the player's has_cover, which needs a player change and stays open there).

server#370: matching surfaces

  • Overview and Library > Books show "N books not matched" with Match automatically, opening the bulk match card.
  • A Not found row in bulk match and the match dialog's empty state say the book is not in the community database yet and still plays as normal, with Add it, a link to https://meta.audiosilo.app/add?q=... prefilled with, in order, an ASIN the admin typed, the book's ASIN, the search words, or title and author. (meta's add page is /add; /contribute takes no prefill.)
  • Health gets its own state, ASIN not in the community database, from the existing unknown_asin_reports table, listed while the unknown-ASIN report (Help complete the community database) is on. Such books still count as matched: making them unmatched would let Match automatically replace an ASIN the admin set (follow-up). The table's userstate rule now moves the content generation on a new or flipped row so the Health cache stays exact (measured: one move per report commit).
  • Findings that were wrong: tag ASINs have not counted as matched since 0067; Overview already linked to Not matched.

server#386: polish

  • Invite dialog: the certificate warning shows for https when the server runs self-signed (any name) or the link is at an IP address, with a sentence for each reason.
  • Invite page: wording for someone who typed a code rather than following a link.
  • A second Share press while the share sheet is open is ignored.
  • Settings > Bridge > Jobs: failed and cancelled jobs keep their last step.
  • LibriVox fetch ids are checked with model.ValidSlug; the Adopted tri-state has named states; one console failureCode; useTrackedJob moved into the bridge feature.

Wire

Admin API only, additive: issue kind unknown_asin in /admin/issues, ?issue=, and the ignore endpoint. No migration, nothing the player reads.

Tests and review

  • Go: the issue kind (listing, gating on the report, ignore allowed for an admin, 403 member, 401 anonymous), the userstate writers against the content generation on both engines. Console: the new surfaces, Add it URL building (encoding of &, #, +, %, =, non-ASCII, lone surrogates), certificate reasons per case, share in flight, jobs' last step.
  • Gate green: scripts/build-admin.sh, go build/vet/test -race, golangci-lint, native-engine vet/test/lint.
  • Review: /simplify + /code-review at the full level (high, Fable 5.2); copy fixes after live verification (gate only).

Verified live (Playwright, real builds)

  • Overview + Books notice opens bulk match; Not found rows and the empty match dialog show the wording and Add it with the expected q; meta's /add page takes the prefill.
  • A real unknown-ASIN report run (through a local proxy so the made-up ASIN never reached meta) shows the Health state; ignoring it holds.
  • Certificate warning: self-signed at a name, TLS off at an https IP, none on plain http.
  • Typed-code invite wording on an iPhone viewport; failed and cancelled jobs' last step (seeded jobs, no Bridge).
  • Second Share press: with a stubbed share only; a real phone run is still owed.

Pairs with #389 (independent) and KodeStar/audiosilo-docs#307.

A match run's Not found rows offered only Match by hand and Open book,
and the match dialog's empty state only "try other words": nothing said
the book still plays, or how to get it into the community database.

Both now say "This book is not in the community database yet. It still
plays as normal.", with an Add it link to meta.audiosilo.app's Add a
book page and a pointer to the Help complete the community database
setting. The page takes one query parameter, q (audiosilo-meta
site/src/lib/search-cta.ts): an Audible ASIN opens that record, other
text is searched. The link prefills an ASIN typed into the dialog, else
the book's own, else the words searched, else title and author.

Part of #370.
A book with an ASIN of its own (a metadata.json, an admin's edit, the
Bridge's backfill) counts as matched, so Match automatically skips it,
yet /meta has nothing for it when the community database lacks that
ASIN. The only trace was the opt-in unknown-ASIN report.

Health gains an "ASIN not in the community database" category
(unknown_asin): a book whose own ASIN the report's last lookup found
unknown (unknown_asin_reports), with no ISBN, no recorded community
match and no community-given ASIN, so a hand match settles it. Its rows
offer Review match and Add it (prefilled with the ASIN). The report is
the only source, so /admin/issues lists the kind only while the report
runs; with it off the card is absent rather than an all-clear 0. No
migration and no player wire change.

Health's summary sits in the list cache, so unknown_asin_reports is no
longer free user state: a row added or removed, or its answer changed,
moves the content generation; renewing checked_at or sent_at alone
still does not.

matchedExpr is unchanged (facets, coverage and match runs still read
an ASIN as matched).

Part of #370.
Match automatically was reachable only from Health > Not matched, and a
new user moving from Audiobookshelf couldn't find it. The Overview and
Library > Books (while browsing) now show "N books not matched" with a
Match automatically link that opens Health > Not matched, where the card
says what a run sends before it starts. The count is the Health
summary's own (ignored books left out), and the prompt is hidden at
zero, which includes community metadata being off.

Part of #370.
The invite dialog warned "Their browser will warn about the
certificate" only for https at an IP address, so a self-signed server
reached by a name such as nas.local gave no warning. The warning now
follows the TLS mode the server runs: any https link while it is
self-signed, none under autocert or with TLS off (a proxy's
certificate). While the mode is unknown (settings loading, or a TLS
mode change waiting for a restart) https at an IP address still stands
in for it.

Part of #386.
On a phone the invite page said "Install AudioSilo, then open this
invite link again and tap Open in app", which reads oddly to someone who
typed the code in rather than followed a link. A typed code now reads
"Get the app, then come back to this page" / "Install AudioSilo, then
come back to this page and tap Open in app", and the reopen hint asks
them to enter the code on this page again; a link keeps the link
wording. Every language block has the three keys.

A new test holds the connect page to what exists: each key its script or
markup asks for is in the dictionary, and each element id the script
looks up is in the page.

Part of #386.
Pressing Share again while the system share sheet was still open made
the browser refuse the second share (InvalidStateError), and the console
fell back to copying the message underneath the open sheet. shareOrCopy
now ignores a press while its share is in flight; once the sheet closes
the next press opens it again.

Part of #386.
Batch B stopped showing a finished job's last step in Settings > Bridge
> Jobs, which was right for a done job but dropped the one clue to where
a failed or cancelled job stopped. Those now show "Last step: <step>"
with its count (the server keeps the progress on both); a done job still
shows none, and open jobs are unchanged.

Part of #386.
The LibriVox fetch checked ids with its own metaID pattern while
contribute and upcoming use audiosilo-meta's model.ValidSlug. One rule
now: a librivox.fetch's work_id and recording_id must be slugs
(lower-case kebab, at most 100 bytes), which every community id is.
Stricter than before, and checked only when a job is queued, so a job
already queued is never refused later.

Part of #386.
FoldLibrivoxFetch and LibrivoxFetchMatch unpacked the result's Adopted
*bool by hand. LibrivoxFetchResult.Folder now reads it as one of three
named states (FolderUnsaid for an older Bridge, FolderPlaced,
FolderAdopted), and both callers compare against those. No change in
behaviour; the fold's existing tests cover each state.

Part of #386.
The not-matched prompt's test navigated with an author filter object the
router's types reject, which only the typecheck caught. It searches
instead, as the page's other tests do.

Part of #370.
Audible's and LibriVox's job wording each read a failure code their own
way (the result's code, else a code the error starts with).
bridge-model's failureCode is now the one rule both use.

useTrackedJob, and the jobPollMs and jobGone it relies on, move from the
LibriVox feature into the Bridge feature (use-bridge.ts, bridge-model.ts):
following any job by id is the Bridge's, and the bridge feature no
longer needs anything from librivox to do it. No change in behaviour;
their tests move with them.

Part of #386.
A blank line left where the sharing flag moved up.

Part of #386.
The invite card worked out whether the server runs self-signed from the saved
settings and restart_pending inline, so the one branch where the address
heuristic comes back (a TLS mode change waiting for a restart) had no test.
runsSelfSigned holds the rule beside inviteReach, with that case covered.

Part of #386.
addBookUrl checked a typed ASIN that parseMatchQuery had already checked, as a
second field beside the book's own; the match dialog now passes the one it
prefers. NotInDatabase's books flag was always the absence of href, so the
wording follows href and a list with a link (or one book without) can't be
asked for. META_SITE is read only in its own file and is no longer exported.

Part of #370.
TestIssueUnknownASIN read only the first page of the list by hand where
listAll follows the cursor, and TestConnectPageNamesWhatExists carried its own
file reader and a second parser of the dictionary's language blocks. dictKeys
is now the one reader of a block's keys, for both i18n tests.

Part of #370 and #386.
Comment lines edited for the named folder states and the slug check ran past
the file's width, the Books prompt's comment told the change's history, and
librivox-page imported use-bridge twice after useTrackedJob moved there.

Part of #370 and #386.
addBookUrl runs while the match dialog renders, and encodeURIComponent throws
a URIError on a lone surrogate (a pasted search cut inside an emoji), which
took the dialog down. Each part of the words is now cleaned of such a half
before it is trimmed, so a query that was only the half falls through to the
title. Tests cover the characters that could split the one q parameter
(&, #, +, %, =), non-ASCII words, a line break and whitespace-only input.

Part of #370.
The link in a sentence repeated ExternalTextLink's classes, icon, target and
rel. It now renders that component at the sentence's own font size, so the
new-tab link style has one home; only the row's button is drawn here.

Part of #370.
The store's rule for unknown_asin_reports was tested with plain UPDATEs only.
The report writes through an upsert on a WITHOUT ROWID table, and Health's
unknown_asin count is cached under the generation: if the pre-update hook saw
the upsert's DO UPDATE differently on either engine, the count would go stale
or every daily recheck would empty the content caches. The test drives
SaveASINChecks, MarkASINsSent and DropUnknownASINs: the generation moves on a
first lookup, a changed answer and a forgotten row, and stays for the rest.

Part of #370.
The new kind had a denied test for its list but not for ignoring one. A
member's and an anonymous caller's ignore are now refused (403, 401) and the
book stays counted.

Part of #370.
The per-cache audit of the content generation still said /admin/books reads
no user state and Health reads only progress, and handleIssues' comment called
progress the one user state it reads. Both cached answers now read which ASINs
the report found unknown, so an audit of a cache against that list would have
missed the table. The Health issues entry gains the unknown_asin kind.

Part of #370.
Keying the warning on the self-signed TLS mode alone dropped it for a server
with TLS off behind a proxy that has its own self-signed certificate, reached
at https://192.168.1.20: the mode is known and not self-signed, so the person
invited met the browser's warning unannounced. The card now warns when the
running mode is self-signed (a name like nas.local too) or the link is https
at an IP address, whatever the mode; a name on a server that isn't
self-signed stays quiet.

Part of #386.
The certificate warning now shows for a self-signed server at a name too, but
its text still said the link was https at an IP address. inviteReach now gives
the reason (selfsigned or address) and the card picks the sentence for it: a
self-signed server says so, and https at an IP address says such an address
rarely has a certificate browsers trust. All six locales.

Part of #386.
In a Not found row the Add it button sat beside the book page's own new-tab
link with the same icon, and below md the icons are all that shows, so the two
read as one. Add it now leads with a book-plus icon.

Part of #370.
KodeStar added a commit that referenced this pull request Nov 14, 2026
…t in setup, the nearest Audible marketplace (#389)

A server installed with Docker (or any headless start) creates `admin`
with a random password and prints it once. Recreate the container and it
is gone, the console had no way to choose your own, and the sign-in page
pointed at a setup link the log never had. This makes first sign-in
recoverable and lets the admin pick their own account.

Closes #369, closes #192.

## What changes

**Lost the first-run password: `reset-admin`**
- New command `audiosilo [--data DIR] reset-admin [username]`. It sets a
fresh random password for the only admin (or the one named) through the
console's own set-password path, prints it, and revokes the first-run
auth code printed beside the old password. Sessions end. A running
server accepts the new password at once, and a cached session ends
within a minute (restart to end it at once).
- It refuses a missing or non-AudioSilo database, pending migrations,
and a backup restore waiting for the next start. Exit codes: 0 ok, 1
refused or failed (if revoking the code fails, the new password is still
printed first), 2 usage.
- Until the first-run admin first signs in to the console with a
password (or changes it), every later start logs a reminder with the
exact command for where it runs: `docker exec -u <uid>:<gid> <container>
audiosilo --data /data reset-admin` in our image (the server's own ids,
so no root-owned files), the full `/app/audiosilo/audiosilo --data
/config` path on the linuxserver image. The plaintext is never stored;
the reminder is a hint only (owner decision, V2.7.0-PLAN decision 6).
- The state is a `server_state` row `first_admin` written only by
`ensureAdmin` when it creates the admin, so upgraded servers and
`--setup` installs never show the reminder. No migration.
- The auth token cache re-reads a cached row once it is a minute old, so
a reset from another process reaches a running server.

**Console setup flow**
- New first step **Your account**: change the username and choose a
password (skippable). Reverses FIRST-RUN-PLAN.md decision 1 (V2.7.0-PLAN
decision 5). The password changes before the rename, so a wrong current
password leaves the name alone.
- **Connect a phone** moves to the end and explains sign-in: scan with
the app, the log's auth code works there, other people need an invite.
- The sign-in page says what a new server's username and password are
and where the password is; the "open the setup link" line shows only
while `/setup` is open.
- **#192:** the Audible marketplace step preselects the marketplace
nearest the admin (browser language region, then time zone,
nearest-marketplace mapping, US fallback) while no region was ever
chosen; config.yaml and `AUDIOSILO_METADATA_REGION` are never
overridden. Settings > Community metadata offers the same suggestion as
a row until a region is chosen. (The issue assumed the default was `us`;
it is "No preference".)

## Wire (all additive)
- `GET /admin/setup`: `printed_password` (bool).
- `GET /admin/settings`: `metadata.region_chosen` (bool).
- `PATCH /admin/users/{id}`: optional `username`; 400
`username_required`, 409 `username_taken`. `POST /admin/users` also
answers `username_required`.
- `POST /auth/password`: a wrong current password is still 401, now with
`code: current_password_incorrect` (shared with the player; shipped
builds unaffected).
- `GET /setup` on a server that never enabled the wizard now 303s to
`/admin` (was 404), the same answer as a closed wizard, so the console's
probe logs no 404 and a signed-out caller learns less than before.
- Setup step ids: `account` first, `phone` last.

## Tests and review
- Go: reset-admin (one admin, named, several unnamed, unknown name, no
admin, foreign file, pending migrations, pending restore, revoke
failure, the code it revokes and keeps), the reminder state (upgrade,
`--setup`, once-only writes, failed mark), rename authz (allowed and
denied), the /setup answers, the token cache re-read. Console: the
account step (order, wrong password, refused name), the locale/time zone
mapping, preselect only when never chosen.
- Gate green: `scripts/build-admin.sh`, `go build/vet/test -race`,
golangci-lint, and the native-engine vet/test/lint.
- Review: /simplify + /code-review two passes at the ★ level (high-x2,
Fable 5.2), then two fix rounds at high (Opus).

## Verified live (Playwright, real builds)
- Fresh headless start: banner; restart: the reminder; `reset-admin`
while running: the new password signs in at once, the old code and
sessions get 401; after a console sign-in, no reminder. A data dir from
main (3b03e1c) upgraded: no reminder. `--setup`: no reminder.
- Docker: the printed `docker exec -u 1026:100 ...` command works on a
non-root volume and files stay 1026:100.
- Sign-in page: hint shown, no console errors, setup line only with
`/setup` open.
- Setup: Your account first (wrong password message in English and
German, rename + new password, skip), phone step last with its Invite
link.
- Marketplace: en-GB/London -> United Kingdom, de-AT/Vienna -> Germany,
unmapped -> United States, env-locked and config values kept, the
Settings offer row.

Not verified: a real NAS (Synology/Unraid) container run.

Pairs with #390 (independent) and
KodeStar/audiosilo-docs#307.
@KodeStar
KodeStar merged commit b440027 into main Nov 14, 2026
4 checks passed
@KodeStar
KodeStar deleted the feat/matching-surfaces branch November 14, 2026 18:04
KodeStar added a commit to KodeStar/audiosilo-docs that referenced this pull request Nov 14, 2026
…ng a missing book; invite and job wording (#307)

Documents v2.7.0 batch C on the server.

- **First run:** where the password is in a NAS or Docker container's
log, the start-up reminder, and `reset-admin` with the exact command per
install type (our image, Unraid/linuxserver, Synology, Linux package, a
terminal), its refusals and exit codes. The setup flow's nine steps,
with Your account first and Connect a phone last, and the marketplace
preselect.
- **Troubleshooting:** "I lost the first-run admin password" now leads
with `reset-admin` before resetting the database.
- **Matching:** the Overview/Books notice, Add it to the community
database (new "Adding a missing book" section in
community/meta-site.md), and Health's "ASIN not in the community
database" state.
- **Invites and Bridge:** both certificate warnings, the typed-code
invite wording, and a failed job's last step.
- **Developer Docs:** the additive API fields and codes
(`printed_password`, `metadata.region_chosen`, `PATCH /admin/users`
`username`, `current_password_incorrect`, `username_required`,
`unknown_asin`, the `/setup` 303), the first-run state, and the token
cache's one-minute re-read.
- **Screenshots:** a new setup/account.png; the setup flow, the login
page, and the Overview and Books shots with the not-matched notice
recaptured.

Reviewed at high (Opus) with every command, string and field
fact-checked against the server branches. `npm run build` green.

Pairs with KodeStar/audiosilo-server#389 and
KodeStar/audiosilo-server#390.
KodeStar added a commit that referenced this pull request Nov 14, 2026
…393)

Part of #366 (moving from Audiobookshelf ends up matched). This is the
identifiers half; the certificate error, the import inside setup and the
match offer follow in a second PR once #389 and #390 merge.

## What

An applied Audiobookshelf import now keeps ABS's ASIN and ISBN for every
ABS book it can pair with one of ours, as a **match hint**:

- New path-keyed table `book_hints` (migration 0074: `library_id`,
`path`, `source`, `asin`, `isbn`, `import_id` with 0 = none, `set_at`).
It survives a rebuild, follows a moved folder (`moveBookState`) and goes
with its library.
- A hint is never the book's own ASIN, never on the player wire and
never on `/admin/books` (tested). Nothing shows until a match run is
applied.
- Match runs and the match dialog send it: a book's own ids first, then
the ABS hint, then tag hints. A tag ASIN goes only when the import gave
neither identifier (metaserve asks the ASIN first and stops on a hit, so
a tag ASIN beside an ABS ISBN would outrank the import). A known ASIN
whose length, title and series agree matches confidently and is recorded
the normal way (`book_matches` how=auto); a disagreeing one goes to
review.
- Pairing: path tier and title tier, every ABS book item, all-access.
Contested and gone items are skipped; books already matched are counted,
not hinted. Undo removes that import's hints; the last applied import
owns them.
- Every import carries them. The review's summary gains an additive
`identifiers` block (`books`, `asin`, `isbn`, `by_title`,
`already_matched`), shown as a review row; the stored payload gains
`identified` (omitempty, payload version unchanged).
- An import that brings identifiers but no listening for the person can
now be applied ("No listening to import"), **unless** it would undo an
import already applied for them: then Apply is off with a warning, and
the server refuses with 409 `import_not_ready` and changes nothing.
"Listening" matches the console's rule exactly (sessions, days,
bookmarks, progress that is new or moves position, finished or finish
date).
- The console's note on what matching sends now names an ABS import.
- The LibriVox placement clears a deleted book's hint along with its
other leftovers.

## Tests

Catalog apply/undo/re-import/move/library delete/rebuild of hints, the
refusal (and that the applied import's rows are intact), the listening
rule table, pairing tiers and skips, plan summary counts, QueryFor
order, an ABS-hinted identifier hit confident vs disagreeing, migration
0074, the API's identifier-only apply and refusal, hints absent from
`/item` and `/admin/books`, admin-only import routes. Console:
identifiers row, identifier-only notice, disabled Apply + warning.

Gate: `scripts/build-admin.sh`, `go build/vet/test -race ./...`,
`golangci-lint run` (0 issues), native-tag vet/test -race/lint.

Reviews: one ★ review (two passes), one fix round.

## Verified live

Scratch server from this branch against a real Audiobookshelf 2.37.2 in
Docker and live meta.audiosilo.app, four tagless books with no
metadata.json:

1. Import with listening: review shows "ASIN or ISBN for matching: 4
books"; after Apply, 4 `book_hints` rows, `books.asin` empty, no hint in
`/item` or `/admin/books`.
2. Match automatically: 4 confident, applied as `book_matches` how=auto
on the hinted recordings (two only cleared the auto bar through the
identifier hit).
3. An ASIN meta doesn't know: no identifier reason, matched by
title/author/length to the right recording, never the fake ASIN.
4. Undo removes the hints; a new import writes them again; rescan keeps
them; a moved folder's hint follows its new path.
5. Identifier-only: a user with nothing applied gets the notice and
Apply works; a user with an applied import sees Apply off and the
warning, a direct POST gets 409 `import_not_ready`, their progress
byte-identical and their import still applied.

## Notes

- The de/es/fr/it/pt strings for the new console text are machine
translated: a native read is welcome.
- Follow-ups are filed separately (hint lifetime after a book is
deleted, a few pairing edge cases, undo dialog and history silent on
hints, index use for undo's deletes).
- Docs (admin/import.md and friends) land in one audiosilo-docs PR after
the second #366 PR.
KodeStar added a commit that referenced this pull request Nov 14, 2026
…etup, offer Match automatically (#395)

Part 1 of #366 (Audiobookshelf import). Part 2, the identifiers kept as
match hints, merged as #393.

## What
- **`abs_certificate`**: when Audiobookshelf's https certificate isn't
trusted (self-signed or a private CA), Connect, Status and a background
fetch now fail with `abs_certificate` (still 502) instead of
`abs_unreachable` / `not_abs`. The console shows it under the address
field and points at ABS's plain http:// address. Nothing is ever trusted
or skipped. Detection is one `errors.As` on
`*tls.CertificateVerificationError`: Go's client handshake wraps every
verify failure (unknown authority, hostname, invalid) in that type.
https to an http-only port stays `abs_unreachable`
(`http.ErrSchemeMismatch`).
- **Import inside setup**: `ImportFlow` (connect, map, review, history)
is extracted from Settings > Import and rendered inline in the setup
flow's Audiobookshelf step (`embedded`), replacing the new-tab link.
Continue and Skip stay. While a books library is scanning, the step says
"Wait for the scan to finish" and Connect is off (from the libraries the
flow already polls, so no wire field).
- **Match offer**: #390's `UnmatchedPrompt` (Match automatically)
appears once an import is applied, in the Past imports card, and in the
setup step once the scan is done. It links to Health (a new tab from
setup) and never starts a run by itself.
- #393's identifiers row and the identifier-only apply guard are
unchanged inside the extracted flow.

Error-code wire change is additive; no player change.

## Tests
Go: a TLS fake gives `abs_certificate` from Status and Connect. A
hostname mismatch with a trusted pool gives it too, and 127.0.0.1
connects as the control. https to an http port stays `abs_unreachable`,
and plain http is unchanged. API: 502 on connect, and `error_code` on a
failed background fetch. Console: the certificate row, ImportFlow inline
in the abs step with the scan gate, the offer after an apply and held
until the imports are read, and imports-model. Gate: build-admin.sh
(1628 tests), go build/vet/test -race, golangci-lint 0.

## Verified live
Real Audiobookshelf 2.37.2 in Docker:
- **Certificate error**: http connects. Behind a private-CA proxy and a
truly self-signed proxy, Connect shows the certificate message under the
address field, and curl returns 502 `abs_certificate`. Switching to
http:// connects. The same results came from the Linux binary inside the
`:edge` image (Alpine, Go's own verifier).
- **Setup step**: on a fresh server with a slowed first scan (97 s), the
gate showed and lifted 0.8 s after the scan ended. Connect, map, fetch,
review (the identifiers row read "4 books") and apply all ran inline.
Exactly one offer appeared, in Past imports, and it opened Health in a
new tab with no match run started. Continue and Skip both moved on.
- **Layout**: no sideways scroll at 390 px or on desktop.

Not verified: the image's native-SQLite (CGO) build. The certificate
check runs before any database work.

## Notes
- de/es/fr/it/pt text for `errors.absCertificate`,
`imports.failure.abs_certificate`, `setup.abs.import` and
`setup.abs.waitScan` is machine-translated and wants a native read.
- Follow-ups go in one issue (scan gate covers Connect only;
no-books-library wording; redirect-to-https hint; offer link icon; the
map table is cramped on a phone; two Back buttons while mapping).

Pairs with KodeStar/audiosilo-docs (docs for #366, PR to follow).
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.

Matching: surface Match automatically, give Not found an "add it" path, flag ASINs the database does not know

1 participant