L10b Betrieb & Aufräumen: Lotse-Betrieb – Aufbewahrung KI-Protokoll und Token-Kontingent
Aufräumpunkt k (Spec §31): - Aufbewahrung: services/lotse/retention.ts leert input/output und createdById von AiGeneration-Einträgen älter als AI_GENERATION_RETENTION_DAYS (Default 180), Metadaten bleiben, Audit je Mandant. Queue/Processor ai-retention, täglicher BullMQ-Job-Scheduler beim Start des craftvia-worker. - Kontingent: services/lotse/budget.ts (Tokens ein+aus je Kalendermonat, TenantSettings-Wert vor Env AI_MONTHLY_TOKEN_LIMIT, 0 = unbegrenzt). Lotse-Entwurf und Sprachnotiz-Zusammenfassung → blocked budget_exceeded mit Klartext; Import-Extraktion fällt auf manuelle Erfassung zurück (Hinweis ai_budget_exceeded). /settings/lotse: Kontingent setzen, Verbrauch anzeigen. - scripts/test-betrieb-audit.ts: Audit nach Commit/Rollback/verschachtelt, Merge atomar und in äußerer Transaktion, Audit „read", Aufbewahrung (Frist, Metadaten, Idempotenz, Mandant B), Kontingent (Mandant/Env/Vormonat/unbegrenzt, Rollen, Audit). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getLocale, getTranslations } from "next-intl/server";
|
||||
import { ArrowLeft, CheckCircle2, ListChecks, MinusCircle, ShieldCheck, XCircle } from "lucide-react";
|
||||
import { LotseMark } from "@/components/lotse/lotse-mark";
|
||||
import { PageHead } from "@/components/mockup-ui";
|
||||
@@ -27,8 +27,9 @@ function ProviderStatus({ ok, labels }: { ok: boolean; labels: { ok: string; off
|
||||
export default async function LotseSettingsPage({ searchParams }: { searchParams: Promise<{ saved?: string; error?: string }> }) {
|
||||
const ctx = await readCtx();
|
||||
if (!can(ctx, "tenant:manage")) redirect("/dashboard");
|
||||
const [sp, s, t] = await Promise.all([searchParams, getLotseSettings(ctx), getTranslations("lotse")]);
|
||||
const [sp, s, t, locale] = await Promise.all([searchParams, getLotseSettings(ctx), getTranslations("lotse"), getLocale()]);
|
||||
const statusLabels = { ok: t("settings.configured"), off: t("settings.notConfigured") };
|
||||
const nf = new Intl.NumberFormat(locale);
|
||||
|
||||
return (
|
||||
<main className="flex-1 p-4 md:p-6">
|
||||
@@ -72,6 +73,36 @@ export default async function LotseSettingsPage({ searchParams }: { searchParams
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div>
|
||||
<label htmlFor="monthlyTokenLimit" className="text-sm font-semibold">
|
||||
{t("settings.budget")}
|
||||
</label>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
{t("settings.budgetHint", { platform: s.budget.platformLimit > 0 ? nf.format(s.budget.platformLimit) : t("settings.budgetUnlimited") })}
|
||||
</p>
|
||||
<input
|
||||
id="monthlyTokenLimit"
|
||||
name="monthlyTokenLimit"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={0}
|
||||
step={1000}
|
||||
defaultValue={s.budget.tenantLimit ?? ""}
|
||||
className="mt-2 h-11 w-full max-w-60 rounded-lg border border-input bg-background px-3 text-sm"
|
||||
/>
|
||||
<p className="mt-2 flex flex-wrap items-center gap-2 text-[12.5px] text-muted-foreground">
|
||||
{t("settings.budgetUsage", {
|
||||
since: new Date(s.budget.periodStart).toLocaleDateString(locale, { timeZone: "UTC" }),
|
||||
used: nf.format(s.budget.used),
|
||||
limit: s.budget.limit > 0 ? nf.format(s.budget.limit) : t("settings.budgetUnlimited"),
|
||||
})}
|
||||
{s.budget.exceeded && (
|
||||
<span className="inline-flex items-center gap-1 font-semibold text-[var(--risk)]">
|
||||
<XCircle className="size-4" aria-hidden /> {t("settings.budgetExceeded")}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button type="submit" className="min-h-11">
|
||||
{t("settings.save")}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user