feat: separate saved-photo lifecycle from the confirmed log (#36) #80

Closed
rockachopa wants to merge 0 commits from timmy/36-photo-lifecycle-controls into main
pull from: timmy/36-photo-lifecycle-controls
Member

Closes #36

What changed

A confirmed log and the image attached to it are now separate things the person owns:

  • Presence without exposure — a journal row that holds a saved photo shows a Photo saved label; no thumbnail is rendered in the list or when the log is opened.
  • Explicit reveal — Show photo renders the stored bytes from this browser only after the person asks for it.
  • Remove photo, keep log — clearing the image leaves every confirmed field (form, colour, urgency, discomfort, note, date) untouched.
  • Delete log is separate — a two-step confirm (Delete this log and its photo → Tap again…) removes the log and its image together; the first tap only arms it.
  • Delete-all — removes the ledger and every stored photo; regression asserted that no data:image bytes remain in storage.
  • Privacy copy now states that one photo can be removed without deleting its confirmed log, and delete-all names photos explicitly.

New src/photo-lifecycle.js decides presence from the stored image bytes (not a filename or flag) and returns new lists from withoutPhoto / withoutEntry; the ledger passed in is never mutated.

Acceptance criteria → evidence

  • Photo presence visible without thumbnails by default — browser acceptance asserts one label, zero .entries-card img, and zero #entry-photo before an explicit request.
  • Deleting a photo does not delete the confirmed log unless requested — acceptance asserts the log count is unchanged, photoDataUrl is cleared, and the other confirmed fields still match; the separate two-step log delete is asserted to require two taps.
  • Delete-all removes ledger and photos and is regression tested — acceptance asserts the ledger key is removed and JSON.stringify(localStorage) contains no data:image.

Tests

  • Unit: npm test — 192 pass, 0 fail (6 new in tests/photo-lifecycle.test.js).
  • Browser: npm run test:photo-lifecycle — pass. Includes visual acceptance (label fits 390px, no horizontal scroll, ≥44px touch targets) and Escape-dismisses-dialog.
  • Regression: all 12 browser acceptance suites pass (ui, photo, mobile-capture, sleek, manual-fallback, provisional, followup, vision-refusal, worker-readiness, pattern-summary, grounded-chat, photo-lifecycle).
  • npm run check:syntax and npm run check:diff pass.

TDD note: each unit behaviour and the browser behaviour were written first and observed failing for the missing feature (module not found; zero photo indicators), then implemented to green.

Demonstration path

scripts/record_release_demo.mjs now records the lifecycle on camera with a synthetic 1x1 PNG constant (no real photo): label without thumbnail → open log without revealing → explicit Show photo → Remove photo with the log and fields intact → delete-all performed (dialog accepted) with an assertion that no image bytes survive. scripts/build_release.py gates four new captions, and a recording of this branch completed successfully (61 captions, receipt includes the release delta).

Boundary

No diagnosis, no new inference, no inference exposure, no model weights, no real medical images, no production service change. Photos remain in this browser only; the chat path still excludes them.

Closes #36 ## What changed A confirmed log and the image attached to it are now separate things the person owns: - **Presence without exposure** — a journal row that holds a saved photo shows a `Photo saved` label; no thumbnail is rendered in the list or when the log is opened. - **Explicit reveal** — `Show photo` renders the stored bytes from this browser only after the person asks for it. - **Remove photo, keep log** — clearing the image leaves every confirmed field (form, colour, urgency, discomfort, note, date) untouched. - **Delete log is separate** — a two-step confirm (`Delete this log and its photo` → `Tap again…`) removes the log and its image together; the first tap only arms it. - **Delete-all** — removes the ledger and every stored photo; regression asserted that no `data:image` bytes remain in storage. - Privacy copy now states that one photo can be removed without deleting its confirmed log, and delete-all names photos explicitly. New `src/photo-lifecycle.js` decides presence from the stored image bytes (not a filename or flag) and returns new lists from `withoutPhoto` / `withoutEntry`; the ledger passed in is never mutated. ## Acceptance criteria → evidence - [x] Photo presence visible without thumbnails by default — browser acceptance asserts one label, zero `.entries-card img`, and zero `#entry-photo` before an explicit request. - [x] Deleting a photo does not delete the confirmed log unless requested — acceptance asserts the log count is unchanged, `photoDataUrl` is cleared, and the other confirmed fields still match; the separate two-step log delete is asserted to require two taps. - [x] Delete-all removes ledger and photos and is regression tested — acceptance asserts the ledger key is removed and `JSON.stringify(localStorage)` contains no `data:image`. ## Tests - Unit: `npm test` — **192 pass, 0 fail** (6 new in `tests/photo-lifecycle.test.js`). - Browser: `npm run test:photo-lifecycle` — pass. Includes visual acceptance (label fits 390px, no horizontal scroll, ≥44px touch targets) and Escape-dismisses-dialog. - Regression: all 12 browser acceptance suites pass (`ui`, `photo`, `mobile-capture`, `sleek`, `manual-fallback`, `provisional`, `followup`, `vision-refusal`, `worker-readiness`, `pattern-summary`, `grounded-chat`, `photo-lifecycle`). - `npm run check:syntax` and `npm run check:diff` pass. TDD note: each unit behaviour and the browser behaviour were written first and observed failing for the missing feature (module not found; zero photo indicators), then implemented to green. ## Demonstration path `scripts/record_release_demo.mjs` now records the lifecycle on camera with a synthetic 1x1 PNG constant (no real photo): label without thumbnail → open log without revealing → explicit `Show photo` → `Remove photo` with the log and fields intact → delete-all performed (dialog accepted) with an assertion that no image bytes survive. `scripts/build_release.py` gates four new captions, and a recording of this branch completed successfully (61 captions, receipt includes the release delta). ## Boundary No diagnosis, no new inference, no inference exposure, no model weights, no real medical images, no production service change. Photos remain in this browser only; the chat path still excludes them.
rockachopa added 1 commit 2026-10-19 08:25:09 +00:00
feat: separate saved-photo lifecycle from the confirmed log (#36)
Quality gates / quality (pull_request) Successful in 3m9s
ebee52fd8a
A confirmed log and the image attached to it are separate things the
person owns. The journal now reports a saved photo as a label instead of
a thumbnail, reveals the image only on explicit request, removes the
photo without touching any confirmed field, and deletes a log through a
separate two-step decision that takes its photo with it. Delete-all is
regression tested to remove the ledger and every stored photo together.

- src/photo-lifecycle.js: presence decided by stored image bytes;
  withoutPhoto/withoutEntry return new lists and never mutate the ledger
- app.js: journal rows expose Open-entry controls; entry sheet states
  presence, offers explicit reveal, remove-photo-keeps-log, and a
  two-step log delete; privacy copy names photo removal and delete-all
- tests: 6 unit cases + browser acceptance (presence, no default
  thumbnail, explicit reveal, remove keeps log, two-step delete,
  delete-all leaves no image bytes, Escape dismiss, 44px targets)
- record_release_demo.mjs: on-camera photo lifecycle scenes driven by a
  synthetic 1x1 PNG, with delete-all actually performed and asserted;
  build_release gates the new captions
- CI/release/syntax gates include the new suite

Closes #36
Author
Member

Release review (2026-10-19 nightly gate; recorded as a comment because Gitea blocks self-approval on a PR authored by the same account).

Verified at head ebee52f in a clean checkout:

  • Focused diff: 11 files, +366/-10, no unrelated churn, no secrets, credentials, model weights, or real medical images.
  • Unit suite: 192/192 pass, including the new photo-lifecycle unit test file.
  • New photo-lifecycle browser acceptance flow passes end to end: presence label, zero default thumbnails, explicit reveal, remove-photo-keeps-log, two-step delete, delete-all leaves no image bytes.
  • Privacy copy matches behavior: photo removal is named separately from log deletion; delete-all names photos.
  • Demo script adds on-camera scenes for these behaviors using a synthetic 1x1 PNG, and build_release.py gates the new captions.
  • Links issue #36 (priority/P1, type/feature), not a human-gate ticket.

Approved for merge and tonight's release.

Release review (2026-10-19 nightly gate; recorded as a comment because Gitea blocks self-approval on a PR authored by the same account). Verified at head ebee52f in a clean checkout: - Focused diff: 11 files, +366/-10, no unrelated churn, no secrets, credentials, model weights, or real medical images. - Unit suite: 192/192 pass, including the new photo-lifecycle unit test file. - New photo-lifecycle browser acceptance flow passes end to end: presence label, zero default thumbnails, explicit reveal, remove-photo-keeps-log, two-step delete, delete-all leaves no image bytes. - Privacy copy matches behavior: photo removal is named separately from log deletion; delete-all names photos. - Demo script adds on-camera scenes for these behaviors using a synthetic 1x1 PNG, and build_release.py gates the new captions. - Links issue #36 (priority/P1, type/feature), not a human-gate ticket. Approved for merge and tonight's release.
rockachopa closed this pull request 2026-10-19 12:02:46 +00:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.