## Outcome Read-only audit completed at commit `500fc56b5f3cf27413ff2d449279bef3bad191e0`. I inspected source, schema, service units, migrations, and synthetic tests only. **No production health rows or media were opened, and no files were modified.** ## Minimal implementation map ### 1. Shared media validation/storage module Create `scripts/health/dashboard_v5/media_validation.py` and consolidate: - `capture_media.detect_mime()`, `validate_image()`, `_read_private()` - `document_review.detect_magic()`, `validate_upload_file()`, `open_quarantine_source()` - Descriptor-based helpers for: - image/JPEG/PNG/HEIC validation; - HEIC decode with local `pillow_heif`; - normalized metadata-free JPEG/PNG generation; - HEVC validation through `ffprobe` using `/proc/self/fd/` and `pass_fds`; - bounded local `ffmpeg` preview/poster generation; - exclusive `0600` output, `0700` roots, SHA-256, `O_NOFOLLOW`, descriptor pinning, and post-write verification. Document intake must preserve the original but pass **only a normalized image** to OCR. Mobile images must no longer promote the uploaded bytes unchanged. ### 2. Additive Sprint 6I-B schema Add: - `scripts/health/dashboard_v5/sprint6i_b_schema.py` - `scripts/health/migrate_sprint6i_b_schema.py` Minimal tables: - `media_objects` - opaque media ID; - `image|video`; - original/preview internal opaque names; - original/preview MIME and hashes; - bounded byte size, dimensions, duration/codec; - processing status and timestamps. - `media_links` - opaque link ID and media ID; - nullable FK targets for `capture_entries`, `symptom_log`, `health_events`, and `dokumente`; - optional canonical `health_day`; - role/order; - CHECK that each row names exactly one target. Keep `capture_attachments`, existing document columns, and V4 untouched for compatibility. New readers can union legacy attachments while new writes use the generic tables. ### 3. Upload contracts Update `dashboard_v5/capture_contract.py::validate_capture_payload()`: - retain bounded multi-image maximum, currently **3**; - add at most one video token; - distinguish token media kind; - prohibit video for unsupported capture contexts if required; - bump the capture contract rather than overloading version 1. Update `dashboard_v5/document_review.py`: - make `validate_upload_file()`, `quarantine_upload()`, `load_quarantine()`, and `open_quarantine_source()` thin wrappers over the shared validator; - modify `extract_pages()` so HEIC/JPEG/PNG OCR receives a normalized temporary image, never the original; - preserve PDF text-layer/OCR behavior. ### 4. Server boundary Update `health_dashboard_server.py`: - `_handle_capture_upload()`: - shared image/HEIC/HEVC validation; - strict claimed MIME plus magic/decoder/probe agreement; - separate image/video limits. - `_handle_document_upload()`: - reuse the same image/HEIC validator while retaining PDF support. - Add `open_media_variant(database, media_id, variant)` using descriptor-safe root traversal, owner/mode/type/size/hash checks. - Add authenticated routes: - `GET|HEAD /api/v1/media//original` - `GET|HEAD /api/v1/media//preview` - Preserve `no-store`, `nosniff`, `no-referrer`, CSP, exact query rejection, and independent GET/HEAD reopening. - Keep current document-original and capture-thumbnail routes as compatibility aliases. ### 5. Worker integration Update `health_dashboard_action_worker.py`: - `apply_capture_action()`: - replace `promote_attachment()` with shared promotion; - support multiple normalized images plus one HEVC video; - deduplicate by original SHA-256; - insert `media_objects` and links to the capture entry/day; - capture `cursor.lastrowid` from derived `symptom_log`/`health_events` inserts and add those links; - rollback only newly created files, never reused canonical objects. - `apply_document_import()`: - promote original and preview as a media object; - link it to `dokumente` and `investigation_day`/document day; - OCR only the normalized preview; - retain the original descriptor through copy/hash validation. - `retry_document_extraction()`: - resolve the linked media object and regenerate/use normalized preview rather than copying by suffix and OCRing the original path. ### 6. Read API Update `dashboard_v5/read_api.py`: - `_capture_timeline()`: - return media kind plus opaque `original_url`/`preview_url`; - retain a legacy `capture_attachments` adapter. - `_day_symptoms()`, `_day_events()`, `_day_documents()`, `_day()`: - select stable row IDs internally and join `media_links`; - expose only opaque media URLs; - include day-linked media without exposing numeric IDs or filenames. - `_record_document_rows()`, `_record_document_detail()`, `_record_document_extracted_preview()`, `_document_review_workspace()`: - derive original/preview statuses from `media_objects`; - retain reviewed-only detail/FTS/report boundaries. - `dispatch_api()`: - no binary streaming here; server owns media routes. - Add only bounded media metadata if a separate JSON endpoint is needed. ### 7. Runtime and tests Update both systemd units so the exact server and worker interpreters contain `pillow_heif`; retain local `ffprobe`/`ffmpeg` access and private roots. Add focused synthetic tests: - `tests/test_dashboard_v5_sprint6i_b.py` - Sprint 6I-B synthetic fixture/browser test - HEIC decode/normalization and EXIF removal; - malformed HEIC and valid-magic/undecodable input; - HEVC wrong codec/container, over-duration, dimensions, truncation; - one-video/multi-image bounds; - symlink, FIFO, mode, size, hash mismatch, and path-swap probes; - descriptor-pinned GET/HEAD; - symptom/event/document/day links; - reviewed-boundary and V4 checksum regression; - DB/media copy-first backup and restore manifest verification. ## Concrete blockers 1. **Runtime mismatch:** `/usr/bin/python3`, used by `health-dashboard.service`, does **not** have `pillow_heif`; the worker venv does. Current host has `ffprobe`/`ffmpeg` 6.1.1 and worker `pillow_heif` 1.4.0. 2. **HEVC policy is unspecified:** maximum bytes, exact “short” duration, dimensions/FPS, accepted containers/HEVC profiles, audio policy, and preview format must be frozen before implementing fail-closed validation. 3. **Current mobile promotion violates normalization intent:** `capture_media.promote_attachment()` writes uploaded JPEG/PNG bytes unchanged, preserving possible EXIF/GPS; only its thumbnail is re-encoded. 4. **HEIC behavior is inconsistent:** mobile explicitly rejects HEIC, while document intake accepts it with `pillow_heif` but then sends the original HEIC directly to Tesseract. 5. **No video model exists:** capture contract, schema MIME CHECK, uploader, worker, read API, and serving routes are image-only. 6. **No unified links exist:** capture media links only to `capture_entries`; derived symptom/event insert IDs are discarded; document originals remain path columns; day association is inferred rather than represented. 7. **Serving is incomplete:** mobile exposes thumbnail only; documents expose original only; `document_originals.py` does not recognize HEIC; there is no generic opaque original/preview route. 8. **Migration/backup scope must expand:** current media backup documentation covers only `private-media/capture/`, not document originals, normalized previews, or video derivatives. **Repository state remained clean; created/modified files: none.**