Skip to content

feat(settings): OPDS sharing pane on the two-mode model - #165

Merged
phildenhoff merged 3 commits into
mainfrom
opds-sharing-ui-v2
Sep 23, 2026
Merged

phildenhoff merged 3 commits into
mainfrom
opds-sharing-ui-v2

Conversation

@phildenhoff

@phildenhoff phildenhoff commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Why

The old sharing pane was built for the pre-rework service: a picker of ~65 network interfaces, status fields the state machine no longer exposes, and no credential surface. It was driving a model that doesn't exist anymore.

The other half is credentials. We only stored an Argon2 verifier, so a generated password was shown once and then gone — getting it back into a reader meant rotating it, and rotating stopped a running share. Having to rotate to "reveal" a password is bad UX, so credentials are now stored reversibly (ADR 0005) and the pane can just show them.

image

What changed

Three commits, reviewable in order:

  1. feat(opds) — backend. Reversible credential storage; the Argon2 verifier and everything that existed to make it safe (semaphore, 503-busy, HMAC result cache, response padding) is deleted, and backoff stays as the guessing defence. Auth reads a hot-swappable snapshot per request, so setting or regenerating credentials doesn't interrupt a running share. New reconfigure applies port/scope changes to a live share and runs every config check before stopping the old listeners. Usernames containing : are rejected, since HTTP Basic can't carry them.
  2. style — one stale comment. feat(library): main-UI library picker, double-buffered switching, Library tab redesign #166 already landed the cursor change itself.
  3. feat(settings) — the pane. Share toggle with a copyable listening URL, Local network / All networks (All networks needs sign-in), port, and reader sign-in with a reveal toggle and press-and-hold regenerate. Settings gain a sharing block (v2→v3), and v3→v4 moves the default port off 8080 — Calibre's own server's default — to 9028; a port you picked is left alone. The web build gets no OPDS client at all.

The pane renders what the service reports, not local guesses. One thing that bit me: the service reports a failed bind as Ok(status) in the error state rather than a rejected command, so the UI originally treated a busy port as success. startOutcome in use-opds-sharing.ts now counts that as failure. A port change that can't bind rolls back to the previous port and restarts there, with "Port not available, please choose another" inline in the Port row. Errors render beside the control that caused them, and a failed start clears when you edit the port or leave the tab.

Reviewer focus

  • use-opds-sharing.ts — polling vs. mutations (the mutation sequence and mutating guard), and startOutcome.
  • crates/citadel-opds/src/service/mod.rs — preflight() and reconfigure.
  • ADR 0005's trade-off: clb_query_opds_credential_secret hands the plaintext password to app JS, so a hypothetical XSS now reaches the secret. We render no untrusted HTML today; the ADR says to revisit if that changes.

Validation

  • cargo test --workspace (0 failures; new tests cover reconfigure rejecting a bad config without tearing down the share — red without the fix — and the colon username), vitest run (249 passed), tsc --noEmit clean. Each of the three commits builds on its own.
  • bun lint: 3 warnings, all in files from main (FirstRunFlow.module.css, Sidebar.tsx, Books.tsx).
  • In the dev app, driven through the webdriver plugin with local sockets holding ports: changing to a busy port rolls back and keeps serving on the old one; enabling on a busy port shows the inline error and the switch stays off; editing the port or switching tabs clears it; a me:home username is flagged inline and not saved. I checked the HDR sheen on the regenerate wave with and without an HDR display.

Not verified: the "Starting…" / "Waiting for a network connection…" labels (too brief to catch), and I didn't point a real OPDS reader at it in this pass.

(In one test run the port rollback didn't happen — the field kept the blocked port and sharing ended up off. Two targeted retries couldn't reproduce it, so I'm flagging it rather than claiming it's fixed.)

Follow-ups

  • If a port is taken on IPv4 but free on IPv6, sharing comes up IPv6-only and shows an IPv6 URL, which plenty of readers can't use. Best effort for now.

Supersedes #151.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@phildenhoff

Copy link
Copy Markdown
Member Author

Prototype screenshots of the four layout variants (see docs/prototypes/ in this branch, or flip live via the floating bar in the settings window on this branch):

A — field with dice (1Password-style): https://raw.githubusercontent.com/everydaythingssoftware/citadel/opds-sharing-ui-v2/docs/prototypes/proto-A.png
B — pairing grid (Apple TV style): https://raw.githubusercontent.com/everydaythingssoftware/citadel/opds-sharing-ui-v2/docs/prototypes/proto-B.png
C — access card (Plex managed-user style): https://raw.githubusercontent.com/everydaythingssoftware/citadel/opds-sharing-ui-v2/docs/prototypes/proto-C.png
D — two-step setup: https://raw.githubusercontent.com/everydaythingssoftware/citadel/opds-sharing-ui-v2/docs/prototypes/proto-D.png

Reference designs from other products:

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

phildenhoff and others added 3 commits September 22, 2026 23:03
…gure

Credentials are stored reversibly (ADR 0005) so the pane can show the
current password instead of forcing a rotation. The Argon2 verifier and
the machinery that made it safe (semaphore, busy response, HMAC result
cache, response padding) are removed; exponential backoff remains the
online-guessing defence.

Auth reads a hot-swappable username/password snapshot per request, so
configuring or generating credentials no longer stops a running share.
Clearing credentials still does.

reconfigure applies a port or scope change to a running share. Every
config check runs before the old listeners are stopped, so a rejected
config leaves the share running. Usernames containing a colon are
rejected, since HTTP Basic could never authenticate them.

New commands: clb_query_opds_credential_secret, clb_cmd_reconfigure_opds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pointer-cursor overrides are gone (arrow cursor on every control), but
this header still said the block signals clickability with pointer
cursors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Sharing tab in Settings for the local OPDS server. Rows layout: share
toggle with the live listening URL (copyable), reachable-from scope, port,
and reader sign-in with username and a reversible password field (reveal
toggle, press-and-hold regenerate with a gold letter wave that flares
HDR-bright on capable displays).

The pane renders backend status, never local guesses. useOpdsSharing
polls the service and serialises mutations; start and reconfigure treat a
failed bind as failure. A port change that fails to bind rolls back to the
previous port and restarts there. Errors render beside the control that
caused them — server errors inline in the Port row, credential errors
under the password — and a failed start clears when the port is edited or
the pane is left.

Sharing settings (scope, port, sign-in, username) persist through the
settings store with a migration from the previous shape.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@phildenhoff
phildenhoff merged commit a2035e1 into main Sep 23, 2026
7 checks passed
@phildenhoff
phildenhoff deleted the opds-sharing-ui-v2 branch September 23, 2026 06:45
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