+
{t('Compliance & AI-Audit')}
+
+ {t('Transparente Übersicht aller AI-Datenflüsse und Sicherheitsereignisse Ihres Mandanten.')}
+
+
+ {/* Mandate selector */}
+
+
+
+
+
+ {!selectedMandateId ? (
+
{t('Bitte wählen Sie einen Mandanten aus.')}
+ ) : (
+ <>
+ {/* Tab bar */}
+
+ {_tabs.map(tab => (
+
+ ))}
+
+
+ {/* ── Tab A: AI Data-Flow Log ── */}
+ {activeTab === 'ai-log' && (
+
+ ,
+ onClick: _handleContentDownload,
+ },
+ ]}
+ />
+
+ )}
+
+ {/* ── Tab B: Audit Log ── */}
+ {activeTab === 'audit-log' && (
+
+
+
+ )}
+
+ {/* ── Tab C: Statistics ── */}
+ {activeTab === 'stats' && (
+
+
+ {[7, 30, 90].map(d => (
+
+ ))}
+
+
+ {statsLoading ? (
+
{t('Lade Statistiken…')}
+ ) : !stats ? (
+
{t('Keine Daten verfügbar.')}
+ ) : (
+ <>
+ {/* KPIs */}
+
+
+
{stats.totalCalls}
+
{t('AI-Aufrufe')}
+
+
+
{stats.neutralizationPercent}%
+
{t('Neutralisierungsquote')}
+
+
+
{Object.keys(stats.callsByModel).length}
+
{t('Genutzte Modelle')}
+
+
+
+ {stats.costPerDay.reduce((s, d) => s + d.cost, 0).toFixed(2)}
+
+
{t('Gesamtkosten (CHF)')}
+
+
+
+ {/* Charts row 1: Calls/Day + Cost/Day */}
+
+
+
{t('AI-Aufrufe pro Tag')}
+ {stats.callsPerDay.length === 0 ? (
+
{t('Keine Daten')}
+ ) : (
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
{t('Kosten-Verlauf (CHF)')}
+ {stats.costPerDay.length === 0 ? (
+
{t('Keine Daten')}
+ ) : (
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ {/* Charts row 2: By Model (pie) + By Feature (bar) */}
+
+
+
{t('AI-Aufrufe nach Modell')}
+ {Object.keys(stats.callsByModel).length === 0 ? (
+
{t('Keine Daten')}
+ ) : (
+
+
+ ({ name, value }))}
+ dataKey="value" nameKey="name"
+ cx="50%" cy="50%" outerRadius={80}
+ label={({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`}
+ >
+ {Object.keys(stats.callsByModel).map((_, i) => (
+ |
+ ))}
+
+
+
+
+ )}
+
+
+
{t('AI-Aufrufe nach Feature')}
+ {Object.keys(stats.callsByFeature).length === 0 ? (
+
{t('Keine Daten')}
+ ) : (
+
+ ({ name, value }))}>
+
+
+
+
+
+
+
+ )}
+
+
+
+ {/* Top Users */}
+ {Object.keys(stats.topUsers).length > 0 && (
+
+
{t('Top-Nutzer nach AI-Aufrufen')}
+
+ ({ name, value }))}
+ layout="vertical" margin={{ left: 8, right: 16 }}
+ >
+
+
+
+
+
+
+
+
+ )}
+ >
+ )}
+
+ )}
+ >
+ )}
+
+ );
+};
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx
index 4114702..e4c59db 100644
--- a/src/pages/Dashboard.tsx
+++ b/src/pages/Dashboard.tsx
@@ -85,11 +85,9 @@ export const DashboardPage: React.FC = () => {
- {t('Du hast Zugriff auf {instanceCount} {instanceWord} in {mandateCount} {mandateWord}.', {
- instanceCount: totalInstances,
- instanceWord: totalInstances === 1 ? t('Feature-Instanz') : t('Feature-Instanzen'),
- mandateCount: totalMandates,
- mandateWord: totalMandates === 1 ? t('Mandant') : t('Mandanten'),
+ {t('{instanceCount} Feature-Instanzen in {mandateCount} Mandanten', {
+ instanceCount: String(totalInstances),
+ mandateCount: String(totalMandates),
})}
)}
diff --git a/src/pages/IntegrationsOverviewPage.tsx b/src/pages/IntegrationsOverviewPage.tsx
index 184883b..1c4af13 100644
--- a/src/pages/IntegrationsOverviewPage.tsx
+++ b/src/pages/IntegrationsOverviewPage.tsx
@@ -191,7 +191,7 @@ export const IntegrationsOverviewPage: React.FC = () => {
return [
{ value: s.aiCallCount, label: t('AI-Aufrufe'), sub: `${s.aiCallPeriodDays} ${t('Tage')}` },
{ value: s.activeWorkflows, label: t('Aktive Workflows'), sub: s.totalWorkflows > 0 ? `${_formatStatNumber(s.totalWorkflows)} ${t('total')}` : undefined },
- { value: s.totalRuns, label: t('Workflow-Runs'), sub: s.totalTokens > 0 ? `${_formatStatNumber(s.totalTokens)} Tokens` : undefined },
+ { value: s.totalRuns, label: t('Workflow-Läufe'), sub: s.totalTokens > 0 ? `${_formatStatNumber(s.totalTokens)} ${t('Tokens')}` : undefined },
{ value: connectedSystems, label: t('Verbundene Systeme') },
];
}, [diagram, t]);
diff --git a/src/pages/Store.tsx b/src/pages/Store.tsx
index 03f07ff..1a36176 100644
--- a/src/pages/Store.tsx
+++ b/src/pages/Store.tsx
@@ -153,7 +153,7 @@ const StorePage: React.FC = () => {
)}
{subscriptionInfo.budgetAiPerUserCHF != null && subscriptionInfo.budgetAiPerUserCHF > 0 && (
-
{t('Demo Configurations')}
-
{t('Load or remove demo environments for presentations and testing.')}
+
{t('Demo-Konfigurationen')}
+
{t('Demo-Umgebungen für Präsentationen und Tests laden oder entfernen.')}
@@ -104,7 +104,7 @@ export const AdminDemoConfigPage: React.FC = () => {
{lastResult && (
-
{lastResult.action === 'load' ? t('Loaded') : t('Removed')}:{' '}
+
{lastResult.action === 'load' ? t('Geladen') : t('Entfernt')}:{' '}
{lastResult.status === 'ok' ? (
<_SummaryDisplay summary={lastResult.summary} />
) : (
@@ -114,9 +114,9 @@ export const AdminDemoConfigPage: React.FC = () => {
)}
{loading && configs.length === 0 ? (
-
{t('Loading...')}
+
{t('Lade…')}
) : configs.length === 0 ? (
-
{t('No demo configurations found.')}
+
{t('Keine Demo-Konfigurationen gefunden.')}
) : (
{configs.map((cfg) => (
@@ -134,7 +134,7 @@ export const AdminDemoConfigPage: React.FC = () => {
disabled={actionInProgress !== null}
>
{actionInProgress === cfg.code ? : }
- {t('Load')}
+ {t('Laden')}
@@ -155,10 +155,11 @@ export const AdminDemoConfigPage: React.FC = () => {
);
};
-function _SummaryDisplay({ summary }: { summary?: Record
}) {
+const _SummaryDisplay: React.FC<{ summary?: Record }> = ({ summary }) => {
+ const { t } = useLanguage();
if (!summary) return null;
const sections = Object.entries(summary).filter(([, v]) => Array.isArray(v) && (v as unknown[]).length > 0);
- if (sections.length === 0) return Done (no changes);
+ if (sections.length === 0) return {t('Abgeschlossen (keine Änderungen)')};
return (
{sections.map(([key, items]) => (
@@ -168,4 +169,4 @@ function _SummaryDisplay({ summary }: { summary?: Record }) {
))}
);
-}
+};
diff --git a/src/pages/billing/BillingDataView.tsx b/src/pages/billing/BillingDataView.tsx
index bc4f017..f03d9a9 100644
--- a/src/pages/billing/BillingDataView.tsx
+++ b/src/pages/billing/BillingDataView.tsx
@@ -274,7 +274,7 @@ export const BillingDataView: React.FC = () => {
try {
await api.post('/api/billing/checkout/confirm', { sessionId: sessionIdParam });
if (!cancelled) {
- setCheckoutMessage({ type: 'success', text: 'Zahlung erfolgreich. Guthaben wurde verbucht.' });
+ setCheckoutMessage({ type: 'success', text: t('Zahlung erfolgreich. Guthaben wurde verbucht.') });
}
} catch (err: any) {
const detail = err?.response?.data?.detail;
diff --git a/src/pages/views/workspace/WorkspaceRagInsightsPage.module.css b/src/pages/views/workspace/WorkspaceRagInsightsPage.module.css
index 712c04e..2633881 100644
--- a/src/pages/views/workspace/WorkspaceRagInsightsPage.module.css
+++ b/src/pages/views/workspace/WorkspaceRagInsightsPage.module.css
@@ -80,3 +80,28 @@
color: #c62828;
padding: 1rem;
}
+
+.recentTable {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.85rem;
+}
+
+.recentTable th {
+ text-align: left;
+ font-weight: 600;
+ color: var(--text-secondary, #666);
+ padding: 0.5rem 0.75rem;
+ border-bottom: 2px solid var(--border-color, #e0e0e0);
+ white-space: nowrap;
+}
+
+.recentTable td {
+ padding: 0.45rem 0.75rem;
+ border-bottom: 1px solid var(--border-color, #f0f0f0);
+ color: var(--text-primary, #1a1a1a);
+}
+
+.recentTable tbody tr:hover {
+ background: var(--bg-secondary, #fafafa);
+}
diff --git a/src/pages/views/workspace/WorkspaceRagInsightsPage.tsx b/src/pages/views/workspace/WorkspaceRagInsightsPage.tsx
index 0079c41..c473326 100644
--- a/src/pages/views/workspace/WorkspaceRagInsightsPage.tsx
+++ b/src/pages/views/workspace/WorkspaceRagInsightsPage.tsx
@@ -41,6 +41,39 @@ function _mimeLabel(key: string, t: (k: string) => string): string {
const CHART_COLORS = ['#1976d2', '#00897b', '#6a1b9a', '#e65100', '#5d4037', '#455a64', '#c62828'];
+function _formatTimestamp(ts: number | null | undefined): string {
+ if (ts == null || ts <= 0) return '–';
+ try {
+ const d = new Date(ts * 1000);
+ return d.toLocaleString('de-CH', {
+ day: '2-digit', month: '2-digit', year: 'numeric',
+ hour: '2-digit', minute: '2-digit',
+ });
+ } catch {
+ return '–';
+ }
+}
+
+function _shortMime(mime: string): string {
+ const m = (mime || '').toLowerCase();
+ if (m.includes('pdf')) return 'PDF';
+ if (m.includes('wordprocessing') || m.includes('msword')) return 'Word';
+ if (m.includes('spreadsheet') || m.includes('excel')) return 'Excel';
+ if (m.includes('presentation') || m.includes('powerpoint')) return 'PowerPoint';
+ if (m.startsWith('text/')) return 'Text';
+ if (m.startsWith('image/')) return 'Bild';
+ if (m.includes('html')) return 'HTML';
+ return mime || '–';
+}
+
+const _STATUS_COLORS: Record = {
+ indexed: '#2e7d32',
+ extracted: '#1565c0',
+ embedding: '#6a1b9a',
+ pending: '#e65100',
+ failed: '#c62828',
+};
+
interface RagKpis {
indexedDocuments: number;
indexedBytesTotal: number;
@@ -51,6 +84,14 @@ interface RagKpis {
workflowEntities: number;
}
+interface RecentlyIndexedDoc {
+ fileName: string;
+ mimeType: string;
+ status: string;
+ extractedAt: number | null;
+ totalSize: number;
+}
+
interface RagStatsResponse {
error?: string;
scope?: {
@@ -63,6 +104,7 @@ interface RagStatsResponse {
documentsByMimeCategory?: Record;
chunksByContentType?: Record;
timelineIndexedDocuments?: Array<{ date: string; indexedDocuments: number }>;
+ recentlyIndexedDocuments?: RecentlyIndexedDoc[];
generatedAtUtc?: string;
}
@@ -181,6 +223,45 @@ export const WorkspaceRagInsightsPage: React.FC = () => {
)}
+ {(stats?.recentlyIndexedDocuments ?? []).length > 0 && (
+