docs: audit the book and the READMEs, and give Seek its role

Sweep of docs/ and every README against the code, after several features
landed since the last one.

Mechanical checks, which find what reading does not: SUMMARY covers every
page and no more; every relative link resolves (docs/src/providers.md
pointed a directory above the book, twice); every #anchor matches a real
heading; every provider crate's Settings field is documented in both its
README and its book page; the feature table matches Cargo.toml, which it
did not -- `rss` was missing and "seven provider features" is now eight.

Prose that predated recent work: the root README's client-config sample
knew only `spectrum` and still claimed every option has a flag; its
spectrum and web-client sections predated the colors, segments, shadows,
pane tabs, register and seek; cbd-web/README.md likewise; intro.md's
provider tree was missing /rss; clients.md omitted volume and mute from
what the update stream carries; rssdy/README.md did not mention that only
audio enclosures become episodes.

The audit also found a defect the docs were right about: `Seek` was never
added to `minimum_role`, so it fell through to the owner-only default
while auth.md and architecture/roles-auth.md both promise a queue-owner
may control playback. With auth configured a queue-owner could play, skip
and change the volume, but got PermissionDenied on `,`/`.`. Seek now sits
with the other playback verbs.

The test meant to prevent that -- "a new RPC must be added to exactly one
list" -- compared the role lists against a hardcoded 24, so a 25th method
kept the suite green. It now reads the method names out of crabidy.proto
and compares sets: a count copied out of a file is not a check against it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-27 21:19:59 +02:00
parent d8bd06219c
commit 87b98193d8
10 changed files with 138 additions and 29 deletions

View File

@ -137,13 +137,26 @@ password = ""
# Show the frequency-spectrum bars under the track progress. Default true.
spectrum = true
# How those bars look. Defaults shown; a color is a "#rrggbb" triple, a
# color name, or a 0-255 palette index, and an unusable value warns and
# falls back. See docs/src/clients/tui.md for what each one does.
spectrum_color = "#bf616a" # the bars (the queue's playing-track red)
spectrum_gradient = true # shade them by height
spectrum_top_color = "#b48ead" # what the shading reaches at the top
spectrum_peak_color = "#81a1c1" # peak-hold shadows; "none" draws none
spectrum_peak_fill = true # false: a thin rule at the peak instead
spectrum_peak_fall = 4.0 # seconds for a full-scale shadow to fall
spectrum_bar_width = 1 # least bar width, in cells
spectrum_bar_gap = 1 # seam dividing two bars, in cells
spectrum_row_gap = 1 # line dividing two rows, in eighths
```
Every option is also available as a command-line flag before the
subcommand (`cbd-tui --address ... --user owner`, `cbd --spectrum
false`); a flag overrides the file value. To write the credentials into
the config once, use the `auth` subcommand (see below) instead of
editing the file by hand:
Every option except the `spectrum_*` appearance settings is also available
as a command-line flag before the subcommand (`cbd-tui --address ... --user
owner`, `cbd --spectrum false`); a flag overrides the file value. To write
the credentials into the config once, use the `auth` subcommand (see below)
instead of editing the file by hand:
```sh
cbd-tui auth owner 'my-password' # sets user + password
@ -361,15 +374,32 @@ Toggle it at runtime with `f`, or set the startup default with
`spectrum = false` in the client config. Servers built without the
`spectrum` feature simply never send bars.
The bars are shaded by height — red at the floor reaching purple at the top
— and divided into segments by a dim seam between bars and a thin line
between value rows. Each bar trails a blue peak-hold shadow marking where it
lately reached, falling away over a few seconds. Every part of that is
configurable, including turning it all off; see
[docs/src/clients/tui.md](docs/src/clients/tui.md).
`,` and `.` seek 15 seconds inside the playing track; `<` and `>` (or
`Ctrl-p`/`Ctrl-n`) skip a whole track. `K`/`J` change the volume and `m`
mutes; the now-playing pane shows the server's level, which tops out at
110%.
## Web client
`crabidy-server` serves a browser client with the same functionality as
the TUI at its own address (`http://<server>:50051/`) — same navigation,
same keys (`j`/`k`/`h`/`l`, `%`, `e`, `d`, `w`, `W`, queue and playback
controls, `?` for help), plus clickable equivalents and a light/dark
theme toggle. It talks gRPC-web to the same service the TUI uses, so it
honors the same `[auth]` roles (it shows a login form when the server
requires credentials).
same keys (`j`/`k`/`h`/`l`, `%`, `e`, `d`, `w`, `W`, marks and visual mode,
the `y`/`d`/`p` register, `,`/`.` to seek, queue and playback controls, `?`
for help), plus clickable equivalents for all of it: the progress bar seeks
where you click, and `library`/`queue` tabs in the top bar switch panes
without a keyboard. On a phone the two panes cannot sit side by side, so
only the focused one is shown and those tabs are the way between them.
There is a light/dark theme toggle. It talks gRPC-web to the same service
the TUI uses, so it honors the same `[auth]` roles (it shows a login form
when the server requires credentials). The `/` live filter is TUI-only for
now.
It is compiled to a WASM bundle and embedded into the server binary,
behind the default-on `web-ui` cargo feature. A plain `cargo build`

