docs(admin): scanning page — the shallow vs. force taxonomy from source
First page of the admin tree, written to the new admin tier (what the
click actually does). Documents every scan surface with its verified
semantics: Force Rescan All Libraries (dashboard, force, all libraries,
one job each with skip-on-active), the per-library Scan button (ALSO a
force scan — hx-vals force:true since the button's birth commit 706be09d,
which corrects the owner's stated 'other scan buttons are shallow'
belief; flagged to the owner), Scan this folder (shallow, single-folder
scope, archive pass skipped for partial scans, podcast roots refused),
Auto-Scan + Scan Interval (the watcher + poll + startup triggers, all
shallow, default 60s range 1-3600), Processing Issues retry (per-item
sequential, deliberately not a library scan), and book-level Rescan /
Reset to Scanned. Plus the standing rules: one scan per library
(coalescing), background jobs with the dashboard progress panel, and
the two-consecutive-scans archive rule.
Glossary gains Scan and Scan this folder (same-commit rule for controls
cited in prose). The real-tree nav test's Administration assertion now
pins shape (Admin Guide, Libraries prefix + usabilityOrder subsequence)
instead of an exact slice, so pages landing one at a time don't churn it.
This commit is contained in:
1 parent
20754f15e2
commit
89fc479bdb
3 files changed
+119
-5
No files matched your search
@@ -0,0 +1,89 @@
|
|||||||
|
<!-- Ground truth: internal/handlers/scanner.go (ScanLibrary, StartScanner,
|
||||||
|
per-folder validation, coalescing), internal/services/media_scanner.go
|
||||||
|
(forceRescan semantics, steady-skip path, watcher/poll/initial
|
||||||
|
triggers, enqueueLibraryScan force:false), internal/services/worker.go
|
||||||
|
(SetForce), templates/admin.templ + web/src/admin.ts (dashboard
|
||||||
|
button, one job per library), templates/admin_library.templ +
|
||||||
|
templates/utils.go (per-library Scan force:true, per-folder scan),
|
||||||
|
templates/admin_settings.templ (Auto-Scan, Scan Interval).
|
||||||
|
Re-verify against these when scanning behavior changes. -->
|
||||||
|
|
||||||
|
# Scanning
|
||||||
|
|
||||||
|
Scanning is how Bookhoard's server reads your library folders and keeps the
|
||||||
|
catalog in step with the files on disk. This page covers every scan an
|
||||||
|
admin can trigger, what each one actually does, and what it costs — so you
|
||||||
|
can pick the right one before clicking.
|
||||||
|
|
||||||
|
## The one distinction: shallow vs. force
|
||||||
|
|
||||||
|
Every scan is one of two kinds:
|
||||||
|
|
||||||
|
- **Shallow** (the default). The scanner walks the folder and skips any
|
||||||
|
file whose size and modification time are unchanged since it was last
|
||||||
|
seen. New and changed files get processed; untouched files cost nothing.
|
||||||
|
Fast, and the right choice for day-to-day keeping-up.
|
||||||
|
- **Force**. Nothing is skipped. Every file is re-read and re-extracted
|
||||||
|
with the scanner's current parsing, and each item's hash identifiers
|
||||||
|
(the SHA-256 fingerprints used for duplicate detection and device book
|
||||||
|
matching) are recomputed. This is the backfill tool: run it after
|
||||||
|
scanner improvements you want applied to already-scanned books, after
|
||||||
|
mass edits in Calibre or on disk, or when metadata looks stale anywhere
|
||||||
|
in a library. It is deliberately slow on big libraries.
|
||||||
|
|
||||||
|
## Every scan control, and what it does
|
||||||
|
|
||||||
|
| Control | Where | Kind | Scope |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **Force Rescan All Libraries** | Admin dashboard → Quick Actions | Force | Every library |
|
||||||
|
| **Scan** (library row) | Admin → Libraries | Force | That library |
|
||||||
|
| **Scan this folder** (folder-row icon) | Admin → Libraries → a library's panel | Shallow | That folder's subtree |
|
||||||
|
| **Auto-Scan** + **Scan Interval (seconds)** | Admin → Settings → Scanning | Shallow | Every library |
|
||||||
|
| **Retry** / **Retry all** | Admin → Libraries → Processing Issues | Per-item | Failed items only |
|
||||||
|
| **Rescan** / **Reset to Scanned** | A book's detail page | Per-item | That one item |
|
||||||
|
|
||||||
|
The details behind the table:
|
||||||
|
|
||||||
|
- **Force Rescan All Libraries** loops over every library and starts a
|
||||||
|
force scan job for each. Libraries that already have a scan running are
|
||||||
|
left alone — the button reports which libraries started and which were
|
||||||
|
skipped because a scan was already in flight.
|
||||||
|
- **Scan** on a library row is the same force rescan, scoped to that one
|
||||||
|
library. It shares the dashboard button's semantics exactly.
|
||||||
|
- **Scan this folder** is the only folder-sized scan, and the only shallow
|
||||||
|
one an admin triggers by hand: unchanged files are skipped, and only
|
||||||
|
that folder's subtree is walked. Because a partial scan can't vouch for
|
||||||
|
what it didn't look at, it never marks items from the library's *other*
|
||||||
|
folders as missing. Podcast download folders refuse this button — they
|
||||||
|
are managed by the feed system, not the scanner.
|
||||||
|
- **Auto-Scan** arms the two automatic triggers, both shallow: a
|
||||||
|
filesystem watcher that fires as files change, and a periodic poll that
|
||||||
|
runs every **Scan Interval (seconds)** (default 60, allowed 1–3600).
|
||||||
|
The same shallow pass also runs once at startup when auto-scan is on.
|
||||||
|
- **Retry** and **Retry all** on a library's Processing Issues page send
|
||||||
|
failed items back through the scanner one at a time — deliberately not
|
||||||
|
a library-wide scan, so fixing a few bad files never re-reads the whole
|
||||||
|
library.
|
||||||
|
- **Rescan** on a book's page re-extracts that item from its file and
|
||||||
|
recomputes its hashes. **Reset to Scanned** clears your manual metadata
|
||||||
|
edits first, so what lands is purely what the scanner derives from the
|
||||||
|
file.
|
||||||
|
|
||||||
|
## Rules worth knowing
|
||||||
|
|
||||||
|
- **One scan per library at a time.** Starting a second scan for a library
|
||||||
|
that already has one pending or running is refused — the dashboard
|
||||||
|
tells you a scan is active rather than stacking a duplicate. The
|
||||||
|
automatic triggers skip silently instead.
|
||||||
|
- **Scans run in the background.** The dashboard's Scanning panel shows
|
||||||
|
per-library progress while jobs run; **Cancel all scans** stops the ones
|
||||||
|
still queued or running, and you can keep working meanwhile.
|
||||||
|
- **Missing files archive after two consecutive scans.** When a
|
||||||
|
library-wide scan (shallow or force) can't find a file twice in a row,
|
||||||
|
the item is archived — reading history is kept in case the file comes
|
||||||
|
back. The Archived Items page in this section manages the results.
|
||||||
|
- **Metadata comes from the files themselves.** The scanner reads sidecar
|
||||||
|
files (Calibre's `metadata.opf` in particular), embedded tags, and
|
||||||
|
filenames — [Calibre Integration](../user/calibre-integration.md) covers
|
||||||
|
the sidecar story. What a scan extracts is exactly what a force rescan
|
||||||
|
re-extracts.
|
||||||
@@ -218,6 +218,14 @@ does: Force-rescans every library — re-reads each file, re-extracts metadata w
|
|||||||
also: Scan All Libraries
|
also: Scan All Libraries
|
||||||
note: renamed 2026-11-02 — the old label under-promised a heavy operation
|
note: renamed 2026-11-02 — the old label under-promised a heavy operation
|
||||||
|
|
||||||
|
label: Scan
|
||||||
|
where: Admin → Libraries → a library's card row
|
||||||
|
does: Force-rescans that one library (same semantics as Force Rescan All Libraries, scoped to a single library).
|
||||||
|
|
||||||
|
label: Scan this folder
|
||||||
|
where: Admin → Libraries → a library's panel → a folder row
|
||||||
|
does: Shallow-scans only that folder's subtree — unchanged files are skipped, and items in the library's other folders are never marked missing by a partial scan.
|
||||||
|
|
||||||
label: Resume rewind
|
label: Resume rewind
|
||||||
where: audio player → player settings; profile settings
|
where: audio player → player settings; profile settings
|
||||||
does: Rewinds playback a configured few seconds when you resume.
|
does: Rewinds playback a configured few seconds when you resume.
|
||||||
|
|||||||
@@ -242,11 +242,28 @@ func TestBuildNavigationUsabilityOrder(t *testing.T) {
|
|||||||
t.Errorf("Devices order = %v, want %v", got, wantDevices)
|
t.Errorf("Devices order = %v, want %v", got, wantDevices)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Administration grows as its pages are written; pin what exists today:
|
// Administration grows as its pages are written; pin its shape, not
|
||||||
// the guide leads (pinned landing), Libraries follows.
|
// its size: the guide leads, Libraries follows, and every other item
|
||||||
wantAdmin := []string{"Admin Guide", "Libraries"}
|
// must appear in usabilityOrder's declared relative order.
|
||||||
if got := titles(find("Administration")); !reflect.DeepEqual(got, wantAdmin) {
|
admin := find("Administration")
|
||||||
t.Errorf("Administration order = %v, want %v", got, wantAdmin)
|
if len(admin.Items) < 2 || admin.Items[0].Title != "Admin Guide" || admin.Items[1].Title != "Libraries" {
|
||||||
|
t.Fatalf("Administration must lead with Admin Guide then Libraries, got %+v", admin.Items)
|
||||||
|
}
|
||||||
|
pos := map[string]int{}
|
||||||
|
for i, p := range usabilityOrder["Administration"] {
|
||||||
|
pos[docURL(p)] = i
|
||||||
|
}
|
||||||
|
last := -1
|
||||||
|
for _, item := range admin.Items[2:] {
|
||||||
|
r, ok := pos[item.URL]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Administration item %q (%s) is not in usabilityOrder", item.Title, item.URL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r <= last {
|
||||||
|
t.Errorf("Administration item %q breaks usabilityOrder sequence", item.Title)
|
||||||
|
}
|
||||||
|
last = r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user