US09-04: Write the User Manual with Generated Screenshots (#110)
Some checks failed
Test / suites (push) Failing after 2m56s
Test / container (push) Failing after 5m27s

This commit was merged in pull request #110.
This commit is contained in:
2026-08-23 23:37:31 +02:00
parent 282e8b51e6
commit f1442527a2
34 changed files with 997 additions and 37 deletions

47
docs/stages/albums.md Normal file
View File

@@ -0,0 +1,47 @@
# Album proposals
[← Documentation index](../index.md) · [Guided first pass](../first-pass.md)
![Reviewing an album proposal](../images/albums.png)
**What it is for.** Turning a folder of analysed photographs into a name a person
would have chosen, before any folder is touched.
**What you decide.** The final name. The proposal is a starting point with its
reasoning attached; you edit it, accept it, or ignore it.
**What it changes.** A row per album: proposed name, rationale, confidence, and your
final name, with a version. **Approving renames nothing.** Not one file moves at this
stage — approval only marks a name as agreed, and the [rename stage](renames.md) is
where it becomes a plan you have to confirm separately.
**What it refuses.** A name containing `/ \ : * ? " < > |`, a reserved device name, or
one that collides with an existing folder is rejected while you type and again on the
server. Approval is refused when the evidence changed since the proposal was
generated — the proposal is stale, and regenerating is the honest fix. Editing with a
stale version returns a conflict and shows you the server's truth rather than
overwriting it.
## Reading the view
The left pane lists albums with their state: `none`, `proposed`, `edited`,
`approved`, `error`, or `stale`. The right pane shows the evidence the name was built
from — date range, dominant tags, locations, counts — then the suggested name, the
rationale, the confidence, and an editable final name.
The default naming shape is predictable and sortable:
```text
YYYY-MM — Place — Event
YYYY — Event
Place — Event
```
## While a rename is unresolved
Generating and approving proposals is blocked while an interrupted rename is
outstanding, with `409 rename_recovery_required`. Resolve the rename first: building
new names on top of a half-applied move is how a library becomes hard to reason
about.
Next: [renames](renames.md).

39
docs/stages/analysis.md Normal file
View File

@@ -0,0 +1,39 @@
# Analysis
[← Documentation index](../index.md) · [Guided first pass](../first-pass.md)
![The analysis view](../images/analysis.png)
**What it is for.** Describing what a photograph shows, so albums can be named from
evidence and the library can be searched by content.
**What you decide.** When to run it, and over what scope. The descriptions themselves
come from the configured vision provider.
**What it changes.** For each eligible photo: a stored result — description, tags,
approximate year, location hint, model and prompt version — and then an EXIF
checkpoint that merges a **managed caption segment** and additive keywords into the
file, reads them back, and verifies that nothing else moved. The file's SHA-256 is
refreshed after the write.
**What it refuses.** Anything not confirmed SFW. Anything not canonical. A malformed
provider response is quarantined rather than stored. Keywords are only ever added,
never removed, because EXIF keywords carry no ownership and deleting a generated one
could delete yours.
## Running it
The view shows eligible, analysed, pending, and error counts, and the job's live
progress. Starting a second mutating job while it runs is refused — that is the
one-writer rule, not a queue.
Cancelling drains safely: completed items stay completed, and resuming continues
rather than restarting. A photo may be sent to the provider twice if a crash happens
mid-item, but its stored result and its EXIF are written once.
## Costs
Each analysed photo is a provider call. Resolving duplicates first is what keeps that
number honest, and the counts here are the ones to check before starting a large run.
Next: [album proposals](albums.md).

43
docs/stages/archive.md Normal file
View File

@@ -0,0 +1,43 @@
# Archive
[← Documentation index](../index.md) · [Guided first pass](../first-pass.md)
![Archive locations and plans](../images/archive.png)
**What it is for.** Moving a finished album out of active storage onto another disk,
while keeping everything the library knows about it.
**What you decide.** Which album, which destination, and whether to accept the
reclaimed space in exchange for needing that medium mounted to see the originals
again.
**What it changes.** Files are copied to the archive location, verified there, and
only then removed from the library. The asset keeps its id, its hashes, its
decisions, its analysis, its upload history, and a durable preview thumbnail;
`current_path` becomes null and availability becomes `archived_online` or
`archived_offline`.
**What it refuses.** Preflight blocks on: an unverified upload, a checksum that no
longer matches the uploaded bytes, an unmounted or unwritable destination,
insufficient free space plus the configured reserve, a destination that already
holds unexpected paths, a missing durable thumbnail, and any conflicting job. The
source is **never** removed before the archived copy is verified — not because Immich
reported success, which is an ingest, not a backup.
## Offline is not missing
An archived photo whose medium is unplugged is `archived_offline`. It still appears
in search, still participates in duplicate detection through its retained hashes and
thumbnail, and is never reported as lost. If a full-resolution comparison is needed,
the application asks you to mount the named medium rather than guessing.
## Restore
Restore is planned and journaled like everything else: the medium must be online, the
bytes are copied back and verified into a collision-free destination, a new active
path is registered, and a reconciliation scan follows. Existing safety, analysis,
EXIF, and upload state stay valid when hashes match — and when they do not, the
result is a visible `divergent` state rather than silent acceptance of a different
file.
Next: [diagnostics](diagnostics.md).

View File

@@ -0,0 +1,49 @@
# Diagnostics and library statistics
[← Documentation index](../index.md) · [Guided first pass](../first-pass.md)
![Library statistics](../images/statistics.png)
**What it is for.** Answering "is this installation healthy, and what is in this
library?" — the first thing to look at when a stage behaves unexpectedly.
**What you decide.** Nothing. Both are read-only.
**What it changes.** Nothing.
**What it refuses.** Nothing — both are reads, and both stay available while a job
holds the library. That is deliberate: the moment you most need to know what the
installation is doing is while it is busy.
## Statistics
The Stats view summarises the library itself: how many photos are analysed, the
common tags, the distribution across albums and years. It answers questions about
your photographs.
## Diagnostics
Diagnostics answers questions about the *installation*, and lives at
`GET /api/v1/diagnostics` and on the command line:
```bash
python -m photo_pipeline diagnostics
```
It reports the database, write-ahead log, thumbnail cache, uploader reports, backups,
and logs as separate sizes, plus free space, the tool versions it found against the
ones the image pinned, and which locks are held and by whom.
Its warnings are the ones worth acting on:
| warning | means |
|---|---|
| `disk_low` / `disk_critical` | free space is running out; mutating stages stop before the reserve |
| `cache_over_quota` | the thumbnail cache exceeds its configured quota |
| `wal_growth` | the write-ahead log is outgrowing its database — usually a long-running reader |
| `tool_version_drift` | the installed `exiftool` or `immich-go` is not the version the image pinned |
| `legacy_process_active` | the frozen command-line tools are writing this library. Stop them |
| `no_roots` | no library root is configured, so there is nothing to work on |
An empty `warnings` list on a fresh installation is what the
[first-run checklist](../installation.md#first-run-checklist) is looking for.

36
docs/stages/duplicates.md Normal file
View File

@@ -0,0 +1,36 @@
# Duplicate review
[← Documentation index](../index.md) · [Guided first pass](../first-pass.md)
![The duplicate cluster list](../images/duplicates.png)
**What it is for.** Deciding which file is *the* copy of a picture, before anything
expensive or irreversible happens to the others.
**What you decide.** For each cluster: keep the recommended canonical, choose a
different one, keep everything as variants, declare it not a duplicate, or defer.
**What it changes.** Only the decision, recorded as an auditable event. **No file is
ever deleted.** Non-canonical copies stay exactly where they are; they are simply
excluded from analysis and upload.
**What it refuses.** Exact byte and identical-pixel matches may be accepted on the
recommendation. Anything fuzzy — a crop, a re-encode, a burst frame — requires an
explicit confirmation, because a perceptual hash is evidence, not proof.
## Reading a cluster
Each member shows its path, role, pHash distance, and size, with synchronised zoom
across the previews. The recommendation is visible but never pre-applied for fuzzy
matches. The confidence band tells you how much the machine is claiming.
Rejecting a pair records a negative link, so a later rescan does not keep proposing
the same wrong match.
## Why first
Resolving duplicates before safety and analysis is what stops you reviewing and
paying for the same photograph three times. It also means the album evidence is built
from one copy of each picture rather than a skewed count.
Next: [safety review](safety.md).

39
docs/stages/inventory.md Normal file
View File

@@ -0,0 +1,39 @@
# Inventory and discovery
[← Documentation index](../index.md) · [Guided first pass](../first-pass.md)
![The inventory view](../images/inventory.png)
**What it is for.** Finding every supported photo in the configured library roots and
giving each one a permanent identity, so that everything afterwards can refer to a
picture rather than a path.
**What you decide.** Nothing. This stage is the only one with no judgement in it.
**What it changes.** On disk, nothing at all — discovery reads. In the database it
records each photo's path, size, timestamps, full-file SHA-256, normalised pixel hash,
and perceptual hash, and it opens a path-history entry.
**What it refuses.** Anything under an `_IGNORE/` directory is never traversed,
counted, hashed, or previewed. Unsupported extensions and unreadable files are
reported rather than skipped silently. Nothing outside the configured roots is
reachable, including through a symlink.
## Reading the view
Each row is one asset: its current path, availability, whether the file is present,
and its size. The filter searches paths; the availability selector separates active
photos from archived ones.
A **rescan** after moving files around outside the application is the normal way to
reconcile: a file that moved keeps its identity, because identity is the hash and the
id, not the location.
## What good looks like
The count matches what you expect, `_IGNORE/` contents are absent, and nothing shows
as missing. A photo listed as missing means the file is no longer at its recorded
path and no new path matched its hashes — that is a question for you, not something
the scanner resolves by guessing.
Next: [duplicate review](duplicates.md).

46
docs/stages/renames.md Normal file
View File

@@ -0,0 +1,46 @@
# Renames
[← Documentation index](../index.md) · [Guided first pass](../first-pass.md)
![The rename plan preview](../images/renames.png)
**What it is for.** Applying approved album names to the actual folders — the first
stage that changes your filesystem.
**What you decide.** Whether to apply the plan, after reading it. The confirmation
names the plan, its version, and its checksum, so what you approve is what runs.
**What it changes.** Folders move. For each operation: the intent is journaled
*before* the disk is touched, the move is made, `assets.current_path` is updated by
id in one transaction, the old path is closed and a new one opened in path history,
the result is verified, and only then is the operation complete.
**What it refuses.** A plan is invalid — not merely warned about — when a source is
missing or changed, two operations target the same destination, a destination already
exists, a path would leave the library or enter `_IGNORE/`, or a source is a symlink.
Case-only renames and cross-filesystem moves are allowed but flagged, because they
take a different, staged route. A plan whose checksum no longer matches is refused
rather than reinterpreted.
## Reading the preview
Every operation shows both full paths, the kind of move (`rename`, `case-only
rename`, `cross-filesystem move`), how many photos it affects, its journal state, and
any issue with its severity. The plan can be exported as JSON before you commit to it.
**A run cannot be stopped part-way.** It can be interrupted — by a crash, a power
cut, a killed container — and interruption is recoverable, but there is no cancel
button once it starts. That is stated on the confirmation, not hidden in a manual.
## After an interruption
The recovery panel lists each unresolved operation with a verdict read from the
journal *and the disk*: resumable, safely rollbackable, or needing a person. It never
guesses from a missing path alone. Until it is resolved, unrelated mutations are
refused with `409 rename_recovery_required`.
Rollback is recovery, not undo. A completed operation is terminal — the button only
appears for operations that are actually reversible. See
[recovery](../recovery.md).
Next: [upload](uploads.md).

43
docs/stages/safety.md Normal file
View File

@@ -0,0 +1,43 @@
# Safety review
[← Documentation index](../index.md) · [Guided first pass](../first-pass.md)
![The safety review queue](../images/safety.png)
**What it is for.** Deciding which photographs may be sent to a cloud vision
provider. This is the privacy gate the rest of the design is built around.
**What you decide.** `sfw`, `nsfw`, or defer — per photo, or in bulk from the filters.
A local model can score first to order the queue, but the score is a suggestion; the
decision is yours.
**What it changes.** The decision is stored in the database *and* projected into the
file: one mutually exclusive `sfw` or `nsfw` keyword written into `Keywords` and
`Subject`. The write is merged with existing metadata, read back, and verified, and
the file's SHA-256 is refreshed afterwards.
**What it refuses.** An undecided or deferred photo does not reach the vision
provider, and does not reach Immich either. If the read-back shows that a field the
stage does not own has changed, the checkpoint is marked `divergent`, the asset is not
marked verified, and the next mutating stage is blocked rather than proceeding on
metadata nobody trusts.
## The rule that matters
> Only a confirmed `sfw` photo may enter cloud content analysis. A confirmed `nsfw`
> photo skips analysis entirely but remains eligible for upload to Immich once its
> keyword is verified.
That is why marking something NSFW is not a punishment: it routes the photo around an
external service while keeping it in your own library workflow.
The gate is re-checked *after* the provider call as well. If a decision flips to NSFW
while an analysis is in flight, the result is discarded rather than stored.
## Reading the view
The tabs filter by state. Each row shows the path, the model's score, its suggestion,
your decision, and an `✓ exif` badge once the keyword is verified on disk. A row
without that badge has a decision the file does not yet carry.
Next: [analysis](analysis.md).

45
docs/stages/uploads.md Normal file
View File

@@ -0,0 +1,45 @@
# Upload
[← Documentation index](../index.md) · [Guided first pass](../first-pass.md)
![Upload preflight](../images/uploads.png)
**What it is for.** Sending an approved album to Immich through `immich-go`, with an
audit trail of exactly which bytes went.
**What you decide.** Which albums, and whether to proceed once the preflight has
listed its blockers. Upload is never an automatic consequence of renaming.
**What it changes.** Nothing locally. On the Immich server, assets appear. Locally a
batch is recorded: the album, the asset ids, the redacted command, the uploader
version, the pre-upload SHA-256 **and** the SHA-1 Immich matches on, the output log,
and the outcome counts.
**What it refuses.** Preflight blocks on: missing credentials, an unreachable server,
a missing `immich-go`, an unresolved rename, an undecided or deferred safety
decision, an unverified EXIF checkpoint, incomplete analysis for an SFW photo, a file
whose bytes changed since it was checkpointed, and a partial scope you have not
explicitly accepted. The approval is then **re-proved immediately before the uploader
runs** — bytes edited after you approved fail with `stale_preflight` and the uploader
never starts.
## Reading the view
The scope shows exactly which albums and how many photos. The command preview is the
real command with the API key removed; the key never reaches the browser and never
reaches a log.
## Uncertain is not failed
If the process dies after the server accepted files, the batch becomes
`unknown_requires_verification` — not "failed", and **not retryable**. The server may
already hold the photographs, and uploading again would create duplicates or
upgrades. Verification asks Immich about the recorded SHA-1 and answers present,
absent, or inconclusive; an inconclusive answer is resolved by you, with the evidence
recorded.
If a file's bytes change after a successful upload, it is marked
`changed_after_upload` and you are warned that uploading again may create or upgrade
an asset rather than doing nothing.
Next: [archive](archive.md).