feat(#240): Mission Control Strip frontend — 5 components + dashboard wiring

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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-15 23:14:43 +02:00
parent 53c5d90340
commit f36bebd1a8
10 changed files with 424 additions and 5 deletions

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages.js';
import { getLocale } from '$lib/paraglide/runtime.js';
type TranscriptionQueueItemDTO = {
id: string;
title: string;
documentDate?: string;
needsExpert: boolean;
annotationCount: number;
textedBlockCount: number;
reviewedBlockCount: number;
};
interface Props {
docs: TranscriptionQueueItemDTO[];
weeklyCount: number;
}
let { docs, weeklyCount }: Props = $props();
function formatDate(dateStr: string): string {
return new Intl.DateTimeFormat(getLocale(), {
day: 'numeric',
month: 'short',
year: 'numeric'
}).format(new Date(dateStr + 'T12:00:00'));
}
function reviewedPct(doc: TranscriptionQueueItemDTO): number {
if (doc.textedBlockCount === 0) return 0;
return Math.round((doc.reviewedBlockCount / doc.textedBlockCount) * 100);
}
</script>
{#if docs.length > 0}
<div class="rounded-sm border border-brand-mint bg-brand-mint/10 p-4">
<div class="mb-1 flex items-center gap-2">
<h3 class="font-sans text-xs font-bold tracking-widest text-gray-400 uppercase">
{m.mission_control_ready_heading()}
</h3>
{#if weeklyCount > 0}
<span class="rounded-full bg-green-50 px-2 py-0.5 text-xs font-semibold text-green-700">
{m.mission_control_weekly_pulse({ count: weeklyCount })}
</span>
{/if}
</div>
<p class="mt-1 mb-3 text-xs text-gray-400">
{m.mission_control_ready_description()}
</p>
<ul class="space-y-1">
{#each docs as doc (doc.id)}
<li>
<a
href="/documents/{doc.id}"
class="flex min-h-[44px] flex-col justify-center rounded px-1 py-2 hover:bg-brand-mint/20 focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-2 focus-visible:outline-none"
>
<span class="font-serif text-sm text-ink">{doc.title}</span>
<div class="mt-0.5 flex items-center gap-2">
{#if doc.documentDate}
<span class="text-xs text-gray-400">{formatDate(doc.documentDate)}</span>
{/if}
{#if doc.textedBlockCount > 0}
<span class="text-xs text-gray-500">
{m.mission_control_reviewed_pct({ pct: reviewedPct(doc) })}
</span>
{/if}
</div>
</a>
</li>
{/each}
</ul>
</div>
{:else}
<div
class="flex min-h-[120px] flex-col items-center justify-center rounded-sm border border-dashed border-brand-mint bg-brand-mint/5 p-6 text-center"
>
<p class="text-xs text-gray-400">{m.mission_control_ready_empty()}</p>
<a
href="/enrich"
class="mt-2 inline-flex items-center gap-1 rounded text-xs font-semibold text-brand-navy hover:underline focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-2 focus-visible:outline-none"
>
{m.mission_control_ready_empty_cta()}
</a>
</div>
{/if}