Add secure credential storage, sessions, and a launch screen

Login now persists properly rather than through the JSON file the test
harness was using.

  dr-plat            SecretStore trait plus a Secret Service backend.
                     Verified against the live GNOME Keyring: store,
                     retrieve, delete, confirm-gone all round-trip.
  Session/SessionStore   splits credentials from settings — the app
                     password goes to the keyring (FR-NC-2), while
                     server, login, chosen root and format selection are
                     ordinary config. A test asserts the credential never
                     appears in the config file.
  LaunchModel        the launch-screen state machine, testable without a
                     display server: sign in, approve in browser, choose
                     folder, tick formats, sign out.
  launch.slint       the screen itself, in its own file.

Absence of a secrets daemon is an explicit degraded mode, not a silent
fallback to plaintext — the screen says sign-in will not persist rather
than letting the user find out next launch. Android's Keystore backend
fails loudly for the same reason: a no-op store would look like it
worked and then lose the credential.

Two bugs caught by tests rather than by running it:

  - fail() after busy() signed the user out, because busy() had already
    discarded the session. A failed *scan* would have logged you out.
    Busy now carries the session.
  - normalise_server upgrades http:// to https:// rather than accepting
    it. NFR-SEC-3 requires TLS, and silently sending a credential in the
    clear is not a decision to make on the user's behalf.

launch.slint is not yet wired into app.slint. Calling slint_build::compile
twice replaces the generated module rather than adding to it, which broke
the other in-flight work on dr-ui; I reverted that immediately. Wiring it
needs an import inside app.slint, which is that work's file to change.

419 tests passing across ten crates.
This commit is contained in:
2026-08-09 15:20:39 +02:00
parent c8bb08e661
commit 09e3043f4c
33 changed files with 3506 additions and 155 deletions
+26
View File
@@ -643,6 +643,16 @@ dir_validator(root)
`folders.etag` must exist from schema v1. Adding it later means a migration plus a full re-scan of
every user's library (ARCH §6.6).
**Validated against a real library, 2026-08-09.** A cold recursive scan of a 17,185-RAW library
(7,836 CR2 + 9,349 DNG) across 334 directories completed in **34.1 s** — `Depth: 1` per directory,
never `Depth: infinity`. Two implementation details worth keeping:
- **The format filter sees through VFS placeholder suffixes**, so a dehydrated `IMG.CR2.nextcloud`
matches as the CR2 it stands for rather than being skipped as an unknown type (§9.0).
- **Pruning is capability-gated, not assumed.** With `LocalEtags` a directory probe costs a request
and proves nothing about children, so it is pure overhead; a test asserts zero probes in that
case. Only `PropagatingEtags` makes an unchanged parent prove an unchanged subtree.
### 8.5 Sidecar conflict resolution
`put` with `Precondition::IfMatch(validator)`. On precondition failure:
@@ -920,6 +930,22 @@ from schema v1.
primary path; server previews are opportunistic. `preview_max_filesize_image` defaults to 50 MB,
excluding many RAWs even where a provider exists.
**Measured against a real instance, 2026-08-09, and the case is stronger than assumed.**
`/core/preview` returned HTTP 400 for *every* parameter combination attempted — including bare
`?fileId=N`, and including a JPEG the same server reported as `nc:has-preview=true`. The cause is
unexplained: it is a server-side preview configuration issue, not a request-shape error on our side.
Recorded as **unexplained rather than understood**, because the distinction matters if someone later
tries to depend on this endpoint. Nothing does today — the connector treats a preview failure as a
miss and falls through to range extraction, which is the designed behaviour rather than a
workaround.
**Range extraction validated on the same library.** 262 KB read from a 21.5 MB DNG in 119 ms —
**1.22% of the file** — yielded camera model and ISO. Extrapolated across the 17,185-file test
library, cataloguing by whole-file fetch would move roughly 370 GB; the range path moves a few MB.
That ratio is the difference between a viable mobile experience and an unusable one, and it is why
§6.7 treats range reads as the mechanism and server previews as a bonus.
### 6.8 Sync is selective, not mirror-style
### 6.9 Android forbids the filesystem-scan model