Three changes from the network-drive audit:
1. Delete-ordering fix. Deleting a media item cascades its annotations,
so the delete must never proceed while the item's library storage is
unreachable — the DB row died, the file survived as an orphan, and
the next scan re-imported it bare (annotations gone forever).
resolveMediaStorage stats the library's folders: reachable → delete
proceeds and the file is removed by ABSOLUTE path (the old code
passed a library-relative path to os.Remove, which never resolved —
file deletion has been a silent no-op); every folder unreachable →
409 with 'storage unreachable', row and annotations intact.
2. Latency: resolveLibrary stats every library folder on every SSR
render; folder reachability is now cached with a 30s TTL.
3. README: Network Storage section documenting the outage hysteresis
(mark → archive → auto-restore), the 90-day retention purge
(ARCHIVE_RETENTION_DAYS, 0 = never), refused deletes while storage
is down, and watch mode not firing on network mounts.
Integration tests: storage-down bulk delete refuses with the row
intact; real-file delete removes the file from disk.
Field-by-field audit of the profile surface (set → save → reopen →
read back, driven through the emulator browser via CDP):
- getTemplateUserWithTheme (every SSR page + the /profile self-edit
form) never mapped FirstName/LastName/CreatedAt — stored names
rendered as empty inputs. Now mapped (timezone was already mapped).
- Admin profile-modal Timezone mapping fixed earlier this round held:
all seven fields verified set → 200 + HX-Refresh → reopen → correct
values, DB cross-checked.
- Role promote/demote via the card select verified against the DB (the
PUT answers HX-Refresh, so the page reload shows the new role).
- Admin password reset verified end-to-end: new password logs in, old
one 401s, original restored.
- admin_library folder-remove hx-vals was string-concatenated JSON —
a folder path containing a quote or backslash made it invalid and
the remove button silently died under htmx 2 (the same class as the
profile-save bug). Now built with json.Marshal behind the js:
prefix. Verified the generated attribute on the live page.
- Other hx-vals/hx-headers sites audited: constants and uuids only
(safe charset), left alone.
The family-lens hardening: library_visibility was only enforced where
file bytes flowed (download/reader/OPDS) — metadata surfaces leaked
hidden libraries to any authenticated account, and registration was
open by default with no admin-side user creation.
- Series are grouped PER LIBRARY now (same name in two libraries =
two entries: manga adaptations / LN splits no longer merge);
/api/series aggregates only the requester's VISIBLE libraries and
/api/series/books REQUIRES a visible library_id (breaking contract;
web + app clients updated in the same change).
- /api/media-items, item detail, and the reader content endpoints
(chapters/panels/outline/THUMBNAILS — page images leaked unguarded
next to a gated GetPage) all enforce the visible set; resolveLibrary
no longer honors a hand-typed library_id outside the visible set.
- Annotation/note/bookmark mutations (update/delete/tombstone, sync
LWW paths incl. conflict resolution) carry ownership predicates:
another account's bare row id is a 404, not a silent delete.
- Admin user creation: POST /api/auth/users (AdminMiddleware) + a
Create User card in the admin Users UI — accounts stay creatable
with self-service registration OFF, which is now the schema default
(fresh installs; the first-admin bootstrap exception is unchanged).
- Tests: series contract re-pinned (per-library, visibility 403s),
new ownership + admin-create integration suites, OPDS all=1 test
now passes an explicit page size (shared-DB growth made the fresh
books fall off page 1), device-cap helper switched to the admin
endpoint.
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 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.
- UpdateSystemConfiguration: when base_url changes, automatically
update opds_base_url and api_base_url derived configs
- convertPending: format time.Time as RFC3339 string instead of
relying on string type assertion which would panic
- helpers.go: Promote getText() from a local closure in frontend.go
to a package-level function so it can be used by resolveLibrary.
Add resolveLibrary(c, cfg, user.ID) helper that:
1. Reads library_id query param (explicit navigation wins)
2. Falls back to selectedLibrary cookie — validates __all__
sentinel or real UUID, rejects garbage values silently
3. Falls back to user's first visible library
Returns LibraryResolution struct with LibraryID, IsAll, LibUUID,
Libraries, and FirstID — eliminating repeated boilerplate across
all SSR routes.
- frontend.go: Replace manual library resolution boilerplate in 5
SSR route handlers (series, tags/detail, bookshelf, dashboard,
collections/:id) with resolveLibrary(). Each route now gets cookie-
aware library selection for free. Collection detail correctly
handles All Libraries mode for both system and user collections.
Dashboard no longer makes a redundant second GetUserVisibleLibraries
call.
- Add FormatInTimezone and FormatTimestamptzInTimezone helpers
in templates/utils.go for timezone-aware time display
- Add Timezone field to templates.User struct
- Pass user timezone from DB to template context in helpers.go
- Add timezone update handling in auth.go UpdateProfile with
validation via time.LoadLocation
- Add UpdateTimezoneSettings handler in system_settings.go for
admin system-wide default timezone using UpdateSystemSetting
Remove redundant type conversions that Go 1.26 makes unnecessary or that
were already no-ops:
- uuid.UUID(x.Bytes) → x.Bytes (uuid.UUID is [16]byte, same as pgtype UUID Bytes)
- pgtype.UUID{Bytes: [16]byte(u), Valid: true} → pgtype.UUID{Bytes: u, Valid: true}
- (*time.Time)(&x.Time) → &x.Time
- json.RawMessage(x) → x where x is already []byte
- []byte(stringVal) → stringVal where []byte is expected
- int()/int64()/byte() casts on values already of the target type
- Decompressor(fn) → fn (type is identical)
Handle previously ignored error returns:
- collections.go: check json.Unmarshal error in GetCollection
- conversion_service.go: check fileSize.Scan() error
- app_test.go: check app.Shutdown() error in benchmark
- Add Token field to templates.User struct for passing JWT to frontend
- Modify getTemplateUserWithTheme() to extract token from HttpOnly cookie
- Inject server-side token into templates for WebSocket connections
This change enables templates to access the authentication token directly
from the server, allowing WebSocket URLs to be constructed with the token
already included. This eliminates the need for client-side localStorage
token management and provides a more secure SSR-native approach.
The token is extracted from the existing HttpOnly cookie that JWT middleware
validates, ensuring no additional security surface is introduced.
Update all router files to use Echo v5 APIs and type signatures.
Changes in router.go:
- Replace echomiddleware.Logger() with RequestLogger() (line 144)
- Update import from echo/v4 to echo/v5
Changes in frontend.go:
- Update frontend handler signatures to use *echo.Context
- Fix middleware registration for v5 compatibility
Changes in auth.go, library.go, scanner.go, sync.go, helpers.go:
- Update handler function signatures to *echo.Context
- Ensure consistent type usage across all route handlers
All routes now properly implement Echo v5's middleware and handler patterns.
- Add renderErrorPage helper for consistent error rendering
- Add ensureUserExistsMiddleware to detect deleted users and redirect to login
- Add catch-all 404 handler for unknown routes
- Gracefully handle data loading failures with error messages instead of crashing
- Log errors for debugging while still rendering pages
Create internal/router/ package to organize route registration:
- router.go: Main router setup and configuration
- auth.go: Authentication routes (login, register, profile, etc.)
- docs.go: Documentation routes
- frontend.go: Frontend SSR routes (/, /login, /admin, etc.)
- helpers.go: Helper functions for template rendering
This is the first step in refactoring 858-line main.go into
a more maintainable structure following Go best practices.
Routes themselves have NOT changed - only organization.