fix(review): cycle 3 — a11y, CSS injection, domain boundary
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m44s
CI / OCR Service Tests (push) Successful in 34s
CI / Unit & Component Tests (pull_request) Failing after 2m33s
CI / OCR Service Tests (pull_request) Successful in 35s
CI / Backend Unit Tests (push) Failing after 2m56s
CI / Backend Unit Tests (pull_request) Failing after 2m48s

- DocumentRow: add data-testid="search-snippet"; sanitize tag.color with safeTagColor()
- ContributorStack: add role="img" aria-label to overflow "…" badge
- DocumentList: year header text-[10px] → text-xs (WCAG 1.4.4 minimum 12px)
- DashboardResumeStrip: sanitize collab.color with safeColor()
- Extract TranscriptionBlockQueryService to fix cross-domain repository access in DocumentService
- Update unit test mocks to use TranscriptionBlockQueryService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-20 01:03:06 +02:00
parent 7f23e88b69
commit f2bed92176
8 changed files with 50 additions and 17 deletions

View File

@@ -21,9 +21,7 @@ import org.raddatz.familienarchiv.model.ScriptType;
import org.raddatz.familienarchiv.model.TrainingLabel;
import org.raddatz.familienarchiv.model.Person;
import org.raddatz.familienarchiv.model.Tag;
import org.raddatz.familienarchiv.repository.CompletionStatsRow;
import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
@@ -64,7 +62,7 @@ public class DocumentService {
private final DocumentVersionService documentVersionService;
private final AnnotationService annotationService;
private final AuditService auditService;
private final TranscriptionBlockRepository transcriptionBlockRepository;
private final TranscriptionBlockQueryService transcriptionBlockQueryService;
private final AuditLogQueryService auditLogQueryService;
public record StoreResult(Document document, boolean isNew) {}
@@ -414,12 +412,7 @@ public class DocumentService {
}
private Map<UUID, Integer> fetchCompletionPercentages(List<UUID> docIds) {
if (docIds.isEmpty()) return Map.of();
Map<UUID, Integer> result = new HashMap<>();
for (CompletionStatsRow row : transcriptionBlockRepository.findCompletionStatsForDocuments(docIds)) {
result.put(row.getDocumentId(), row.getCompletionPercentage());
}
return result;
return transcriptionBlockQueryService.getCompletionStats(docIds);
}
private Sort resolveSort(DocumentSort sort, String dir) {

View File

@@ -0,0 +1,27 @@
package org.raddatz.familienarchiv.service;
import lombok.RequiredArgsConstructor;
import org.raddatz.familienarchiv.repository.CompletionStatsRow;
import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class TranscriptionBlockQueryService {
private final TranscriptionBlockRepository blockRepository;
public Map<UUID, Integer> getCompletionStats(List<UUID> documentIds) {
if (documentIds.isEmpty()) return Map.of();
Map<UUID, Integer> result = new HashMap<>();
for (CompletionStatsRow row : blockRepository.findCompletionStatsForDocuments(documentIds)) {
result.put(row.getDocumentId(), row.getCompletionPercentage());
}
return result;
}
}

View File

@@ -11,7 +11,6 @@ import org.raddatz.familienarchiv.dto.DocumentSort;
import org.raddatz.familienarchiv.model.Document;
import org.raddatz.familienarchiv.model.DocumentStatus;
import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
@@ -33,7 +32,7 @@ class DocumentServiceSortTest {
@Mock DocumentVersionService documentVersionService;
@Mock AnnotationService annotationService;
@Mock AuditLogQueryService auditLogQueryService;
@Mock TranscriptionBlockRepository transcriptionBlockRepository;
@Mock TranscriptionBlockQueryService transcriptionBlockQueryService;
@InjectMocks DocumentService documentService;
// ─── searchDocuments — DATE sort ──────────────────────────────────────────

View File

@@ -22,7 +22,6 @@ import org.raddatz.familienarchiv.model.DocumentStatus;
import org.raddatz.familienarchiv.model.Person;
import org.raddatz.familienarchiv.model.Tag;
import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
@@ -55,7 +54,7 @@ class DocumentServiceTest {
@Mock AnnotationService annotationService;
@Mock AuditService auditService;
@Mock AuditLogQueryService auditLogQueryService;
@Mock TranscriptionBlockRepository transcriptionBlockRepository;
@Mock TranscriptionBlockQueryService transcriptionBlockQueryService;
@InjectMocks DocumentService documentService;
// ─── deleteDocument ───────────────────────────────────────────────────────

View File

@@ -38,6 +38,8 @@ function safeColor(color: string): string {
{/each}
{#if hasMore}
<span
role="img"
aria-label="Weitere Mitwirkende"
class="-ml-1.5 inline-flex h-[22px] w-[22px] flex-shrink-0 items-center justify-center rounded-full bg-[#e4e2d7] font-sans text-xs font-bold text-ink-3 ring-2 ring-white"
>

View File

@@ -7,6 +7,10 @@ interface Props {
}
const { resumeDoc }: Props = $props();
function safeColor(color: string): string {
return /^#[0-9a-fA-F]{6}$/.test(color) ? color : '#8c9aa3';
}
</script>
{#if resumeDoc === null}
@@ -94,7 +98,7 @@ const { resumeDoc }: Props = $props();
{#each resumeDoc.collaborators.slice(0, 3) as collab (collab.initials + collab.color)}
<span
class="-ml-1 inline-flex h-6 w-6 items-center justify-center rounded-full font-sans text-[10px] font-bold text-white ring-2 ring-white"
style="background:{collab.color}">{collab.initials}</span
style="background:{safeColor(collab.color)}">{collab.initials}</span
>
{/each}
</div>

View File

@@ -29,6 +29,10 @@ function tagClass(matched: boolean): string {
? 'inline-flex items-center gap-1 rounded px-2 py-1.5 text-xs font-bold tracking-widest uppercase bg-primary text-primary-fg transition-colors'
: 'inline-flex items-center gap-1 rounded px-2 py-1.5 text-xs font-bold tracking-widest uppercase bg-muted text-ink hover:bg-primary hover:text-primary-fg transition-colors';
}
function safeTagColor(color: string | null | undefined): string {
return color && /^#[0-9a-fA-F]{6}$/.test(color) ? color : '#cdcbbf';
}
</script>
<li class="group transition-colors duration-200 hover:bg-muted/50">
@@ -52,7 +56,10 @@ function tagClass(matched: boolean): string {
<!-- Snippet -->
{#if snippetSegments}
<p class="mb-2 line-clamp-2 font-sans text-sm text-ink-2 italic">
<p
data-testid="search-snippet"
class="mb-2 line-clamp-2 font-sans text-sm text-ink-2 italic"
>
{#each snippetSegments as seg, i (i)}
{#if seg.highlight}
<mark
@@ -117,7 +124,9 @@ function tagClass(matched: boolean): string {
onclick={(e) => { e.stopPropagation(); goto('/documents?tag=' + encodeURIComponent(tag.name)); }}
>
{#if tag.color}
<span class="h-1.5 w-1.5 rounded-full" style="background-color: {tag.color};"
<span
class="h-1.5 w-1.5 rounded-full"
style="background-color: {safeTagColor(tag.color)};"
></span>
{/if}
{tag.name}

View File

@@ -74,7 +74,7 @@ const yearGroups = $derived.by(() => {
class="mb-4 overflow-hidden border border-line bg-surface shadow-sm"
>
<div class="border-b border-line bg-muted px-5 py-2">
<span class="font-sans text-[10px] font-bold tracking-widest text-ink-3 uppercase"
<span class="font-sans text-xs font-bold tracking-widest text-ink-3 uppercase"
>{group.year}</span
>
</div>