From 7a7430851c6eec2cba37e4a02c1ac053c5e510ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sun, 22 Mar 2026 22:46:34 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20complete=20audit=20coverage=20=E2=80=94?= =?UTF-8?q?=20comment,=20webhook,=20system-role,=20dispo,=20scenario?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - comment.ts: create (body preview), resolve, delete - webhook.ts: create, update, delete, test (result in summary) - system-role-config.ts: update with before/after - dispo.ts: commitImportBatch (IMPORT with counts), cancelImportBatch - scenario.ts: applyScenario (CREATE with allocation count) Audit coverage now: 29/36 routers (81%). Remaining 7 are read-only (dashboard, staffing, chargeability-report, computation-graph, report, insights.detectAnomalies, notification read/dismiss). Co-Authored-By: claude-flow --- packages/api/src/router/comment.ts | 37 ++++++++++- packages/api/src/router/dispo.ts | 33 +++++++++- packages/api/src/router/scenario.ts | 15 ++++- packages/api/src/router/system-role-config.ts | 17 +++++ packages/api/src/router/webhook.ts | 63 +++++++++++++++++-- 5 files changed, 157 insertions(+), 8 deletions(-) diff --git a/packages/api/src/router/comment.ts b/packages/api/src/router/comment.ts index 3de7a0f..6782e1f 100644 --- a/packages/api/src/router/comment.ts +++ b/packages/api/src/router/comment.ts @@ -3,6 +3,7 @@ import { TRPCError } from "@trpc/server"; import { SystemRole } from "@planarchy/shared"; import { createTRPCRouter, protectedProcedure } from "../trpc.js"; import { createNotification } from "../lib/create-notification.js"; +import { createAuditEntry } from "../lib/audit.js"; // ─── Helpers ────────────────────────────────────────────────────────────────── @@ -155,6 +156,17 @@ export const commentRouter = createTRPCRouter({ ); } + void createAuditEntry({ + db: ctx.db, + entityType: "Comment", + entityId: comment.id, + entityName: input.body.slice(0, 50), + action: "CREATE", + userId: ctx.dbUser?.id, + after: comment as unknown as Record, + source: "ui", + }); + return comment; }), @@ -188,13 +200,26 @@ export const commentRouter = createTRPCRouter({ }); } - return ctx.db.comment.update({ + const updated = await ctx.db.comment.update({ where: { id: input.id }, data: { resolved: input.resolved }, include: { author: { select: { id: true, name: true, email: true, image: true } }, }, }); + + void createAuditEntry({ + db: ctx.db, + entityType: "Comment", + entityId: input.id, + action: "UPDATE", + userId: ctx.dbUser?.id, + summary: input.resolved ? "Resolved comment" : "Unresolved comment", + after: updated as unknown as Record, + source: "ui", + }); + + return updated; }), /** Delete a comment (author or admin only). Hard-deletes, including all replies. */ @@ -227,5 +252,15 @@ export const commentRouter = createTRPCRouter({ }); await ctx.db.comment.delete({ where: { id: input.id } }); + + void createAuditEntry({ + db: ctx.db, + entityType: "Comment", + entityId: input.id, + action: "DELETE", + userId: ctx.dbUser?.id, + before: existing as unknown as Record, + source: "ui", + }); }), }); diff --git a/packages/api/src/router/dispo.ts b/packages/api/src/router/dispo.ts index 4feaea4..4cdb3c2 100644 --- a/packages/api/src/router/dispo.ts +++ b/packages/api/src/router/dispo.ts @@ -11,6 +11,7 @@ import { import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { adminProcedure, createTRPCRouter } from "../trpc.js"; +import { createAuditEntry } from "../lib/audit.js"; // ─── Shared schemas ────────────────────────────────────────────────────────── @@ -175,10 +176,23 @@ export const dispoRouter = createTRPCRouter({ }); } - return ctx.db.importBatch.update({ + const cancelled = await ctx.db.importBatch.update({ where: { id: input.id }, data: { status: ImportBatchStatus.CANCELLED }, }); + + void createAuditEntry({ + db: ctx.db, + entityType: "ImportBatch", + entityId: input.id, + action: "UPDATE", + userId: ctx.dbUser?.id, + summary: "Cancelled import batch", + after: cancelled as unknown as Record, + source: "ui", + }); + + return cancelled; }), // ── 6. listStagedResources ─────────────────────────────────────────────── @@ -414,10 +428,25 @@ export const dispoRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { - return commitDispoImportBatch(ctx.db, { + const result = await commitDispoImportBatch(ctx.db, { importBatchId: input.importBatchId, ...(input.allowTbdUnresolved !== undefined ? { allowTbdUnresolved: input.allowTbdUnresolved } : {}), ...(input.importTbdProjects !== undefined ? { importTbdProjects: input.importTbdProjects } : {}), }); + + const counts = result as unknown as Record; + void createAuditEntry({ + db: ctx.db, + entityType: "ImportBatch", + entityId: input.importBatchId, + entityName: input.importBatchId, + action: "IMPORT", + userId: ctx.dbUser?.id, + summary: `Committed import batch (${JSON.stringify(counts)})`, + after: counts, + source: "ui", + }); + + return result; }), }); diff --git a/packages/api/src/router/scenario.ts b/packages/api/src/router/scenario.ts index e208202..d7c1395 100644 --- a/packages/api/src/router/scenario.ts +++ b/packages/api/src/router/scenario.ts @@ -2,6 +2,7 @@ import { calculateAllocation, countWorkingDays } from "@planarchy/engine/allocat import { z } from "zod"; import { TRPCError } from "@trpc/server"; import { createTRPCRouter, controllerProcedure, protectedProcedure } from "../trpc.js"; +import { createAuditEntry } from "../lib/audit.js"; const DEFAULT_AVAILABILITY = { monday: 8, @@ -485,7 +486,7 @@ export const scenarioRouter = createTRPCRouter({ const project = await ctx.db.project.findUnique({ where: { id: projectId }, - select: { id: true }, + select: { id: true, name: true }, }); if (!project) { throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" }); @@ -548,6 +549,18 @@ export const scenarioRouter = createTRPCRouter({ created.push(newAssignment.id); } + void createAuditEntry({ + db: ctx.db, + entityType: "ScenarioApplication", + entityId: projectId, + entityName: project.name, + action: "CREATE", + userId: ctx.dbUser?.id, + summary: `Applied scenario to project "${project.name}" (${created.length} allocations created/modified)`, + metadata: { appliedCount: created.length, assignmentIds: created }, + source: "ui", + }); + return { appliedCount: created.length }; }), }); diff --git a/packages/api/src/router/system-role-config.ts b/packages/api/src/router/system-role-config.ts index 94dd1a3..897cd25 100644 --- a/packages/api/src/router/system-role-config.ts +++ b/packages/api/src/router/system-role-config.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { adminProcedure, createTRPCRouter, invalidateRoleDefaultsCache, protectedProcedure } from "../trpc.js"; +import { createAuditEntry } from "../lib/audit.js"; export const systemRoleConfigRouter = createTRPCRouter({ /** List all role configs (sorted by sortOrder) */ @@ -21,6 +22,10 @@ export const systemRoleConfigRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { + const existing = await ctx.db.systemRoleConfig.findUnique({ + where: { role: input.role as never }, + }); + const data: Record = {}; if (input.label !== undefined) data.label = input.label; if (input.description !== undefined) data.description = input.description; @@ -35,6 +40,18 @@ export const systemRoleConfigRouter = createTRPCRouter({ // Invalidate cached role defaults so changes take effect immediately invalidateRoleDefaultsCache(); + void createAuditEntry({ + db: ctx.db, + entityType: "SystemRoleConfig", + entityId: input.role, + entityName: result.label, + action: "UPDATE", + userId: ctx.dbUser?.id, + before: (existing ?? {}) as unknown as Record, + after: result as unknown as Record, + source: "ui", + }); + return result; }), }); diff --git a/packages/api/src/router/webhook.ts b/packages/api/src/router/webhook.ts index 96f3d76..15f2e89 100644 --- a/packages/api/src/router/webhook.ts +++ b/packages/api/src/router/webhook.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { TRPCError } from "@trpc/server"; import { createTRPCRouter, adminProcedure } from "../trpc.js"; import { WEBHOOK_EVENTS } from "../lib/webhook-dispatcher.js"; +import { createAuditEntry } from "../lib/audit.js"; const webhookEventEnum = z.enum(WEBHOOK_EVENTS as unknown as [string, ...string[]]); @@ -36,7 +37,7 @@ export const webhookRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { - return ctx.db.webhook.create({ + const webhook = await ctx.db.webhook.create({ data: { name: input.name, url: input.url, @@ -45,6 +46,19 @@ export const webhookRouter = createTRPCRouter({ isActive: input.isActive, }, }); + + void createAuditEntry({ + db: ctx.db, + entityType: "Webhook", + entityId: webhook.id, + entityName: webhook.name, + action: "CREATE", + userId: ctx.dbUser?.id, + after: webhook as unknown as Record, + source: "ui", + }); + + return webhook; }), /** Update an existing webhook. */ @@ -67,7 +81,7 @@ export const webhookRouter = createTRPCRouter({ throw new TRPCError({ code: "NOT_FOUND", message: "Webhook not found" }); } - return ctx.db.webhook.update({ + const updated = await ctx.db.webhook.update({ where: { id: input.id }, data: { ...(input.data.name !== undefined ? { name: input.data.name } : {}), @@ -77,6 +91,20 @@ export const webhookRouter = createTRPCRouter({ ...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}), }, }); + + void createAuditEntry({ + db: ctx.db, + entityType: "Webhook", + entityId: input.id, + entityName: updated.name, + action: "UPDATE", + userId: ctx.dbUser?.id, + before: existing as unknown as Record, + after: updated as unknown as Record, + source: "ui", + }); + + return updated; }), /** Delete a webhook. */ @@ -88,6 +116,17 @@ export const webhookRouter = createTRPCRouter({ throw new TRPCError({ code: "NOT_FOUND", message: "Webhook not found" }); } await ctx.db.webhook.delete({ where: { id: input.id } }); + + void createAuditEntry({ + db: ctx.db, + entityType: "Webhook", + entityId: input.id, + entityName: existing.name, + action: "DELETE", + userId: ctx.dbUser?.id, + before: existing as unknown as Record, + source: "ui", + }); }), /** Send a test payload to a webhook URL. */ @@ -127,6 +166,8 @@ export const webhookRouter = createTRPCRouter({ const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5_000); + let result: { success: boolean; statusCode: number; statusText: string }; + try { const response = await fetch(wh.url, { method: "POST", @@ -134,13 +175,13 @@ export const webhookRouter = createTRPCRouter({ body, signal: controller.signal, }); - return { + result = { success: response.ok, statusCode: response.status, statusText: response.statusText, }; } catch (err) { - return { + result = { success: false, statusCode: 0, statusText: err instanceof Error ? err.message : "Unknown error", @@ -148,5 +189,19 @@ export const webhookRouter = createTRPCRouter({ } finally { clearTimeout(timeout); } + + void createAuditEntry({ + db: ctx.db, + entityType: "Webhook", + entityId: wh.id, + entityName: wh.name, + action: "UPDATE", + userId: ctx.dbUser?.id, + summary: `Tested webhook (result: ${result.success ? "success" : "failed"})`, + metadata: result as unknown as Record, + source: "ui", + }); + + return result; }), });