feat: Annotation-backed collaborative transcription system #176

Closed
opened 2026-04-05 09:29:30 +02:00 by marcel · 8 comments
Owner

Summary

Implement a collaborative transcription system where users draw turquoise rectangles on PDF scans to mark text regions, then type transcriptions into linked text blocks. Includes block-level comment threads with quoted text selections, version history, and mode exclusivity (transcribe mode vs annotate mode).

This is the core transcription editing experience — the counterpart to the read mode (separate issue).

Motivation

Family members need to collaboratively transcribe handwritten letters. The annotation-backed approach links each transcription block to a specific region on the scan, enabling scroll-sync and visual correspondence between handwriting and typed text.

Spec

📄 docs/specs/annotation-transcription-final-spec.html — open locally in browser for full mockups

Key screens

Screen Description
S1 Desktop transcribe mode — block cards with comment threads
S2 Comment flow detail — select text → auto-quote → discuss
S3 History panel open — version timeline per block
S4 Mobile transcribe mode

Core concepts

Annotation-backed blocks

  • User draws a turquoise rectangle on the PDF scan
  • A transcription_block is created, linked to the annotation via annotation_id
  • Block appears in the right panel as an editable card with contenteditable text
  • Blocks are ordered by sort_order (manual drag) or annotation Y-position

Mode exclusivity

  • Transcribe mode: only turquoise annotation rects can be drawn. Yellow comment annotations are read-only/dimmed
  • Annotate mode: only yellow comment rects. Turquoise transcription rects are dimmed
  • Modes are mutually exclusive — toggled via mode switcher + Annotieren button

Block-level comment threads with quoted selections

  • Comments are anchored to block_id only — no char-offset anchoring (avoids OT/CRDT complexity)
  • Users select text within a block → selection is auto-quoted into comment body as markdown blockquote (> "Breslau")
  • The quote is a frozen snapshot, not a structural anchor — doesn't break when text changes
  • "Kommentieren" button in block footer opens thread; "Text markieren für Zitat" hint guides users

Version history

  • "Verlauf" button in transcription toolbar opens history panel
  • Shows version timeline per block with diff highlighting
  • Tracks who changed what and when

Data model changes

New table: transcription_blocks

Column Type Notes
id UUID PK
annotation_id UUID FK → document_annotations Links block to PDF region
document_id UUID FK → documents Denormalized for query convenience
text TEXT The transcription content
label VARCHAR Optional label (e.g. "Anrede", "Grußformel")
sort_order INT Manual ordering
created_by UUID FK → app_users
updated_by UUID FK → app_users
created_at TIMESTAMP
updated_at TIMESTAMP

Modified table: document_comments

Column Change
block_id Add — nullable UUID FK → transcription_blocks

No char_offset_start/end columns — deliberately avoided. Char offsets shift when text is edited, requiring OT/CRDT infrastructure (Y.js future work, not MVP). A stale quote is better than a broken offset.

API endpoints

Method Path Description
GET /api/documents/{id}/transcription-blocks List blocks for document
POST /api/documents/{id}/transcription-blocks Create block
PUT /api/transcription-blocks/{id} Update block text/label/order
DELETE /api/transcription-blocks/{id} Delete block
GET /api/transcription-blocks/{id}/history Version history
GET /api/transcription-blocks/{id}/comments Comments for block
POST /api/transcription-blocks/{id}/comments Add comment to block

Component architecture

Component Purpose
TranscriptionEditView.svelte Right panel in transcribe mode — renders block cards
TranscriptionBlock.svelte Single editable block card with contenteditable, footer, comment thread
BlockCommentThread.svelte Comment thread for a block with quoted selections
TranscriptionToolbar.svelte Toolbar with sort, history, add-block controls
AnnotationLayer.svelte Modified — support turquoise + yellow rects with mode-based dimming

Acceptance criteria

  • Users can draw turquoise rectangles on PDF to create transcription blocks
  • Blocks appear in right panel as editable cards linked to PDF regions
  • Scroll-sync: clicking a block scrolls PDF to annotation, and vice versa
  • Block-level comment threads with "Kommentieren" button
  • Text selection in block auto-quotes into comment body
  • Mode exclusivity: transcribe and annotate modes are mutually exclusive
  • Yellow annotations dimmed/read-only in transcribe mode
  • Turquoise annotations dimmed/read-only in annotate mode
  • Version history accessible via "Verlauf" button
  • transcription_blocks table created via Flyway migration
  • document_comments.block_id column added via Flyway migration
  • Auto-save on block text changes (debounced 1.5s)
  • Manual block reordering via drag or sort controls
  • Mobile layout: stacked vertical (PDF top, blocks below)
  • All i18n keys added for de/en/es