View File

@ -25,12 +25,25 @@ functionality as `cbd-tui`, served by `crabidy-server` itself.
## Functionality
Everything the TUI does: browse the library (`j`/`k`/`h`/`l`, click),
marks, create/rename/delete nodes (`%`/`e`/`d`), bookmark and capture
(`w`/`W`, with live progress lines and skipped-track marking), the full queue
and playback controls, volume, shuffle/repeat, and a `?` help overlay listing
the keys. Keys mirror the TUI; every key also has a clickable control. A
light/dark theme follows the OS and can be toggled (persisted). The accent
Everything the TUI does: browse the library (`j`/`k`/`h`/`l`, click), marks
and visual mode (`s`, `v`/`V`) in both panes, the one-slot register (`y`
yanks, `d`/`c`/`C` fill it as they remove, `p`/`P` paste), create/rename/
delete nodes (`%`/`e`/`d`), bookmark and capture (`w`/`W`, with live progress
lines and skipped-track marking), the full queue and playback controls,
seeking (`,`/`.` for 15 seconds, `<`/`>` for a whole track, or a click on the
progress bar), volume, shuffle/repeat, and a `?` help overlay listing the
keys. Keys mirror the TUI; every key also has a clickable control. The `/`
live filter is the one thing that is TUI-only so far.
Two layout details earn their own note:
- `library`/`queue` **tabs** in the top bar switch panes and show which one
the keys go to — what `Tab` does, reachable by thumb.
- Below 700px the panes cannot sit side by side, so **only the focused pane
is rendered**. It is not collapsed to a strip: a strip's truncated rows
still take taps, which sent them to the wrong pane.
A light/dark theme follows the OS and can be toggled (persisted). The accent
color is the crab orange-red.
When the server requires credentials, a login form collects the role

View File

