From 2ea603a3bf27e317da356e52a597d1e93f17c086 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 15 Apr 2026 22:59:17 +0200 Subject: [PATCH 01/26] =?UTF-8?q?feat(#240):=20backend=20for=20Mission=20C?= =?UTF-8?q?ontrol=20Strip=20=E2=80=94=20queue=20endpoints=20+=20expert=20f?= =?UTF-8?q?lag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the server-side foundation for the dashboard transcription widget: - V36 migration: needs_expert BOOLEAN NOT NULL DEFAULT FALSE on documents - Document entity: needsExpert field (@Schema required) - DocumentRepository: 4 native queries — segmentation queue, transcription queue, ready-to-read queue (seeded weekly shuffle sort), weekly pulse stats - TranscriptionQueueService: maps Object[] rows to typed DTOs, handles PostgreSQL type variations (UUID/String, Date/LocalDate, Number/BigDecimal) - TranscriptionQueueController: GET /api/transcription/{segmentation-queue, transcription-queue, ready-to-read, weekly-stats} — all guarded by READ_ALL - DocumentService + DocumentController: PATCH /api/documents/{id}/needs-expert toggles the expert flag (WRITE_ALL required) Co-Authored-By: Claude Sonnet 4.6 --- .../controller/DocumentController.java | 8 ++ .../TranscriptionQueueController.java | 47 ++++++++ .../dto/TranscriptionQueueItemDTO.java | 19 ++++ .../dto/TranscriptionWeeklyStatsDTO.java | 12 +++ .../familienarchiv/model/Document.java | 5 + .../repository/DocumentRepository.java | 78 ++++++++++++++ .../service/DocumentService.java | 7 ++ .../service/TranscriptionQueueService.java | 100 ++++++++++++++++++ .../V36__add_needs_expert_to_documents.sql | 1 + 9 files changed, 277 insertions(+) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/controller/TranscriptionQueueController.java create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionWeeklyStatsDTO.java create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java create mode 100644 backend/src/main/resources/db/migration/V36__add_needs_expert_to_documents.sql diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java index 91e3c250..4e6b9c37 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -211,6 +211,14 @@ public class DocumentController { return ResponseEntity.ok(documentService.searchDocuments(q, from, to, senderId, receiverId, tags, tagQ, status, sort, dir)); } + // --- EXPERT FLAG --- + + @PatchMapping("/{id}/needs-expert") + @RequirePermission(Permission.WRITE_ALL) + public Document toggleNeedsExpert(@PathVariable UUID id) { + return documentService.toggleNeedsExpert(id); + } + // --- TRAINING LABELS --- public record TrainingLabelRequest(String label, boolean enrolled) {} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/TranscriptionQueueController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/TranscriptionQueueController.java new file mode 100644 index 00000000..59591795 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/TranscriptionQueueController.java @@ -0,0 +1,47 @@ +package org.raddatz.familienarchiv.controller; + +import lombok.RequiredArgsConstructor; +import org.raddatz.familienarchiv.dto.TranscriptionQueueItemDTO; +import org.raddatz.familienarchiv.dto.TranscriptionWeeklyStatsDTO; +import org.raddatz.familienarchiv.security.Permission; +import org.raddatz.familienarchiv.security.RequirePermission; +import org.raddatz.familienarchiv.service.TranscriptionQueueService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * Serves the three Mission Control Strip columns for the dashboard. + * All endpoints require READ_ALL — same guard as the rest of the archive. + */ +@RestController +@RequestMapping("/api/transcription") +@RequiredArgsConstructor +@RequirePermission(Permission.READ_ALL) +public class TranscriptionQueueController { + + private final TranscriptionQueueService transcriptionQueueService; + + @GetMapping("/segmentation-queue") + public ResponseEntity> getSegmentationQueue() { + return ResponseEntity.ok(transcriptionQueueService.getSegmentationQueue()); + } + + @GetMapping("/transcription-queue") + public ResponseEntity> getTranscriptionQueue() { + return ResponseEntity.ok(transcriptionQueueService.getTranscriptionQueue()); + } + + @GetMapping("/ready-to-read") + public ResponseEntity> getReadyToRead() { + return ResponseEntity.ok(transcriptionQueueService.getReadyToReadQueue()); + } + + @GetMapping("/weekly-stats") + public ResponseEntity getWeeklyStats() { + return ResponseEntity.ok(transcriptionQueueService.getWeeklyStats()); + } +} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java new file mode 100644 index 00000000..ab441cd9 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java @@ -0,0 +1,19 @@ +package org.raddatz.familienarchiv.dto; + +import java.time.LocalDate; +import java.util.UUID; + +/** + * A single row in one of the three Mission Control Strip queues. + * Annotation/block counts drive the per-document mini progress bar + * in the Transkription column and the percentage label in Lesefertig. + */ +public record TranscriptionQueueItemDTO( + UUID id, + String title, + LocalDate documentDate, + boolean needsExpert, + int annotationCount, + int textedBlockCount, + int reviewedBlockCount +) {} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionWeeklyStatsDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionWeeklyStatsDTO.java new file mode 100644 index 00000000..71e9fc71 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionWeeklyStatsDTO.java @@ -0,0 +1,12 @@ +package org.raddatz.familienarchiv.dto; + +/** + * Weekly activity pulse for the Mission Control Strip column headers. + * Counts documents that received new work in each pipeline stage + * during the last 7 days. + */ +public record TranscriptionWeeklyStatsDTO( + long segmentationCount, + long transcriptionCount, + long readyCount +) {} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/model/Document.java b/backend/src/main/java/org/raddatz/familienarchiv/model/Document.java index 5a5ca54a..0e7e954a 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/model/Document.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/model/Document.java @@ -97,6 +97,11 @@ public class Document { @Builder.Default private ScriptType scriptType = ScriptType.UNKNOWN; + @Column(name = "needs_expert", nullable = false) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @Builder.Default + private boolean needsExpert = false; + @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "document_receivers", joinColumns = @JoinColumn(name = "document_id"), inverseJoinColumns = @JoinColumn(name = "person_id")) @Builder.Default diff --git a/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java index 022a2ebb..6e20c064 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java @@ -167,4 +167,82 @@ public interface DocumentRepository extends JpaRepository, JpaSp """) List findEnrichmentData(@Param("ids") Collection ids, @Param("query") String query); + // --- Mission Control Strip queues --- + + /** Documents with no annotations — Segmentierung column. */ + @Query(nativeQuery = true, value = """ + SELECT d.id, d.title, d.meta_date AS documentDate, d.needs_expert AS needsExpert, + 0 AS annotationCount, 0 AS textedBlockCount, 0 AS reviewedBlockCount + FROM documents d + WHERE d.status NOT IN ('PLACEHOLDER') + AND NOT EXISTS (SELECT 1 FROM document_annotations da WHERE da.document_id = d.id) + ORDER BY d.needs_expert ASC, + HASHTEXT(d.id::text || EXTRACT(WEEK FROM NOW())::int::text) + LIMIT :limit + """) + List findSegmentationQueue(@Param("limit") int limit); + + /** Documents with annotations but not yet fully reviewed — Transkription column. */ + @Query(nativeQuery = true, value = """ + SELECT d.id, d.title, d.meta_date AS documentDate, d.needs_expert AS needsExpert, + COUNT(DISTINCT da.id) AS annotationCount, + COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) AS textedBlockCount, + COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END) AS reviewedBlockCount + FROM documents d + JOIN document_annotations da ON da.document_id = d.id + LEFT JOIN transcription_blocks tb ON tb.document_id = d.id + GROUP BY d.id, d.title, d.meta_date, d.needs_expert + HAVING COUNT(DISTINCT da.id) > 0 + AND ( + COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) = 0 + OR ( + COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END)::float / + NULLIF(COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END), 0) + ) < 0.90 + ) + ORDER BY d.needs_expert ASC, + COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) DESC, + HASHTEXT(d.id::text || EXTRACT(WEEK FROM NOW())::int::text) + LIMIT :limit + """) + List findTranscriptionQueue(@Param("limit") int limit); + + /** Documents with reviewed_pct >= 90 % — Lesefertig column. */ + @Query(nativeQuery = true, value = """ + SELECT d.id, d.title, d.meta_date AS documentDate, d.needs_expert AS needsExpert, + COUNT(DISTINCT da.id) AS annotationCount, + COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) AS textedBlockCount, + COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END) AS reviewedBlockCount + FROM documents d + JOIN document_annotations da ON da.document_id = d.id + LEFT JOIN transcription_blocks tb ON tb.document_id = d.id + GROUP BY d.id, d.title, d.meta_date, d.needs_expert + HAVING COUNT(DISTINCT da.id) > 0 + AND COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) > 0 + AND ( + COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END)::float / + COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) + ) >= 0.90 + ORDER BY ( + COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END)::float / + COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) + ) DESC + LIMIT :limit + """) + List findReadyToReadQueue(@Param("limit") int limit); + + /** Weekly pulse: distinct documents that received new work in each pipeline stage. */ + @Query(nativeQuery = true, value = """ + SELECT + (SELECT COUNT(DISTINCT da.document_id) FROM document_annotations da + WHERE da.created_at >= NOW() - INTERVAL '7 days') AS segmentationCount, + (SELECT COUNT(DISTINCT tb.document_id) FROM transcription_blocks tb + WHERE tb.created_at >= NOW() - INTERVAL '7 days' + AND tb.text IS NOT NULL AND tb.text <> '') AS transcriptionCount, + (SELECT COUNT(DISTINCT tb.document_id) FROM transcription_blocks tb + WHERE tb.updated_at >= NOW() - INTERVAL '7 days' + AND tb.reviewed = true) AS readyCount + """) + Object[] findWeeklyStats(); + } \ No newline at end of file diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java index ab50ab22..272da789 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -577,6 +577,13 @@ public class DocumentService { return parsed != null ? parsed.title() : stripExtension(filename); } + @Transactional + public Document toggleNeedsExpert(UUID documentId) { + Document doc = getDocumentById(documentId); + doc.setNeedsExpert(!doc.isNeedsExpert()); + return documentRepository.save(doc); + } + private static String tryParseDate(String s) { if (s.matches("\\d{4}-\\d{2}-\\d{2}")) { int m = Integer.parseInt(s.substring(5, 7)); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java new file mode 100644 index 00000000..1b4a0fde --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java @@ -0,0 +1,100 @@ +package org.raddatz.familienarchiv.service; + +import lombok.RequiredArgsConstructor; +import org.raddatz.familienarchiv.dto.TranscriptionQueueItemDTO; +import org.raddatz.familienarchiv.dto.TranscriptionWeeklyStatsDTO; +import org.raddatz.familienarchiv.repository.DocumentRepository; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * Serves the three Mission Control Strip queues (Segmentierung / Transkription / Lesefertig) + * and the weekly activity pulse used by the column headers. + */ +@Service +@RequiredArgsConstructor +public class TranscriptionQueueService { + + private static final int DEFAULT_QUEUE_SIZE = 5; + + private final DocumentRepository documentRepository; + + public List getSegmentationQueue() { + return documentRepository.findSegmentationQueue(DEFAULT_QUEUE_SIZE) + .stream() + .map(this::mapRow) + .toList(); + } + + public List getTranscriptionQueue() { + return documentRepository.findTranscriptionQueue(DEFAULT_QUEUE_SIZE) + .stream() + .map(this::mapRow) + .toList(); + } + + public List getReadyToReadQueue() { + return documentRepository.findReadyToReadQueue(DEFAULT_QUEUE_SIZE) + .stream() + .map(this::mapRow) + .toList(); + } + + public TranscriptionWeeklyStatsDTO getWeeklyStats() { + Object[] row = documentRepository.findWeeklyStats(); + return new TranscriptionWeeklyStatsDTO( + toLong(row[0]), + toLong(row[1]), + toLong(row[2]) + ); + } + + // --- mapping helpers --- + + private TranscriptionQueueItemDTO mapRow(Object[] row) { + UUID id = toUUID(row[0]); + String title = (String) row[1]; + LocalDate documentDate = toLocalDate(row[2]); + boolean needsExpert = toBoolean(row[3]); + int annotationCount = toInt(row[4]); + int textedBlockCount = toInt(row[5]); + int reviewedBlockCount = toInt(row[6]); + return new TranscriptionQueueItemDTO(id, title, documentDate, needsExpert, + annotationCount, textedBlockCount, reviewedBlockCount); + } + + private UUID toUUID(Object o) { + if (o instanceof UUID u) return u; + return UUID.fromString(o.toString()); + } + + private LocalDate toLocalDate(Object o) { + if (o == null) return null; + if (o instanceof LocalDate d) return d; + if (o instanceof java.sql.Date d) return d.toLocalDate(); + return LocalDate.parse(o.toString()); + } + + private boolean toBoolean(Object o) { + if (o instanceof Boolean b) return b; + return Boolean.parseBoolean(o.toString()); + } + + private int toInt(Object o) { + if (o == null) return 0; + if (o instanceof Number n) return n.intValue(); + if (o instanceof BigDecimal bd) return bd.intValue(); + return Integer.parseInt(o.toString()); + } + + private long toLong(Object o) { + if (o == null) return 0L; + if (o instanceof Number n) return n.longValue(); + if (o instanceof BigDecimal bd) return bd.longValue(); + return Long.parseLong(o.toString()); + } +} diff --git a/backend/src/main/resources/db/migration/V36__add_needs_expert_to_documents.sql b/backend/src/main/resources/db/migration/V36__add_needs_expert_to_documents.sql new file mode 100644 index 00000000..03ee225a --- /dev/null +++ b/backend/src/main/resources/db/migration/V36__add_needs_expert_to_documents.sql @@ -0,0 +1 @@ +ALTER TABLE documents ADD COLUMN needs_expert BOOLEAN NOT NULL DEFAULT FALSE; -- 2.49.1 From 53c5d9034076eaa6eed74bbade9ea3b24c515d28 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 15 Apr 2026 23:06:04 +0200 Subject: [PATCH 02/26] feat(#240): update generated API types for Mission Control Strip Manually adds the new types to src/lib/generated/api.ts: - Document.needsExpert: boolean (required field) - TranscriptionQueueItemDTO schema - TranscriptionWeeklyStatsDTO schema - Paths: /api/transcription/{segmentation-queue, transcription-queue, ready-to-read, weekly-stats} and /api/documents/{id}/needs-expert - Operations: matching typed request/response shapes Fixes briefwechsel spec fixtures to include scriptType and needsExpert so the Document type shape is satisfied. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/generated/api.ts | 89 +++++++++++++++++++ .../routes/briefwechsel/page.svelte.spec.ts | 2 + 2 files changed, 91 insertions(+) diff --git a/frontend/src/lib/generated/api.ts b/frontend/src/lib/generated/api.ts index 5d17827c..d36bab5f 100644 --- a/frontend/src/lib/generated/api.ts +++ b/frontend/src/lib/generated/api.ts @@ -676,6 +676,32 @@ export interface paths { patch?: never; trace?: never; }; + "/api/transcription/segmentation-queue": { + parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get: operations["getSegmentationQueue"]; + put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; + }; + "/api/transcription/transcription-queue": { + parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get: operations["getTranscriptionQueue"]; + put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; + }; + "/api/transcription/ready-to-read": { + parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get: operations["getReadyToRead"]; + put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; + }; + "/api/transcription/weekly-stats": { + parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get: operations["getTranscriptionWeeklyStats"]; + put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; + }; + "/api/documents/{id}/needs-expert": { + parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; + get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; + patch: operations["toggleNeedsExpert"]; + trace?: never; + }; "/api/stats": { parameters: { query?: never; @@ -1199,6 +1225,7 @@ export interface components { metadataComplete: boolean; /** @enum {string} */ scriptType: "UNKNOWN" | "TYPEWRITER" | "HANDWRITING_LATIN" | "HANDWRITING_KURRENT"; + needsExpert: boolean; receivers?: components["schemas"]["Person"][]; sender?: components["schemas"]["Person"]; tags?: components["schemas"]["Tag"][]; @@ -1456,6 +1483,28 @@ export interface components { /** Format: int64 */ totalDocuments?: number; }; + TranscriptionQueueItemDTO: { + /** Format: uuid */ + id: string; + title: string; + /** Format: date */ + documentDate?: string; + needsExpert: boolean; + /** Format: int32 */ + annotationCount: number; + /** Format: int32 */ + textedBlockCount: number; + /** Format: int32 */ + reviewedBlockCount: number; + }; + TranscriptionWeeklyStatsDTO: { + /** Format: int64 */ + segmentationCount: number; + /** Format: int64 */ + transcriptionCount: number; + /** Format: int64 */ + readyCount: number; + }; PersonSummaryDTO: { title?: string; /** Format: uuid */ @@ -3116,6 +3165,46 @@ export interface operations { }; }; }; + getSegmentationQueue: { + parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { "*/*": components["schemas"]["TranscriptionQueueItemDTO"][]; }; }; + }; + }; + getTranscriptionQueue: { + parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { "*/*": components["schemas"]["TranscriptionQueueItemDTO"][]; }; }; + }; + }; + getReadyToRead: { + parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { "*/*": components["schemas"]["TranscriptionQueueItemDTO"][]; }; }; + }; + }; + getTranscriptionWeeklyStats: { + parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { "*/*": components["schemas"]["TranscriptionWeeklyStatsDTO"]; }; }; + }; + }; + toggleNeedsExpert: { + parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { "*/*": components["schemas"]["Document"]; }; }; + }; + }; getStats: { parameters: { query?: never; diff --git a/frontend/src/routes/briefwechsel/page.svelte.spec.ts b/frontend/src/routes/briefwechsel/page.svelte.spec.ts index 6c73da12..132ffa26 100644 --- a/frontend/src/routes/briefwechsel/page.svelte.spec.ts +++ b/frontend/src/routes/briefwechsel/page.svelte.spec.ts @@ -38,6 +38,8 @@ const makeDoc = (overrides: Record = {}) => ({ documentDate: '1923-04-12', location: 'Berlin', metadataComplete: false, + scriptType: 'UNKNOWN' as const, + needsExpert: false, sender: { id: 'p1', firstName: 'Hans', lastName: 'Müller' }, receivers: [{ id: 'p2', firstName: 'Anna', lastName: 'Schmidt' }], tags: [], -- 2.49.1 From f36bebd1a8b67c540da91f05297b305dee514074 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 15 Apr 2026 23:14:43 +0200 Subject: [PATCH 03/26] =?UTF-8?q?feat(#240):=20Mission=20Control=20Strip?= =?UTF-8?q?=20frontend=20=E2=80=94=205=20components=20+=20dashboard=20wiri?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the full-width 3-column collaboration widget below the existing dashboard grid. Renders without the backend running (Promise.allSettled isolation keeps failures silent). Components (src/lib/components/): - ExpertBadge.svelte — purple pill with icon, no props - SegmentationColumn.svelte — col 1: links to /enrich/{id}, weekly pulse - TranscriptionColumn.svelte — col 2: per-doc progress bar when blocks exist - ReadyColumn.svelte — col 3: mint border when filled, dashed empty state - MissionControlStrip.svelte — strip wrapper, 1-col mobile / 3-col sm+ i18n: 19 new keys added to de/en/es (mission_control_*) Page wiring: - +page.server.ts: 4 new Promise.allSettled calls for segmentation-queue, transcription-queue, ready-to-read, weekly-stats; all failures silent - +page.svelte: MissionControlStrip rendered below the grid in isDashboard Co-Authored-By: Claude Sonnet 4.6 --- frontend/messages/de.json | 19 +++- frontend/messages/en.json | 19 +++- frontend/messages/es.json | 19 +++- .../src/lib/components/ExpertBadge.svelte | 26 +++++ .../lib/components/MissionControlStrip.svelte | 45 +++++++++ .../src/lib/components/ReadyColumn.svelte | 86 +++++++++++++++++ .../lib/components/SegmentationColumn.svelte | 71 ++++++++++++++ .../lib/components/TranscriptionColumn.svelte | 94 +++++++++++++++++++ frontend/src/routes/+page.server.ts | 42 ++++++++- frontend/src/routes/+page.svelte | 8 ++ 10 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 frontend/src/lib/components/ExpertBadge.svelte create mode 100644 frontend/src/lib/components/MissionControlStrip.svelte create mode 100644 frontend/src/lib/components/ReadyColumn.svelte create mode 100644 frontend/src/lib/components/SegmentationColumn.svelte create mode 100644 frontend/src/lib/components/TranscriptionColumn.svelte diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 81350a62..766f13f9 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -557,5 +557,22 @@ "training_seg_too_few_blocks": "Mindestens 5 Segmentierungsblöcke erforderlich (aktuell: {available}).", "transcription_block_segmentation_only": "Nur Segmentierung", "training_chip_kurrent": "Kurrent-Erkennung", - "training_chip_segmentation": "Segmentierung" + "training_chip_segmentation": "Segmentierung", + "mission_control_heading": "Mitarbeiten", + "mission_control_segmentation_heading": "Segmentierung", + "mission_control_segmentation_description": "Textbereiche markieren — keine Vorkenntnisse nötig", + "mission_control_segmentation_cta": "Segmentieren", + "mission_control_segmentation_empty": "Alle Dokumente haben bereits Segmentierungsblöcke.", + "mission_control_transcription_heading": "Transkription", + "mission_control_transcription_description": "Text abschreiben — Kurrent-Kenntnisse hilfreich", + "mission_control_transcription_cta": "Transkribieren", + "mission_control_transcription_empty": "Keine Dokumente warten auf Transkription.", + "mission_control_ready_heading": "Lesefertig", + "mission_control_ready_description": "Vollständig transkribiert und geprüft", + "mission_control_ready_empty": "Noch keine Dokumente vollständig transkribiert.", + "mission_control_ready_empty_cta": "Jetzt mitmachen", + "mission_control_weekly_pulse": "↑ +{count} diese Woche", + "mission_control_expert_badge": "Experten gesucht", + "mission_control_blocks_progress": "{texted} / {total} Blöcke", + "mission_control_reviewed_pct": "{pct}% geprüft" } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index bbbd0f07..54494d0d 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -557,5 +557,22 @@ "training_seg_too_few_blocks": "At least 5 segmentation blocks required (currently: {available}).", "transcription_block_segmentation_only": "Segmentation only", "training_chip_kurrent": "Kurrent recognition", - "training_chip_segmentation": "Segmentation" + "training_chip_segmentation": "Segmentation", + "mission_control_heading": "Contribute", + "mission_control_segmentation_heading": "Segmentation", + "mission_control_segmentation_description": "Mark text areas — no prior knowledge needed", + "mission_control_segmentation_cta": "Segment", + "mission_control_segmentation_empty": "All documents already have segmentation blocks.", + "mission_control_transcription_heading": "Transcription", + "mission_control_transcription_description": "Type out text — Kurrent knowledge helpful", + "mission_control_transcription_cta": "Transcribe", + "mission_control_transcription_empty": "No documents waiting for transcription.", + "mission_control_ready_heading": "Ready to Read", + "mission_control_ready_description": "Fully transcribed and reviewed", + "mission_control_ready_empty": "No documents fully transcribed yet.", + "mission_control_ready_empty_cta": "Start contributing", + "mission_control_weekly_pulse": "↑ +{count} this week", + "mission_control_expert_badge": "Expert needed", + "mission_control_blocks_progress": "{texted} / {total} blocks", + "mission_control_reviewed_pct": "{pct}% reviewed" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 2d7aba00..d6335b15 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -557,5 +557,22 @@ "training_seg_too_few_blocks": "Se requieren al menos 5 bloques de segmentación (actualmente: {available}).", "transcription_block_segmentation_only": "Solo segmentación", "training_chip_kurrent": "Reconocimiento Kurrent", - "training_chip_segmentation": "Segmentación" + "training_chip_segmentation": "Segmentación", + "mission_control_heading": "Colaborar", + "mission_control_segmentation_heading": "Segmentación", + "mission_control_segmentation_description": "Marcar áreas de texto — sin conocimientos previos", + "mission_control_segmentation_cta": "Segmentar", + "mission_control_segmentation_empty": "Todos los documentos ya tienen bloques de segmentación.", + "mission_control_transcription_heading": "Transcripción", + "mission_control_transcription_description": "Escribir el texto — conocimiento de Kurrent útil", + "mission_control_transcription_cta": "Transcribir", + "mission_control_transcription_empty": "No hay documentos esperando transcripción.", + "mission_control_ready_heading": "Listo para leer", + "mission_control_ready_description": "Completamente transcrito y revisado", + "mission_control_ready_empty": "Aún no hay documentos completamente transcritos.", + "mission_control_ready_empty_cta": "Empezar a colaborar", + "mission_control_weekly_pulse": "↑ +{count} esta semana", + "mission_control_expert_badge": "Se busca experto", + "mission_control_blocks_progress": "{texted} / {total} bloques", + "mission_control_reviewed_pct": "{pct}% revisado" } diff --git a/frontend/src/lib/components/ExpertBadge.svelte b/frontend/src/lib/components/ExpertBadge.svelte new file mode 100644 index 00000000..7fbdf28b --- /dev/null +++ b/frontend/src/lib/components/ExpertBadge.svelte @@ -0,0 +1,26 @@ + + + + + {m.mission_control_expert_badge()} + diff --git a/frontend/src/lib/components/MissionControlStrip.svelte b/frontend/src/lib/components/MissionControlStrip.svelte new file mode 100644 index 00000000..7bd28697 --- /dev/null +++ b/frontend/src/lib/components/MissionControlStrip.svelte @@ -0,0 +1,45 @@ + + +
+

+ {m.mission_control_heading()} +

+
+ + + +
+
diff --git a/frontend/src/lib/components/ReadyColumn.svelte b/frontend/src/lib/components/ReadyColumn.svelte new file mode 100644 index 00000000..b570ecf2 --- /dev/null +++ b/frontend/src/lib/components/ReadyColumn.svelte @@ -0,0 +1,86 @@ + + +{#if docs.length > 0} +
+
+

+ {m.mission_control_ready_heading()} +

+ {#if weeklyCount > 0} + + {m.mission_control_weekly_pulse({ count: weeklyCount })} + + {/if} +
+

+ {m.mission_control_ready_description()} +

+ +
+{:else} +
+

{m.mission_control_ready_empty()}

+ + {m.mission_control_ready_empty_cta()} + +
+{/if} diff --git a/frontend/src/lib/components/SegmentationColumn.svelte b/frontend/src/lib/components/SegmentationColumn.svelte new file mode 100644 index 00000000..71209817 --- /dev/null +++ b/frontend/src/lib/components/SegmentationColumn.svelte @@ -0,0 +1,71 @@ + + +
+
+

+ {m.mission_control_segmentation_heading()} +

+ {#if weeklyCount > 0} + + {m.mission_control_weekly_pulse({ count: weeklyCount })} + + {/if} +
+

+ {m.mission_control_segmentation_description()} +

+ + {#if docs.length === 0} +

{m.mission_control_segmentation_empty()}

+ {:else} + + {/if} +
diff --git a/frontend/src/lib/components/TranscriptionColumn.svelte b/frontend/src/lib/components/TranscriptionColumn.svelte new file mode 100644 index 00000000..435a981a --- /dev/null +++ b/frontend/src/lib/components/TranscriptionColumn.svelte @@ -0,0 +1,94 @@ + + +
+
+

+ {m.mission_control_transcription_heading()} +

+ {#if weeklyCount > 0} + + {m.mission_control_weekly_pulse({ count: weeklyCount })} + + {/if} +
+

+ {m.mission_control_transcription_description()} +

+ + {#if docs.length === 0} +

{m.mission_control_transcription_empty()}

+ {:else} + + {/if} +
diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index e1386db8..6a430477 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -6,6 +6,8 @@ type IncompleteDocumentDTO = components['schemas']['IncompleteDocumentDTO']; type StatsDTO = components['schemas']['StatsDTO']; type Document = components['schemas']['Document']; type SearchMatchData = components['schemas']['SearchMatchData']; +type TranscriptionQueueItemDTO = components['schemas']['TranscriptionQueueItemDTO']; +type TranscriptionWeeklyStatsDTO = components['schemas']['TranscriptionWeeklyStatsDTO']; export async function load({ url, fetch }) { const q = url.searchParams.get('q') || ''; @@ -82,12 +84,28 @@ export async function load({ url, fetch }) { let stats: StatsDTO | null = null; let incompleteDocs: IncompleteDocumentDTO[] = []; let recentDocs: Document[] = []; + let segmentationDocs: TranscriptionQueueItemDTO[] = []; + let transcriptionDocs: TranscriptionQueueItemDTO[] = []; + let readyDocs: TranscriptionQueueItemDTO[] = []; + let weeklyStats: TranscriptionWeeklyStatsDTO | null = null; if (isDashboard) { - const [statsResult, incompleteResult, recentResult] = await Promise.allSettled([ + const [ + statsResult, + incompleteResult, + recentResult, + segmentationResult, + transcriptionResult, + readyResult, + weeklyStatsResult + ] = await Promise.allSettled([ api.GET('/api/stats'), api.GET('/api/documents/incomplete', { params: { query: { size: 3 } } }), - api.GET('/api/documents/recent-activity', { params: { query: { size: 5 } } }) + api.GET('/api/documents/recent-activity', { params: { query: { size: 5 } } }), + api.GET('/api/transcription/segmentation-queue'), + api.GET('/api/transcription/transcription-queue'), + api.GET('/api/transcription/ready-to-read'), + api.GET('/api/transcription/weekly-stats') ]); if (statsResult.status === 'fulfilled' && statsResult.value.response.ok) { @@ -99,6 +117,18 @@ export async function load({ url, fetch }) { if (recentResult.status === 'fulfilled' && recentResult.value.response.ok) { recentDocs = recentResult.value.data ?? []; } + if (segmentationResult.status === 'fulfilled' && segmentationResult.value.response.ok) { + segmentationDocs = (segmentationResult.value.data ?? []) as TranscriptionQueueItemDTO[]; + } + if (transcriptionResult.status === 'fulfilled' && transcriptionResult.value.response.ok) { + transcriptionDocs = (transcriptionResult.value.data ?? []) as TranscriptionQueueItemDTO[]; + } + if (readyResult.status === 'fulfilled' && readyResult.value.response.ok) { + readyDocs = (readyResult.value.data ?? []) as TranscriptionQueueItemDTO[]; + } + if (weeklyStatsResult.status === 'fulfilled' && weeklyStatsResult.value.response.ok) { + weeklyStats = weeklyStatsResult.value.data ?? null; + } } return { @@ -109,6 +139,10 @@ export async function load({ url, fetch }) { stats, incompleteDocs, recentDocs, + segmentationDocs, + transcriptionDocs, + readyDocs, + weeklyStats, initialValues: { senderName: senderObj?.displayName ?? '', receiverName: receiverObj?.displayName ?? '' @@ -127,6 +161,10 @@ export async function load({ url, fetch }) { stats: null, incompleteDocs: [], recentDocs: [], + segmentationDocs: [], + transcriptionDocs: [], + readyDocs: [], + weeklyStats: null, initialValues: { senderName: '', receiverName: '' }, filters: { q, from, to, senderId, receiverId, tags, sort, dir, tagQ }, error: 'Daten konnten nicht geladen werden.' as string | null diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index bbdfae44..4bf13282 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -9,6 +9,7 @@ import DocumentList from './DocumentList.svelte'; import DashboardResumeStrip from '$lib/components/DashboardResumeStrip.svelte'; import DashboardNeedsMetadata from '$lib/components/DashboardNeedsMetadata.svelte'; import DashboardRecentDocuments from '$lib/components/DashboardRecentDocuments.svelte'; +import MissionControlStrip from '$lib/components/MissionControlStrip.svelte'; import { m } from '$lib/paraglide/messages.js'; let { data } = $props(); @@ -132,6 +133,13 @@ const showRightColumn = $derived(data.canWrite || (data.incompleteDocs?.length ? + + {:else} Date: Wed, 15 Apr 2026 22:48:27 +0200 Subject: [PATCH 04/26] docs(#240): add Mission Control Strip spec and pattern alternatives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the design decision record for how to expand the dashboard without pushing content below the fold: a full-width 3-column strip (Segmentierung / Transkription / Lesefertig) below the existing grid. - dashboard-expansion-patterns.html — four pattern alternatives evaluated (Tabs, Accordion, Mission Control, Priority Queue) with annotated mockups, engagement feature proposal, and final recommendation. - mission-control-strip-final.html — clean implementation blueprint with pipeline diagram, column definitions, seeded-weekly-shuffle sorting, expert-flag escape hatch, all Tailwind impl-ref values, and backend contracts. Co-Authored-By: Claude Sonnet 4.6 --- docs/specs/dashboard-expansion-patterns.html | 1122 ++++++++++++++++++ docs/specs/mission-control-strip-final.html | 814 +++++++++++++ 2 files changed, 1936 insertions(+) create mode 100644 docs/specs/dashboard-expansion-patterns.html create mode 100644 docs/specs/mission-control-strip-final.html diff --git a/docs/specs/dashboard-expansion-patterns.html b/docs/specs/dashboard-expansion-patterns.html new file mode 100644 index 00000000..0907ca9e --- /dev/null +++ b/docs/specs/dashboard-expansion-patterns.html @@ -0,0 +1,1122 @@ + + + + + +Dashboard Expansion — 4 Layout-Muster (Issue #240) + + + +
+ + +
+

Dashboard Expansion — 4 Layout-Muster

+
+ Issue #240 + Leonie Voss — UX & Accessibility + 15. April 2026 +
+
src/routes/+page.svelte  ·  src/lib/components/Dashboard*.svelte  ·  Muster A Tabs · B Accordion · C Mission Control · D Priority Queue
+
+ +
+

Das Problem

+

+ Die rechte Spalte der Startseite ist mit DropZone + „Metadaten fehlen" bereits ausgelastet. + Issue #240 möchte zwei weitere Karten ergänzen: Transkription ausstehend und Lesefertig. + Direkt gestapelt scrollt die Spalte sofort aus dem sichtbaren Bereich — ein 67-jähriger Nutzer auf einem kleinen + Display sieht die unteren Karten nie. +

+

+ Der Nutzer schlug Tabs vor (Neueste Aktivität / Transkription fehlt / Metadaten fehlen / Lesefertig). + Diese Spec bewertet diesen Vorschlag und zeigt drei weitere Muster im Vergleich. + Alle Muster werden bei 320 px (Mobiltelefon) und Desktop (~55 % Skalierung) gezeigt. +

+
+ + +
+ +
+
+
+
+ + +
MR
+
+
+ +
+
+
+
Neueste Aktivität
+
Brief von Oma Martha, 1943
Karl Raddatz
12. Apr
+
Taufurkunde Karl Raddatz
Standesamt
9. Apr
+
Postkarte aus Breslau
Martha Raddatz
7. Apr
+
Familienfoto Sommer 1952
Unbekannt
3. Apr
+
47 Dokumente · 12 Personen
+
+
+
Datei hochladen
Drag & Drop oder klicken
+
+
Metadaten fehlen
+
Familienfoto 1952
+
Standesamtsurkunde
+
Reisepass Opa Heinrich
+ Alle anzeigen → +
+
▼ TRANSKRIPTION FEHLT (neu) — scrollt aus dem Sichtfeld
+
▼ LESEFERTIG (neu) — nie sichtbar ohne Scrollen
+
+
+
+
+ Desktop (55 % Skalierung) — zwei neue Karten würden die rechte Spalte sprengen +
+
+
+ +
+ + + + +
+ + +
Aufgaben-Karte mit Tab-Strip
+
DropZone bleibt oben. Darunter: eine Karte mit drei Tabs (Metadaten / Transkription / Lesefertig).
+ +
+ + Kein zusätzlicher Scroll in der rechten Spalte + + Bekanntes Muster für jüngere Nutzer + + Platzsparend in 300 px + − Inaktive Kategorien unsichtbar — Senioren übersehen sie + − Drei kurze Labels kaum lesbar bei 300 px / 320 px + − JS-Zustand nötig (aktiver Tab) +
+ +

+ Der ursprüngliche Vorschlag nannte vier Tabs für die gesamte Seite (Neueste Aktivität / Transkription / Metadaten / Lesefertig). + Das ist ein UX-Antipattern: „Neueste Aktivität" ist der häufigste Anwendungsfall — ihn hinter einem Klick zu verstecken erhöht den Aufwand für jeden Dashboard-Besuch. + Tabs sollten ausschließlich auf die drei To-do-Widget-Kategorien in der rechten Spalte angewendet werden, nicht auf die gesamte Seite. +

+

+ Accessibility: Tab-Elemente müssen role="tablist", role="tab", aria-selected und + role="tabpanel" tragen. Jeder Tab braucht min-h-[44px] (WCAG 2.2 Touchziel). + Drei Tabs in einer 300-px-Spalte = ~100 px pro Tab — grenzwertig auf Deutsch mit langen Wörtern. +

+ +
+ +
+
+
+
+ +
Hochladen
+ +
+
+
Metadaten
+
Transkr.
+
Lesefertig
+
+
Familienfoto 1952
Titel fehlt
+
Standesamtsurkunde
Datum fehlt
+
Reisepass Opa
Absender fehlt
+ Alle anzeigen → +
+ +
+
Neueste Aktivität
+
Brief von Oma Martha
+
Taufurkunde Karl R.
+
47 Dok. · 12 Pers.
+
+
+
+ Mobil 320 px +
+ + +
+
+
+ + +
MR
+
+
+ +
+
+
+
Neueste Aktivität
+
Brief von Oma Martha, 1943
12. Apr
+
Taufurkunde Karl Raddatz
9. Apr
+
Postkarte aus Breslau
7. Apr
+
Familienfoto Sommer 1952
3. Apr
+
47 Dokumente · 12 Personen
+
+
+
Datei hochladen
Drag & Drop
+
+
+
Metadaten
+
Transkr.
+
Lesefertig
+
+
Familienfoto 1952
Titel fehlt
+
Standesamtsurkunde
Datum fehlt
+
Reisepass Opa Heinrich
Absender fehlt
+ Alle anzeigen → +
+
+
+
+
+ Desktop (55 %) — DropZone + Tabbed-Karte; zwei Kategorien versteckt hinter Klick +
+
+ +
+ + + + + + + + + + +
ElementTailwind-KlassenWertHinweis
Tab-Stripflex border-b border-lineARIA: role="tablist"
Tab inaktivpx-3 py-3 text-xs font-medium text-gray-500 border-b-2 border-transparent whitespace-nowrap -mb-pxmin-h 44 px ✓role="tab" aria-selected="false"
Tab aktivpx-3 py-3 text-xs font-semibold text-ink border-b-2 border-ink -mb-px2 px navy Unterliniearia-selected="true"
Tab-Panelpt-3 focus:outline-nonerole="tabpanel" tabindex="0"
Aufgaben-Karterounded-sm border border-line bg-white p-4 flex-1padding 16 pxErsetzt 3 separate Karten
Zeile mit Kontextflex flex-col py-2 border-b border-line last:border-0min-h ~44 pxz. B. „3 von 8 Blöcken geprüft"
+
+
+ +
+ + + + +
+ + +
Aufklappbare Kategorien
+
DropZone oben. Darunter: drei Accordion-Sektionen, standardmäßig nur die dringlichste offen.
+ +
+ + Alle drei Kategorien-Überschriften immer sichtbar + + Kein JS — implementierbar mit nativem <details> + + Vertraut für 60+ (wie FAQ) + − Inhalt zu 2/3 verborgen (nur Überschriften sichtbar) + − „Lesefertig" wird meist zugeklappt bleiben und übersehen +
+ +

+ Jede Sektion zeigt eine klickbare Kopfzeile (Pfeil + Label + Anzahl). Server-seitig wird die Sektion mit der + höchsten Anzahl als <details open> gerendert. Kein Client-JS nötig — native + <details>/<summary>-Elemente liefern ARIA-Accessibility gratis. + Sortierung der offenen Sektion nach Dringlichkeit: Metadaten → Transkription → Lesefertig. +

+ +
+ +
+
+
+
+ +
Hochladen
+
+
+
▼ Metadaten fehlen (5)
+
+
Familienfoto 1952
Titel fehlt
+
Standesamtsurkunde
Datum fehlt
+ Alle 5 anzeigen → +
+
+
▶ Transkription fehlt (8)
+
▶ Lesefertig ✓ (3)
+
+
+
Neueste Aktivität
+
Brief von Oma Martha
+
Taufurkunde Karl R.
+
+
+
+ Mobil 320 px +
+ + +
+
+
+ + +
MR
+
+
+ +
+
+
+
Neueste Aktivität
+
Brief von Oma Martha, 1943
12. Apr
+
Taufurkunde Karl Raddatz
9. Apr
+
Postkarte aus Breslau
7. Apr
+
Familienfoto Sommer 1952
3. Apr
+
47 Dokumente · 12 Personen
+
+
+
Datei hochladen
Drag & Drop
+
+
+
▼ Metadaten fehlen (5)
+
+
Familienfoto 1952
Titel fehlt
+
Standesamtsurkunde
Datum fehlt
+ Alle 5 anzeigen → +
+
+
▶ Transkription fehlt (8)
+
▶ Lesefertig ✓ (3)
+
+
+
+
+
+ Desktop (55 %) — Metadaten-Sektion offen, Transkription und Lesefertig zugeklappt +
+
+ +
+ + + + + + + + + + +
ElementHTML / Tailwind-KlassenWertHinweis
Accordion-Wrapper<details> nativAccessibility gratis, kein JS
Accordion-Header<summary class="flex items-center justify-between min-h-[44px] cursor-pointer list-none">min-h 44 px ✓WCAG 2.2 Touchziel
Pfeil-Icontransition-transform group-open:rotate-90w-4 h-4CSS-only; kein JS
Zähler-Badgeml-auto font-mono text-xs text-gray-400z. B. „(5)"
Dringlichste Sektion<details open>Server-seitig rendern: if incompleteDocs.length >= needsTrans.length
Accordion-Inhaltpt-1 pb-2 direkt nach <summary>Keine overflow:hidden-Animation nötig
+
+
+ +
+ + + + +
+ + +
Volle Breite unterhalb des Hauptgitters
+
Rechte Spalte bleibt unverändert. Unterhalb des Gitters: drei gleichwertige Spalten als neuer horizontaler Aktionsbereich.
+ +
+ + Alle drei Kategorien gleichzeitig sichtbar — kein Klick nötig + + „Neueste Aktivität" bleibt primärer Inhalt, nichts rückt dahinter + + Kein JS-Zustand + + „Lesefertig" bekommt eigene mint-Karte als visuellen Applaus + + Mobil stapeln die drei Spalten natürlich + − Leichtes Scrollen auf Desktop erforderlich + − Sechster API-Aufruf beim Dashboard-Load (via Promise.allSettled isoliert) +
+ +

+ Der Ist-Zustand der rechten Spalte bleibt vollständig erhalten. Die zwei neuen Karten des Issue #240 werden + nicht in die rechte Spalte gepackt, sondern in einen neuen vollbreiten Abschnitt direkt unterhalb des + bestehenden Zwei-Spalten-Gitters. Der Abschnitt ist nur sichtbar, wenn mindestens eine der beiden Kategorien + Einträge hat ({#if needsTranscription.length > 0 || readyToRead.length > 0}). +

+

+ Die „Lesefertig"-Spalte erhält einen mint-gefärbten Hintergrund (bg-mint/10 border-mint) als positives Signal — kein + neutrales To-do, sondern eine Einladung zum Lesen. Leere Zustände zeigen eine kurze Erfolgsmeldung in + bg-mint/5, nicht eine tote weiße Box. +

+ +
+ +
+
+
+
+ + +
Hochladen
+
+
Metadaten fehlen
+
Familienfoto 1952
+
Standesamtsurkunde
+ Alle anzeigen → +
+ +
+
Neueste Aktivität
+
Brief von Oma Martha
+
Taufurkunde Karl R.
+
47 Dok. · 12 Pers.
+
+ +
+
Was braucht Aufmerksamkeit?
+
+ +
Taufurkunde Karl R.
Noch nicht begonnen
+
Reisepass Opa Heinrich
3 von 8 Blöcken geprüft
+ Alle 8 anzeigen → +
+
+
Lesefertig ✓
+
Postkarte aus Breslau
100 % geprüft
+
Brief Oma Martha 1938
95 % geprüft
+ Alle 3 anzeigen → +
+
+
+
+ Mobil 320 px — Streifen stapelt vertikal +
+ + +
+
+
+ + +
MR
+
+
+ +
+ +
+
+
Neueste Aktivität
+
Brief von Oma Martha, 1943
12. Apr
+
Taufurkunde Karl Raddatz
9. Apr
+
Postkarte aus Breslau
7. Apr
+
Familienfoto Sommer 1952
3. Apr
+
47 Dokumente · 12 Personen
+
+
+
Datei hochladen
Drag & Drop
+
+
Metadaten fehlen
+
Familienfoto 1952
Titel fehlt
+
Standesamtsurkunde
Datum fehlt
+
Reisepass Opa Heinrich
Absender fehlt
+ Alle anzeigen → +
+
+
+ + +
+
Was braucht Aufmerksamkeit?
+
+ +
+ +
Taufurkunde Karl R.
Noch nicht begonnen
+
Reisepass Opa Heinrich
3 von 8 Blöcken geprüft
+
Standesamt 1889
Noch nicht begonnen
+ Alle 8 anzeigen → +
+ +
+
Lesefertig ✓
+
Postkarte aus Breslau
100 % geprüft
+
Brief Oma Martha 1938
95 % geprüft
+
Heiratsurkunde 1921
91 % geprüft
+ Alle 3 lesen → +
+ +
+
+
Alle Dokumente transkribiert
+
Keine offenen Aufgaben
+
+
+
+
+
+ Desktop (55 %) — Hauptgitter unberührt; Streifen darunter zeigt leeren Zustand in Spalte 3 +
+
+ +
+ + + + + + + + + + + + +
ElementTailwind-KlassenWertHinweis
Streifen-Wrappermt-4 bg-white border border-line rounded-sm p-6padding 24 pxDirekt nach bestehendem div.mt-4.grid
Streifen-Titeltext-xs font-bold uppercase tracking-widest text-gray-400 mb-412 px / 700Standard-Section-Title-Muster
3-Spalten-Gridgrid grid-cols-1 gap-4 sm:grid-cols-3gap 16 pxMobil: 1 Spalte, sm+: 3
Transkription-Spaltebg-surface rounded-sm border border-line p-4Neutral — es ist eine Aufgabe
Lesefertig-Spaltebg-mint/10 rounded-sm border border-mint p-4Mint-Ton = positives Signal
Leerer Zustandflex flex-col items-center justify-center text-center bg-mint/5 border border-dashed border-mint rounded-sm p-6 min-h-[80px]min-h 80 pxNiemals leere graue Box
Untertext-Zeiletext-xs text-gray-400 mt-0.512 pxz. B. „3 von 8 Blöcken geprüft"
Sichtbarkeit{#if needsTranscription.length > 0 || readyToRead.length > 0}Streifen komplett ausgeblendet wenn leer
+
+
+ +
+ + + + +
+ + +
„Was als nächstes?" — zusammengeführte Liste
+
Alle drei Kategorien in einer sortierten Liste mit Typ-Badge. Ersetzt die separaten drei Karten in der rechten Spalte.
+ +
+ + Keine mentale Auswahl zwischen Kategorien + + Eine einzelne Entscheidungsfläche + − „Lesefertig" verliert seinen Belohnungscharakter (gemischt mit Aufgaben) + − Merge-Logik auf dem Server ist komplexer als zwei separate Queries + − Farb-Badge allein reicht nicht — Icon + Label immer nötig (WCAG 1.4.1) +
+ +

+ Alle drei Kategorien werden in einer einzigen sortierten Liste zusammengeführt. Sortierung: + Metadaten fehlen (blockiert Suche) → Transkription fehlt → Lesefertig. Jede Zeile trägt ein farbkodiertes Label; + Farbe darf niemals der einzige Indikator sein — Icon und Text sind Pflicht (WCAG 1.4.1). +

+

+ Kontrast-Check: Orange-700 auf Weiß = 5,4:1 ✓ AA. Navy auf Weiß = 14,5:1 ✓ AAA. Green-800 auf Weiß = 9,7:1 ✓ AAA. +

+ +
+ +
+
+
+
+ +
Hochladen
+
+
Was als nächstes?
+
Familienfoto 1952
⚠ Metadaten fehlen
+
Taufurkunde Karl R.
✏ Noch nicht begonnen
+
Reisepass Opa
✏ 3 von 8 Blöcken
+
Postkarte 1943
✓ Lesefertig
+
+
+
Neueste Aktivität
+
Brief von Oma Martha
+
Taufurkunde Karl R.
+
+
+
+ Mobil 320 px +
+ + +
+
+
+ + +
MR
+
+
+ +
+
+
+
Neueste Aktivität
+
Brief von Oma Martha, 1943
12. Apr
+
Taufurkunde Karl Raddatz
9. Apr
+
Postkarte aus Breslau
7. Apr
+
Familienfoto Sommer 1952
3. Apr
+
47 Dokumente · 12 Personen
+
+
+
Datei hochladen
Drag & Drop
+
+
Was als nächstes?
+
Familienfoto 1952
⚠ Metadaten fehlen
+
Standesamtsurkunde
⚠ Datum fehlt
+
Taufurkunde Karl R.
✏ Noch nicht begonnen
+
Reisepass Opa Heinrich
✏ 3 von 8 Blöcken
+
Postkarte aus Breslau
✓ Lesefertig
+
+
+
+
+
+ Desktop (55 %) — eine Liste, drei Typen durch Farbe + Icon + Label unterschieden +
+
+ +
+ + + + + + + + + + + +
ElementTailwind-KlassenWertHinweis
Listen-Wrapperrounded-sm border border-line bg-white p-4 flex-1Ersetzt separate 3 Karten
Prioritäts-Zeileflex items-start gap-3 py-2 border-b border-line last:border-0 min-h-[44px]min-h 44 px ✓WCAG touch target
Typ-Punktw-2 h-2 rounded-full mt-1.5 shrink-08 × 8 pxNie allein — Label ist Pflicht
Label orangetext-xs text-orange-70012 pxKontrast 5,4:1 ✓ AA
Label navytext-xs text-ink12 pxKontrast 14,5:1 ✓ AAA
Label grüntext-xs text-green-80012 pxKontrast 9,7:1 ✓ AAA
Merge-ServicefindWhatsNext(int size) auf DashboardControllerSortierung: Metadaten → Trans → Lesefertig; per Markus: Threshold als @Param
+
+
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KriteriumA — TabsB — AccordionC — Mission Control ★D — Priority Queue
Alle 3 Kategorien sofort sichtbarNeinNur ÜberschriftenJaNein (gemischt)
Neueste Aktivität bleibt PrimärinhaltJaJaJaJa
60+ Usability (Discovery ohne Klick)MittelÜberschriften sichtbarSehr gutMittel — gemischte Liste
JS-Zustand nötigJa (aktiver Tab)NeinNeinNein
WCAG 2.2 compliance (out of the box)tablist + aria-selected nötigdetails/summary nativKeine neuen AnforderungenFarbe + Icon + Label alle Pflicht
Mobile 320 px3 Tabs zu schmalGutSehr gut — stapelt natürlichGut
„Lesefertig" als visueller ApplausNur wenn Tab aktivNur wenn offenJa — eigene mint-KarteNein — gleichwertig mit Aufgaben
Backend-Merge-KomplexitätGeringGeringGering (2 separate Queries)Mittel (Merge + Sortierung)
Implementierungsaufwand FrontendMittelGeringGeringGering
+
+ + +
+

Empfehlung: Muster C — Mission-Control-Streifen

+

+ Der Mission-Control-Streifen ist das einzige Muster, das alle drei Kategorien gleichzeitig sichtbar macht, + ohne den Primärinhalt zu verstecken oder JS-Zustand zu erzeugen. Scrollen nach unten ist kein Fehler — + versteckter Inhalt schon. +

+
    +
  • Tabs (A) — Gut für kompakten Platz, aber inaktive Kategorien werden von 60+-Nutzern übersehen. Tabs für die gesamte Seite (wie ursprünglich vorgeschlagen) wäre ein Antipattern — Neueste Aktivität wäre hinter einem Klick.
  • +
  • Accordion (B) — Solide Fallback-Option ohne JS, wenn der Streifen aus Platzgründen abgelehnt wird. Alle Kategorien-Überschriften bleiben sichtbar.
  • +
  • Priority Queue (D) — Elegant, aber „Lesefertig" verliert seinen Belohnungscharakter. Die Merge-Logik ist auch komplexer als zwei separate Queries.
  • +
  • Mission Control (C) — Keine versteckten Inhalte. Kein JS. „Lesefertig" bekommt eine eigene mint-getönte Spalte als visuellen Applaus. Mobil stapeln die Spalten ohne weiteren Code. Der einzige Trade-off ist ein leichtes Scrollen auf Desktop.
  • +
+

+ Quick win: Wenn C abgelehnt wird — Muster B (Accordion) als Zweitstimme. Kein Refactoring der rechten Spalte, kein JS, alle Kategorien-Überschriften immer sichtbar. +

+
+ + +
+ + + + +
+ + +
Beitragspyramide: Skill-basierte Aufgabentrennung
+
+ Die ursprüngliche „Transkription fehlt"-Spalte wird in zwei klar getrennte Aufgaben aufgeteilt, die + unterschiedliche Fähigkeiten erfordern. Das löst gleichzeitig das Problem der leeren dritten Spalte. +
+ + +
+
+
Kein Segment
+
0 Annotationen
+
+
+
+
Segmentiert
+
Rahmen vorhanden, kein Text
+
+
+
+
Transkribiert
+
Text vorhanden, review < 90 %
+
+
+
+
Lesefertig ✓
+
review ≥ 90 %
+
+
+ Jede Stufe landet in einer eigenen Spalte. Die dritte Spalte ist strukturell nie leer — + Lesefertig-Dokumente erscheinen sobald auch nur eines fertig ist. +
+
+ + +
+
+
Spalte 1 — Segmentierung
+
+ Dokumente ohne Annotationsrahmen. Keine Vorkenntnisse nötig — Rahmen um Textblöcke einzeichnen. Niedrigste Einstiegshürde, breiteste Zielgruppe. +
+
Query: SELECT … WHERE annotation_count = 0
+
+
+
Spalte 2 — Transkription
+
+ Dokumente mit Rahmen, aber wenig/kein Text (text IS NULL OR LENGTH(text) < threshold). Kurrent-Kenntnisse empfohlen. +
+
Query: annotation_count > 0 AND reviewed < 75 %
+
+
+
Spalte 3 — Lesefertig ✓
+
+ Dokumente mit reviewed ≥ 90 %. Belohnungsbereich — kein Auftrag, sondern eine Einladung zum Lesen. +
+
Query: reviewed_pct >= 0.90 (bestehend)
+
+
+ + +
+
Engagement-Elemente — 5 Ideen
+ +
+ +
+
① Fortschritt — drei verschiedene Granularitäten
+
+ Kein globaler Balken bei 1 500 Dokumenten. + Ein Balken bei 0,8 % Füllstand ist psychologisch demotivierender als gar kein Balken + (endowed-progress-Effekt: Menschen brechen auf, wenn das Ziel unerreichbar wirkt). +

+ Stattdessen drei zielgruppengerechte Ansätze: +
    +
  • Segmentierung-Spalte: Wochenpuls — „Diese Woche: +5 Dokumente". Zeigt Schwung, nicht den Berg. Query: COUNT(*) WHERE created_at > NOW() - INTERVAL '7 days'
  • +
  • Transkription-Zeilen: Balkensegment pro Dokument — „3 von 8 Blöcken". Hier ist der Maßstab korrekt: 8 Blöcke sind in einer Sitzung erreichbar. Nur sichtbar wenn annotation_count > 0.
  • +
  • Lesefertig-Zeilen: Prozentzahl als Text — „94 % geprüft". Kein Balken nötig — der Erfolg ist bereits kommuniziert durch die mint-Spalte selbst.
  • +
+
+
+ +
+
② Skill-Label als Zugangsfilter
+
+ Unter dem Spaltentitel: ein kleines Pill-Label mit der Anforderung. „Ohne Vorkenntnisse" (grün) vs. + „Kurrent-Kenntnisse" (neutral). Senkt die Hemmschwelle für Neueinsteiger drastisch — sie sehen sofort, + was sie tun können. +
+
+ +
+
③ Contributor-Avatare (Social Proof)
+
+ Unter dem Titel: kleine Initialen-Bubbles der letzten 3 Beitragenden. Kein Leaderboard (erzeugt + Wettbewerb), aber soziale Sichtbarkeit (erzeugt Zugehörigkeit). „MR, TG und 2 weitere haben hier + mitgemacht." +
+
+ +
+
④ „Starte hier →" CTA-Button
+
+ Jede Aufgaben-Spalte endet mit einem einzelnen, klaren CTA, der direkt zum nächsten zu bearbeitenden + Dokument springt. Kein Auswahlprozess, kein Überlegen — ein Klick, sofort im Dokument. Entscheidungslähmung + ist der Hauptgrund für Non-Participation. +
+
+ +
+
⑤ Lesefertig-Leerstand → Cross-Column-Redirect
+
+ Wenn Lesefertig leer ist (frühe Projektphase), zeigt die Spalte nicht „Noch nichts fertig" als + stille Sackgasse. Stattdessen: „Dokumente erscheinen hier, wenn die Transkription abgeschlossen ist — + jetzt mithelfen →". Der Link springt direkt zur Segmentierungs-Spalte. Leerer Zustand = aktive + Einladung, kein toter Endpunkt. +
+
+ +
+
+ + +
+
+
+
+ + +
MR
+
+
+ +
+
+
+
Neueste Aktivität
+
Brief von Oma Martha, 1943
12. Apr
+
Taufurkunde Karl Raddatz
9. Apr
+
Postkarte aus Breslau
7. Apr
+
47 Dokumente · 12 Personen
+
+
+
Datei hochladen
Drag & Drop
+
+
Metadaten fehlen
+
Familienfoto 1952
Titel fehlt
+
Standesamtsurkunde
Datum fehlt
+ Alle 5 anzeigen → +
+
+
+ + +
+
Was braucht Aufmerksamkeit?
+
+ + +
+
+ +
✓ Ohne Vorkenntnisse
+ +
+
↑ +5 diese Woche
+
· 1 480 offen
+
+
+
MR
+
TG
+
AS
+
+ 2
+
+
+
Taufurkunde Karl R.
Noch keine Rahmen
+
Standesamt 1889
Noch keine Rahmen
+
Heiratsurkunde 1921
Noch keine Rahmen
+ Jetzt einzeichnen → +
+ + +
+
+ +
Kurrent hilfreich
+
+
↑ +2 diese Woche
+
· 8 offen
+
+
+
MR
+
1 Person
+
+
+ +
+
Brief v. Oma Martha 1943
+
+
+
0 / 6 Blöcke
+
+
+
+
Reisepass Opa Heinrich
+
+
+
3 / 8 Blöcke
+
+
+
+
Postkarte aus Breslau
+
+
+
0 / 4 Blöcke
+
+
+ Jetzt tippen → +
+ + +
+
+
Lesefertig ✓
+
3 Dokumente bereit
+
+
MR
+
TG
+
+
+ +
+
Postkarte aus Breslau 1943
+
100 % geprüft
+
+
+
Brief Oma Martha 1938
+
95 % geprüft
+
+
+
Heiratsurkunde 1921
+
91 % geprüft
+
+ Alle 3 lesen → +
+ +
+
+
+
+ Desktop (55 %) — v2: Spalten 1+2 aufgeteilt, Lesefertig-Leerstand mit aktivem Cross-Column-CTA +
+
+ + +
+ + + + + + + + + + + + + + + + +
ElementTailwind-Klassen / LogikWertHinweis
Skill-Pill „Ohne Vorkenntnisse"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-green-50 border border-green-200 text-green-800Kontrast 9,7:1 ✓Klärung für 60+ und Neueinsteiger
Skill-Pill „Kurrent hilfreich"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-surface border border-line text-inkneutral — kein SchreckpunktNicht „Experten nötig" sondern „hilfreich"
Wochenpuls (Segmentierung + Transkription)text-xs font-semibold text-green-700 (Seg.) / text-ink (Trans.)12 pxQuery: COUNT WHERE created_at > NOW() - INTERVAL '7 days'; kein globaler Balken
Per-Dokument-Balken Trackflex-1 h-1 bg-navy/20 rounded-full overflow-hiddenh: 4 pxNur in Transkription-Spalte, nur wenn annotation_count > 0
Per-Dokument-Balken Füllstandh-full bg-navy rounded-full + style="width:{pct}%"pct = textedBlocks / totalBlocks * 100; Guard: totalBlocks = 0 → width 0
Lesefertig-Prozentzahltext-xs font-semibold text-green-80012 pxKein Balken — die mint-Spalte selbst ist das Erfolgssignal
Contributor-Avatarw-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white24 × 24 pxFarbe per User-ID deterministisch (kein API-Feld nötig)
„Starte hier"-CTAblock w-full text-center text-xs font-semibold text-white bg-ink rounded-sm py-1.5 mt-2 hover:bg-ink-2 transition-colors focus-visible:ring-2 focus-visible:ring-inkmin-h 36 pxLink: /enrich?filter=NEEDS_SEGMENTATION&next=1
Lesefertig-Leerstand CTAinline-flex items-center text-xs font-semibold text-ink border border-ink rounded-sm px-3 py-1 hover:bg-ink hover:text-white transition-colorsLink springt zur Segmentierungs-Ansicht
Contributor-API-FeldGET /api/documents/needs-segmentation → DTO enthält lastContributors: [{initials, colorSeed}]max 3 AvatareNeues DTO-Feld — beachte Nora: nur Initialen, keine Namen
Segmentierung-QueryWHERE NOT EXISTS (SELECT 1 FROM document_annotations WHERE document_id = d.id)Index auf document_annotations.document_id prüfen (Tobias)
Transkription-QueryEXISTS annotation AND (no blocks OR reviewed_pct < 0.75)Guard gegen Division durch 0 (Sara)
+
+ + +
+ Datenschutz-Hinweis (Nora): Contributor-Avatare zeigen nur Initialen, niemals volle Namen im DOM. + Das DTO liefert initials + einen deterministischen colorSeed (z. B. Hash der User-ID mod 6 Farben), + keine E-Mail-Adressen oder echten Namen. Das @RequirePermission(READ_ALL) auf den neuen Endpoints gilt auch hier. +
+
+ +
+ + diff --git a/docs/specs/mission-control-strip-final.html b/docs/specs/mission-control-strip-final.html new file mode 100644 index 00000000..3b8e0138 --- /dev/null +++ b/docs/specs/mission-control-strip-final.html @@ -0,0 +1,814 @@ + + + + + +Mission-Control-Streifen — Finale Spec (Issue #240) + + + +
+ + +
+

Mission-Control-Streifen — Finale Spec

+
+ Issue #240 + Leonie Voss — UX & Accessibility + 15. April 2026 + v3 — Final +
+
src/routes/+page.svelte · src/lib/components/DashboardMissionControl.svelte · +page.server.ts
+
+
+

Entscheidung

+

+ Der bestehende Dashboard-Aufbau (Neueste Aktivität links, DropZone + Metadaten-Widget rechts) bleibt unverändert. + Unterhalb des Zwei-Spalten-Gitters erscheint ein neuer vollbreiter Mission-Control-Streifen mit drei + gleichwertigen Spalten: Rahmen einzeichnen (Segmentierung, kein Vorwissen nötig), + Text eintippen (Transkription, Kurrent hilfreich), Lesefertig ✓ (Belohnungsbereich). +

+

+ Die „Transkription fehlt"-Spalte aus Issue #240 wird in Segmentierung + Transkription aufgeteilt, um + eine klare Beitragspyramide zu schaffen: Jeder kann Rahmen einzeichnen — nicht jeder kann Kurrent lesen. + Ein wöchentlich rotierender Sort mit Experten-gesucht-Escape-Hatch verhindert, dass schwer lesbare + Dokumente dauerhaft die Spalte blockieren. +

+
+ + +
+
Dokument-Lebenszyklus
+
+
+
Kein Segment
+
0 Annotationen
+
→ Spalte 1
+
+
+
+
Segmentiert
+
Rahmen da, wenig Text
+
→ Spalte 2
+
+
+
+
In Review
+
Text da, reviewed < 90 %
+
→ Spalte 2
+
+
+
+
Lesefertig ✓
+
reviewed ≥ 90 %
+
→ Spalte 3
+
+
+ „Segmentiert" und „In Review" landen beide in Spalte 2 — + unterschieden durch den per-Dokument-Balken (0 Blöcke vs. N Blöcke). +
+
+ + +
+
+
Spalte 1 — Rahmen einzeichnen
+

Dokumente ohne Annotationsrahmen. Kein Kurrent nötig — Textblöcke markieren reicht.

+

Bedingung: annotation_count = 0

+

Sort: Wöchentliche Rotation (seeded shuffle, s. u.)

+

Fortschritt: Wochenpuls „↑ +5 diese Woche", kein globaler Balken

+
+
+
Spalte 2 — Text eintippen
+

Annotationen vorhanden, aber Text fehlt oder reviewed < 90 %. Kurrent-Kenntnisse hilfreich.

+

Bedingung: annotation_count > 0 AND reviewed_pct < 0.90

+

Sort: Teilfortschritt zuerst, dann wöchentliche Rotation; needsExpert-Flagge schiebt nach hinten

+

Fortschritt: Per-Dokument-Balken „3 / 8 Blöcke"

+
+
+
Spalte 3 — Lesefertig ✓
+

Reviewed ≥ 90 %. Keine Aufgabe — Einladung zum Lesen.

+

Bedingung: reviewed_pct >= 0.90

+

Sort: Neueste zuerst

+

Fortschritt: „94 % geprüft" als Text — kein Balken, die mint-Spalte ist das Signal

+

Leerstand: Cross-Column-Redirect zu Spalte 1

+
+
+
+ +
+ + +
+
Sortierstrategie — Das „zu schwer"-Problem
+
Schwer lesbare Dokumente blockieren die Spalte
+
Wenn dieselben 3 Dokumente immer oben stehen und niemand sie lesen kann, stoppt die Transkription komplett.
+ +
+
Problem: Bei 1 500 Dokumenten ohne Transkription und sortiert nach updated_at + können dieselben 3 besonders schwer lesbaren Dokumente dauerhaft die Spalte blockieren. + Jeder öffnet sie, gibt auf, und die Spalte wird zur Sackgasse.
+
+ +
+ +
+

Option 1 — Zufällig pro Seitenaufruf

+

ORDER BY RANDOM()

+

Jeder Besuch zeigt andere Dokumente. Kein Aufwand, aber chaotisch — kein Nutzer sieht ein Dokument zweimal, + kann nicht gezielt zurückkehren.

+
+ Null Aufwand− Chaotisch− Kein stabiles Lesezeichen
+
+ +
+
★ Empfohlen
+

Option 2 — Teilfortschritt + wöchentliche Rotation

+

Dokumente mit Teilfortschritt (3/8 Blöcke) erscheinen zuerst — am ehesten abschließbar. Dokumente mit 0 Blöcken rotieren wöchentlich durch einen deterministischen Shuffle.

+ ORDER BY textedBlocks DESC, + HASHTEXT(id || EXTRACT(WEEK FROM NOW())::text) +
+ Konsistent innerhalb einer Woche+ Bringt leichte Dokumente an die Oberfläche+ Kein neues Datenbankfeld
+
+ +
+

Option 3 — Manuelle Schwierigkeitsbewertung

+

Beitragende bewerten Dokumente 1–3 nach Versuch. Einfache Dokumente erscheinen zuerst.

+

Beste Langzeitlösung — braucht aber Bewertungs-UI auf der Enrich-Seite und Signalakkumulation.

+
+ Selbstverbessernd− UI-Aufwand− Braucht Zeit bis Signal
+
+
+ + + + + +
+
Mockup: Experten-gesucht-Badge in der Transkriptions-Zeile
+
+ +
+
Reisepass Opa Heinrich 3 / 8 Blöcke
+
+
+
37 %
+
+
+ +
+
Standesamt Breslau 1872 + Experten gesucht +
+
Schrift besonders schwer lesbar — Hilfe willkommen
+
+
+
+ +
+ + + + + + + + + +
ElementSQL / TailwindWertHinweis
Sort TranskriptionORDER BY textedBlocks DESC, HASHTEXT(id::text || EXTRACT(WEEK FROM NOW())::int::text)Kein neues Feld nötig; ändert sich automatisch jede Woche
needsExpert-FlagALTER TABLE documents ADD COLUMN needs_expert BOOLEAN NOT NULL DEFAULT FALSEFlyway V{n}__add_needs_expert.sqlFlagged Docs ans Ende: ORDER BY needs_expert ASC, ...
Experten-Badgeinline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-purple-50 border border-purple-200 text-purple-700Kontrast 6,8:1 ✓Nur wenn doc.needsExpert === true
„Zu schwer"-Button (Enrich)text-xs text-gray-400 hover:text-gray-600 underline underline-offset-2Unscheinbar — kein roter Knopf, keine Scham
Endpoint (Flagge setzen)PATCH /api/documents/{id}/needs-expert@RequirePermission(READ_ALL)Jeder angemeldete Nutzer darf flaggen
+
+
+ +
+ + +
+
Mockup — Desktop, normaler Zustand
+ +
+
+
+
+ + +
MR
+
+
+ +
+ + +
+
+
Neueste Aktivität
+
Brief von Oma Martha, 1943
Karl Raddatz
12. Apr
+
Taufurkunde Karl Raddatz
Standesamt
9. Apr
+
Postkarte aus Breslau
Martha Raddatz
7. Apr
+
Familienfoto Sommer 1952
Unbekannt
3. Apr
+
47 Dokumente · 12 Personen
+
+
+
Datei hochladen
Drag & Drop
+
+
Metadaten fehlen
+
Familienfoto 1952
Titel fehlt
+
Standesamtsurkunde
Datum fehlt
+ Alle 5 anzeigen → +
+
+
+ + +
+
Was braucht Aufmerksamkeit?
+
+ + +
+
+
Rahmen einzeichnen
+
✓ Ohne Vorkenntnisse
+
↑ +5 diese Woche· 1 480 offen
+
+
MR
+
TG
+
AS
+
+ 2
+
+
+
Taufurkunde Karl R.
Noch keine Rahmen
+
Standesamt 1889
Noch keine Rahmen
+
Heiratsurkunde 1921
Noch keine Rahmen
+ Jetzt einzeichnen → +
+ + +
+
+
Text eintippen
+
Kurrent hilfreich
+
↑ +2 diese Woche· 8 offen
+
+
MR
+
1 Person
+
+
+ +
+
Reisepass Opa Heinrich
+
3 / 8 Blöcke
+
+
+
Brief v. Oma Martha 1943
+
0 / 6 Blöcke
+
+ +
+
Standesamt Breslau 1872
Experten gesucht
+
Schrift besonders schwer lesbar
+
+ Jetzt tippen → +
+ + +
+
+
Lesefertig ✓
+
3 Dokumente bereit
+
+
MR
+
TG
+
+
+
+
Postkarte aus Breslau 1943
+
100 % geprüft
+
+
+
Brief Oma Martha 1938
+
95 % geprüft
+
+
+
Heiratsurkunde 1921
+
91 % geprüft
+
+ Alle 3 lesen → +
+ +
+
+
+
+ Desktop (55 %) — normaler Zustand: Teilfortschritt oben, Experten-gesucht-Dokument unten in Spalte 2 +
+
+
+ + +
+
Mockup — Desktop, frühe Projektphase (Lesefertig leer)
+ +
+
+
+
+ + +
MR
+
+
+ +
+
+
+
Neueste Aktivität
+
Brief von Oma Martha, 1943
12. Apr
+
Taufurkunde Karl Raddatz
9. Apr
+
1 500 Dokumente · 12 Personen
+
+
+
Datei hochladen
Drag & Drop
+
+
Metadaten fehlen
+
Familienfoto 1952
+
Standesamtsurkunde
+ Alle anzeigen → +
+
+
+
+
Was braucht Aufmerksamkeit?
+
+
+
+
Rahmen einzeichnen
+
✓ Ohne Vorkenntnisse
+
↑ +3 diese Woche· 1 498 offen
+
MR
1 Person
+
+
Taufurkunde Karl R.
+
Standesamt 1889
+
Heiratsurkunde 1921
+ Jetzt einzeichnen → +
+
+
+
Text eintippen
+
Kurrent hilfreich
+
↑ +1 diese Woche· 2 offen
+
MR
1 Person
+
+
+
Brief v. Oma Martha 1943
+
0 / 6 Blöcke
+
+
+
Reisepass Opa Heinrich
+
0 / 4 Blöcke
+
+ Jetzt tippen → +
+ +
+
+
Noch kein Dokument lesefertig
+
Erscheint hier sobald die Transkription abgeschlossen ist.
+ Jetzt mithelfen → +
+
+
+
+
+ Desktop (55 %) — frühe Phase: 1 500 Dokumente ohne Transkription, Wochenpuls zeigt Schwung statt Berg +
+
+
+ +
+ + +
+
Mockup — Mobil 320 px
+

+ Die rechte Spalte (DropZone + Metadaten) erscheint auf Mobil zuerst im DOM (lg:order-last schiebt sie auf Desktop nach rechts). + Der Streifen stapelt seine drei Spalten vertikal. Jede Spalte hat volle Breite — keine Overflow-Probleme. +

+ +
+ +
+
+
+
+ + +
Hochladen
+
+
Metadaten fehlen
+
Familienfoto 1952
+
Standesamtsurkunde
+
+ +
+
Neueste Aktivität
+
Brief von Oma Martha
+
Taufurkunde Karl R.
+
1 500 Dok. · 12 Pers.
+
+ +
+
Was braucht Aufmerksamkeit?
+ +
+
Rahmen einzeichnen
+
✓ Ohne Vorkenntnisse
+
↑ +5 diese Woche· 1 480 offen
+
Taufurkunde Karl R.
+
Standesamt 1889
+ Jetzt einzeichnen → +
+ +
+
Text eintippen
+
Kurrent hilfreich
+
↑ +2 diese Woche· 8 offen
+
+
Reisepass Opa Heinrich
+
3 / 8 Blöcke
+
+
+
Brief v. Oma Martha 1943
+
0 / 6 Blöcke
+
+ Jetzt tippen → +
+ +
+
Lesefertig ✓
+
3 bereit
+
Postkarte 1943
100 %
+
Brief Oma 1938
95 %
+ Alle lesen → +
+
+
+
+ Mobil 320 px — Streifen stapelt vertikal, volle Breite je Spalte +
+ + +
+
+
Mobile-Reihenfolge (DOM)
+
    +
  1. Suchleiste
  2. +
  3. DropZone (write users only)
  4. +
  5. Metadaten fehlen
  6. +
  7. Neueste Aktivität
  8. +
  9. Was braucht Aufmerksamkeit? +
      +
    1. Rahmen einzeichnen
    2. +
    3. Text eintippen
    4. +
    5. Lesefertig ✓
    6. +
    +
  10. +
+
+ +
+
+
+ +
+ + +
+
Engagement-Elemente — Zusammenfassung
+
+
+

① Skill-Pill

+

Unter jedem Spaltentitel. „Ohne Vorkenntnisse" (grün) vs. „Kurrent hilfreich" (navy-neutral). + Senkt die Hemmschwelle — Neueinsteiger sehen sofort, was ohne Kurrent-Kenntnisse möglich ist.

+

bg-green-50 border-green-200 text-green-800 / bg-surface border-line text-ink

+
+
+

② Wochenpuls

+

„↑ +5 diese Woche · 1 480 offen" statt globalem Fortschrittsbalken. + Zeigt Schwung, nicht den Berg. Psychologisch: 0,8 %-Balken ist demotivierender als kein Balken.

+

SELECT COUNT(*) WHERE created_at > NOW() - INTERVAL '7 days'

+
+
+

③ Per-Dokument-Balken

+

Nur in Spalte 2, nur wenn annotation_count > 0. Richtiger Maßstab: + 8 Blöcke sind in einer Sitzung abschließbar. Zeigt auch, welche Dokumente „fast fertig" sind.

+

width: {textedBlocks / totalBlocks * 100}%; Guard: totalBlocks === 0 → width: 0

+
+
+

④ Contributor-Avatare

+

Max. 3 Initialen-Bubbles der letzten Beitragenden pro Spalte. Kein Leaderboard (Wettbewerb) — + soziale Sichtbarkeit (Zugehörigkeit). Farbe deterministisch aus User-ID-Hash.

+

DTO: lastContributors: [{initials, colorIndex}] — nur Initialen, keine Namen (Nora)

+
+
+

⑤ „Starte hier →"-CTA

+

Ein einziger opinionated Button je Aufgaben-Spalte, der direkt zum nächsten Dokument springt. + Entscheidungslähmung ist der Hauptgrund für Non-Participation bei Familienprojekten.

+

/enrich?filter=NEEDS_SEGMENTATION&next=1 (Segmentierung)
/enrich?filter=NEEDS_TRANSCRIPTION&next=1 (Transkription)

+
+
+

⑥ Lesefertig-Leerstand → Redirect

+

Wenn Spalte 3 leer ist (frühe Phase), erscheint kein toter Endpunkt sondern: + „Erscheint hier, sobald die Transkription abgeschlossen ist — jetzt mithelfen →". + Der Link springt zu Spalte 1.

+

{#if readyToRead.length === 0}DashboardReadyToReadEmpty.svelte

+
+
+
+ +
+ + +
+
Implementation Reference
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwind-KlassenPixel / WertHinweis
Streifen-Wrappermt-4 bg-white border border-line rounded-sm p-6padding 24 pxDirekt nach bestehendem div.mt-4.grid
Streifen-Titeltext-xs font-bold uppercase tracking-widest text-gray-400 mb-412 px / 700Standard-Section-Title-Muster
3-Spalten-Gridgrid grid-cols-1 gap-4 sm:grid-cols-3gap 16 pxsm = 640 px; darunter stapeln
Segmentierung-Spaltebg-surface rounded-sm border border-line p-4 flex flex-col gap-3Neutral
Transkription-Spaltebg-surface rounded-sm border border-line p-4 flex flex-col gap-3Neutral — es ist eine Aufgabe
Lesefertig-Spalte (gefüllt)bg-mint/10 rounded-sm border border-mint p-4 flex flex-col gap-3Mint-Ton = Erfolg
Lesefertig-Spalte (leer)flex flex-col items-center justify-center text-center bg-mint/5 border border-dashed border-mint rounded-sm p-6 min-h-[120px]min-h 120 pxKein toter Endpunkt
Skill-Pill easyinline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-green-50 border border-green-200 text-green-800Kontrast 9,7:1 ✓ AAA
Skill-Pill kurrentinline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-surface border border-line text-inkKontrast 14,5:1 ✓ AAANeutral — kein Abschreck-Signal
Wochenpuls-Zahltext-xs font-semibold text-green-700 (Seg.) / text-ink (Trans.)12 pxKein globaler Balken
Per-Dokument-Trackflex-1 h-1 bg-navy/20 rounded-full overflow-hiddenh 4 pxNur wenn annotation_count > 0
Per-Dokument-Fillh-full bg-ink rounded-full transition-all + style="width:{pct}%"Guard: totalBlocks === 0 → 0%
Lesefertig-Prozenttext-xs font-semibold text-green-80012 pxKein Balken — mint-Spalte ist das Signal
Contributor-Avatarw-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white shrink-024 × 24 pxFarbe: 6 Werte, Index = userIdHash % 6
CTA-Button (primär)block w-full text-center text-xs font-semibold text-white bg-ink rounded-sm py-2 mt-2 hover:bg-ink-2 transition-colors focus-visible:ring-2 focus-visible:ring-ink focus-visible:ring-offset-1min-h 36 pxaria-label mit Dokumenttitel falls nötig
CTA-Button (ghost, Leerstand)inline-flex items-center text-xs font-semibold text-ink border border-ink rounded-sm px-3 py-2 hover:bg-ink hover:text-white transition-colorsmin-h 36 px
Experten-gesucht-Badgeinline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-purple-50 border border-purple-200 text-purple-700Kontrast 6,8:1 ✓ AANur wenn doc.needsExpert === true
Sichtbarkeit Streifen{#if needsSegmentation.length > 0 || needsTranscription.length > 0 || readyToRead.length > 0}Streifen verschwindet wenn alle drei Buckets leer
Dokument-Zeile Mindesthöhemin-h-[44px] flex items-start py-244 px ✓ WCAG 2.2Gilt für alle klickbaren Zeilen
+
+
+ +
+ + +
+
Backend — neue Endpoints & Queries
+
+ + + + + + + + + + + + +
Endpoint / QueryBedingungSortAuth
GET /api/documents/needs-segmentation?size=3NOT EXISTS (SELECT 1 FROM document_annotations WHERE document_id = d.id)HASHTEXT(id::text || week::text)READ_ALL
GET /api/documents/needs-transcription?size=3EXISTS annotation AND (no blocks OR reviewed_pct < 0.90)textedBlocks DESC, needs_expert ASC, HASHTEXT(...)READ_ALL
GET /api/documents/ready-to-read?size=3reviewed_pct >= 0.90updated_at DESCREAD_ALL
PATCH /api/documents/{id}/needs-expertSetzt needs_expert = trueREAD_ALL (jeder Nutzer darf flaggen)
GET /api/stats/strip-activityWochenpuls: COUNT(*) WHERE created_at > NOW() - INTERVAL '7 days' pro BucketREAD_ALL
Flyway-MigrationALTER TABLE documents ADD COLUMN needs_expert BOOLEAN NOT NULL DEFAULT FALSEV{n}__add_needs_expert_flag.sql
Index prüfen (Tobias)document_annotations(document_id), transcription_blocks(document_id, reviewed)EXPLAIN ANALYZE vor Merge
Division durch 0 (Sara)Alle reviewed_pct-Queries: CASE WHEN COUNT(*) = 0 THEN 0 ELSE SUM(...)::float / COUNT(*) END
+
+
+ +
+ + +
+
Neue Svelte-Komponenten
+
+
+

DashboardMissionControl.svelte

+

Wrapper für den vollbreiten Streifen. Props: needsSegmentation, needsTranscription, + readyToRead, weeklyActivity. Rendert die drei Spalten und ist komplett unsichtbar wenn alle Arrays leer sind.

+
+
+

DashboardSegmentationCol.svelte

+

Spalte 1: Skill-Pill, Wochenpuls, Avatare, Dokumentliste, CTA. Keine Balken — keine Dokument-Metadaten vorhanden.

+
+
+

DashboardTranscriptionCol.svelte

+

Spalte 2: Skill-Pill, Wochenpuls, Avatare, per-Dokument-Balken, Experten-Badge bei needsExpert, CTA.

+
+
+

DashboardReadyToReadCol.svelte

+

Spalte 3: Zeigt gefüllten Zustand (Liste mit %-Text) oder leeren Zustand (Cross-Column-Redirect zu Segmentierung).

+
+
+
+
+ Bestehende Komponente bleibt: DashboardNeedsMetadata.svelte ist unverändert — + sie lebt weiterhin in der rechten Spalte. Der Mission-Control-Streifen ist vollständig additiv und ändert nichts am bestehenden Layout. +
+
+
+ +
+ + -- 2.49.1 From 9404ec34ce9b07ee466a724025e7143824b6d3bb Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 16 Apr 2026 08:10:44 +0200 Subject: [PATCH 05/26] fix(#240): add missing V36 index migration and rename needs_expert to V37 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V36 (add_index_transcription_blocks_document_id) was applied to the dev database during a previous local session but never committed to git. Flyway checksum mismatch prevented the backend from starting. - V36__add_index_transcription_blocks_document_id.sql: restored from the index that already exists in the database (idx_transcription_blocks_document_id) - V36__add_needs_expert_to_documents.sql → V37__add_needs_expert_to_documents.sql Co-Authored-By: Claude Sonnet 4.6 --- ...rt_to_documents.sql => V37__add_needs_expert_to_documents.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/main/resources/db/migration/{V36__add_needs_expert_to_documents.sql => V37__add_needs_expert_to_documents.sql} (100%) diff --git a/backend/src/main/resources/db/migration/V36__add_needs_expert_to_documents.sql b/backend/src/main/resources/db/migration/V37__add_needs_expert_to_documents.sql similarity index 100% rename from backend/src/main/resources/db/migration/V36__add_needs_expert_to_documents.sql rename to backend/src/main/resources/db/migration/V37__add_needs_expert_to_documents.sql -- 2.49.1 From aa8fb70d10321b6efd529c1de49c1c97c32d3f31 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 16 Apr 2026 09:11:21 +0200 Subject: [PATCH 06/26] fix(#240): redirect Mission Control Strip links to document detail page The /enrich route is for metadata (title, date, sender/receiver). Segmentation and transcription work happens on the document detail page. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/SegmentationColumn.svelte | 2 +- frontend/src/lib/components/TranscriptionColumn.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/SegmentationColumn.svelte b/frontend/src/lib/components/SegmentationColumn.svelte index 71209817..b48ea67c 100644 --- a/frontend/src/lib/components/SegmentationColumn.svelte +++ b/frontend/src/lib/components/SegmentationColumn.svelte @@ -51,7 +51,7 @@ function formatDate(dateStr: string): string { {#each docs as doc (doc.id)}
  • diff --git a/frontend/src/lib/components/TranscriptionColumn.svelte b/frontend/src/lib/components/TranscriptionColumn.svelte index 435a981a..d9d6fbfb 100644 --- a/frontend/src/lib/components/TranscriptionColumn.svelte +++ b/frontend/src/lib/components/TranscriptionColumn.svelte @@ -56,7 +56,7 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number { {#each docs as doc (doc.id)}
  • -- 2.49.1 From 94b5d1a5a850fc97ce9e0c888c1fd145001e05b9 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 16 Apr 2026 10:15:21 +0200 Subject: [PATCH 07/26] fix(#240): align Mission Control Strip UI with final spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Strip heading: "Mitarbeiten" → "Was braucht Aufmerksamkeit?" - Column 1 heading: "Segmentierung" → "Rahmen einzeichnen"; add green skill pill "✓ Ohne Vorkenntnisse"; heading color gray → ink (navy) - Column 2 heading: "Transkription" → "Text eintippen"; add navy skill pill "Kurrent hilfreich"; heading color gray → ink; weekly pulse color green → ink (task, not achievement); progress bar track bg-gray-200/h-1.5 → bg-ink/20/h-1; add transition-all to fill - Column 3 heading: "Lesefertig" → "Lesefertig ✓"; heading color gray → green-800; add "N Dokumente bereit" subtitle in green; add "Alle N lesen →" link at bottom; reviewed % color gray → green-800 - All columns: add CTA buttons at bottom (Jetzt einzeichnen / Jetzt tippen); empty state removed from cols 1 & 2 (columns hide when empty); empty-state ghost CTA in col 3 restyled as bordered button with hover:bg-ink - Strip: add visibility guard — hides when all three lists are empty - i18n: add mission_control_seg_skill_pill, mission_control_trans_skill_pill, mission_control_ready_subtitle, mission_control_ready_all_cta in de/en/es; update heading and CTA copy in all three locales Co-Authored-By: Claude Sonnet 4.6 --- frontend/messages/de.json | 16 +++--- frontend/messages/en.json | 16 +++--- frontend/messages/es.json | 16 +++--- .../lib/components/MissionControlStrip.svelte | 31 ++++++----- .../src/lib/components/ReadyColumn.svelte | 37 +++++++------ .../lib/components/SegmentationColumn.svelte | 43 ++++++++------- .../lib/components/TranscriptionColumn.svelte | 53 ++++++++++--------- 7 files changed, 122 insertions(+), 90 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 766f13f9..bd431f04 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -558,17 +558,21 @@ "transcription_block_segmentation_only": "Nur Segmentierung", "training_chip_kurrent": "Kurrent-Erkennung", "training_chip_segmentation": "Segmentierung", - "mission_control_heading": "Mitarbeiten", - "mission_control_segmentation_heading": "Segmentierung", + "mission_control_heading": "Was braucht Aufmerksamkeit?", + "mission_control_segmentation_heading": "Rahmen einzeichnen", "mission_control_segmentation_description": "Textbereiche markieren — keine Vorkenntnisse nötig", - "mission_control_segmentation_cta": "Segmentieren", + "mission_control_seg_skill_pill": "✓ Ohne Vorkenntnisse", + "mission_control_segmentation_cta": "Jetzt einzeichnen →", "mission_control_segmentation_empty": "Alle Dokumente haben bereits Segmentierungsblöcke.", - "mission_control_transcription_heading": "Transkription", + "mission_control_transcription_heading": "Text eintippen", "mission_control_transcription_description": "Text abschreiben — Kurrent-Kenntnisse hilfreich", - "mission_control_transcription_cta": "Transkribieren", + "mission_control_trans_skill_pill": "Kurrent hilfreich", + "mission_control_transcription_cta": "Jetzt tippen →", "mission_control_transcription_empty": "Keine Dokumente warten auf Transkription.", - "mission_control_ready_heading": "Lesefertig", + "mission_control_ready_heading": "Lesefertig ✓", "mission_control_ready_description": "Vollständig transkribiert und geprüft", + "mission_control_ready_subtitle": "{count} Dokumente bereit", + "mission_control_ready_all_cta": "Alle {count} lesen →", "mission_control_ready_empty": "Noch keine Dokumente vollständig transkribiert.", "mission_control_ready_empty_cta": "Jetzt mitmachen", "mission_control_weekly_pulse": "↑ +{count} diese Woche", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 54494d0d..c62a7569 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -558,17 +558,21 @@ "transcription_block_segmentation_only": "Segmentation only", "training_chip_kurrent": "Kurrent recognition", "training_chip_segmentation": "Segmentation", - "mission_control_heading": "Contribute", - "mission_control_segmentation_heading": "Segmentation", + "mission_control_heading": "What needs attention?", + "mission_control_segmentation_heading": "Draw regions", "mission_control_segmentation_description": "Mark text areas — no prior knowledge needed", - "mission_control_segmentation_cta": "Segment", + "mission_control_seg_skill_pill": "✓ No prior knowledge", + "mission_control_segmentation_cta": "Start marking →", "mission_control_segmentation_empty": "All documents already have segmentation blocks.", - "mission_control_transcription_heading": "Transcription", + "mission_control_transcription_heading": "Type the text", "mission_control_transcription_description": "Type out text — Kurrent knowledge helpful", - "mission_control_transcription_cta": "Transcribe", + "mission_control_trans_skill_pill": "Kurrent helpful", + "mission_control_transcription_cta": "Start typing →", "mission_control_transcription_empty": "No documents waiting for transcription.", - "mission_control_ready_heading": "Ready to Read", + "mission_control_ready_heading": "Ready to read ✓", "mission_control_ready_description": "Fully transcribed and reviewed", + "mission_control_ready_subtitle": "{count} documents ready", + "mission_control_ready_all_cta": "Read all {count} →", "mission_control_ready_empty": "No documents fully transcribed yet.", "mission_control_ready_empty_cta": "Start contributing", "mission_control_weekly_pulse": "↑ +{count} this week", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index d6335b15..2e610535 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -558,17 +558,21 @@ "transcription_block_segmentation_only": "Solo segmentación", "training_chip_kurrent": "Reconocimiento Kurrent", "training_chip_segmentation": "Segmentación", - "mission_control_heading": "Colaborar", - "mission_control_segmentation_heading": "Segmentación", + "mission_control_heading": "¿Qué necesita atención?", + "mission_control_segmentation_heading": "Marcar regiones", "mission_control_segmentation_description": "Marcar áreas de texto — sin conocimientos previos", - "mission_control_segmentation_cta": "Segmentar", + "mission_control_seg_skill_pill": "✓ Sin conocimientos previos", + "mission_control_segmentation_cta": "Empezar a marcar →", "mission_control_segmentation_empty": "Todos los documentos ya tienen bloques de segmentación.", - "mission_control_transcription_heading": "Transcripción", + "mission_control_transcription_heading": "Escribir el texto", "mission_control_transcription_description": "Escribir el texto — conocimiento de Kurrent útil", - "mission_control_transcription_cta": "Transcribir", + "mission_control_trans_skill_pill": "Kurrent útil", + "mission_control_transcription_cta": "Empezar a escribir →", "mission_control_transcription_empty": "No hay documentos esperando transcripción.", - "mission_control_ready_heading": "Listo para leer", + "mission_control_ready_heading": "Listo para leer ✓", "mission_control_ready_description": "Completamente transcrito y revisado", + "mission_control_ready_subtitle": "{count} documentos listos", + "mission_control_ready_all_cta": "Leer todos ({count}) →", "mission_control_ready_empty": "Aún no hay documentos completamente transcritos.", "mission_control_ready_empty_cta": "Empezar a colaborar", "mission_control_weekly_pulse": "↑ +{count} esta semana", diff --git a/frontend/src/lib/components/MissionControlStrip.svelte b/frontend/src/lib/components/MissionControlStrip.svelte index 7bd28697..c1baeca0 100644 --- a/frontend/src/lib/components/MissionControlStrip.svelte +++ b/frontend/src/lib/components/MissionControlStrip.svelte @@ -30,16 +30,21 @@ interface Props { let { segmentationDocs, transcriptionDocs, readyDocs, weeklyStats }: Props = $props(); -
    -

    - {m.mission_control_heading()} -

    -
    - - - -
    -
    +{#if segmentationDocs.length > 0 || transcriptionDocs.length > 0 || readyDocs.length > 0} +
    +

    + {m.mission_control_heading()} +

    +
    + + + +
    +
    +{/if} diff --git a/frontend/src/lib/components/ReadyColumn.svelte b/frontend/src/lib/components/ReadyColumn.svelte index b570ecf2..7508b15c 100644 --- a/frontend/src/lib/components/ReadyColumn.svelte +++ b/frontend/src/lib/components/ReadyColumn.svelte @@ -34,20 +34,22 @@ function reviewedPct(doc: TranscriptionQueueItemDTO): number { {#if docs.length > 0} -
    -
    -

    - {m.mission_control_ready_heading()} -

    - {#if weeklyCount > 0} - - {m.mission_control_weekly_pulse({ count: weeklyCount })} - - {/if} +
    {:else}

    {m.mission_control_ready_empty()}

    {m.mission_control_ready_empty_cta()} diff --git a/frontend/src/lib/components/SegmentationColumn.svelte b/frontend/src/lib/components/SegmentationColumn.svelte index b48ea67c..76f9de80 100644 --- a/frontend/src/lib/components/SegmentationColumn.svelte +++ b/frontend/src/lib/components/SegmentationColumn.svelte @@ -29,24 +29,23 @@ function formatDate(dateStr: string): string { } -
    -
    -

    - {m.mission_control_segmentation_heading()} -

    - {#if weeklyCount > 0} - - {m.mission_control_weekly_pulse({ count: weeklyCount })} +{#if docs.length > 0} +
    +
    +

    + {m.mission_control_segmentation_heading()} +

    + + {m.mission_control_seg_skill_pill()} - {/if} -
    -

    - {m.mission_control_segmentation_description()} -

    - - {#if docs.length === 0} -

    {m.mission_control_segmentation_empty()}

    - {:else} + {#if weeklyCount > 0} +

    + {m.mission_control_weekly_pulse({ count: weeklyCount })} +

    + {/if} +
      {#each docs as doc (doc.id)}
    • @@ -67,5 +66,11 @@ function formatDate(dateStr: string): string {
    • {/each}
    - {/if} -
    + + {m.mission_control_segmentation_cta()} + +
    +{/if} diff --git a/frontend/src/lib/components/TranscriptionColumn.svelte b/frontend/src/lib/components/TranscriptionColumn.svelte index d9d6fbfb..05aea68a 100644 --- a/frontend/src/lib/components/TranscriptionColumn.svelte +++ b/frontend/src/lib/components/TranscriptionColumn.svelte @@ -34,24 +34,23 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number { } -
    -
    -

    - {m.mission_control_transcription_heading()} -

    - {#if weeklyCount > 0} - - {m.mission_control_weekly_pulse({ count: weeklyCount })} +{#if docs.length > 0} +
    +
    +

    + {m.mission_control_transcription_heading()} +

    + + {m.mission_control_trans_skill_pill()} - {/if} -
    -

    - {m.mission_control_transcription_description()} -

    - - {#if docs.length === 0} -

    {m.mission_control_transcription_empty()}

    - {:else} + {#if weeklyCount > 0} +

    + {m.mission_control_weekly_pulse({ count: weeklyCount })} +

    + {/if} +
      {#each docs as doc (doc.id)}
    • @@ -72,13 +71,13 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number {
      {m.mission_control_blocks_progress({ - texted: doc.textedBlockCount, - total: doc.annotationCount - })} + texted: doc.textedBlockCount, + total: doc.annotationCount + })} -
      +
      @@ -90,5 +89,11 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number {
    • {/each}
    - {/if} -
    + + {m.mission_control_transcription_cta()} + +
    +{/if} -- 2.49.1 From 4af2e4ad17709cdc74e9c49cd0c26578114ebdee Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 16 Apr 2026 10:19:39 +0200 Subject: [PATCH 08/26] fix(#240): remove dead "Alle lesen" link and add hover shadow to ReadyColumn Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/ReadyColumn.svelte | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/ReadyColumn.svelte b/frontend/src/lib/components/ReadyColumn.svelte index 7508b15c..775d9bc5 100644 --- a/frontend/src/lib/components/ReadyColumn.svelte +++ b/frontend/src/lib/components/ReadyColumn.svelte @@ -34,7 +34,9 @@ function reviewedPct(doc: TranscriptionQueueItemDTO): number { {#if docs.length > 0} -
    +

    @@ -72,9 +74,6 @@ function reviewedPct(doc: TranscriptionQueueItemDTO): number {

  • {/each} - - {m.mission_control_ready_all_cta({ count: docs.length })} - {:else}
    Date: Thu, 16 Apr 2026 10:22:06 +0200 Subject: [PATCH 09/26] fix(#240): fix invisible hover on column 1 & 2 doc links brand-sand/30 on white background is near-invisible; use full hover:bg-brand-sand instead. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/SegmentationColumn.svelte | 2 +- frontend/src/lib/components/TranscriptionColumn.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/SegmentationColumn.svelte b/frontend/src/lib/components/SegmentationColumn.svelte index 76f9de80..5da5d074 100644 --- a/frontend/src/lib/components/SegmentationColumn.svelte +++ b/frontend/src/lib/components/SegmentationColumn.svelte @@ -51,7 +51,7 @@ function formatDate(dateStr: string): string {
  • {doc.title} diff --git a/frontend/src/lib/components/TranscriptionColumn.svelte b/frontend/src/lib/components/TranscriptionColumn.svelte index 05aea68a..76d4b09b 100644 --- a/frontend/src/lib/components/TranscriptionColumn.svelte +++ b/frontend/src/lib/components/TranscriptionColumn.svelte @@ -56,7 +56,7 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number {
  • {doc.title} -- 2.49.1 From 86a216918f59a988abe6a2af59c4a1085d02bc39 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 16 Apr 2026 10:31:20 +0200 Subject: [PATCH 10/26] fix(#240): make Mission Control Strip dark-mode compatible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all hardcoded Tailwind colours with semantic tokens: - bg-white → bg-surface (outer strip container) - text-gray-400 → text-ink-3 (dates, meta text, empty-state copy) - text-green-800 / text-green-700 → text-ink / text-ink-2 (headings, pulse, reviewed %) - bg-green-50 / border-green-200 → bg-accent-bg / border-line (skill pill, weekly pulse badge) - bg-ink text-white → bg-primary text-primary-fg (CTA buttons; dark: mint bg + navy text) - hover:text-white → hover:text-primary-fg (ghost CTA hover text) - focus-visible:ring-brand-navy → focus-visible:ring-focus-ring (all doc links) Co-Authored-By: Claude Sonnet 4.6 --- .../lib/components/MissionControlStrip.svelte | 4 ++-- frontend/src/lib/components/ReadyColumn.svelte | 16 ++++++++-------- .../src/lib/components/SegmentationColumn.svelte | 10 +++++----- .../lib/components/TranscriptionColumn.svelte | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/frontend/src/lib/components/MissionControlStrip.svelte b/frontend/src/lib/components/MissionControlStrip.svelte index c1baeca0..00221f0d 100644 --- a/frontend/src/lib/components/MissionControlStrip.svelte +++ b/frontend/src/lib/components/MissionControlStrip.svelte @@ -31,8 +31,8 @@ let { segmentationDocs, transcriptionDocs, readyDocs, weeklyStats }: Props = $pr {#if segmentationDocs.length > 0 || transcriptionDocs.length > 0 || readyDocs.length > 0} -
    -

    +
    +

    {m.mission_control_heading()}

    {m.mission_control_seg_skill_pill()} {#if weeklyCount > 0} -

    +

    {m.mission_control_weekly_pulse({ count: weeklyCount })}

    {/if} @@ -51,7 +51,7 @@ function formatDate(dateStr: string): string {
  • {doc.title} @@ -60,7 +60,7 @@ function formatDate(dateStr: string): string { {/if}
    {#if doc.documentDate} - {formatDate(doc.documentDate)} + {formatDate(doc.documentDate)} {/if}
  • @@ -68,7 +68,7 @@ function formatDate(dateStr: string): string { {m.mission_control_segmentation_cta()} diff --git a/frontend/src/lib/components/TranscriptionColumn.svelte b/frontend/src/lib/components/TranscriptionColumn.svelte index 76d4b09b..bf0d52b4 100644 --- a/frontend/src/lib/components/TranscriptionColumn.svelte +++ b/frontend/src/lib/components/TranscriptionColumn.svelte @@ -56,7 +56,7 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number {
  • {doc.title} @@ -65,11 +65,11 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number { {/if}
    {#if doc.documentDate} - {formatDate(doc.documentDate)} + {formatDate(doc.documentDate)} {/if} {#if doc.textedBlockCount > 0}
    - + {m.mission_control_blocks_progress({ texted: doc.textedBlockCount, total: doc.annotationCount @@ -83,7 +83,7 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number {
  • {:else} - + {/if}
  • @@ -91,7 +91,7 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number { {m.mission_control_transcription_cta()} -- 2.49.1 From 9fb1821db56c4b9a258370fe0d862da68e30d26f Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 16 Apr 2026 10:34:47 +0200 Subject: [PATCH 11/26] fix(#240): remove CTA buttons and dead i18n keys from Mission Control Strip The enrich page already handles task routing; the buttons in the segmentation and transcription columns were redundant. Removes the unused mission_control_segmentation_cta, mission_control_transcription_cta, and mission_control_ready_all_cta keys from all three locale files. Co-Authored-By: Claude Sonnet 4.6 --- frontend/messages/de.json | 3 --- frontend/messages/en.json | 3 --- frontend/messages/es.json | 3 --- frontend/src/lib/components/SegmentationColumn.svelte | 6 ------ frontend/src/lib/components/TranscriptionColumn.svelte | 6 ------ 5 files changed, 21 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index bd431f04..87ebe4b6 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -562,17 +562,14 @@ "mission_control_segmentation_heading": "Rahmen einzeichnen", "mission_control_segmentation_description": "Textbereiche markieren — keine Vorkenntnisse nötig", "mission_control_seg_skill_pill": "✓ Ohne Vorkenntnisse", - "mission_control_segmentation_cta": "Jetzt einzeichnen →", "mission_control_segmentation_empty": "Alle Dokumente haben bereits Segmentierungsblöcke.", "mission_control_transcription_heading": "Text eintippen", "mission_control_transcription_description": "Text abschreiben — Kurrent-Kenntnisse hilfreich", "mission_control_trans_skill_pill": "Kurrent hilfreich", - "mission_control_transcription_cta": "Jetzt tippen →", "mission_control_transcription_empty": "Keine Dokumente warten auf Transkription.", "mission_control_ready_heading": "Lesefertig ✓", "mission_control_ready_description": "Vollständig transkribiert und geprüft", "mission_control_ready_subtitle": "{count} Dokumente bereit", - "mission_control_ready_all_cta": "Alle {count} lesen →", "mission_control_ready_empty": "Noch keine Dokumente vollständig transkribiert.", "mission_control_ready_empty_cta": "Jetzt mitmachen", "mission_control_weekly_pulse": "↑ +{count} diese Woche", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index c62a7569..0e558379 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -562,17 +562,14 @@ "mission_control_segmentation_heading": "Draw regions", "mission_control_segmentation_description": "Mark text areas — no prior knowledge needed", "mission_control_seg_skill_pill": "✓ No prior knowledge", - "mission_control_segmentation_cta": "Start marking →", "mission_control_segmentation_empty": "All documents already have segmentation blocks.", "mission_control_transcription_heading": "Type the text", "mission_control_transcription_description": "Type out text — Kurrent knowledge helpful", "mission_control_trans_skill_pill": "Kurrent helpful", - "mission_control_transcription_cta": "Start typing →", "mission_control_transcription_empty": "No documents waiting for transcription.", "mission_control_ready_heading": "Ready to read ✓", "mission_control_ready_description": "Fully transcribed and reviewed", "mission_control_ready_subtitle": "{count} documents ready", - "mission_control_ready_all_cta": "Read all {count} →", "mission_control_ready_empty": "No documents fully transcribed yet.", "mission_control_ready_empty_cta": "Start contributing", "mission_control_weekly_pulse": "↑ +{count} this week", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 2e610535..d0000e33 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -562,17 +562,14 @@ "mission_control_segmentation_heading": "Marcar regiones", "mission_control_segmentation_description": "Marcar áreas de texto — sin conocimientos previos", "mission_control_seg_skill_pill": "✓ Sin conocimientos previos", - "mission_control_segmentation_cta": "Empezar a marcar →", "mission_control_segmentation_empty": "Todos los documentos ya tienen bloques de segmentación.", "mission_control_transcription_heading": "Escribir el texto", "mission_control_transcription_description": "Escribir el texto — conocimiento de Kurrent útil", "mission_control_trans_skill_pill": "Kurrent útil", - "mission_control_transcription_cta": "Empezar a escribir →", "mission_control_transcription_empty": "No hay documentos esperando transcripción.", "mission_control_ready_heading": "Listo para leer ✓", "mission_control_ready_description": "Completamente transcrito y revisado", "mission_control_ready_subtitle": "{count} documentos listos", - "mission_control_ready_all_cta": "Leer todos ({count}) →", "mission_control_ready_empty": "Aún no hay documentos completamente transcritos.", "mission_control_ready_empty_cta": "Empezar a colaborar", "mission_control_weekly_pulse": "↑ +{count} esta semana", diff --git a/frontend/src/lib/components/SegmentationColumn.svelte b/frontend/src/lib/components/SegmentationColumn.svelte index e82d7f4e..f8ab88bd 100644 --- a/frontend/src/lib/components/SegmentationColumn.svelte +++ b/frontend/src/lib/components/SegmentationColumn.svelte @@ -66,11 +66,5 @@ function formatDate(dateStr: string): string { {/each} - - {m.mission_control_segmentation_cta()} -
    {/if} diff --git a/frontend/src/lib/components/TranscriptionColumn.svelte b/frontend/src/lib/components/TranscriptionColumn.svelte index bf0d52b4..7c265045 100644 --- a/frontend/src/lib/components/TranscriptionColumn.svelte +++ b/frontend/src/lib/components/TranscriptionColumn.svelte @@ -89,11 +89,5 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number { {/each} - - {m.mission_control_transcription_cta()} - {/if} -- 2.49.1 From ca0cf4903ce98ef4c9e76776ad8174cf2484dfb4 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 16 Apr 2026 10:52:14 +0200 Subject: [PATCH 12/26] refactor(#240): remove needsExpert feature completely Drops the needsExpert / needs_expert flag end-to-end: DB migration (V37, never applied), Document entity field, PATCH endpoint, service method, DTO field, all three queue queries, ExpertBadge component, i18n key, generated API types, and test fixture. Co-Authored-By: Claude Sonnet 4.6 --- .../controller/DocumentController.java | 8 ------ .../dto/TranscriptionQueueItemDTO.java | 1 - .../familienarchiv/model/Document.java | 5 ---- .../repository/DocumentRepository.java | 16 +++++------- .../service/DocumentService.java | 7 ----- .../service/TranscriptionQueueService.java | 14 +++------- .../V37__add_needs_expert_to_documents.sql | 1 - frontend/messages/de.json | 1 - frontend/messages/en.json | 1 - frontend/messages/es.json | 1 - .../src/lib/components/ExpertBadge.svelte | 26 ------------------- .../lib/components/MissionControlStrip.svelte | 1 - .../src/lib/components/ReadyColumn.svelte | 1 - .../lib/components/SegmentationColumn.svelte | 9 +------ .../lib/components/TranscriptionColumn.svelte | 9 +------ frontend/src/lib/generated/api.ts | 2 -- .../routes/briefwechsel/page.svelte.spec.ts | 1 - 17 files changed, 13 insertions(+), 91 deletions(-) delete mode 100644 backend/src/main/resources/db/migration/V37__add_needs_expert_to_documents.sql delete mode 100644 frontend/src/lib/components/ExpertBadge.svelte diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java index 4e6b9c37..91e3c250 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -211,14 +211,6 @@ public class DocumentController { return ResponseEntity.ok(documentService.searchDocuments(q, from, to, senderId, receiverId, tags, tagQ, status, sort, dir)); } - // --- EXPERT FLAG --- - - @PatchMapping("/{id}/needs-expert") - @RequirePermission(Permission.WRITE_ALL) - public Document toggleNeedsExpert(@PathVariable UUID id) { - return documentService.toggleNeedsExpert(id); - } - // --- TRAINING LABELS --- public record TrainingLabelRequest(String label, boolean enrolled) {} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java index ab441cd9..f577b32b 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java @@ -12,7 +12,6 @@ public record TranscriptionQueueItemDTO( UUID id, String title, LocalDate documentDate, - boolean needsExpert, int annotationCount, int textedBlockCount, int reviewedBlockCount diff --git a/backend/src/main/java/org/raddatz/familienarchiv/model/Document.java b/backend/src/main/java/org/raddatz/familienarchiv/model/Document.java index 0e7e954a..5a5ca54a 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/model/Document.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/model/Document.java @@ -97,11 +97,6 @@ public class Document { @Builder.Default private ScriptType scriptType = ScriptType.UNKNOWN; - @Column(name = "needs_expert", nullable = false) - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) - @Builder.Default - private boolean needsExpert = false; - @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "document_receivers", joinColumns = @JoinColumn(name = "document_id"), inverseJoinColumns = @JoinColumn(name = "person_id")) @Builder.Default diff --git a/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java index 6e20c064..499b486b 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentRepository.java @@ -171,27 +171,26 @@ public interface DocumentRepository extends JpaRepository, JpaSp /** Documents with no annotations — Segmentierung column. */ @Query(nativeQuery = true, value = """ - SELECT d.id, d.title, d.meta_date AS documentDate, d.needs_expert AS needsExpert, + SELECT d.id, d.title, d.meta_date AS documentDate, 0 AS annotationCount, 0 AS textedBlockCount, 0 AS reviewedBlockCount FROM documents d WHERE d.status NOT IN ('PLACEHOLDER') AND NOT EXISTS (SELECT 1 FROM document_annotations da WHERE da.document_id = d.id) - ORDER BY d.needs_expert ASC, - HASHTEXT(d.id::text || EXTRACT(WEEK FROM NOW())::int::text) + ORDER BY HASHTEXT(d.id::text || EXTRACT(WEEK FROM NOW())::int::text) LIMIT :limit """) List findSegmentationQueue(@Param("limit") int limit); /** Documents with annotations but not yet fully reviewed — Transkription column. */ @Query(nativeQuery = true, value = """ - SELECT d.id, d.title, d.meta_date AS documentDate, d.needs_expert AS needsExpert, + SELECT d.id, d.title, d.meta_date AS documentDate, COUNT(DISTINCT da.id) AS annotationCount, COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) AS textedBlockCount, COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END) AS reviewedBlockCount FROM documents d JOIN document_annotations da ON da.document_id = d.id LEFT JOIN transcription_blocks tb ON tb.document_id = d.id - GROUP BY d.id, d.title, d.meta_date, d.needs_expert + GROUP BY d.id, d.title, d.meta_date HAVING COUNT(DISTINCT da.id) > 0 AND ( COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) = 0 @@ -200,8 +199,7 @@ public interface DocumentRepository extends JpaRepository, JpaSp NULLIF(COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END), 0) ) < 0.90 ) - ORDER BY d.needs_expert ASC, - COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) DESC, + ORDER BY COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) DESC, HASHTEXT(d.id::text || EXTRACT(WEEK FROM NOW())::int::text) LIMIT :limit """) @@ -209,14 +207,14 @@ public interface DocumentRepository extends JpaRepository, JpaSp /** Documents with reviewed_pct >= 90 % — Lesefertig column. */ @Query(nativeQuery = true, value = """ - SELECT d.id, d.title, d.meta_date AS documentDate, d.needs_expert AS needsExpert, + SELECT d.id, d.title, d.meta_date AS documentDate, COUNT(DISTINCT da.id) AS annotationCount, COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) AS textedBlockCount, COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END) AS reviewedBlockCount FROM documents d JOIN document_annotations da ON da.document_id = d.id LEFT JOIN transcription_blocks tb ON tb.document_id = d.id - GROUP BY d.id, d.title, d.meta_date, d.needs_expert + GROUP BY d.id, d.title, d.meta_date HAVING COUNT(DISTINCT da.id) > 0 AND COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) > 0 AND ( diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java index 272da789..ab50ab22 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -577,13 +577,6 @@ public class DocumentService { return parsed != null ? parsed.title() : stripExtension(filename); } - @Transactional - public Document toggleNeedsExpert(UUID documentId) { - Document doc = getDocumentById(documentId); - doc.setNeedsExpert(!doc.isNeedsExpert()); - return documentRepository.save(doc); - } - private static String tryParseDate(String s) { if (s.matches("\\d{4}-\\d{2}-\\d{2}")) { int m = Integer.parseInt(s.substring(5, 7)); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java index 1b4a0fde..9d500e5d 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java @@ -59,11 +59,10 @@ public class TranscriptionQueueService { UUID id = toUUID(row[0]); String title = (String) row[1]; LocalDate documentDate = toLocalDate(row[2]); - boolean needsExpert = toBoolean(row[3]); - int annotationCount = toInt(row[4]); - int textedBlockCount = toInt(row[5]); - int reviewedBlockCount = toInt(row[6]); - return new TranscriptionQueueItemDTO(id, title, documentDate, needsExpert, + int annotationCount = toInt(row[3]); + int textedBlockCount = toInt(row[4]); + int reviewedBlockCount = toInt(row[5]); + return new TranscriptionQueueItemDTO(id, title, documentDate, annotationCount, textedBlockCount, reviewedBlockCount); } @@ -79,11 +78,6 @@ public class TranscriptionQueueService { return LocalDate.parse(o.toString()); } - private boolean toBoolean(Object o) { - if (o instanceof Boolean b) return b; - return Boolean.parseBoolean(o.toString()); - } - private int toInt(Object o) { if (o == null) return 0; if (o instanceof Number n) return n.intValue(); diff --git a/backend/src/main/resources/db/migration/V37__add_needs_expert_to_documents.sql b/backend/src/main/resources/db/migration/V37__add_needs_expert_to_documents.sql deleted file mode 100644 index 03ee225a..00000000 --- a/backend/src/main/resources/db/migration/V37__add_needs_expert_to_documents.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE documents ADD COLUMN needs_expert BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 87ebe4b6..cf360bc9 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -573,7 +573,6 @@ "mission_control_ready_empty": "Noch keine Dokumente vollständig transkribiert.", "mission_control_ready_empty_cta": "Jetzt mitmachen", "mission_control_weekly_pulse": "↑ +{count} diese Woche", - "mission_control_expert_badge": "Experten gesucht", "mission_control_blocks_progress": "{texted} / {total} Blöcke", "mission_control_reviewed_pct": "{pct}% geprüft" } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 0e558379..734783e6 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -573,7 +573,6 @@ "mission_control_ready_empty": "No documents fully transcribed yet.", "mission_control_ready_empty_cta": "Start contributing", "mission_control_weekly_pulse": "↑ +{count} this week", - "mission_control_expert_badge": "Expert needed", "mission_control_blocks_progress": "{texted} / {total} blocks", "mission_control_reviewed_pct": "{pct}% reviewed" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index d0000e33..d685d776 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -573,7 +573,6 @@ "mission_control_ready_empty": "Aún no hay documentos completamente transcritos.", "mission_control_ready_empty_cta": "Empezar a colaborar", "mission_control_weekly_pulse": "↑ +{count} esta semana", - "mission_control_expert_badge": "Se busca experto", "mission_control_blocks_progress": "{texted} / {total} bloques", "mission_control_reviewed_pct": "{pct}% revisado" } diff --git a/frontend/src/lib/components/ExpertBadge.svelte b/frontend/src/lib/components/ExpertBadge.svelte deleted file mode 100644 index 7fbdf28b..00000000 --- a/frontend/src/lib/components/ExpertBadge.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - - - {m.mission_control_expert_badge()} - diff --git a/frontend/src/lib/components/MissionControlStrip.svelte b/frontend/src/lib/components/MissionControlStrip.svelte index 00221f0d..52664b54 100644 --- a/frontend/src/lib/components/MissionControlStrip.svelte +++ b/frontend/src/lib/components/MissionControlStrip.svelte @@ -8,7 +8,6 @@ type TranscriptionQueueItemDTO = { id: string; title: string; documentDate?: string; - needsExpert: boolean; annotationCount: number; textedBlockCount: number; reviewedBlockCount: number; diff --git a/frontend/src/lib/components/ReadyColumn.svelte b/frontend/src/lib/components/ReadyColumn.svelte index 3d5f999a..4ab49256 100644 --- a/frontend/src/lib/components/ReadyColumn.svelte +++ b/frontend/src/lib/components/ReadyColumn.svelte @@ -6,7 +6,6 @@ type TranscriptionQueueItemDTO = { id: string; title: string; documentDate?: string; - needsExpert: boolean; annotationCount: number; textedBlockCount: number; reviewedBlockCount: number; diff --git a/frontend/src/lib/components/SegmentationColumn.svelte b/frontend/src/lib/components/SegmentationColumn.svelte index f8ab88bd..2534734f 100644 --- a/frontend/src/lib/components/SegmentationColumn.svelte +++ b/frontend/src/lib/components/SegmentationColumn.svelte @@ -1,13 +1,11 @@ diff --git a/frontend/src/lib/components/TranscriptionColumn.svelte b/frontend/src/lib/components/TranscriptionColumn.svelte index f0d38de7..4aec87e0 100644 --- a/frontend/src/lib/components/TranscriptionColumn.svelte +++ b/frontend/src/lib/components/TranscriptionColumn.svelte @@ -44,7 +44,7 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number { {m.mission_control_trans_skill_pill()} {#if weeklyCount > 0} -

    +

    {m.mission_control_weekly_pulse({ count: weeklyCount })}

    {/if} @@ -68,7 +68,7 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number { total: doc.annotationCount })} -
    +