## Summary Implement a **collaborative transcription system** where users draw turquoise rectangles on PDF scans to mark text regions, then type transcriptions into linked text blocks. Includes block-level comment threads with quoted text selections, version history, and mode exclusivity (transcribe mode vs annotate mode). This is the core transcription editing experience — the counterpart to the read mode (separate issue). ## Motivation Family members need to collaboratively transcribe handwritten letters. The annotation-backed approach links each transcription block to a specific region on the scan, enabling scroll-sync and visual correspondence between handwriting and typed text. ## Spec 📄 **[`docs/specs/annotation-transcription-final-spec.html`](../blob/main/docs/specs/annotation-transcription-final-spec.html)** — open locally in browser for full mockups ### Key screens | Screen | Description | |--------|-------------| | S1 | Desktop transcribe mode — block cards with comment threads | | S2 | Comment flow detail — select text → auto-quote → discuss | | S3 | History panel open — version timeline per block | | S4 | Mobile transcribe mode | ### Core concepts #### Annotation-backed blocks - User draws a turquoise rectangle on the PDF scan - A `transcription_block` is created, linked to the annotation via `annotation_id` - Block appears in the right panel as an editable card with contenteditable text - Blocks are ordered by `sort_order` (manual drag) or annotation Y-position #### Mode exclusivity - **Transcribe mode**: only turquoise annotation rects can be drawn. Yellow comment annotations are read-only/dimmed - **Annotate mode**: only yellow comment rects. Turquoise transcription rects are dimmed - Modes are mutually exclusive — toggled via mode switcher + Annotieren button #### Block-level comment threads with quoted selections - Comments are anchored to `block_id` only — **no char-offset anchoring** (avoids OT/CRDT complexity) - Users select text within a block → selection is auto-quoted into comment body as markdown blockquote (`> "Breslau"`) - The quote is a frozen snapshot, not a structural anchor — doesn't break when text changes - "Kommentieren" button in block footer opens thread; "Text markieren für Zitat" hint guides users #### Version history - "Verlauf" button in transcription toolbar opens history panel - Shows version timeline per block with diff highlighting - Tracks who changed what and when ### Data model changes #### New table: `transcription_blocks` | Column | Type | Notes | |--------|------|-------| | `id` | UUID PK | | | `annotation_id` | UUID FK → document_annotations | Links block to PDF region | | `document_id` | UUID FK → documents | Denormalized for query convenience | | `text` | TEXT | The transcription content | | `label` | VARCHAR | Optional label (e.g. "Anrede", "Grußformel") | | `sort_order` | INT | Manual ordering | | `created_by` | UUID FK → app_users | | | `updated_by` | UUID FK → app_users | | | `created_at` | TIMESTAMP | | | `updated_at` | TIMESTAMP | | #### Modified table: `document_comments` | Column | Change | |--------|--------| | `block_id` | **Add** — nullable UUID FK → transcription_blocks | **No `char_offset_start/end` columns** — deliberately avoided. Char offsets shift when text is edited, requiring OT/CRDT infrastructure (Y.js future work, not MVP). A stale quote is better than a broken offset. ### API endpoints | Method | Path | Description | |--------|------|-------------| | GET | `/api/documents/{id}/transcription-blocks` | List blocks for document | | POST | `/api/documents/{id}/transcription-blocks` | Create block | | PUT | `/api/transcription-blocks/{id}` | Update block text/label/order | | DELETE | `/api/transcription-blocks/{id}` | Delete block | | GET | `/api/transcription-blocks/{id}/history` | Version history | | GET | `/api/transcription-blocks/{id}/comments` | Comments for block | | POST | `/api/transcription-blocks/{id}/comments` | Add comment to block | ### Component architecture | Component | Purpose | |-----------|---------| | `TranscriptionEditView.svelte` | Right panel in transcribe mode — renders block cards | | `TranscriptionBlock.svelte` | Single editable block card with contenteditable, footer, comment thread | | `BlockCommentThread.svelte` | Comment thread for a block with quoted selections | | `TranscriptionToolbar.svelte` | Toolbar with sort, history, add-block controls | | `AnnotationLayer.svelte` | Modified — support turquoise + yellow rects with mode-based dimming | ## Acceptance criteria - [ ] Users can draw turquoise rectangles on PDF to create transcription blocks - [ ] Blocks appear in right panel as editable cards linked to PDF regions - [ ] Scroll-sync: clicking a block scrolls PDF to annotation, and vice versa - [ ] Block-level comment threads with "Kommentieren" button - [ ] Text selection in block auto-quotes into comment body - [ ] Mode exclusivity: transcribe and annotate modes are mutually exclusive - [ ] Yellow annotations dimmed/read-only in transcribe mode - [ ] Turquoise annotations dimmed/read-only in annotate mode - [ ] Version history accessible via "Verlauf" button - [ ] `transcription_blocks` table created via Flyway migration - [ ] `document_comments.block_id` column added via Flyway migration - [ ] Auto-save on block text changes (debounced 1.5s) - [ ] Manual block reordering via drag or sort controls - [ ] Mobile layout: stacked vertical (PDF top, blocks below) - [ ] All i18n keys added for de/en/es
marcel added the collaborationfeatureui labels 2026-04-05 09:30:09 +02:00
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Questions & Observations

  • This is a large feature — 7 new API endpoints, 2 Flyway migrations, 5+ new Svelte components, contenteditable text editing, drag-and-drop reordering, auto-save, comment threads, version history. This needs to be broken into multiple PRs. I'd suggest: (1) backend data model + CRUD endpoints, (2) frontend annotation drawing + block rendering, (3) comment threads, (4) version history, (5) auto-save + reordering. Each PR is independently shippable and testable.

  • contenteditable is a minefield — the spec says blocks use contenteditable for text editing. This means we need to handle: paste events (strip HTML, keep plain text only), undo/redo (browser default or custom?), keyboard shortcuts, cursor position management, IME input for non-Latin scripts. Have we considered using a <textarea> with auto-resize instead? Simpler, more predictable, and avoids the entire class of contenteditable bugs. The visual difference is negligible with proper styling.

  • Auto-save debounced 1.5s — the AC says "auto-save on block text changes (debounced 1.5s)." What happens when the user navigates away before the debounce fires? We need either beforeunload protection or an immediate flush on blur/navigation. Also: what's the HTTP method? PATCH for partial updates (just the text field) or PUT for the full block? The endpoint table shows PUT for update — that means we send the full block every time, even if only one character changed. Consider a PATCH endpoint for text-only updates to reduce payload.

  • document_id denormalized on transcription_blocks — the issue notes this is "for query convenience." That's fine, but it creates a consistency risk: annotation.document_id could theoretically differ from block.document_id. Should we add a DB constraint or just trust the application layer? I'd suggest a CHECK constraint or at minimum a comment in the migration explaining why it's denormalized.

  • Version history — the endpoint GET /api/transcription-blocks/{id}/history implies we're tracking block revisions. But the data model has no transcription_block_history table. Are we using Hibernate Envers, a manual audit table, or something else? This needs to be specced at the data model level before implementation.

