Three dashboard complaints, one build:
- The widget collapses to a single summary row (This Week · minutes ·
pages · sessions · books · streak · Full stats) on every screen size
and expands to the full grid via a details/summary toggle — the
carousel sections start immediately below it again.
- The 'Your Library' heading moved into the library switcher subbar
(LibrarySwitcher gains an optional title param; collections/series
keep the plain 'Library' label). The standalone heading block is
gone.
- Root-caused the JS-switch vanishing: renderDashboardCollections
replaces ALL of #collections-container's innerHTML, so anything
server-rendered inside it (title, widget) died on the first library
switch. The widget now re-fetches ITSELF: a /dashboard/week-widget
fragment (same resolveLibrary family-lens rules as the page) is
pulled by htmx whenever the switcher dispatches week-refresh —
carrying the current #library-select value — so the widget both
survives switches and RE-SCOPES to the switched library. Initial SSR
renders the same scope the sections use.
ThisWeek (and the streak calendar) gained an optional library filter.
Tests: TestReadingStatsWeekLibraryScope — per-library totals, scoped
streaks, invisible-library isolation, malformed id 400. Full suite
PASS.
Two visibility fixes so the UI reflects the shelf's real state on the
next scan instead of only after the archive gate:
- Missing items disappear at once: the user-facing filters already hid
archived rows; the same listings now also require missing_scan_count =
0. A deleted or moved-then-not-yet-repointed file vanishes from the
UI immediately, while purge timing stays gated on archived_at plus the
retention window - grace protects data, not visibility. Restored
automatically when the file returns.
- Steam Deck SD-card model for unmounted storage: resolveLibrary stats
each library's folder roots and flags libraries with no live folder
as Offline (LibraryData gains the field, computed at request time so
mounts/unmounts react instantly). The bookshelf shows an empty shelf
plus a 'storage is not connected' notice for an offline selected
library, and both the shared LibrarySwitcher and the bookshelf's
inline select label offline libraries with their true holding counts.
Nothing is marked or purged while offline.
- TotalMediaCount skips offline libraries, so the 'All Books/Libraries'
totals match what is actually visible.
Verified live: renaming uploads/Manga away produced the notice, an
empty shelf, and the offline dropdown label with a corrected total;
renaming it back restored all 38 cards with zero dirty rows.
The settings icon is a circle with radiating spokes, which reads as a
sun/light-mode toggle rather than a dashboard control. Swap it for the
grid icon, the standard dashboard-layout affordance.
Replace the top-nav with a fixed left sidebar + slim content topbar
(the Komga/Audiobookshelf layout), the signature change versus the
conservative tighten-ui branch.
- App shell: .app-sidebar (off-canvas on mobile via Alpine mobileMenuOpen,
pinned at 16rem on lg), .app-topbar (fixed, blurred, 4rem), and
.app-subbar for in-page sticky bars (parks under the topbar). Content is
auto-offset via body:has(.app-sidebar) so reader.templ/error.templ (which
have no sidebar) are untouched.
- Header rebuilt as the sidebar: logo + vertical nav (activeClass), an
inline Appearance picker driven by ThemeOptions/WoodOptions, and an
inline account menu / sign-in (preserving the inline htmx login). The
search lives in the topbar so its dropdown still anchors correctly.
- Bolder primitives: .card -> rounded-2xl, cinematic .book-card-cover
hover overlay with a quick-action affordance, .brand-gradient hero
surface, .hero-backdrop (blurred cover) and .stat-card utilities.
The UI had no surface showing how many media items have been imported.
Surface the total in the library switcher shown on the Dashboard, Series,
and Collections pages (via the LibrarySwitcher component) and in the
Bookshelf's inline library filter.
- Add a MediaCount field to LibraryData and a TotalMediaCount helper to
sum counts for the "All Libraries" / "All Books" option.
- resolveLibrary() now fetches per-library counts (one query) and maps
them onto each LibraryData entry, so the switcher reflects the active
scope without changing the component's signature.
- Each library option renders "(N)" and the "All" option renders the
grand total across the user's visible libraries.
The "All" total is the sum of the user's visible libraries, correctly
respecting per-user library visibility rather than a raw global count.
Regenerated templ files for library_switcher and bookshelf.
Add reusable library switcher infrastructure that can be used across
dashboard, collections list, and collection detail pages.
New files:
- templates/library_switcher.templ: Shared LibrarySwitcher component
with variadic actions slot for page-specific buttons (e.g. dashboard
settings/refresh). Includes DashboardActions sub-component.
- web/src/library-switcher.ts: Shared module providing:
- initLibrarySwitcher(): syncs dropdown with localStorage, attaches
change listener with configurable onSwitch callback
- switchWithTransition(): generic fade-out -> spinner -> fetch ->
fade-in transition used by all pages
- getCurrentLibraryId(): reads "selectedLibrary" from localStorage
Modified:
- web/src/main.ts: import new library-switcher module
- web/src/types/api.d.ts: add book_count field to CollectionData