Docs - docs/ARCHITECTURE.md: how the pieces fit and why. The request path, the six enforced layers, the data model, the scanner, the job queue, direct-play playback, and the three protocol surfaces over one application. - docs/decisions/: seven ADRs with the alternatives that were rejected — SQLite-with-a-seam, direct-play-only, mark-missing-never-delete, the modular monolith, the one item-access chokepoint, the table-backed queue, and fixed-width timestamps. - README: the layout section was stale (it predated catalog, home, scanner, providers, engine, probe, playback, apitoken, arch and the dto packages) and now lists what exists, plus links to the new docs. Two real bugs, both found by verifying the docs instead of trusting them 1. The CLI could not run on a fresh install. `Config.Validate` created the data and config directories, but the CLI never called it, so `vellum user add` — the recovery path when you are locked out — failed with "unable to open database file" until the server had booted at least once. Split the directory creation into `Config.EnsureDirs`, call it from the CLI, and keep the media root check out of it: an admin command must not fail because a library is unmounted. The CLI tests now point DBPath inside DataDir, exactly as LoadConfig derives it, so they exercise the fresh-install path; removing EnsureDirs fails three of them. 2. A fresh clone could not build at all. `//go:embed all:dist` fails to compile when web/dist is absent, and web/.gitignore expected a `dist/.gitkeep` that was never created. Added it, so a clone builds and the server logs "web UI not embedded; run the frontend build to include it" — the intended behaviour for someone who only wants the API. Also: the httpapi tests depended on a built frontend and FAILED rather than skipping, so the suite was red on any fresh clone. findAsset and readAsset now skip with a message naming the missing step. They still RUN when a bundle is present (verified: zero skips here), so they have not been quietly disabled. Verified from a simulated clone of tracked files only: go build exit 0, 25 packages pass, the server boots and applies all 12 migrations, and the CLI creates a user and takes a backup. gofmt clean, vet exit 0, svelte-check 0/0.
2.6 KiB
2.6 KiB
ADR-003: A vanished file is marked missing, never deleted
Status
Accepted
Date
2026-10-12
Context
A library root is not always available. Drives unmount, NFS has hiccups, and people reorganise folders while a scan is running.
The obvious behaviour for a scanner is to delete rows whose files are gone. That is also the most destructive thing it could do.
Decision
A scan that cannot find a file marks it missing = 1 and records
missing_since. It never deletes. Removal is a separate, explicit, admin-only
action.
Alternatives Considered
Delete on scan
- Pros: simple; the database always reflects the disk.
- Cons: deleting a
media_itemsrow cascades intoprogress. If a drive is unmounted for an hour, every reading position, audiobook chapter and watch-later marker for that drive is gone permanently. - Rejected: it destroys the one thing in the schema that cannot be regenerated. Everything else can be rebuilt by rescanning.
Delete after a grace period, automatically
- Pros: reclaims space without anyone thinking about it.
- Cons: a drive left disconnected for longer than the period loses its history silently. The user gets no warning and no undo.
- Rejected: the failure mode is silent and irreversible.
Keep missing but never surface it
- Pros: none. This was the original state.
- Cons: the worst of both worlds — you cannot see what is missing, and it never goes away. A deleted file was invisible and permanent.
- Rejected: this is the gap this ADR closes.
Consequences
- Reads filter on
missing = 0, so a missing item disappears from every shelf, list and search result without the row being touched. missing_sinceis written only on the transition into missing, so the age says when the file first vanished rather than when it was last rescanned. It is cleared when a file is restored.GET /api/v1/missinglists what has vanished, scoped to the caller's visible libraries.POST /api/v1/libraries/{id}/purge(admin only) deletes only items that are missing and have no present file. It requires an explicit confirmation in the UI that names the count and says progress cannot be regenerated.- The purge cascade —
media_files,progress,item_people,provider_ids,metadata_fetch,assets,collection_items,favorites,item_views,game_saves, child items viaparent_id— is enumerated in the migration, so the destructiveness is visible at the point of the decision. - Verified: purging 19 items leaves 18, only the missing one goes, and a non-missing item's files are untouched. A second purge returns 0.