diff --git a/.phase18-state.json b/.phase18-state.json index e2561a8..0b6ba16 100644 --- a/.phase18-state.json +++ b/.phase18-state.json @@ -1,5 +1,8 @@ { "completed_features": [], - "current_feature": "api-key-management", - "started_at": "2026-05-23T07:29:44.977564" + "current_feature": "audit-log-filters", + "started_at": "2026-05-23T07:29:44.977564", + "attempted_features": [ + "api-key-management" + ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 007c5a3..c598c99 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -2152,3 +2152,22 @@ src/index.ts(27,25): error TS2769: No overload matches this call. Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy +- `07:31:45` **INFO** Committed feature api-key-management +- `07:31:45` **INFO** Pushed: rc=0 + +## Phase-3 Feature: audit-log-filters (2026-05-23 07:31:45) + +- `07:31:45` **INFO** Description: Audit-Log mit Filter (user, action, date-range) +- `07:31:45` **INFO** Generating apps/api/src/routes/audit-log.ts (ERWEITERT — behalte GET /. Füge Query-Params ?userId=, ?action=, ?from…) +- `07:31:58` **INFO** wrote 1346 chars in 12.9s (attempt 1) +- `07:31:58` **INFO** Generating apps/web/src/pages/AuditLog.tsx (ERWEITERT — behalte Tabelle. Füge Filter-Bar: User-Select, Action-Sear…) +- `07:32:55` **INFO** wrote 6761 chars in 56.5s (attempt 1) +- `07:32:55` **INFO** Running tsc --noEmit on api… +- `07:32:56` **WARN** tsc errors: +src/index.ts(27,25): error TS2769: No overload matches this call. + Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'. + Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 842a2f4..a890a9a 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -121,4 +121,20 @@ export const apiKeys = pgTable("api_keys", { createdAt: timestamp("created_at").notNull().defaultNow(), lastUsedAt: timestamp("last_used_at"), revokedAt: timestamp("revoked_at"), -}) \ No newline at end of file +}) +export const timeEntryComments = pgTable("time_entry_comments", { + id: uuid("id").primaryKey().defaultRandom(), + entryId: uuid("entry_id").notNull().references(() => timeEntries.id, { onDelete: "cascade" }), + userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), + body: text("body").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), +}) + +export const savedViews = pgTable("saved_views", { + id: uuid("id").primaryKey().defaultRandom(), + userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + entityType: text("entity_type").notNull(), + filters: text("filters").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), +}) diff --git a/apps/api/src/routes/audit-log.ts b/apps/api/src/routes/audit-log.ts index 4e1240d..c48edc2 100644 --- a/apps/api/src/routes/audit-log.ts +++ b/apps/api/src/routes/audit-log.ts @@ -1,7 +1,7 @@ import { FastifyInstance } from "fastify" import { db } from "../db" import { auditLog } from "../db/schema" -import { desc } from "drizzle-orm" +import { desc, and, eq, gte, lte } from "drizzle-orm" export default async function auditLogRoutes(fastify: FastifyInstance) { fastify.addHook("preHandler", async (request, reply) => { @@ -21,11 +21,25 @@ export default async function auditLogRoutes(fastify: FastifyInstance) { return reply.code(403).send({ message: "Forbidden: Admin role required" }) } + const { userId, action, from, to } = request.query as { + userId?: string, + action?: string, + from?: string, + to?: string + } + + const conds = [] + if (userId) conds.push(eq(auditLog.userId, userId)) + if (action) conds.push(eq(auditLog.action, action)) + if (from) conds.push(gte(auditLog.createdAt, new Date(from))) + if (to) conds.push(lte(auditLog.createdAt, new Date(to))) + const logs = await db .select() .from(auditLog) + .where(conds.length ? and(...conds) : undefined as any) .orderBy(desc(auditLog.createdAt)) - .limit(100) + .limit(1000) return logs }) diff --git a/apps/web/src/pages/AuditLog.tsx b/apps/web/src/pages/AuditLog.tsx index e671653..cc9f607 100644 --- a/apps/web/src/pages/AuditLog.tsx +++ b/apps/web/src/pages/AuditLog.tsx @@ -6,6 +6,13 @@ import type { AuditLogEntry } from "@emberclone/shared" export default function AuditLog() { const [me, setMe] = useState<{ role: string } | null>(null) const [isMeLoading, setIsMeLoading] = useState(true) + + const [filters, setFilters] = useState({ + userId: "", + action: "", + startDate: "", + endDate: "" + }) const { data: currentUser } = useQuery({ queryKey: ["me"], @@ -17,8 +24,8 @@ export default function AuditLog() { }) const { data: logs, isLoading, isError } = useQuery({ - queryKey: ["auditLogs"], - queryFn: () => api.listAuditLog(), + queryKey: ["auditLogs", filters], + queryFn: () => api.listAuditLog(filters), enabled: !!currentUser && currentUser.role === "admin" }) @@ -37,16 +44,81 @@ export default function AuditLog() { ) } + const handleFilterChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFilters(prev => ({ ...prev, [name]: value })) + } + + const resetFilters = () => { + setFilters({ userId: "", action: "", startDate: "", endDate: "" }) + } + if (isLoading) return
Loading audit logs...
if (isError) return
Error loading audit logs.
return (
-
-

Audit Log

-

System-wide activity and security events

+
+
+

Audit Log

+

System-wide activity and security events

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -85,7 +157,7 @@ export default function AuditLog() { ) : ( )}
- No audit logs found. + No audit logs found matching the current filters.