feat: activity audit log written from all resource routes

Add the activity_log table, a best-effort recordActivity() service and a
GET /api/activity feed, and write entries on create/update/delete of
patients, notes, appointments, prescriptions and tasks. The Activity page
now shows the real audit trail (actor, action, patient context, time);
the fabricated signing hashes / approval badges are gone — that vision
stays deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-07 19:46:22 +03:00
parent 25254dd4c1
commit 75940313a4
16 changed files with 2436 additions and 142 deletions
+29
View File
@@ -0,0 +1,29 @@
import { index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import type { ActivityEntityType } from "../../types/activity.js";
import { organization, user } from "./auth.js";
// One row per record change, scoped to a clinic (organization). Written
// best-effort from the resource routes (patients / notes / appointments /
// prescriptions / tasks). `actorName` is denormalized so the feed renders even
// after the user is removed (FK is set null).
export const activityLog = pgTable(
"activity_log",
{
id: uuid("id").primaryKey().defaultRandom(),
organizationId: text("organization_id")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
actorId: text("actor_id").references(() => user.id, {
onDelete: "set null",
}),
actorName: text("actor_name").notNull(),
action: text("action").notNull(),
entityType: text("entity_type").$type<ActivityEntityType>().notNull(),
entityId: text("entity_id"),
patientName: text("patient_name"),
patientFileNumber: text("patient_file_number"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(t) => [index("activity_org_created_idx").on(t.organizationId, t.createdAt)],
);
+1
View File
@@ -4,3 +4,4 @@ export * from "./notes.js";
export * from "./appointments.js";
export * from "./prescriptions.js";
export * from "./tasks.js";
export * from "./activity.js";