Suggestions

  • TDD plan for the backend: Start with TranscriptionBlockService — test create, update, delete, and reorder. Then TranscriptionBlockController — test endpoint routing, permission checks, validation. Then the comment thread endpoints. Each layer gets its own test class.

  • Component decomposition: The 5 components listed look right. TranscriptionBlock.svelte is the densest — it owns the contenteditable, the block header (label, number), the footer (Kommentieren button, comment count), and the comment thread. That's potentially 4 visual regions → consider splitting the footer into BlockFooter.svelte and the contenteditable into BlockEditor.svelte.

  • Annotation drawing — the "draw a rectangle on the PDF" interaction is non-trivial. This is essentially a canvas interaction layer. Is there an existing library we're using for annotations, or is this custom? The answer significantly affects the implementation complexity.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Questions & Observations - **This is a large feature** — 7 new API endpoints, 2 Flyway migrations, 5+ new Svelte components, contenteditable text editing, drag-and-drop reordering, auto-save, comment threads, version history. This needs to be broken into multiple PRs. I'd suggest: (1) backend data model + CRUD endpoints, (2) frontend annotation drawing + block rendering, (3) comment threads, (4) version history, (5) auto-save + reordering. Each PR is independently shippable and testable. - **`contenteditable` is a minefield** — the spec says blocks use `contenteditable` for text editing. This means we need to handle: paste events (strip HTML, keep plain text only), undo/redo (browser default or custom?), keyboard shortcuts, cursor position management, IME input for non-Latin scripts. Have we considered using a `<textarea>` with auto-resize instead? Simpler, more predictable, and avoids the entire class of contenteditable bugs. The visual difference is negligible with proper styling. - **Auto-save debounced 1.5s** — the AC says "auto-save on block text changes (debounced 1.5s)." What happens when the user navigates away before the debounce fires? We need either `beforeunload` protection or an immediate flush on blur/navigation. Also: what's the HTTP method? PATCH for partial updates (just the text field) or PUT for the full block? The endpoint table shows PUT for update — that means we send the full block every time, even if only one character changed. Consider a PATCH endpoint for text-only updates to reduce payload. - **`document_id` denormalized on `transcription_blocks`** — the issue notes this is "for query convenience." That's fine, but it creates a consistency risk: annotation.document_id could theoretically differ from block.document_id. Should we add a DB constraint or just trust the application layer? I'd suggest a CHECK constraint or at minimum a comment in the migration explaining why it's denormalized. - **Version history** — the endpoint `GET /api/transcription-blocks/{id}/history` implies we're tracking block revisions. But the data model has no `transcription_block_history` table. Are we using Hibernate Envers, a manual audit table, or something else? This needs to be specced at the data model level before implementation. ### Suggestions - **TDD plan for the backend**: Start with `TranscriptionBlockService` — test create, update, delete, and reorder. Then `TranscriptionBlockController` — test endpoint routing, permission checks, validation. Then the comment thread endpoints. Each layer gets its own test class. - **Component decomposition**: The 5 components listed look right. `TranscriptionBlock.svelte` is the densest — it owns the contenteditable, the block header (label, number), the footer (Kommentieren button, comment count), and the comment thread. That's potentially 4 visual regions → consider splitting the footer into `BlockFooter.svelte` and the contenteditable into `BlockEditor.svelte`. - **Annotation drawing** — the "draw a rectangle on the PDF" interaction is non-trivial. This is essentially a canvas interaction layer. Is there an existing library we're using for annotations, or is this custom? The answer significantly affects the implementation complexity.
Author
Owner

🏗️ Markus Keller — Application Architect

Questions & Observations

  • This is the biggest feature in the project so far — new domain entity, new table, new service, new controller, modifications to the existing annotation and comment systems, contenteditable editing, auto-save, version history. This needs a phased implementation plan, not a single issue. I'd recommend splitting into at least 3 issues: (1) data model + backend CRUD, (2) frontend transcription UI, (3) comment threads + history.

  • Domain boundary question: TranscriptionBlock vs DocumentAnnotation — the block has an annotation_id FK, making it dependent on the annotation domain. But the block also has its own document_id, text, label, sort_order, created_by, updated_by. Is TranscriptionBlock its own domain or a sub-entity of DocumentAnnotation? This determines: does it get its own service (TranscriptionBlockService) or does it live inside the existing annotation service? I'd argue for its own service — the block has its own CRUD lifecycle, its own comment threads, its own version history. The annotation is just a spatial anchor.

  • document_comments.block_id FK — adding a nullable FK to the existing comments table ties two domains together at the DB level. This is acceptable for MVP, but be aware: deleting a transcription block now needs to handle cascading comment cleanup. ON DELETE SET NULL or ON DELETE CASCADE? SET NULL preserves the comment but orphans it; CASCADE deletes the thread. The issue doesn't specify.

  • Version history storage — no table defined for history. Options:

    1. Hibernate Envers — automatic audit table (transcription_blocks_AUD), minimal code. But it's a framework coupling.
    2. Manual transcription_block_versions table — explicit, queryable, no framework magic.
    3. PostgreSQL temporal tables — using SYSTEM_TIME versioning. Most powerful but complex.

    For MVP, I'd recommend option 2: a simple transcription_block_versions table with (id, block_id, text, changed_by, changed_at). One INSERT per save. Simple query for history. No framework dependency.

  • Auto-save architecture — debounced 1.5s from the frontend, hitting PUT /api/transcription-blocks/{id}. This means: concurrent edits by two users on the same block will cause last-write-wins conflicts. Is that acceptable for MVP? If not, you need optimistic locking (@Version column + 409 Conflict response). The issue doesn't address concurrent editing — worth deciding now.

Suggestions

  • Flyway migration order: Create transcription_blocks table first, then alter document_comments to add block_id FK. Two separate migration files. The FK on document_comments should reference transcription_blocks(id) with ON DELETE SET NULL — orphaning a comment is better than silently deleting discussion.

  • API path design: The mixed paths (/api/documents/{id}/transcription-blocks for list/create, /api/transcription-blocks/{id} for update/delete) are fine — they follow the existing pattern. But the comment endpoints under /api/transcription-blocks/{id}/comments should verify that the authenticated user has READ_ALL permission on the parent document, not just on the block itself. Permission check must flow through the document.

