Vellum/docs/decisions/ADR-003-mark-missing-never-delete.md
mithral 051169a18a Documentation, and make a fresh clone actually work
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.
2026-10-12 00:18:11 -04:00

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.