feat(ui): hide missing books immediately and mark offline libraries

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.
This commit is contained in:
John O'Keefe
2026-09-13 12:00:49 -04:00
parent 249b1dfe93
commit fe5ab9e5f8
8 changed files with 373 additions and 125 deletions
+13
View File
@@ -12,6 +12,7 @@ import (
"strconv"
"time"
"bookhoard/internal/config"
"bookhoard/internal/database"
"bookhoard/internal/handlers"
"bookhoard/internal/services"
@@ -295,6 +296,18 @@ func registerFrontendRoutes(cfg *Config) {
libraryID := libRes.LibraryID
libData := libRes.Libraries
// Steam Deck behavior: an offline library (all folders missing, e.g.
// an unmounted external drive) shows an empty shelf with a notice
// instead of querying items. Nothing is marked or purged.
if !libRes.IsAll {
for _, l := range libData {
if l.ID == libraryID && l.Offline {
errorMsg = l.Name + " is currently unavailable - its storage is not connected. Your books are safe and will return when the drive is reattached."
break
}
}
}
// Fetch saved filters for SSR
userUUID, _ := uuid.Parse(user.ID)
var savedFilters []database.SavedFilters
+24 -5
View File
@@ -4,6 +4,7 @@ import (
"context"
"log"
"net/url"
"os"
"time"
"bookhoard/internal/database"
@@ -94,11 +95,11 @@ const selectedLibraryCookie = "selectedLibrary"
const allLibrariesSentinel = "__all__"
type LibraryResolution struct {
LibraryID string
IsAll bool
LibUUID pgtype.UUID
Libraries []templates.LibraryData
FirstID string
LibraryID string
IsAll bool
LibUUID pgtype.UUID
Libraries []templates.LibraryData
FirstID string
}
func getText(t pgtype.Text) string {
@@ -132,12 +133,30 @@ func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolu
res.Libraries = make([]templates.LibraryData, len(libraries))
for i, lib := range libraries {
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
// Steam Deck SD-card behavior: a library whose folders are all
// missing (unmounted drive) renders offline - hidden contents, no
// marking, no purging - and returns when the storage does.
offline := false
folders, folderErr := cfg.Queries.GetLibraryFolders(c.Request().Context(), lib.ID)
if folderErr != nil || len(folders) == 0 {
offline = true
} else {
anyFolderExists := false
for _, folder := range folders {
if _, statErr := os.Stat(folder.FolderPath); statErr == nil {
anyFolderExists = true
break
}
}
offline = !anyFolderExists
}
res.Libraries[i] = templates.LibraryData{
ID: libUUID.String(),
Name: lib.Name,
Description: getText(lib.Description),
TypeName: lib.TypeName,
MediaCount: countMap[libUUID.String()],
Offline: offline,
}
}