feat(transcription): let read-only users read transcriptions (read tab only, no edit) (#697) #700

Merged
marcel merged 12 commits from feat/issue-697-readers-read-transcriptions into main 2026-05-31 13:28:38 +02:00
Owner

Closes #697.

Lets a READ_ALL user open and read an existing transcription on the document detail page — read view only, with every edit affordance suppressed. Writers are unchanged.

Behaviour

  • READ_ALL + has transcription → "Transkription lesen" entry control; opens the read view with a plain "Transkription" header (no Lesen/Bearbeiten tabs), no per-block edit/delete/review, no draw.
  • READ_ALL + no transcription → no entry control.
  • WRITE_ALL → unchanged: "Transkribieren", both tabs, full controls.

Approach

hasTranscription is a cheap server-computed boolean on the document detail payload (decided with the maintainer), available at first paint — no client store, no full block fetch, no layout shift.

Backend

  • TranscriptionBlockRepository.existsByDocumentId (cheap EXISTS) → TranscriptionBlockQueryService.hasBlocks.
  • Document gains a @Transient hasTranscription, populated in DocumentService.getDocumentById (the query service was already injected — no new dependency, no cycle).
  • Regenerated the Document TS type.
  • Regression: explicit READ_ALL → 403 on block/annotation write endpoints (create/update/reorder/review/annotation create+patch) and READ_ALL → 200 on the read endpoint — the endpoints are the real boundary, the UI gate is convenience.

Frontend

  • Widened the entry gate to (canWrite || hasTranscription) && isPdf on both the desktop top-bar and the mobile menu; reader label "Transkription lesen".
  • TranscriptionPanelHeader gains canEdit — readers get a plain "Transkription" heading instead of the toggle, status line kept.
  • Confined readers to read mode: onModeChange guarded, panelMode defaults to 'read', writer-only OCR status check skipped.
  • Threaded canAnnotate={canWrite} through DocumentViewerPdfViewer so the annotation layer's canDraw (which also gates delete/resize) is off for readers.
  • i18n: transcription_read_label, transcription_panel_title (de/en/es).

Tests

  • Backend: repository existsByDocumentId, query-service hasBlocks, DocumentService populates hasTranscription, and the READ_ALL→403 / READ_ALL→200 permission boundary (239 backend tests green locally; Testcontainers repo test green).
  • Frontend (run in CI): DocumentTopBarActions / DocumentMobileMenu reader-gate cases, TranscriptionPanelHeader canEdit, PdfViewer draw-surface gating, and an E2E happy path (reader opens read view, no edit tab, cannot switch to edit).

TDD throughout (red → green → atomic commit). Companion to #696 (the simple write-control gates, already on main).

🤖 Generated with Claude Code

Closes #697. Lets a `READ_ALL` user open and read an existing transcription on the document detail page — read view only, with every edit affordance suppressed. Writers are unchanged. ## Behaviour - **READ_ALL + has transcription** → "Transkription lesen" entry control; opens the read view with a plain "Transkription" header (no Lesen/Bearbeiten tabs), no per-block edit/delete/review, no draw. - **READ_ALL + no transcription** → no entry control. - **WRITE_ALL** → unchanged: "Transkribieren", both tabs, full controls. ## Approach `hasTranscription` is a **cheap server-computed boolean on the document detail payload** (decided with the maintainer), available at first paint — no client store, no full block fetch, no layout shift. ### Backend - `TranscriptionBlockRepository.existsByDocumentId` (cheap EXISTS) → `TranscriptionBlockQueryService.hasBlocks`. - `Document` gains a `@Transient hasTranscription`, populated in `DocumentService.getDocumentById` (the query service was already injected — no new dependency, no cycle). - Regenerated the `Document` TS type. - Regression: explicit READ_ALL → 403 on block/annotation **write** endpoints (create/update/reorder/review/annotation create+patch) and READ_ALL → 200 on the read endpoint — the endpoints are the real boundary, the UI gate is convenience. ### Frontend - Widened the entry gate to `(canWrite || hasTranscription) && isPdf` on both the desktop top-bar and the mobile menu; reader label "Transkription lesen". - `TranscriptionPanelHeader` gains `canEdit` — readers get a plain "Transkription" heading instead of the toggle, status line kept. - Confined readers to read mode: `onModeChange` guarded, `panelMode` defaults to `'read'`, writer-only OCR status check skipped. - Threaded `canAnnotate={canWrite}` through `DocumentViewer` → `PdfViewer` so the annotation layer's `canDraw` (which also gates delete/resize) is off for readers. - i18n: `transcription_read_label`, `transcription_panel_title` (de/en/es). ## Tests - Backend: repository `existsByDocumentId`, query-service `hasBlocks`, `DocumentService` populates `hasTranscription`, and the READ_ALL→403 / READ_ALL→200 permission boundary (239 backend tests green locally; Testcontainers repo test green). - Frontend (run in CI): `DocumentTopBarActions` / `DocumentMobileMenu` reader-gate cases, `TranscriptionPanelHeader` `canEdit`, `PdfViewer` draw-surface gating, and an E2E happy path (reader opens read view, no edit tab, cannot switch to edit). TDD throughout (red → green → atomic commit). Companion to #696 (the simple write-control gates, already on main). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
marcel added 9 commits 2026-05-31 12:23:57 +02:00
Cheap EXISTS query backing a server-side "has a transcription" signal so
read-only users can be offered the read view at first paint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Domain-service wrapper over existsByDocumentId so other domains can ask
"does this document have any transcription blocks?" without reaching into
the repository.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getDocumentById now populates a transient hasTranscription boolean so the
document detail page can gate the transcription entry control at first
paint (no client store, no full block fetch, no layout shift).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Read-only users will soon be able to open the transcription read view, so
the write endpoints become the real authorization boundary. Explicitly
assert a READ_ALL-only principal is forbidden from create/update/reorder/
review block writes and annotation create/patch (the prior tests only used
a no-authority principal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirrors the new server-computed boolean on the document detail payload so
the frontend can gate the transcription entry control at first paint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
transcription_read_label ("Transkription lesen") for the read-only entry
control and transcription_panel_title ("Transkription") for the plain
header readers see instead of the Lesen/Bearbeiten toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TranscriptionPanelHeader gains a canEdit prop (default true). Editors keep
the Lesen/Bearbeiten segmented toggle; read-only users get a plain
"Transkription" heading instead of a lone single-option pill, while the
"N Abschnitte" status line stays visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On the document detail page, pass canEdit={canWrite} to the panel header,
guard onModeChange so a reader can never flip to edit, and default panelMode
to 'read' for readers. Thread canAnnotate={canWrite} through DocumentViewer
to PdfViewer so the annotation layer's canDraw (which also gates delete and
resize) is off for readers — they can open and read, but not draw, edit, or
delete. The writer-only OCR status check is also skipped for readers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test(e2e): read-only user reads a transcription, no edit affordances (#697)
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m35s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m27s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 19s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m2s
bee309e40c
CI happy path: seed a PDF document with a transcription block as admin, then
as the READ_ALL "reader" open it — assert the "Transkription lesen" control,
the read text, a plain "Transkription" header, and the absence of the
Lesen/Bearbeiten tabs (panel cannot switch to edit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marcel added 2 commits 2026-05-31 12:44:32 +02:00
Move the hasTranscription existence query out of the shared getDocumentById
into a dedicated getDocumentDetail used solely by GET /api/documents/{id}.
The flag is only consumed by the detail page, so the extra EXISTS query no
longer runs for the many internal getDocumentById callers (e.g. the
Geschichte resolve loop and the dashboard resume path). Behaviour of the
detail endpoint is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test(security): lock READ_ALL -> 403 on comment-write endpoints (#697)
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m34s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m26s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m4s
beaf86558d
Round out the "read-only users can't write anything" boundary: a READ_ALL
principal is forbidden from posting a block comment, replying, and editing a
comment (the prior tests only used a no-authority principal for create).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marcel added 1 commit 2026-05-31 12:59:14 +02:00
test(document): run OCR-status page tests as a writer (#697)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m16s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m30s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 19s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s
bd3c1ced1d
The OCR status check is now gated behind canWrite (readers do no write-path
work), so the two OCR-status page tests must render as a writer — OCR is a
writer action. Without canWrite the status check never fires and the "OCR
läuft" spinner never mounts. Fixes the CI regression introduced by confining
read-only users to the read view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marcel merged commit 2cc43c3c44 into main 2026-05-31 13:28:38 +02:00
marcel deleted branch feat/issue-697-readers-read-transcriptions 2026-05-31 13:28:39 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#700