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
+12
View File
@@ -20,6 +20,7 @@ pub fn resolve(key: &str) -> String {
"op.brilliance" => "Brilliance".into(),
"op.vibrance" => "Vibrance".into(),
"op.saturation" => "Saturation".into(),
"op.framing" => "Crop & Rotate".into(),
// Parameters
"param.temperature" => "Temperature".into(),
@@ -33,6 +34,17 @@ pub fn resolve(key: &str) -> String {
"param.vibrance" => "Vibrance".into(),
"param.saturation" => "Saturation".into(),
// Framing. "Straighten" rather than "Angle" because that is the task
// the control performs; the number it reports is still degrees.
"param.angle" => "Straighten".into(),
"param.rotation" => "Rotate".into(),
"param.flip_h" => "Flip Horizontal".into(),
"param.flip_v" => "Flip Vertical".into(),
"param.crop_x" => "Crop Left".into(),
"param.crop_y" => "Crop Top".into(),
"param.crop_w" => "Crop Width".into(),
"param.crop_h" => "Crop Height".into(),
other => derive(other),
}
}