@ -75,7 +75,7 @@ pub fn minimum_role(grpc_path: &str) -> Role {
// Every other queue and playback verb.
"Queue" | "Replace" | "Remove" | "Insert" | "ClearQueue" | "SetCurrent"
| "ToggleShuffle" | "ToggleRepeat" | "TogglePlay" | "Stop" | "ChangeVolume"
| "ToggleMute" | "Next" | "Prev" | "RestartTrack" => Role::QueueOwner,
| "ToggleMute" | "Next" | "Prev" | "RestartTrack" | "Seek" => Role::QueueOwner,
// Library writes (CaptureLibraryNode, SaveQueue,
// RenameLibraryNode, DeleteLibraryNode) and anything unmapped.
_ => Role::Owner,
@ -84,7 +84,7 @@ pub fn minimum_role(grpc_path: &str) -> Role {
/// Hashes a password into the PHC string `crabidy-server.toml` expects
/// (argon2id, default parameters, fresh random salt). Backs the
/// `crabidy-server hash-password` helper.
/// `crabidy-server guard` subcommand.
pub fn hash_password(password: &str) -> Result<String, String> {
use argon2::password_hash::{rand_core::OsRng, SaltString};
use argon2::PasswordHasher;
@ -355,6 +355,7 @@ mod tests {
"Next",
"Prev",
"RestartTrack",
"Seek",
];
let owner = [
"CaptureLibraryNode",
@ -362,10 +363,28 @@ mod tests {
"RenameLibraryNode",
"DeleteLibraryNode",
];
// The full service, from crabidy.proto — 24 methods. A new RPC
// must be added to exactly one list (and the layer keeps it
// owner-only until then).
assert_eq!(appender.len() + queue_owner.len() + owner.len(), 24);
// Every RPC must appear in exactly one list above. Taken from the
// proto itself rather than a count copied out of it: a hardcoded
// total let `Seek` be added to the service and land on the
// owner-only fallthrough with the suite still green, which is the
// one thing this test exists to prevent.
let proto = include_str!("../../crabidy-core/crabidy/v1/crabidy.proto");
let service: std::collections::BTreeSet<&str> = proto
.lines()
.filter_map(|line| line.trim().strip_prefix("rpc "))
.filter_map(|rest| rest.split('(').next())
.map(str::trim)
.collect();
let mapped: std::collections::BTreeSet<&str> = appender
.iter()
.chain(queue_owner.iter())
.chain(owner.iter())
.copied()
.collect();
assert_eq!(
mapped, service,
"every RPC of the service needs a role (left: mapped, right: the proto)"
);
for method in appender {
let path = format!("{SERVICE_PREFIX}{method}");
assert_eq!(minimum_role(&path), Role::QueueAppender, "{method}");

View File

@ -22,8 +22,8 @@ below it (owner ⊃ queue-owner ⊃ queue-appender):
- **owner** — the normal user: everything, including all library
writes.
- **queue-owner** — anything on the queue and playback (append, remove,
reorder, clear, shuffle, repeat, play/stop, next/prev, volume, mute,
), but **no library writes**: no bookmarks (`w`), no captures (`W`),
reorder, clear, shuffle, repeat, play/stop, next/prev, seek, volume,
mute), but **no library writes**: no bookmarks (`w`), no captures (`W`),
no saving, renaming, or deleting.
- **queue-appender** — may browse and search the library and **append**
tracks to the queue; nothing else. (Searching is allowed because

View File

@ -36,12 +36,13 @@ The same list goes into the server's startup log, so a support question
| `abs` | the `/abs` audiobookshelf provider |
| `soundcloud` | the `/soundcloud` provider |
| `jamendo` | the `/jamendo` provider |
| `rss` | the `/rss` podcast-subscription provider |
| `fs` | local files **and persistent state** — see below |
| `opus` | Ogg-Opus decoding (`symphonia` + a bundled libopus C build) |
| `spectrum` | the server-side FFT feeding clients' spectrum bars |
| `web-ui` | the embedded web client (`tonic-web` + the wasm bundle) |
Plus two conveniences: `all-providers` enables the seven provider features
Plus two conveniences: `all-providers` enables the eight provider features
at once, and `cbd` (the bundle) mirrors every feature above and adds
`notifications` for the TUI's desktop "now playing" popups (`notify-rust`,
which on Linux pulls a D-Bus stack).

View File

@ -4,8 +4,8 @@ A client is anything that drives the server. Every client speaks the
same gRPC service (see [Architecture](./architecture.md)): it sends
**commands** (browse the library, change the queue, control playback)
and subscribes to the **update stream** that pushes the current queue,
play state, track position, capture progress, and the frequency
spectrum as they change. Nothing is polled — each client redraws from
play state, track position, output level and mute, capture progress, and
the frequency spectrum as they change. Nothing is polled — each client redraws from
the pushed updates, so several clients driving one server always agree
on what is playing.

View File

@ -64,6 +64,7 @@ segment is a **provider** mounted as a subtree:
├── fs a local music folder
├── fyyd podcast search
├── jamendo Creative-Commons music
├── rss podcast subscriptions
├── soundcloud SoundCloud search, likes, and playlists
├── tidal Tidal streaming
├── youtube YouTube search & playlists

View File

@ -55,11 +55,11 @@ independent things decide it:
1. **Was it built in?** Every provider sits behind a Cargo feature (all on by
default). A binary built without one can never mount it — see [Tailored
builds](../build-features.md), and `crabidy-server features` to see what a
builds](./build-features.md), and `crabidy-server features` to see what a
binary has.
2. **Is it enabled?** The `providers` list in `crabidy-server.toml` turns
compiled-in providers on and off without a rebuild — see
[Configuration](../config.md#enabling-and-disabling-providers).
[Configuration](./config.md#enabling-and-disabling-providers).
3. **Did it initialize?** Only a client that came up successfully is mounted.
```admonish note

View File

@ -1774,3 +1774,41 @@ would divide by zero and freeze every shadow on screen.
Seven appearance keys is a lot of config surface, justified by these being
pure appearance with no right answer: each one is a question that came back
with a different answer than the default assumed.
## Documentation audit (2026-07-27)
A sweep of `docs/` and every README against the code, prompted by several
features landing after the last one. Mechanical checks first — they find what
reading cannot:
- every `docs/src/**.md` appears in `SUMMARY.md` and vice versa;
- every relative link in the book and the READMEs resolves (two did not:
`docs/src/providers.md` pointed at `../build-features.md` and `../config.md`,
which from `src/providers.md` lands a directory above the book);
- every `#anchor` in an internal link matches a heading that exists;
- every provider crate's `Settings` field appears in its README and its book
page (all did, except Tidal's endpoint and token fields, which its page
deliberately delegates to `tidaldy/README.md`);
- the `crabidy-server` feature table against `Cargo.toml` (`rss` was missing,
and "seven provider features" had become eight).
Stale prose, all of it from work that landed after the previous sweep: the
root README's client-config sample knew only `spectrum`, still claimed every
option has a command-line flag, and described the spectrum before it had
colors, segments or shadows; its web section predated the pane tabs, the
register and seek; `cbd-web/README.md` predated the same; `docs/src/intro.md`'s
provider tree was missing `/rss`; `docs/src/clients.md` listed what the update
stream carries without volume and mute.
The audit also turned up a **defect the docs were right about**: `Seek` was
never added to `minimum_role`, so it fell through to the owner-only default
while `auth.md` and `architecture/roles-auth.md` both promise a queue-owner
may control playback. With auth configured, a queue-owner could play, skip and
change the volume but got `PermissionDenied` on `,`/`.`.
The test meant to prevent exactly that — "a new RPC must be added to exactly
one list" — compared the three role lists against a hardcoded 24. `Seek` made
the service 25 methods and the suite stayed green. It now reads the method
names out of `crabidy.proto` with `include_str!` and compares sets, so the
next RPC cannot be forgotten. A count copied out of a file is not a check
against that file.

View File

@ -43,6 +43,13 @@ A subscription is queueable and downloadable, so you can queue or `W`-capture
a whole feed. Episodes stream directly from the enclosure URL — no sidecar,
no helper binary.
Only **audio** enclosures become episodes: an item is taken when its
enclosure type says audio, or when the type is missing or generic and the URL
looks like audio (including extensionless `/feed/mp3`-style URLs). Video and
image enclosures are skipped, so a show that publishes both plays its audio
and a blog feed does not list its featured images as tracks. See
`docs/src/providers/rss.md` for the exact rule.
**Nothing is cached.** Every visit to a subscription fetches the feed, so an
episode published a minute ago is there. One memo exists purely so that
listing a feed and then queueing its 40 episodes costs one fetch rather than