## 🏗️ Markus Keller — Application Architect ### Questions & Observations - **This is the biggest feature in the project so far** — new domain entity, new table, new service, new controller, modifications to the existing annotation and comment systems, contenteditable editing, auto-save, version history. This needs a phased implementation plan, not a single issue. I'd recommend splitting into at least 3 issues: (1) data model + backend CRUD, (2) frontend transcription UI, (3) comment threads + history. - **Domain boundary question: `TranscriptionBlock` vs `DocumentAnnotation`** — the block has an `annotation_id` FK, making it dependent on the annotation domain. But the block also has its own `document_id`, `text`, `label`, `sort_order`, `created_by`, `updated_by`. Is `TranscriptionBlock` its own domain or a sub-entity of `DocumentAnnotation`? This determines: does it get its own service (`TranscriptionBlockService`) or does it live inside the existing annotation service? I'd argue for **its own service** — the block has its own CRUD lifecycle, its own comment threads, its own version history. The annotation is just a spatial anchor. - **`document_comments.block_id` FK** — adding a nullable FK to the existing comments table ties two domains together at the DB level. This is acceptable for MVP, but be aware: deleting a transcription block now needs to handle cascading comment cleanup. `ON DELETE SET NULL` or `ON DELETE CASCADE`? SET NULL preserves the comment but orphans it; CASCADE deletes the thread. The issue doesn't specify. - **Version history storage** — no table defined for history. Options: 1. **Hibernate Envers** — automatic audit table (`transcription_blocks_AUD`), minimal code. But it's a framework coupling. 2. **Manual `transcription_block_versions` table** — explicit, queryable, no framework magic. 3. **PostgreSQL temporal tables** — using `SYSTEM_TIME` versioning. Most powerful but complex. For MVP, I'd recommend option 2: a simple `transcription_block_versions` table with `(id, block_id, text, changed_by, changed_at)`. One INSERT per save. Simple query for history. No framework dependency. - **Auto-save architecture** — debounced 1.5s from the frontend, hitting `PUT /api/transcription-blocks/{id}`. This means: concurrent edits by two users on the same block will cause last-write-wins conflicts. Is that acceptable for MVP? If not, you need optimistic locking (`@Version` column + `409 Conflict` response). The issue doesn't address concurrent editing — worth deciding now. ### Suggestions - **Flyway migration order**: Create `transcription_blocks` table first, then alter `document_comments` to add `block_id` FK. Two separate migration files. The FK on `document_comments` should reference `transcription_blocks(id)` with `ON DELETE SET NULL` — orphaning a comment is better than silently deleting discussion. - **API path design**: The mixed paths (`/api/documents/{id}/transcription-blocks` for list/create, `/api/transcription-blocks/{id}` for update/delete) are fine — they follow the existing pattern. But the comment endpoints under `/api/transcription-blocks/{id}/comments` should verify that the authenticated user has READ_ALL permission on the parent document, not just on the block itself. Permission check must flow through the document.
Author
Owner

🧪 Sara Holt — QA Engineer & Test Strategist

Questions & Observations

  • 15 acceptance criteria — good coverage, but several are compound and need splitting for proper test mapping:

    • "Scroll-sync: clicking a block scrolls PDF to annotation, and vice versa" → two test cases (block→PDF, PDF→block)
    • "Mode exclusivity: transcribe and annotate modes are mutually exclusive" → test entering transcribe while in annotate, and vice versa
    • "Auto-save on block text changes (debounced 1.5s)" → test the debounce timing, test that save fires on blur, test that save fires on navigation away
  • Missing acceptance criteria:

    • Permission checks: Who can create/edit/delete transcription blocks? Only users with WRITE_ALL? Can a user edit another user's block?
    • Concurrent editing: What happens when two users edit the same block simultaneously? Last-write-wins? Conflict detection?
    • Block deletion: What happens to the PDF annotation when a block is deleted? Is the annotation also removed, or does it become orphaned?
    • Empty block: Can a user save a block with empty text? What's the minimum content?
    • Maximum text length: Is there a limit on block text? The column is TEXT (unlimited), but the frontend should probably have a reasonable limit.
    • Comment with quoted text that no longer exists: The quote is a "frozen snapshot" — what does the UI show when the quoted text has been edited away? Just the stale quote? A visual indicator?
  • Version history — the AC says "Version history accessible via Verlauf button" but doesn't define what's in the history. What fields are tracked? Just text changes? Label changes? Reordering? Who sees the history — all users or only WRITE_ALL?

Suggestions

  • Test strategy (this is a big feature):
    • Backend unit: TranscriptionBlockService — create, update, delete, reorder, version history retrieval. Permission checks. Cascade behavior on delete.
    • Backend integration (Testcontainers): Flyway migrations run cleanly. FK constraints enforced. block_id on comments works. Version history inserts on save.
    • Frontend unit (Vitest): TranscriptionBlock.svelte — renders text, handles contenteditable input, fires save callback. BlockCommentThread.svelte — renders comments, shows quoted text, handles new comment submission.
    • E2E (Playwright): Draw annotation on PDF → block appears → type text → auto-save fires → refresh page → text persists. Comment flow: select text → click Kommentieren → quote appears → submit → thread visible. Mode switching: enter transcribe → yellow annotations dimmed → switch to annotate → turquoise dimmed.
    • Estimated CI impact: +30–45s for new backend integration tests, +60s for new E2E scenarios. Within budget.
## 🧪 Sara Holt — QA Engineer & Test Strategist ### Questions & Observations - **15 acceptance criteria** — good coverage, but several are compound and need splitting for proper test mapping: - "Scroll-sync: clicking a block scrolls PDF to annotation, and vice versa" → two test cases (block→PDF, PDF→block) - "Mode exclusivity: transcribe and annotate modes are mutually exclusive" → test entering transcribe while in annotate, and vice versa - "Auto-save on block text changes (debounced 1.5s)" → test the debounce timing, test that save fires on blur, test that save fires on navigation away - **Missing acceptance criteria**: - **Permission checks**: Who can create/edit/delete transcription blocks? Only users with `WRITE_ALL`? Can a user edit another user's block? - **Concurrent editing**: What happens when two users edit the same block simultaneously? Last-write-wins? Conflict detection? - **Block deletion**: What happens to the PDF annotation when a block is deleted? Is the annotation also removed, or does it become orphaned? - **Empty block**: Can a user save a block with empty text? What's the minimum content? - **Maximum text length**: Is there a limit on block text? The column is `TEXT` (unlimited), but the frontend should probably have a reasonable limit. - **Comment with quoted text that no longer exists**: The quote is a "frozen snapshot" — what does the UI show when the quoted text has been edited away? Just the stale quote? A visual indicator? - **Version history** — the AC says "Version history accessible via Verlauf button" but doesn't define what's in the history. What fields are tracked? Just text changes? Label changes? Reordering? Who sees the history — all users or only WRITE_ALL? ### Suggestions - **Test strategy (this is a big feature)**: - **Backend unit**: `TranscriptionBlockService` — create, update, delete, reorder, version history retrieval. Permission checks. Cascade behavior on delete. - **Backend integration (Testcontainers)**: Flyway migrations run cleanly. FK constraints enforced. `block_id` on comments works. Version history inserts on save. - **Frontend unit (Vitest)**: `TranscriptionBlock.svelte` — renders text, handles contenteditable input, fires save callback. `BlockCommentThread.svelte` — renders comments, shows quoted text, handles new comment submission. - **E2E (Playwright)**: Draw annotation on PDF → block appears → type text → auto-save fires → refresh page → text persists. Comment flow: select text → click Kommentieren → quote appears → submit → thread visible. Mode switching: enter transcribe → yellow annotations dimmed → switch to annotate → turquoise dimmed. - **Estimated CI impact**: +30–45s for new backend integration tests, +60s for new E2E scenarios. Within budget.
Author
Owner

