feat(ai): clinic-wide AI kill-switch (admin-controlled)

- new org_ai_policy table + GET/PUT /api/ai/policy (read for any member,
  write owner/admin only); migration 0018
- /api/chat hard-blocks (403) when AI is off for the caller
- Settings → AI "Availability" section: enable AI, or disable for
  employees only (owners/admins keep access); read-only for non-admins
- sidebar, command palette and route guard hide/redirect the AI chat
  when it's disabled for the current user

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-15 18:42:50 +03:00
parent 6fdb7bdb46
commit addddc8972
14 changed files with 3589 additions and 6 deletions
+1
View File
@@ -12,3 +12,4 @@ export * from "./notifications.js";
export * from "./settings.js";
export * from "./ai.js";
export * from "./ai-chat.js";
export * from "./org-ai-policy.js";
+22
View File
@@ -0,0 +1,22 @@
import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { organization } from "./auth.js";
// Clinic-wide AI availability, controlled by owners/admins. One row per clinic
// (organization). Absent row = AI enabled for everyone (the default).
// aiEnabled = false → AI hidden + blocked for the whole clinic.
// disabledForEmployees = true → AI hidden + blocked for non-admins; owners
// and admins keep access.
export const orgAiPolicy = pgTable("org_ai_policy", {
organizationId: text("organization_id")
.primaryKey()
.references(() => organization.id, { onDelete: "cascade" }),
aiEnabled: boolean("ai_enabled").notNull().default(true),
disabledForEmployees: boolean("disabled_for_employees")
.notNull()
.default(false),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
});