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:
@@ -0,0 +1,11 @@
|
||||
import { purgeExpiredAiGenerations } from "@/server/services/lotse/retention";
|
||||
|
||||
/**
|
||||
* Daily retention job for the AI log (L10b, Spec §31). Scheduled by the craftvia worker
|
||||
* (`scheduleRecurringJobs`); the payload carries no tenant — the service iterates all tenants and
|
||||
* writes through dbForTenant.
|
||||
*/
|
||||
export async function process(): Promise<void> {
|
||||
const res = await purgeExpiredAiGenerations();
|
||||
console.info(`[ai-retention] ${res.pseudonymised} entries older than ${res.days} days pseudonymised (${res.tenants} tenants)`);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export const PROCESSORS: Partial<Record<JobQueueName, () => Promise<JobProcessor
|
||||
transcription: () => import("./transcription").then((m) => m.process),
|
||||
"report-pdf": () => import(/* turbopackIgnore: true */ "./report-pdf").then((m) => m.process), // worker-only (react-dom/server + Chromium), kept out of the app bundle
|
||||
"image-derivatives": () => import("./image-derivatives").then((m) => m.process),
|
||||
"ai-retention": () => import("./ai-retention").then((m) => m.process), // L10b: daily AI log retention (scheduled by the worker)
|
||||
};
|
||||
|
||||
/** Inline fallback when no Redis is available (dev/demo). */
|
||||
|
||||
@@ -12,6 +12,7 @@ export const JOB_QUEUES = {
|
||||
transcription: "transcription",
|
||||
reportPdf: "report-pdf",
|
||||
imageDerivatives: "image-derivatives",
|
||||
aiRetention: "ai-retention",
|
||||
} as const;
|
||||
|
||||
export type JobQueueName = (typeof JOB_QUEUES)[keyof typeof JOB_QUEUES];
|
||||
@@ -80,6 +81,24 @@ export async function enqueueJob(name: JobQueueName, payload: JobPayload): Promi
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurring jobs (L10b), registered once by the craftvia worker at start. BullMQ job schedulers
|
||||
* are idempotent per id, so several worker replicas do not create duplicates.
|
||||
* - ai-retention: daily pseudonymisation of AI log contents (services/lotse/retention.ts)
|
||||
*/
|
||||
export async function scheduleRecurringJobs(connection: Redis): Promise<void> {
|
||||
const q = new Queue<JobPayload>(JOB_QUEUES.aiRetention, { connection });
|
||||
try {
|
||||
await q.upsertJobScheduler(
|
||||
"ai-retention-daily",
|
||||
{ every: 24 * 60 * 60 * 1000 },
|
||||
{ name: JOB_QUEUES.aiRetention, data: { tenantId: "*", entityId: "retention" }, opts: { removeOnComplete: { count: 30 }, removeOnFail: { count: 30 } } },
|
||||
);
|
||||
} finally {
|
||||
await q.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeJobQueues(): Promise<void> {
|
||||
await Promise.all([...queues.values()].map((q) => q.close()));
|
||||
queues.clear();
|
||||
|
||||
Reference in New Issue
Block a user