Moving the library or queue cursor with the keyboard (j/k, page keys,
first/last) updated the selection but never scrolled the list, so the
selected row could slide out of the scroll box and disappear.
Each pane now runs an effect that re-scrolls its `.selected` row into
view whenever the cursor moves. The queue cursor is its own signal, so
this fires on moves but not on every stream-driven queue refresh; the
library cursor lives in the library pane signal. The scroll is deferred
to the next animation frame (the freshly-rendered row must be in the
DOM) and uses `block: nearest`, so an already-visible row does not jump.
Click/drag selection needs no help -- the pointer is already on-screen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relying on the proactive prompt alone was not enough: it only fires on
first connect, only when no credentials are stored, and only against an
auth-enabled server, so a manual affordance was missing.
The top bar now shows a "log in" button whenever the server reports auth
is enabled (Init.auth_enabled, kept in a store signal), opening the same
dismissible credentials dialog. Once credentials are stored it becomes
"log out", which clears them and reloads to drop back to the guest role.
The button is hidden on servers with no auth, where sending credentials
would only earn an UNAUTHENTICATED lock-out.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The browser client already sent stored credentials and showed a login
form, but only when the server answered UNAUTHENTICATED -- i.e. only
when every role was guarded. With a fallback role configured, an
anonymous browser silently connected as that role and was never offered
a way to log in as a higher one.
The server now reports its auth on/off switch on the InitResponse
(auth_enabled, field 8), which is reachable anonymously. The RPC handler
stamps it from Authenticator::enabled(); the playback loop, which owns
queue state and not the auth config, leaves it false.
On first connect with no stored credentials against an auth-enabled
server, the web client raises the login dialog. It is dismissible --
"continue as guest" keeps the unauthenticated fallback role -- and is
shown once per session so stream reconnects do not nag. When the server
denies anonymous access outright (UNAUTHENTICATED), the same dialog
appears without the guest option, because credentials are then the only
way in.
Docs: architecture/roles-auth.md and web-client.md updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clients cache library listings except for server-mutable folder roots.
/orphans is recomputed from the store on every visit, so a cached listing
froze the orphan set until a client restart. Add /orphans to the TUI's
mutable-roots list so entering the provider always re-walks.
The web client's list was also stale from the store refactor — it still
named the removed /captures, /queues, /bookmarks providers and omitted
/crabidy, so /crabidy (and now /orphans) listings went stale there too.
Reset it to the real mutable roots: /crabidy, /fs, /orphans.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage 1-2 of the crabidy-store dev-flow (architecture/crabidy-store.md,
quality/, plan/): one /crabidy provider replacing queues/bookmarks/
captures, with track tomls linking into a content-addressed store that
de-duplicates by provider id and content hash.
Additive, build stays green:
- proto: Track.provider_item_id + is_captured; LibraryNode.is_captured;
LibraryNodeChild.is_captured (swept all literals).
- fsdy: Playable::Store + PlayableSpec.store, 5-way cardinality,
from_track_store, Client.with_store_root + store resolution.
- crabidy_store.rs: StoreSidecar/ProviderEntry/StoreIndex/CrabidyStore
type + method surface (bodies stubbed for the implement stage).
- supersede bookmarks/captures/capture-deletion docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A row of frequency bars under the track progress, on by default and
toggleable with the client spectrum config option. Because the audio
plays on the server and clients may be remote, the spectrum is produced
server-side, not captured locally: audio-player taps its own output
into a lock-free ring on the audio thread (one store per sample, no
locks), crabidy-server runs a Hann + realfft over 2048 samples at 20fps,
folds it into log-spaced bars, and broadcasts them as a new SpectrumFrame
on the update stream. The task idles when nothing is playing or no
client is listening. The TUI renders block-glyph bars in the now-playing
pane; the web client renders the same bins as CSS bars.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
crabidy-server now serves a browser client with the same functionality
as the TUI at its own address, behind the default-on web-ui feature.
The new cbd-web crate is a client-side Leptos/WASM app talking gRPC-web
(tonic-web-wasm-client) over the same crabidy-core client and proto the
TUI uses, so parity is structural: library browsing, search terms,
marks, bookmarks/captures with live progress and confirmed deletion,
the full queue and playback controls, and the update stream with
reconnect. Keys mirror the TUI; every key also has a clickable control.
Styling is hand-written modern CSS with a single crab orange-red accent
and light/dark themes.
The server wraps its existing gRPC service in tonic-web and composes one
axum router (auth layer -> grpc-web -> service, web bundle as fallback);
axum::serve replaces tonic transport, and native gRPC (h2c) still works.
The bundle is embedded via include_dir behind a build.rs that falls back
to a placeholder so a plain cargo build needs no wasm toolchain. To make
crabidy-core build for wasm, tonic is codegen-only there (transport
generation disabled) and native config loading is target-gated.
devenv gains the wasm toolchain and build-web/serve-web scripts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>