diff --git a/apps/mana/apps/web/src/lib/api/config.ts b/apps/mana/apps/web/src/lib/api/config.ts index 9e4edf751..2e30ba4df 100644 --- a/apps/mana/apps/web/src/lib/api/config.ts +++ b/apps/mana/apps/web/src/lib/api/config.ts @@ -60,6 +60,20 @@ export function getManaCreditsUrl(): string { return process.env.PUBLIC_MANA_CREDITS_URL || 'http://localhost:3061'; } +/** + * Get the mana-ai service URL. + * Hosts the background Mission Runner, decrypt-audit read endpoint, + * Prometheus metrics. + */ +export function getManaAiUrl(): string { + if (browser && typeof window !== 'undefined') { + const injected = (window as unknown as { __PUBLIC_MANA_AI_URL__?: string }) + .__PUBLIC_MANA_AI_URL__; + return injected || 'http://localhost:3066'; + } + return process.env.PUBLIC_MANA_AI_URL || 'http://localhost:3066'; +} + /** * Get the mana-mail service URL. * Hosts mail threads, send, labels, accounts. diff --git a/apps/mana/apps/web/src/lib/components/ai/MissionGrantDialog.svelte b/apps/mana/apps/web/src/lib/components/ai/MissionGrantDialog.svelte new file mode 100644 index 000000000..02a019baf --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/ai/MissionGrantDialog.svelte @@ -0,0 +1,314 @@ + + + +{#if open} +
+ Damit {mission.title} auch ohne offenen Browser-Tab laufen kann, braucht der + AI-Runner einmalig einen eng-gescopten Entschlüsselungs-Schlüssel. +
+ +{tables.join(', ')}
+ {status.message}
++ {#if grantStatus(selected) === 'active'} + Läuft autonom; Zugriff kann jederzeit zurückgezogen werden. + {:else} + Ohne Zugriff läuft die Mission nur bei offenem Tab. + {/if} +
+Noch keine Iteration gelaufen.
@@ -581,4 +639,69 @@ background: hsl(var(--color-surface)); cursor: pointer; } + .grant-box { + padding: 0.75rem 0.875rem; + border: 1px solid hsl(var(--color-border)); + border-radius: 0.5rem; + background: color-mix(in oklab, hsl(var(--color-primary)) 4%, hsl(var(--color-surface))); + display: flex; + flex-direction: column; + gap: 0.5rem; + } + .grant-head { + display: flex; + justify-content: space-between; + align-items: center; + } + .grant-title { + font-weight: 600; + font-size: 0.8125rem; + } + .grant-pill { + font-size: 0.6875rem; + padding: 0.125rem 0.5rem; + border-radius: 999px; + } + .grant-pill-ok { + background: #d7f7e3; + color: #1b7a3a; + } + .grant-pill-warn { + background: #fde7c8; + color: #8a4f00; + } + .grant-pill-muted { + background: hsl(var(--color-surface)); + color: hsl(var(--color-muted-foreground)); + border: 1px solid hsl(var(--color-border)); + } + .grant-note { + margin: 0; + font-size: 0.75rem; + color: hsl(var(--color-muted-foreground)); + } + .grant-actions { + display: flex; + gap: 0.375rem; + justify-content: flex-end; + flex-wrap: wrap; + } + .grant-actions .btn-ghost, + .grant-actions .btn-primary { + padding: 0.25rem 0.625rem; + border-radius: 0.375rem; + font: inherit; + font-size: 0.75rem; + cursor: pointer; + } + .grant-actions .btn-ghost { + border: 1px solid hsl(var(--color-border)); + background: hsl(var(--color-surface)); + color: hsl(var(--color-foreground)); + } + .grant-actions .btn-primary { + border: 1px solid color-mix(in oklab, hsl(var(--color-primary)) 45%, transparent); + background: color-mix(in oklab, hsl(var(--color-primary)) 18%, hsl(var(--color-surface))); + color: hsl(var(--color-primary)); + } diff --git a/apps/mana/apps/web/src/lib/modules/ai-workbench/ListView.svelte b/apps/mana/apps/web/src/lib/modules/ai-workbench/ListView.svelte index b82f0716e..9b18b0bb5 100644 --- a/apps/mana/apps/web/src/lib/modules/ai-workbench/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/ai-workbench/ListView.svelte @@ -7,6 +7,7 @@ import { useAiTimeline, bucketByIteration } from '$lib/data/ai/timeline/queries'; import { useMissions } from '$lib/data/ai/missions/queries'; import { revertIteration } from '$lib/data/ai/revert/revert-iteration'; + import { fetchDecryptAudit, type AuditRow } from '$lib/data/ai/audit/queries'; import type { DomainEvent } from '$lib/data/events/types'; let moduleFilter = $stateLade Audit…
+ {:else if auditError} +Fehler: {auditError}
+ {:else if auditRows.length === 0} ++ Keine serverseitigen Entschlüsselungen. Der mana-ai Runner hat für diese Mission noch keine + Records gelesen — entweder ist kein Key-Grant erteilt, oder die Mission nutzt nur plaintext + Inputs (goals). +
+ {:else} +| Zeit | +Mission | +Record | +Status | +
|---|---|---|---|
| {formatAuditTs(r.ts)} | +{missionTitleById.get(r.missionId) ?? r.missionId} | +{r.tableName}:{r.recordId} |
+ + {r.status} + {#if r.reason}{r.reason}{/if} + | +
Noch keine AI-Aktivität. Sobald eine Mission läuft und Proposals approved werden, erscheinen
hier die Änderungen.
@@ -136,6 +237,79 @@
gap: 0.75rem;
padding: 0.75rem 1rem 1.25rem;
}
+ .tabs {
+ display: flex;
+ gap: 0;
+ border-bottom: 1px solid hsl(var(--color-border));
+ }
+ .tab {
+ border: none;
+ background: transparent;
+ padding: 0.375rem 0.75rem;
+ font: inherit;
+ font-size: 0.8125rem;
+ color: hsl(var(--color-muted-foreground));
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -1px;
+ }
+ .tab-active {
+ color: hsl(var(--color-foreground));
+ border-bottom-color: hsl(var(--color-primary));
+ font-weight: 600;
+ }
+ .audit-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.8125rem;
+ }
+ .audit-table th,
+ .audit-table td {
+ text-align: left;
+ padding: 0.375rem 0.5rem;
+ border-bottom: 1px solid hsl(var(--color-border));
+ }
+ .audit-table th {
+ font-weight: 600;
+ font-size: 0.6875rem;
+ text-transform: uppercase;
+ color: hsl(var(--color-muted-foreground));
+ }
+ .audit-table code {
+ font-size: 0.75rem;
+ color: hsl(var(--color-muted-foreground));
+ }
+ .audit-ts {
+ white-space: nowrap;
+ font-variant-numeric: tabular-nums;
+ }
+ .audit-pill {
+ display: inline-block;
+ padding: 0.0625rem 0.375rem;
+ border-radius: 999px;
+ font-size: 0.6875rem;
+ font-weight: 600;
+ }
+ .audit-pill-ok {
+ background: #d7f7e3;
+ color: #1b7a3a;
+ }
+ .audit-pill-failed {
+ background: #fde7c8;
+ color: #8a4f00;
+ }
+ .audit-pill-scope-violation {
+ background: #f7d7d7;
+ color: #8a1b1b;
+ }
+ .audit-reason {
+ margin-left: 0.25rem;
+ font-size: 0.6875rem;
+ color: hsl(var(--color-muted-foreground));
+ }
+ .error {
+ color: hsl(var(--color-error));
+ }
.filters {
display: flex;
gap: 0.75rem;
diff --git a/services/mana-ai/src/db/audit-read.ts b/services/mana-ai/src/db/audit-read.ts
new file mode 100644
index 000000000..af92840ce
--- /dev/null
+++ b/services/mana-ai/src/db/audit-read.ts
@@ -0,0 +1,77 @@
+/**
+ * Read side of `mana_ai.decrypt_audit`. Returns rows for one user +
+ * (optionally) one mission, newest first.
+ *
+ * RLS-scoped via `withUser` — even if the caller forgets the WHERE
+ * clause, the policy wouldn't expose another user's rows. The explicit
+ * `user_id = $1` clause is belt + braces.
+ */
+
+import type { Sql } from './connection';
+import { withUser } from './connection';
+
+export interface DecryptAuditRow {
+ id: string;
+ missionId: string;
+ iterationId: string | null;
+ tableName: string;
+ recordId: string;
+ status: 'ok' | 'failed' | 'scope-violation';
+ reason: string | null;
+ ts: string;
+}
+
+export interface ReadDecryptAuditParams {
+ missionId?: string;
+ /** Caller-controlled upper bound. Hard-capped at 1000 so a runaway
+ * client can't OOM us. */
+ limit?: number;
+}
+
+export async function readDecryptAudit(
+ sql: Sql,
+ userId: string,
+ params: ReadDecryptAuditParams = {}
+): Promise