feat: separate saved-photo lifecycle from the confirmed log (#36)
Quality gates / quality (pull_request) Successful in 3m9s

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
This commit is contained in:
timmy committed 2026-10-19 08:24:44 +00:00
1 parent fe5c3d40c0
commit ebee52fd8a
11 files changed
+366 -10

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
// Photo lifecycle for saved entries. A confirmed log and the image attached to it are separate
// things a person owns: presence is reported from the stored image bytes, removing the image never
// touches the log, and deleting the log is a separate, explicit decision.
const IMAGE_DATA_URL = /^data:image\/[a-z0-9.+-]+(?:;base64)?,[A-Za-z0-9+/=%.-]+$/i;
export function photoState(entry) {
return IMAGE_DATA_URL.test(String(entry?.photoDataUrl || ''))
? { hasPhoto: true, label: 'Photo saved' }
: { hasPhoto: false, label: '' };
}
export function withoutPhoto(entries, id) {
if (!Array.isArray(entries)) return [];
if (!entries.some(entry => entry.id === id)) return entries;
return entries.map(entry => (entry.id === id ? { ...entry, photoDataUrl: '' } : entry));
}
export function withoutEntry(entries, id) {
if (!Array.isArray(entries)) return [];
if (!entries.some(entry => entry.id === id)) return entries;
return entries.filter(entry => entry.id !== id);
}
export function photoCount(entries) {
return Array.isArray(entries) ? entries.filter(entry => photoState(entry).hasPhoto).length : 0;
}