i18n(ai-workbench): translate ListView via $_() — tabs, filters, audit table, timeline buckets

- Tabs (Timeline / Datenzugriff)
- Filter labels (Modul/Mission/Agent) with shared "alle" option
- Time-range buttons routed via dynamic key labelKey
- Audit: loading, error_prefix interpolation, empty paragraph, 4 column headers
- Timeline empty state
- Bucket revert button (title + Läuft… / Rückgängig label) + event-count tooltip + event-link "Zum Modul"
- Confirm + alert summary parts ({n} zurückgenommen / nicht unterstützt / fehlgeschlagen) + "Revert fehlgeschlagen — siehe Console." fallback
- Date/time formatters switched to get(locale) ?? 'de'

Baselines: hardcoded 1090 → 1082 (8 cleared); missing-keys baseline +1 (ai-workbench.list_view.range_* dynamic key).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-27 14:31:33 +02:00
parent 08ad86ec59
commit 391017bcfa
3 changed files with 64 additions and 36 deletions

View file

@ -11,6 +11,8 @@
import { fetchDecryptAudit, type AuditRow } from '$lib/data/ai/audit/queries'; import { fetchDecryptAudit, type AuditRow } from '$lib/data/ai/audit/queries';
import { isMissionGrantsEnabled } from '$lib/api/config'; import { isMissionGrantsEnabled } from '$lib/api/config';
import type { DomainEvent } from '$lib/data/events/types'; import type { DomainEvent } from '$lib/data/events/types';
import { _, locale } from 'svelte-i18n';
import { get } from 'svelte/store';
let moduleFilter = $state<string | null>(null); let moduleFilter = $state<string | null>(null);
let missionFilter = $state<string | null>(null); let missionFilter = $state<string | null>(null);
@ -54,10 +56,16 @@
return title ? `${e.type} · ${title}` : e.type; return title ? `${e.type} · ${title}` : e.type;
} }
function formatTime(iso: string) { function formatTime(iso: string) {
return new Date(iso).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); return new Date(iso).toLocaleTimeString(get(locale) ?? 'de', {
hour: '2-digit',
minute: '2-digit',
});
} }
function formatDate(iso: string) { function formatDate(iso: string) {
return new Date(iso).toLocaleDateString('de-DE', { day: 'numeric', month: 'short' }); return new Date(iso).toLocaleDateString(get(locale) ?? 'de', {
day: 'numeric',
month: 'short',
});
} }
// ── Tab switcher: timeline ↔ decrypt audit ───────────── // ── Tab switcher: timeline ↔ decrypt audit ─────────────
@ -92,7 +100,7 @@
}); });
function formatAuditTs(iso: string): string { function formatAuditTs(iso: string): string {
return new Date(iso).toLocaleString('de-DE', { return new Date(iso).toLocaleString(get(locale) ?? 'de', {
day: '2-digit', day: '2-digit',
month: '2-digit', month: '2-digit',
hour: '2-digit', hour: '2-digit',
@ -102,17 +110,27 @@
let revertingKey = $state<string | null>(null); let revertingKey = $state<string | null>(null);
async function handleRevert(key: string, missionId: string, iterationId: string) { async function handleRevert(key: string, missionId: string, iterationId: string) {
if (!confirm('Alle AI-Writes dieser Iteration zurücknehmen?')) return; if (!confirm($_('ai-workbench.list_view.confirm_revert'))) return;
revertingKey = key; revertingKey = key;
try { try {
const stats = await revertIteration(missionId, iterationId); const stats = await revertIteration(missionId, iterationId);
const parts = [`${stats.reverted} zurückgenommen`]; const parts = [
if (stats.skippedUnsupported > 0) parts.push(`${stats.skippedUnsupported} nicht unterstützt`); $_('ai-workbench.list_view.revert_summary_done', { values: { n: stats.reverted } }),
if (stats.failed > 0) parts.push(`${stats.failed} fehlgeschlagen`); ];
if (stats.skippedUnsupported > 0)
parts.push(
$_('ai-workbench.list_view.revert_summary_unsupported', {
values: { n: stats.skippedUnsupported },
})
);
if (stats.failed > 0)
parts.push(
$_('ai-workbench.list_view.revert_summary_failed', { values: { n: stats.failed } })
);
alert(parts.join(' · ')); alert(parts.join(' · '));
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert('Revert fehlgeschlagen — siehe Console.'); alert($_('ai-workbench.list_view.revert_alert_failed'));
} finally { } finally {
revertingKey = null; revertingKey = null;
} }
@ -129,7 +147,7 @@
aria-selected={tab === 'timeline'} aria-selected={tab === 'timeline'}
onclick={() => (tab = 'timeline')} onclick={() => (tab = 'timeline')}
> >
Timeline {$_('ai-workbench.list_view.tab_timeline')}
</button> </button>
{#if grantsEnabled} {#if grantsEnabled}
<button <button
@ -140,7 +158,7 @@
aria-selected={tab === 'audit'} aria-selected={tab === 'audit'}
onclick={() => (tab = 'audit')} onclick={() => (tab = 'audit')}
> >
Datenzugriff {$_('ai-workbench.list_view.tab_audit')}
</button> </button>
{/if} {/if}
</div> </div>
@ -148,9 +166,9 @@
<div class="filters"> <div class="filters">
{#if tab === 'timeline'} {#if tab === 'timeline'}
<label> <label>
<span class="lbl">Modul</span> <span class="lbl">{$_('ai-workbench.list_view.label_module')}</span>
<select bind:value={moduleFilter}> <select bind:value={moduleFilter}>
<option value={null}>alle</option> <option value={null}>{$_('ai-workbench.list_view.option_all')}</option>
{#each allModules as m} {#each allModules as m}
<option value={m}>{m}</option> <option value={m}>{m}</option>
{/each} {/each}
@ -158,26 +176,26 @@
</label> </label>
{/if} {/if}
<label> <label>
<span class="lbl">Mission</span> <span class="lbl">{$_('ai-workbench.list_view.label_mission')}</span>
<select bind:value={missionFilter}> <select bind:value={missionFilter}>
<option value={null}>alle</option> <option value={null}>{$_('ai-workbench.list_view.option_all')}</option>
{#each missions.value as m (m.id)} {#each missions.value as m (m.id)}
<option value={m.id}>{m.title}</option> <option value={m.id}>{m.title}</option>
{/each} {/each}
</select> </select>
</label> </label>
<label> <label>
<span class="lbl">Agent</span> <span class="lbl">{$_('ai-workbench.list_view.label_agent')}</span>
<select bind:value={agentFilter}> <select bind:value={agentFilter}>
<option value={null}>alle</option> <option value={null}>{$_('ai-workbench.list_view.option_all')}</option>
{#each agents.value as a (a.id)} {#each agents.value as a (a.id)}
<option value={a.id}>{a.avatar ?? '🤖'} {a.name}</option> <option value={a.id}>{a.avatar ?? '🤖'} {a.name}</option>
{/each} {/each}
</select> </select>
</label> </label>
{#if tab === 'timeline'} {#if tab === 'timeline'}
<div class="range-group" role="tablist" aria-label="Zeitraum"> <div class="range-group" role="tablist" aria-label={$_('ai-workbench.list_view.range_aria')}>
{#each [{ id: '24h', label: '24h' }, { id: '7d', label: '7T' }, { id: 'all', label: 'alle' }] as const as opt} {#each [{ id: '24h', labelKey: 'range_24h' }, { id: '7d', labelKey: 'range_7d' }, { id: 'all', labelKey: 'range_all' }] as const as opt}
<button <button
type="button" type="button"
class="range-btn" class="range-btn"
@ -185,7 +203,7 @@
aria-pressed={timeRangeFilter === opt.id} aria-pressed={timeRangeFilter === opt.id}
onclick={() => (timeRangeFilter = opt.id)} onclick={() => (timeRangeFilter = opt.id)}
> >
{opt.label} {$_('ai-workbench.list_view.' + opt.labelKey)}
</button> </button>
{/each} {/each}
</div> </div>
@ -194,23 +212,23 @@
{#if tab === 'audit'} {#if tab === 'audit'}
{#if auditLoading} {#if auditLoading}
<p class="empty">Lade Audit…</p> <p class="empty">{$_('ai-workbench.list_view.audit_loading')}</p>
{:else if auditError} {:else if auditError}
<p class="empty error">Fehler: {auditError}</p> <p class="empty error">
{$_('ai-workbench.list_view.audit_error_prefix', { values: { error: auditError } })}
</p>
{:else if auditRows.length === 0} {:else if auditRows.length === 0}
<p class="empty"> <p class="empty">
Keine serverseitigen Entschlüsselungen. Der mana-ai Runner hat für diese Mission noch keine {$_('ai-workbench.list_view.audit_empty')}
Records gelesen — entweder ist kein Key-Grant erteilt, oder die Mission nutzt nur plaintext
Inputs (goals).
</p> </p>
{:else} {:else}
<table class="audit-table"> <table class="audit-table">
<thead> <thead>
<tr> <tr>
<th>Zeit</th> <th>{$_('ai-workbench.list_view.audit_col_time')}</th>
<th>Mission</th> <th>{$_('ai-workbench.list_view.audit_col_mission')}</th>
<th>Record</th> <th>{$_('ai-workbench.list_view.audit_col_record')}</th>
<th>Status</th> <th>{$_('ai-workbench.list_view.audit_col_status')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -230,8 +248,7 @@
{/if} {/if}
{:else if buckets.length === 0} {:else if buckets.length === 0}
<p class="empty"> <p class="empty">
Noch keine AI-Aktivität. Sobald eine Mission läuft und Proposals approved werden, erscheinen {$_('ai-workbench.list_view.timeline_empty')}
hier die Änderungen.
</p> </p>
{:else} {:else}
<ol class="timeline"> <ol class="timeline">
@ -251,8 +268,11 @@
<span class="agent-name">{bucketAgent?.name ?? b.agentDisplayName}</span> <span class="agent-name">{bucketAgent?.name ?? b.agentDisplayName}</span>
<span class="mission-sep">·</span> <span class="mission-sep">·</span>
{missionTitleById.get(b.missionId) ?? b.missionId} {missionTitleById.get(b.missionId) ?? b.missionId}
<span class="event-count" title="{b.events.length} Änderungen in dieser Iteration" <span
>{b.events.length}</span class="event-count"
title={$_('ai-workbench.list_view.event_count_title', {
values: { n: b.events.length },
})}>{b.events.length}</span
> >
</span> </span>
{#if b.rationale} {#if b.rationale}
@ -264,10 +284,14 @@
class="revert" class="revert"
disabled={revertingKey !== null} disabled={revertingKey !== null}
onclick={() => handleRevert(b.key, b.missionId, b.iterationId)} onclick={() => handleRevert(b.key, b.missionId, b.iterationId)}
title="Alle Änderungen dieser Iteration zurücknehmen" title={$_('ai-workbench.list_view.revert_title')}
> >
<ArrowCounterClockwise size={13} weight="bold" /> <ArrowCounterClockwise size={13} weight="bold" />
<span>{revertingKey === b.key ? 'Läuft…' : 'Rückgängig'}</span> <span
>{revertingKey === b.key
? $_('ai-workbench.list_view.revert_running')
: $_('ai-workbench.list_view.revert_label')}</span
>
</button> </button>
</header> </header>
<ul class="events"> <ul class="events">
@ -275,7 +299,11 @@
<li class="event"> <li class="event">
<span class="mod">{e.meta.appId}</span> <span class="mod">{e.meta.appId}</span>
<span class="desc">{describeEvent(e)}</span> <span class="desc">{describeEvent(e)}</span>
<a class="link" href={`/${e.meta.appId}`} title="Zum Modul"> <a
class="link"
href={`/${e.meta.appId}`}
title={$_('ai-workbench.list_view.event_link_title')}
>
<ArrowSquareOut size={11} /> <ArrowSquareOut size={11} />
</a> </a>
</li> </li>

View file

@ -45,7 +45,6 @@
"apps/mana/apps/web/src/lib/modules/admin/tabs/UsersTab.svelte": 3, "apps/mana/apps/web/src/lib/modules/admin/tabs/UsersTab.svelte": 3,
"apps/mana/apps/web/src/lib/modules/ai-health/ListView.svelte": 2, "apps/mana/apps/web/src/lib/modules/ai-health/ListView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/ai-insights/ListView.svelte": 4, "apps/mana/apps/web/src/lib/modules/ai-insights/ListView.svelte": 4,
"apps/mana/apps/web/src/lib/modules/ai-workbench/ListView.svelte": 8,
"apps/mana/apps/web/src/lib/modules/articles/components/AddUrlForm.svelte": 4, "apps/mana/apps/web/src/lib/modules/articles/components/AddUrlForm.svelte": 4,
"apps/mana/apps/web/src/lib/modules/articles/components/HighlightMenu.svelte": 5, "apps/mana/apps/web/src/lib/modules/articles/components/HighlightMenu.svelte": 5,
"apps/mana/apps/web/src/lib/modules/articles/components/HomeSectionSources.svelte": 1, "apps/mana/apps/web/src/lib/modules/articles/components/HomeSectionSources.svelte": 1,

View file

@ -5,6 +5,7 @@
"apps/mana/apps/web/src/lib/components/PwaUpdatePrompt.svelte": 3, "apps/mana/apps/web/src/lib/components/PwaUpdatePrompt.svelte": 3,
"apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte": 2, "apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/ai-missions/ListView.svelte": 2, "apps/mana/apps/web/src/lib/modules/ai-missions/ListView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/ai-workbench/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/broadcast/views/DetailView.svelte": 1, "apps/mana/apps/web/src/lib/modules/broadcast/views/DetailView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/credits/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/credits/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte": 1,