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.
65 lines
2.6 KiB
Markdown
65 lines
2.6 KiB
Markdown
# 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_items` row cascades into `progress`. 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_since` is 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/missing` lists 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 via `parent_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.
|