🔒 Nora "NullX" Steiner — Application Security Engineer

Questions & Observations

  • New attack surface: 7 API endpoints — this is the first feature that introduces user-generated content stored in the database (transcription text, comments with quoted selections). Each endpoint needs authorization checks.

  • contenteditable → stored text — users type into a contenteditable div, and this text is stored as-is in the text column. Critical question: is the text stored as plain text or HTML? If contenteditable produces HTML (which it does by default — <div>, <br>, <b> tags from paste/formatting), and that HTML is rendered back via {@html} in Svelte, we have a stored XSS vulnerability. The text MUST be either:

    1. Stripped to plain text on save (backend sanitization), or
    2. Always rendered as text content (Svelte's {text} interpolation, never {@html}), or
    3. Sanitized with a whitelist (DOMPurify) if rich text is needed.

    This is the single most important security consideration in this feature. The issue doesn't specify the text format.

  • Comment body with markdown blockquotes — comments contain > "Breslau" quoted selections. If the comment body is rendered as markdown, any markdown injection (links, images, HTML in markdown) could be a vector. Ensure the markdown renderer (if used) sanitizes HTML within markdown. If comments are plain text with just the > visual treatment, this is fine.

  • Block deletion cascadedocument_comments.block_id FK. If a malicious user can delete a block they don't own, they can orphan or cascade-delete other users' comments. Verify: delete endpoint checks that the user has WRITE_ALL permission AND (optionally) is the block creator or an admin.

  • Auto-save rate limiting — debounced 1.5s on the frontend, but a malicious client can bypass the debounce and flood the PUT endpoint. Add server-side rate limiting or at minimum throttle per-user per-block (e.g., max 1 update per second per block).

  • Version history data exposureGET /api/transcription-blocks/{id}/history returns who changed what and when. This includes changed_by user information. Ensure this endpoint requires at least READ_ALL permission on the parent document, and that the user info returned is limited (display name only, not email or internal user IDs).

Suggestions

  • Add to AC: "Transcription block text is stored as plain text. No HTML is persisted. contenteditable output is stripped to text content before save."
  • Add to AC: "All transcription block endpoints require READ_ALL for GET, WRITE_ALL for POST/PUT/DELETE, scoped to the parent document."
  • Add backend validation: max text length per block (e.g., 10,000 characters). Prevents abuse and keeps the DB healthy.
  • Add @RequirePermission(Permission.WRITE_ALL) on create/update/delete endpoints. READ_ALL on list/get/history/comments.
## 🔒 Nora "NullX" Steiner — Application Security Engineer ### Questions & Observations - **New attack surface: 7 API endpoints** — this is the first feature that introduces user-generated content stored in the database (transcription text, comments with quoted selections). Each endpoint needs authorization checks. - **`contenteditable` → stored text** — users type into a contenteditable div, and this text is stored as-is in the `text` column. **Critical question**: is the text stored as plain text or HTML? If contenteditable produces HTML (which it does by default — `<div>`, `<br>`, `<b>` tags from paste/formatting), and that HTML is rendered back via `{@html}` in Svelte, we have a **stored XSS** vulnerability. The text MUST be either: 1. Stripped to plain text on save (backend sanitization), or 2. Always rendered as text content (Svelte's `{text}` interpolation, never `{@html}`), or 3. Sanitized with a whitelist (DOMPurify) if rich text is needed. This is the single most important security consideration in this feature. The issue doesn't specify the text format. - **Comment body with markdown blockquotes** — comments contain `> "Breslau"` quoted selections. If the comment body is rendered as markdown, any markdown injection (links, images, HTML in markdown) could be a vector. Ensure the markdown renderer (if used) sanitizes HTML within markdown. If comments are plain text with just the `>` visual treatment, this is fine. - **Block deletion cascade** — `document_comments.block_id` FK. If a malicious user can delete a block they don't own, they can orphan or cascade-delete other users' comments. Verify: delete endpoint checks that the user has WRITE_ALL permission AND (optionally) is the block creator or an admin. - **Auto-save rate limiting** — debounced 1.5s on the frontend, but a malicious client can bypass the debounce and flood the PUT endpoint. Add server-side rate limiting or at minimum throttle per-user per-block (e.g., max 1 update per second per block). - **Version history data exposure** — `GET /api/transcription-blocks/{id}/history` returns who changed what and when. This includes `changed_by` user information. Ensure this endpoint requires at least READ_ALL permission on the parent document, and that the user info returned is limited (display name only, not email or internal user IDs). ### Suggestions - **Add to AC**: "Transcription block text is stored as plain text. No HTML is persisted. `contenteditable` output is stripped to text content before save." - **Add to AC**: "All transcription block endpoints require READ_ALL for GET, WRITE_ALL for POST/PUT/DELETE, scoped to the parent document." - **Add backend validation**: max text length per block (e.g., 10,000 characters). Prevents abuse and keeps the DB healthy. - **Add `@RequirePermission(Permission.WRITE_ALL)`** on create/update/delete endpoints. READ_ALL on list/get/history/comments.
Author
Owner

🎨 Leonie Voss — UI/UX Design Lead

Questions & Observations

  • Contenteditable styling — the block cards use contenteditable for text input. This is the most delicate UI element in the feature. Key concerns:

    • Focus state: when a user clicks into a block to edit, the block needs a clear focus indicator — a turquoise left border or subtle shadow change. The user must know which block they're editing, especially when there are 5+ blocks visible.
    • Placeholder text: an empty block should show placeholder text ("Text hier eingeben...") in muted italic, disappearing on focus. CSS [contenteditable]:empty::before can handle this.
    • Selection color: text selection within contenteditable should use the turquoise accent color (::selection { background: rgba(0,199,177,.2) }) to visually connect the editing experience to the turquoise annotation color.
  • Block card density — each block has: numbered badge, label, contenteditable text area, footer with Kommentieren button + comment count + hint text. On a 4-block letter, the right panel is quite dense. Ensure adequate spacing: at least 12px between blocks, 8px padding inside each card. The footer should be visually quieter than the text area — don't let UI chrome compete with the content.

  • "Text markieren für Zitat" hint — this hint in the block footer guides users to select text before commenting. But it's always visible, even when no one is commenting. Consider showing it only when the comment thread is open or when the Kommentieren button is hovered. Permanent hints become visual noise after the first use.

  • Mobile transcribe mode (S4) — on mobile (stacked vertical), the contenteditable blocks need extra care:

    • Virtual keyboard pushes content up — ensure the active block remains visible above the keyboard
    • Touch selection for quote creation is harder than mouse selection — consider a long-press menu or a "Zitieren" button that appears after text selection on touch devices
    • Block reordering via drag is nearly impossible on mobile — provide up/down arrow buttons as an alternative
  • Mode exclusivity visual feedback — when switching from annotate to transcribe mode, the yellow annotations dim and turquoise annotations brighten. This transition should be animated (opacity 300ms ease) so the user understands what changed. An abrupt switch could be confusing for 60+ users.

Suggestions

  • Block numbering — the turquoise numbered badges on both the PDF annotations and the block cards are the visual anchor linking the two panels. These numbers must always match and be prominent. If a user reorders blocks, the numbers must update on both sides simultaneously. Test with 10+ blocks to ensure the numbers remain readable at smaller sizes.

  • Auto-save indicator — the spec mentions auto-save but the AC doesn't describe a visual indicator. Users (especially 60+ users) need reassurance that their work is saved. Suggest a subtle "Gespeichert ✓" text in the status bar or block footer that appears after each successful save, fading after 2s. And a "Speichere..." indicator during the save request.

## 🎨 Leonie Voss — UI/UX Design Lead ### Questions & Observations - **Contenteditable styling** — the block cards use `contenteditable` for text input. This is the most delicate UI element in the feature. Key concerns: - **Focus state**: when a user clicks into a block to edit, the block needs a clear focus indicator — a turquoise left border or subtle shadow change. The user must know which block they're editing, especially when there are 5+ blocks visible. - **Placeholder text**: an empty block should show placeholder text ("Text hier eingeben...") in muted italic, disappearing on focus. CSS `[contenteditable]:empty::before` can handle this. - **Selection color**: text selection within contenteditable should use the turquoise accent color (`::selection { background: rgba(0,199,177,.2) }`) to visually connect the editing experience to the turquoise annotation color. - **Block card density** — each block has: numbered badge, label, contenteditable text area, footer with Kommentieren button + comment count + hint text. On a 4-block letter, the right panel is quite dense. Ensure adequate spacing: at least 12px between blocks, 8px padding inside each card. The footer should be visually quieter than the text area — don't let UI chrome compete with the content. - **"Text markieren für Zitat" hint** — this hint in the block footer guides users to select text before commenting. But it's always visible, even when no one is commenting. Consider showing it only when the comment thread is open or when the Kommentieren button is hovered. Permanent hints become visual noise after the first use. - **Mobile transcribe mode (S4)** — on mobile (stacked vertical), the contenteditable blocks need extra care: - Virtual keyboard pushes content up — ensure the active block remains visible above the keyboard - Touch selection for quote creation is harder than mouse selection — consider a long-press menu or a "Zitieren" button that appears after text selection on touch devices - Block reordering via drag is nearly impossible on mobile — provide up/down arrow buttons as an alternative - **Mode exclusivity visual feedback** — when switching from annotate to transcribe mode, the yellow annotations dim and turquoise annotations brighten. This transition should be animated (opacity 300ms ease) so the user understands what changed. An abrupt switch could be confusing for 60+ users. ### Suggestions - **Block numbering** — the turquoise numbered badges on both the PDF annotations and the block cards are the visual anchor linking the two panels. These numbers must always match and be prominent. If a user reorders blocks, the numbers must update on both sides simultaneously. Test with 10+ blocks to ensure the numbers remain readable at smaller sizes. - **Auto-save indicator** — the spec mentions auto-save but the AC doesn't describe a visual indicator. Users (especially 60+ users) need reassurance that their work is saved. Suggest a subtle "Gespeichert ✓" text in the status bar or block footer that appears after each successful save, fading after 2s. And a "Speichere..." indicator during the save request.
Author
Owner

🔧 Tobias Wendt — DevOps & Platform Engineer

Questions & Observations

  • Flyway migrations — two new migrations: create transcription_blocks table, alter document_comments to add block_id. These run automatically on backend startup. No manual intervention needed. Standard pattern, no concerns.

  • Auto-save traffic impact — debounced 1.5s means each active editor generates ~40 PUT requests per minute of continuous typing. With the expected user base (small family, 1-3 concurrent editors), this is negligible. But if the feature grows, consider: are we writing a full row UPDATE on every save, or just the text column? Full row UPDATE is fine for now, but a PATCH approach would reduce WAL volume in PostgreSQL.

  • Version history storage growth — if we insert a version row on every auto-save (every 1.5s of typing), a single editing session on one block could generate hundreds of version rows. Consider: should version history be throttled (e.g., one version per 30 seconds of inactivity) rather than one per save? Or: should we compact versions older than 24h into a single "session summary"? The unbounded growth of a _versions table could become a disk space concern over years.

  • PDF rendering + annotation drawing — drawing rectangles on a PDF requires PDF.js or similar. This is a client-side dependency — no infrastructure impact. But the PDF files themselves are served from MinIO/S3. If the PDF is large (multi-page scanned letters at 300dpi), the initial load time could be noticeable. Ensure the PDF viewer supports page-by-page loading rather than requiring the full file upfront.

  • No new environment variables, no new services — the feature adds backend endpoints and frontend components but no new infrastructure. The transcription_blocks table lives in the existing PostgreSQL database. Comments are in the existing document_comments table. Clean.

Suggestions

  • Database index: add an index on transcription_blocks(document_id, sort_order) for the list endpoint. And on document_comments(block_id) for the comment thread queries. These should be in the Flyway migration.

  • Backup consideration: transcription text is user-generated content that cannot be re-derived from the uploaded files. It's the output of manual human work. Ensure the existing PostgreSQL backup strategy (WAL-G or pg_dump) covers this table. It should by default, but worth verifying the backup is actually tested with a restore after this feature ships.

## 🔧 Tobias Wendt — DevOps & Platform Engineer ### Questions & Observations - **Flyway migrations** — two new migrations: create `transcription_blocks` table, alter `document_comments` to add `block_id`. These run automatically on backend startup. No manual intervention needed. Standard pattern, no concerns. - **Auto-save traffic impact** — debounced 1.5s means each active editor generates ~40 PUT requests per minute of continuous typing. With the expected user base (small family, 1-3 concurrent editors), this is negligible. But if the feature grows, consider: are we writing a full row UPDATE on every save, or just the text column? Full row UPDATE is fine for now, but a PATCH approach would reduce WAL volume in PostgreSQL. - **Version history storage growth** — if we insert a version row on every auto-save (every 1.5s of typing), a single editing session on one block could generate hundreds of version rows. Consider: should version history be throttled (e.g., one version per 30 seconds of inactivity) rather than one per save? Or: should we compact versions older than 24h into a single "session summary"? The unbounded growth of a `_versions` table could become a disk space concern over years. - **PDF rendering + annotation drawing** — drawing rectangles on a PDF requires PDF.js or similar. This is a client-side dependency — no infrastructure impact. But the PDF files themselves are served from MinIO/S3. If the PDF is large (multi-page scanned letters at 300dpi), the initial load time could be noticeable. Ensure the PDF viewer supports page-by-page loading rather than requiring the full file upfront. - **No new environment variables, no new services** — the feature adds backend endpoints and frontend components but no new infrastructure. The `transcription_blocks` table lives in the existing PostgreSQL database. Comments are in the existing `document_comments` table. Clean. ### Suggestions - **Database index**: add an index on `transcription_blocks(document_id, sort_order)` for the list endpoint. And on `document_comments(block_id)` for the comment thread queries. These should be in the Flyway migration. - **Backup consideration**: transcription text is user-generated content that cannot be re-derived from the uploaded files. It's the output of manual human work. Ensure the existing PostgreSQL backup strategy (WAL-G or pg_dump) covers this table. It should by default, but worth verifying the backup is actually tested with a restore after this feature ships.
Author
Owner

🎨 Leonie Voss — UI/UX Discussion Summary

Worked through 10 open UI/UX items with the team. All resolved.

Resolved items

  1. Textarea over contenteditable — Use <textarea> with CSS auto-resize, styled to look seamless (no visible border, matching serif font, resize: none). Eliminates the entire class of contenteditable bugs (paste HTML, undo/redo inconsistency, IME issues) and resolves NullX's stored XSS concern — textarea content is always plain text. Update the spec and AC accordingly.

  2. Per-block save indicator — Each block shows its own save state in the footer, right-aligned, text-xs text-ink-2:

    • Idle: nothing shown
    • Saving: "Speichere..." with subtle pulse
    • Saved: "Gespeichert ✓" in muted green, fades after 2s
    • Error: "Nicht gespeichert — erneut versuchen" in text-error, persistent with retry button
  3. Turquoise left border on focus — Active block gets border-l-2 border-turquoise. The matching PDF annotation simultaneously brightens from dimmed (30%) to full opacity. Creates a visual link between "this block" and "that rectangle on the scan."

  4. Quote hint shown on focus only — "Text markieren für Zitat" hint appears only when the block is focused (pairs with the turquoise left border focus state). Unfocused blocks stay clean — just text and a minimal footer with Kommentieren button and save status.

  5. Desktop drag + mobile arrows — Desktop: drag handle (⠿ grip icon) on the left of each block. Mobile (< 768px): replace drag handle with ▲/▼ arrow buttons, 44×44px tap targets. Same position, right interaction per device.

  6. 300ms mode transition animationtransition: opacity 300ms ease on all annotation rectangles during mode switch. Dimmed annotations fade to 30%, active annotations fade to 100% simultaneously. Users with prefers-reduced-motion get instant switch.

  7. Three-layer error escalation for save failures:

    • beforeunload confirmation dialog when unsaved changes exist and user navigates away
    • Auto-retry once after 5 seconds on save failure
    • Red left border (border-l-2 border-error) on blocks with failed saves — visible from a distance, uses same spatial position as the turquoise focus border
  8. Block card spacinggap-3 (12px) between blocks. p-4 (16px) card padding. Seamless textarea with py-2 internal padding. Footer separated by pt-2 border-t border-line, elements at text-xs text-ink-2. Numbered badge: 24px turquoise circle, top-left, slightly overlapping. Optional label: text-xs font-medium uppercase tracking-wide above textarea. Key principle: text area dominates, chrome recedes.

  9. Stale quote detection — Simple block.text.includes(quotedString) check when rendering comments. If the quoted text is still found in the block, render the blockquote normally. If not found, add a muted label below: "Zitat aus älterer Version" (text-xs text-ink-2). No visual drama — just informational.

  10. Drawing interaction affordance — Three layers for discoverability:

    • Cursor: cursor: crosshair when hovering over the PDF in transcribe mode
    • Empty state CTA: "Markiere einen Bereich auf dem Scan, um mit der Transkription zu beginnen" with illustration in the right panel when no blocks exist
    • First-draw tooltip: brief tooltip near cursor on first PDF hover in transcribe mode per session: "Klicken und ziehen, um einen Textbereich zu markieren". Disappears after 3s or on first click. Not shown again in the same session.

Overall read

This is a complex feature but the UI decisions are now well-grounded. The textarea decision is the biggest win — it cuts implementation complexity dramatically while improving accessibility and security. The per-block save indicator and error escalation are essential for our 60+ users handling irreplaceable family documents. Ship with confidence.

## 🎨 Leonie Voss — UI/UX Discussion Summary Worked through 10 open UI/UX items with the team. All resolved. ### Resolved items 1. **Textarea over contenteditable** — Use `<textarea>` with CSS auto-resize, styled to look seamless (no visible border, matching serif font, `resize: none`). Eliminates the entire class of contenteditable bugs (paste HTML, undo/redo inconsistency, IME issues) and resolves NullX's stored XSS concern — textarea content is always plain text. Update the spec and AC accordingly. 2. **Per-block save indicator** — Each block shows its own save state in the footer, right-aligned, `text-xs text-ink-2`: - Idle: nothing shown - Saving: "Speichere..." with subtle pulse - Saved: "Gespeichert ✓" in muted green, fades after 2s - Error: "Nicht gespeichert — erneut versuchen" in `text-error`, persistent with retry button 3. **Turquoise left border on focus** — Active block gets `border-l-2 border-turquoise`. The matching PDF annotation simultaneously brightens from dimmed (30%) to full opacity. Creates a visual link between "this block" and "that rectangle on the scan." 4. **Quote hint shown on focus only** — "Text markieren für Zitat" hint appears only when the block is focused (pairs with the turquoise left border focus state). Unfocused blocks stay clean — just text and a minimal footer with Kommentieren button and save status. 5. **Desktop drag + mobile arrows** — Desktop: drag handle (⠿ grip icon) on the left of each block. Mobile (< 768px): replace drag handle with ▲/▼ arrow buttons, 44×44px tap targets. Same position, right interaction per device. 6. **300ms mode transition animation** — `transition: opacity 300ms ease` on all annotation rectangles during mode switch. Dimmed annotations fade to 30%, active annotations fade to 100% simultaneously. Users with `prefers-reduced-motion` get instant switch. 7. **Three-layer error escalation for save failures**: - `beforeunload` confirmation dialog when unsaved changes exist and user navigates away - Auto-retry once after 5 seconds on save failure - Red left border (`border-l-2 border-error`) on blocks with failed saves — visible from a distance, uses same spatial position as the turquoise focus border 8. **Block card spacing** — `gap-3` (12px) between blocks. `p-4` (16px) card padding. Seamless textarea with `py-2` internal padding. Footer separated by `pt-2 border-t border-line`, elements at `text-xs text-ink-2`. Numbered badge: 24px turquoise circle, top-left, slightly overlapping. Optional label: `text-xs font-medium uppercase tracking-wide` above textarea. Key principle: **text area dominates, chrome recedes.** 9. **Stale quote detection** — Simple `block.text.includes(quotedString)` check when rendering comments. If the quoted text is still found in the block, render the blockquote normally. If not found, add a muted label below: "Zitat aus älterer Version" (`text-xs text-ink-2`). No visual drama — just informational. 10. **Drawing interaction affordance** — Three layers for discoverability: - **Cursor**: `cursor: crosshair` when hovering over the PDF in transcribe mode - **Empty state CTA**: "Markiere einen Bereich auf dem Scan, um mit der Transkription zu beginnen" with illustration in the right panel when no blocks exist - **First-draw tooltip**: brief tooltip near cursor on first PDF hover in transcribe mode per session: "Klicken und ziehen, um einen Textbereich zu markieren". Disappears after 3s or on first click. Not shown again in the same session. ### Overall read This is a complex feature but the UI decisions are now well-grounded. The textarea decision is the biggest win — it cuts implementation complexity dramatically while improving accessibility and security. The per-block save indicator and error escalation are essential for our 60+ users handling irreplaceable family documents. Ship with confidence.
Author
Owner

🏗️ Markus Keller — Application Architect

Interactive architecture discussion with the project owner. All 7 items were resolved.

Resolved Items

  1. Domain boundary: TranscriptionBlock ownershipTranscriptionService as a facade. Owns TranscriptionBlockRepository, delegates annotation validation to AnnotationService. Transcription is its own bounded context — not a sub-concern of annotations.

  2. Comment FK on block deleteON DELETE CASCADE. Deleting a block deletes its comments. The UI must show a confirmation dialog with the comment count before proceeding.

  3. Concurrent editing → Optimistic locking via @Version on TranscriptionBlock. Stale writes receive a 409 Conflict. Frontend shows a "block was modified by someone else, reload" message. Per-block saves keep the conflict scope narrow.

  4. Version history → Manual transcription_block_versions table and entity created in #176. TranscriptionService.updateBlock() writes a version row on every save. No retrieval API or UI yet — history is captured from day one, consumption comes later.

  5. Block reordering → Single transactional endpoint PUT /documents/{docId}/transcription-blocks/reorder receiving the full ordering. Atomic all-or-nothing — partial failures roll back.

  6. Annotation↔Block lifecycle → The block is the aggregate root. The annotation is a dependent — no standalone annotation delete exists. Creating a block creates its annotation; deleting a block CASCADEs to the annotation (and per item 2, to comments). Users can only move/resize annotations, never delete them independently.

  7. API nesting → Document-level REST resources: /api/documents/{docId}/transcription-blocks/*. Annotation coordinates travel as a nested object in the block request/response payload. Authorization checks happen at the document level.

Architectural Overview

The key insight from this discussion is that transcription is its own bounded context, not a sub-feature of annotations. The TranscriptionService facade owns the block lifecycle, the annotation is a visual dependent of the block (not the other way around), and the API is rooted at the document level. This gives a clean domain model that scales naturally as read mode (#177), version history retrieval, and future features are added.

The data integrity chain is deliberate and strict: delete block → CASCADE annotation + CASCADE comments, with UI confirmation at the trigger point. Optimistic locking prevents silent overwrites. Version history is captured from day one with zero retrieval overhead until it's needed.

## 🏗️ Markus Keller — Application Architect Interactive architecture discussion with the project owner. All 7 items were resolved. ### Resolved Items 1. **Domain boundary: TranscriptionBlock ownership** → `TranscriptionService` as a facade. Owns `TranscriptionBlockRepository`, delegates annotation validation to `AnnotationService`. Transcription is its own bounded context — not a sub-concern of annotations. 2. **Comment FK on block delete** → `ON DELETE CASCADE`. Deleting a block deletes its comments. The UI must show a confirmation dialog with the comment count before proceeding. 3. **Concurrent editing** → Optimistic locking via `@Version` on `TranscriptionBlock`. Stale writes receive a 409 Conflict. Frontend shows a "block was modified by someone else, reload" message. Per-block saves keep the conflict scope narrow. 4. **Version history** → Manual `transcription_block_versions` table and entity created in #176. `TranscriptionService.updateBlock()` writes a version row on every save. No retrieval API or UI yet — history is captured from day one, consumption comes later. 5. **Block reordering** → Single transactional endpoint `PUT /documents/{docId}/transcription-blocks/reorder` receiving the full ordering. Atomic all-or-nothing — partial failures roll back. 6. **Annotation↔Block lifecycle** → The block is the aggregate root. The annotation is a dependent — no standalone annotation delete exists. Creating a block creates its annotation; deleting a block CASCADEs to the annotation (and per item 2, to comments). Users can only move/resize annotations, never delete them independently. 7. **API nesting** → Document-level REST resources: `/api/documents/{docId}/transcription-blocks/*`. Annotation coordinates travel as a nested object in the block request/response payload. Authorization checks happen at the document level. ### Architectural Overview The key insight from this discussion is that **transcription is its own bounded context**, not a sub-feature of annotations. The `TranscriptionService` facade owns the block lifecycle, the annotation is a visual dependent of the block (not the other way around), and the API is rooted at the document level. This gives a clean domain model that scales naturally as read mode (#177), version history retrieval, and future features are added. The data integrity chain is deliberate and strict: delete block → CASCADE annotation + CASCADE comments, with UI confirmation at the trigger point. Optimistic locking prevents silent overwrites. Version history is captured from day one with zero retrieval overhead until it's needed.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#176