create-safest-tools 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +71 -0
  3. package/bin/create-safest-tools.mjs +12 -0
  4. package/package.json +34 -0
  5. package/src/cli.mjs +128 -0
  6. package/src/config.mjs +151 -0
  7. package/src/scaffold.mjs +30 -0
  8. package/template/LICENSE +201 -0
  9. package/template/README.md +35 -0
  10. package/template/console/README.md +28 -0
  11. package/template/console/analytics/AnalyticsWorkspace.tsx +55 -0
  12. package/template/console/analytics/controller.ts +133 -0
  13. package/template/console/analytics/events.ts +33 -0
  14. package/template/console/analytics/store.ts +33 -0
  15. package/template/console/analytics/types.ts +38 -0
  16. package/template/console/appeals/AppealWorkspace.tsx +73 -0
  17. package/template/console/appeals/controller.ts +125 -0
  18. package/template/console/appeals/events.ts +33 -0
  19. package/template/console/appeals/store.ts +35 -0
  20. package/template/console/appeals/types.ts +37 -0
  21. package/template/console/assistant/AssistantWorkspace.tsx +133 -0
  22. package/template/console/assistant/types.ts +34 -0
  23. package/template/console/auth/AccountJourney.tsx +69 -0
  24. package/template/console/auth/InvitationAcceptance.tsx +145 -0
  25. package/template/console/auth/OAuthButtons.tsx +78 -0
  26. package/template/console/auth/OperatorLogin.tsx +110 -0
  27. package/template/console/auth/PasswordRecovery.tsx +132 -0
  28. package/template/console/auth/account-route.ts +15 -0
  29. package/template/console/auth/browser-navigation.ts +7 -0
  30. package/template/console/auth/events.ts +15 -0
  31. package/template/console/command/CommandCentre.tsx +119 -0
  32. package/template/console/command/controller.ts +267 -0
  33. package/template/console/command/events.ts +19 -0
  34. package/template/console/command/store.ts +36 -0
  35. package/template/console/command/types.ts +61 -0
  36. package/template/console/components/AnalystIdentity.tsx +25 -0
  37. package/template/console/components/PageHeader.tsx +27 -0
  38. package/template/console/configuration/ConfigurationDialog.tsx +200 -0
  39. package/template/console/configuration/ConfigurationWorkspace.tsx +66 -0
  40. package/template/console/configuration/FormBuilder.tsx +143 -0
  41. package/template/console/configuration/api.ts +40 -0
  42. package/template/console/configuration/events.ts +14 -0
  43. package/template/console/configuration/types.ts +79 -0
  44. package/template/console/lib/format.ts +49 -0
  45. package/template/console/lib/http.ts +96 -0
  46. package/template/console/main.tsx +225 -0
  47. package/template/console/operations/OperationsWorkspace.tsx +126 -0
  48. package/template/console/operations/controller.ts +237 -0
  49. package/template/console/operations/events.ts +37 -0
  50. package/template/console/operations/store.ts +32 -0
  51. package/template/console/operations/types.ts +107 -0
  52. package/template/console/people/PeopleWorkspace.tsx +81 -0
  53. package/template/console/people/types.ts +25 -0
  54. package/template/console/profile/ProfileWorkspace.tsx +136 -0
  55. package/template/console/profile/events.ts +9 -0
  56. package/template/console/profile/types.ts +6 -0
  57. package/template/console/quality/QualityWorkspace.tsx +87 -0
  58. package/template/console/quality/controller.ts +154 -0
  59. package/template/console/quality/events.ts +34 -0
  60. package/template/console/quality/store.ts +36 -0
  61. package/template/console/quality/types.ts +70 -0
  62. package/template/console/queues/QueueEditor.tsx +173 -0
  63. package/template/console/queues/QueueWorkspace.tsx +109 -0
  64. package/template/console/queues/controller.ts +194 -0
  65. package/template/console/queues/events.ts +34 -0
  66. package/template/console/queues/store.ts +37 -0
  67. package/template/console/queues/types.ts +102 -0
  68. package/template/console/registry/RegistryDialog.tsx +198 -0
  69. package/template/console/registry/RegistryWorkspace.tsx +108 -0
  70. package/template/console/registry/events.ts +14 -0
  71. package/template/console/registry/types.ts +52 -0
  72. package/template/console/reports/ReportDrawer.tsx +159 -0
  73. package/template/console/reports/ReportWorkspace.tsx +90 -0
  74. package/template/console/reports/controller.ts +517 -0
  75. package/template/console/reports/events.ts +42 -0
  76. package/template/console/reports/store.ts +39 -0
  77. package/template/console/reports/types.ts +230 -0
  78. package/template/console/settings/BrandEditor.tsx +126 -0
  79. package/template/console/settings/ChannelDialog.tsx +241 -0
  80. package/template/console/settings/SettingsWorkspace.tsx +155 -0
  81. package/template/console/settings/events.ts +19 -0
  82. package/template/console/settings/handoff.ts +22 -0
  83. package/template/console/settings/types.ts +93 -0
  84. package/template/console/shell/WorkspaceShell.tsx +160 -0
  85. package/template/console/shell/controller.ts +182 -0
  86. package/template/console/shell/events.ts +25 -0
  87. package/template/console/shell/navigation.ts +61 -0
  88. package/template/console/shell/store.ts +56 -0
  89. package/template/console/shell/types.ts +86 -0
  90. package/template/console/workflows/CreateWorkflowDialog.tsx +87 -0
  91. package/template/console/workflows/WorkflowCanvas.tsx +42 -0
  92. package/template/console/workflows/WorkflowDialogs.tsx +6 -0
  93. package/template/console/workflows/WorkflowStudio.tsx +331 -0
  94. package/template/console/workflows/WorkflowWorkspace.tsx +82 -0
  95. package/template/console/workflows/events.ts +17 -0
  96. package/template/console/workflows/graph.ts +156 -0
  97. package/template/console/workflows/templates.ts +70 -0
  98. package/template/console/workflows/types.ts +201 -0
  99. package/template/gitignore.template +18 -0
  100. package/template/migrations/0001_reports_foundation.sql +444 -0
  101. package/template/migrations/0002_human_report_loop.sql +65 -0
  102. package/template/migrations/0003_delivery_reliability.sql +18 -0
  103. package/template/migrations/0004_public_intake.sql +14 -0
  104. package/template/migrations/0005_operations_visibility.sql +24 -0
  105. package/template/migrations/0006_ai_governance.sql +198 -0
  106. package/template/migrations/0007_ai_release_gates.sql +6 -0
  107. package/template/migrations/0008_retention_analytics_exports.sql +52 -0
  108. package/template/migrations/0009_retention_derived_copies.sql +14 -0
  109. package/template/migrations/0010_ai_quality_controls.sql +26 -0
  110. package/template/migrations/0011_analyst_presence.sql +28 -0
  111. package/template/migrations/0012_queue_policies.sql +87 -0
  112. package/template/migrations/0013_routing_agents.sql +63 -0
  113. package/template/migrations/0014_webhook_enrichments.sql +122 -0
  114. package/template/migrations/0015_queue_owned_ai.sql +55 -0
  115. package/template/migrations/0016_operator_accounts.sql +67 -0
  116. package/template/migrations/0017_operator_profiles_and_recovery.sql +33 -0
  117. package/template/migrations/0018_platform_configuration.sql +382 -0
  118. package/template/migrations/0019_workflow_authoring_runtime.sql +372 -0
  119. package/template/migrations/0020_tasks_findings_assistant_budgets.sql +427 -0
  120. package/template/migrations/0021_abuse_evidence_operations.sql +324 -0
  121. package/template/migrations/0022_workflow_dispatch_operations.sql +42 -0
  122. package/template/migrations/0023_component_connection_execution.sql +75 -0
  123. package/template/migrations/0024_access_runtime_integrity.sql +72 -0
  124. package/template/migrations/0025_installation_timezone.sql +11 -0
  125. package/template/migrations/0026_ai_and_egress_execution_controls.sql +49 -0
  126. package/template/migrations/0027_prompt_and_ai_registry.sql +37 -0
  127. package/template/migrations/0028_step_attempt_ai_provenance.sql +13 -0
  128. package/template/migrations/0029_evidence_fetch_transport.sql +5 -0
  129. package/template/migrations/0030_evidence_dlq_incidents.sql +45 -0
  130. package/template/migrations/0031_shadow_quality_integrity.sql +7 -0
  131. package/template/migrations/0032_action_delivery_outbox.sql +55 -0
  132. package/template/migrations/0033_configuration_and_assistant_drafts.sql +43 -0
  133. package/template/migrations/0034_installation_integrations.sql +31 -0
  134. package/template/migrations/0035_workspace_governance.sql +21 -0
  135. package/template/migrations/0036_published_routing_baseline.sql +15 -0
  136. package/template/migrations/0037_builtin_phishing_specialist.sql +71 -0
  137. package/template/migrations/0038_remove_deprecated_enrichment_runtime.sql +228 -0
  138. package/template/migrations/0039_secure_reporting_channels.sql +45 -0
  139. package/template/migrations/0040_notification_only_reporting.sql +24 -0
  140. package/template/migrations/0041_better_auth_credentials.sql +17 -0
  141. package/template/package.json +47 -0
  142. package/template/public/_headers +27 -0
  143. package/template/public/app-icon-192.png +0 -0
  144. package/template/public/app-icon-512.png +0 -0
  145. package/template/public/brand-icon.svg +7 -0
  146. package/template/public/brand-tokens.css +80 -0
  147. package/template/public/console/auth-shell.js +19340 -0
  148. package/template/public/customer-brand.js +58 -0
  149. package/template/public/embed/embed.css +139 -0
  150. package/template/public/embed/embed.js +408 -0
  151. package/template/public/embed/index.html +91 -0
  152. package/template/public/favicon.svg +7 -0
  153. package/template/public/fonts/Manrope-Variable.ttf +0 -0
  154. package/template/public/fonts/Newsreader-Italic-Variable.ttf +0 -0
  155. package/template/public/fonts/Newsreader-Variable.ttf +0 -0
  156. package/template/public/index.html +123 -0
  157. package/template/public/logo-primary.svg +7 -0
  158. package/template/public/logo-reversed.svg +7 -0
  159. package/template/public/manifest.webmanifest +21 -0
  160. package/template/public/public-report.js +184 -0
  161. package/template/public/report/index.html +39 -0
  162. package/template/public/report/public-report.css +20 -0
  163. package/template/public/social-card.png +0 -0
  164. package/template/public/styles.css +1521 -0
  165. package/template/public/widget.css +80 -0
  166. package/template/public/widget.js +220 -0
  167. package/template/reports.config.example.json +38 -0
  168. package/template/reports.schema.json +89 -0
  169. package/template/scripts/reports-auth-onboarding.mjs +180 -0
  170. package/template/scripts/reports-backup.mjs +276 -0
  171. package/template/scripts/reports-cloudflare-preflight.mjs +152 -0
  172. package/template/scripts/reports-deploy.mjs +173 -0
  173. package/template/scripts/reports-plan.mjs +226 -0
  174. package/template/scripts/reports-restore.mjs +180 -0
  175. package/template/scripts/reports-secrets.mjs +73 -0
  176. package/template/scripts/reports-uninstall-plan.mjs +32 -0
  177. package/template/src/agent-executor.ts +330 -0
  178. package/template/src/ai-observability.ts +163 -0
  179. package/template/src/ai-registry-validation.ts +244 -0
  180. package/template/src/ai-registry.ts +292 -0
  181. package/template/src/api-cursor.ts +84 -0
  182. package/template/src/assistant.ts +554 -0
  183. package/template/src/audit.ts +39 -0
  184. package/template/src/backup-service.ts +269 -0
  185. package/template/src/budget-control.ts +127 -0
  186. package/template/src/component-executor.ts +845 -0
  187. package/template/src/configuration-registry.ts +550 -0
  188. package/template/src/connection-egress.ts +430 -0
  189. package/template/src/connection-oauth.ts +368 -0
  190. package/template/src/evidence-service.ts +335 -0
  191. package/template/src/human-tasks.ts +316 -0
  192. package/template/src/index.ts +4114 -0
  193. package/template/src/installation-admin.ts +301 -0
  194. package/template/src/intake-abuse.ts +156 -0
  195. package/template/src/platform-registry-validation.ts +367 -0
  196. package/template/src/platform-registry.ts +753 -0
  197. package/template/src/report-admin.ts +342 -0
  198. package/template/src/report-ai-quality.ts +390 -0
  199. package/template/src/report-ai-validation.ts +80 -0
  200. package/template/src/report-ai.ts +857 -0
  201. package/template/src/report-auth.ts +292 -0
  202. package/template/src/report-better-auth.ts +583 -0
  203. package/template/src/report-context-schema.ts +132 -0
  204. package/template/src/report-context.ts +126 -0
  205. package/template/src/report-crypto.ts +142 -0
  206. package/template/src/report-delivery.ts +579 -0
  207. package/template/src/report-form-validation.ts +51 -0
  208. package/template/src/report-governance.ts +510 -0
  209. package/template/src/report-http.ts +90 -0
  210. package/template/src/report-operations.ts +685 -0
  211. package/template/src/report-operator-accounts.ts +810 -0
  212. package/template/src/report-presence.ts +398 -0
  213. package/template/src/report-queue-validation.ts +309 -0
  214. package/template/src/report-queues.ts +478 -0
  215. package/template/src/report-repository.ts +972 -0
  216. package/template/src/report-router-agent.ts +328 -0
  217. package/template/src/report-router-validation.ts +81 -0
  218. package/template/src/report-routing.ts +178 -0
  219. package/template/src/report-turnstile.ts +142 -0
  220. package/template/src/report-types.ts +224 -0
  221. package/template/src/report-validation.ts +279 -0
  222. package/template/src/report-workflow-validation.ts +119 -0
  223. package/template/src/report-workflow.ts +886 -0
  224. package/template/src/shadow-quality.ts +278 -0
  225. package/template/src/workflow-actions.ts +782 -0
  226. package/template/src/workflow-compiler.ts +230 -0
  227. package/template/src/workflow-dynamic-runtime.ts +613 -0
  228. package/template/src/workflow-effects.ts +316 -0
  229. package/template/src/workflow-expressions.ts +191 -0
  230. package/template/src/workflow-platform-types.ts +121 -0
  231. package/template/src/workflow-platform-validation.ts +540 -0
  232. package/template/src/workflow-repository.ts +916 -0
  233. package/template/src/workflow-runs.ts +686 -0
  234. package/template/src/workflow-simulator.ts +211 -0
  235. package/template/src/workspace-branding.ts +128 -0
  236. package/template/src/workspace-governance.ts +279 -0
  237. package/template/tsconfig.console.json +23 -0
  238. package/template/tsconfig.json +22 -0
  239. package/template/vite.console.config.ts +20 -0
  240. package/template/worker-configuration.d.ts +65 -0
@@ -0,0 +1,267 @@
1
+ import { requestJson } from "../lib/http";
2
+ import { getOperationsWorkspaceSnapshot, subscribeToOperationsWorkspace } from "../operations/store";
3
+ import { getQueueWorkspaceSnapshot, subscribeToQueueWorkspace } from "../queues/store";
4
+ import type { QueueRecord } from "../queues/types";
5
+ import { getReportWorkspaceSnapshot, subscribeToReportWorkspace } from "../reports/store";
6
+ import { settingsEvents } from "../settings/events";
7
+ import type { SetupStatus } from "../settings/types";
8
+ import { getShellSnapshot, subscribeToShell } from "../shell/store";
9
+ import { setCommandSnapshot } from "./store";
10
+ import type { CommandAttentionItem, CommandQueueLoad, CommandRunSummary, CommandSnapshot } from "./types";
11
+
12
+ interface CommandReport {
13
+ id: string;
14
+ reference: string;
15
+ reasonCode: string;
16
+ queueId?: string;
17
+ state: string;
18
+ priority: number;
19
+ receivedAt: string;
20
+ claim?: { reviewerId?: string } | null;
21
+ }
22
+
23
+ interface WorkflowRun {
24
+ id: string;
25
+ reportId?: string;
26
+ workflowName?: string;
27
+ workflowKey?: string;
28
+ version?: string | number;
29
+ state: string;
30
+ openTaskCount?: number;
31
+ startedAt?: string;
32
+ createdAt: string;
33
+ }
34
+
35
+ interface ReportsResponse { reports?: CommandReport[] }
36
+ interface QueuesResponse { queues?: QueueRecord[] }
37
+ interface WorkflowRunsResponse { runs?: WorkflowRun[] }
38
+
39
+ const closedReportStates = new Set(["resolved_by_human", "resolved_by_ai", "closed"]);
40
+ const activeRunStates = new Set(["created", "dispatch_pending", "queued", "running", "waiting", "paused"]);
41
+
42
+ let actorId = "";
43
+ let reports: CommandReport[] = [];
44
+ let queues: QueueRecord[] = [];
45
+ let runs: WorkflowRun[] = [];
46
+ let setup: SetupStatus | null = null;
47
+ let loading = true;
48
+ let reportSignature = "";
49
+ let queueSignature = "";
50
+ let refreshScheduled = false;
51
+ let started = false;
52
+
53
+ function reportDataSignature(): string {
54
+ return reports.map((report) => `${report.id}:${report.state}:${report.priority}:${report.claim?.reviewerId || ""}`).sort().join("|");
55
+ }
56
+
57
+ function reportStoreSignature(): string {
58
+ return getReportWorkspaceSnapshot().reports
59
+ .map((report) => `${report.id}:${report.state}:${report.priority}:${report.owner?.reviewerId || ""}`)
60
+ .sort()
61
+ .join("|");
62
+ }
63
+
64
+ function queueDataSignature(): string {
65
+ return queues.map((queue) => `${queue.id}:${queue.status}:${queue.activePolicy?.version || 0}`).sort().join("|");
66
+ }
67
+
68
+ function queueStoreSignature(): string {
69
+ return getQueueWorkspaceSnapshot().queues
70
+ .map((queue) => `${queue.id}:${queue.status}:${queue.activePolicy?.version || 0}`)
71
+ .sort()
72
+ .join("|");
73
+ }
74
+
75
+ function readable(value: string): string {
76
+ const copy = value.replaceAll("_", " ");
77
+ if (!copy) return "";
78
+ const sentence = `${copy.charAt(0).toUpperCase()}${copy.slice(1)}`;
79
+ const acronyms: Record<string, string> = { ai: "AI", api: "API", csv: "CSV", d1: "D1", http: "HTTP", https: "HTTPS", id: "ID", json: "JSON", r2: "R2", sla: "SLA", url: "URL" };
80
+ return sentence.replace(/\b(ai|api|csv|d1|http|https|id|json|r2|sla|url)\b/giu, (match) => acronyms[match.toLowerCase()] ?? match);
81
+ }
82
+
83
+ function relativeTime(value: string | undefined): string {
84
+ const milliseconds = Date.now() - Date.parse(value || "");
85
+ if (!Number.isFinite(milliseconds)) return "Unknown";
86
+ const minutes = Math.max(0, Math.floor(milliseconds / 60_000));
87
+ if (minutes < 1) return "Just now";
88
+ if (minutes < 60) return `${minutes}m ago`;
89
+ const hours = Math.floor(minutes / 60);
90
+ if (hours < 48) return `${hours}h ago`;
91
+ return `${Math.floor(hours / 24)}d ago`;
92
+ }
93
+
94
+ function reset(): void {
95
+ reports = [];
96
+ queues = [];
97
+ runs = [];
98
+ setup = null;
99
+ loading = true;
100
+ reportSignature = "";
101
+ queueSignature = "";
102
+ }
103
+
104
+ function buildSnapshot(): CommandSnapshot {
105
+ const session = getShellSnapshot().session;
106
+ const openReports = reports.filter((report) => !closedReportStates.has(report.state));
107
+ const needsHuman = openReports.filter((report) => !report.claim || ["human_review", "waiting_for_human", "awaiting_reporter"].includes(report.state));
108
+ const activeRuns = runs.filter((run) => activeRunStates.has(run.state));
109
+ const operations = getOperationsWorkspaceSnapshot().operations;
110
+ const incidentCount = Number(operations?.incidents.open || 0) + Number(operations?.incidents.acknowledged || 0);
111
+ const displayName = session?.displayName || "there";
112
+
113
+ const attention: CommandAttentionItem[] = needsHuman.slice(0, 4).map((report) => ({
114
+ id: `report-${report.id}`,
115
+ icon: report.priority >= 80 ? "!" : "⌁",
116
+ title: `${report.reasonCode ? readable(report.reasonCode) : "Report"} needs review`,
117
+ detail: `${report.reference} · ${queues.find((queue) => queue.id === report.queueId)?.name || "Unrouted"} · ${relativeTime(report.receivedAt)}`,
118
+ meta: report.claim ? "Claimed" : "Unclaimed",
119
+ urgent: report.priority >= 80,
120
+ action: { type: "report", reportId: report.id },
121
+ }));
122
+ if (incidentCount > 0) attention.push({
123
+ id: "operations-incidents",
124
+ icon: "◇",
125
+ title: "Operations work needs attention",
126
+ detail: "An unresolved operational incident needs acknowledgement or recovery.",
127
+ meta: `${incidentCount} open`,
128
+ urgent: true,
129
+ action: { type: "view", view: "operations" },
130
+ });
131
+
132
+ const queueLoads: CommandQueueLoad[] = queues.filter((queue) => queue.status === "active").slice(0, 5).map((queue) => {
133
+ const queuedReports = openReports.filter((report) => report.queueId === queue.id);
134
+ const targetMinutes = Number(queue.activePolicy?.responseSlaMinutes || 60);
135
+ const oldest = queuedReports.reduce((value, report) => Math.min(value, Date.parse(report.receivedAt)), Date.now());
136
+ const ageMinutes = queuedReports.length ? Math.max(0, (Date.now() - oldest) / 60_000) : 0;
137
+ return {
138
+ id: queue.id,
139
+ name: queue.name,
140
+ targetMinutes,
141
+ openCount: queuedReports.length,
142
+ pressure: Math.min(100, Math.round((ageMinutes / Math.max(1, targetMinutes)) * 100)),
143
+ };
144
+ });
145
+
146
+ const recentRuns: CommandRunSummary[] = runs.slice(0, 5).map((run) => {
147
+ const report = reports.find((candidate) => candidate.id === run.reportId);
148
+ const waitingForHuman = Number(run.openTaskCount || 0) > 0;
149
+ return {
150
+ id: run.id,
151
+ title: run.workflowName || run.workflowKey || `Run ${run.id.slice(0, 8)}`,
152
+ detail: `${report?.reference || run.reportId || "Report"} · v${run.version || "—"}`,
153
+ status: waitingForHuman ? "Waiting for human" : readable(run.state),
154
+ tone: run.state === "failed" || run.state === "repair_required" ? "danger" : waitingForHuman || run.state === "waiting" ? "warning" : "neutral",
155
+ age: relativeTime(run.startedAt || run.createdAt).replace(" ago", ""),
156
+ action: run.reportId ? { type: "report", reportId: run.reportId } : { type: "view", view: "operations" },
157
+ };
158
+ });
159
+
160
+ const optionalSetup = new Set(setup?.optional || []);
161
+ const incompleteSetupCount = setup?.features.filter((feature) => feature.state !== "ready" && !optionalSetup.has(feature.featureKey)).length || 0;
162
+ return {
163
+ loading,
164
+ generatedAt: new Date().toISOString(),
165
+ firstName: displayName.split(" ")[0] || "there",
166
+ metrics: {
167
+ openReports: openReports.length,
168
+ needsHuman: needsHuman.length,
169
+ activeRuns: activeRuns.length,
170
+ unresolvedIncidents: incidentCount,
171
+ },
172
+ attention,
173
+ queueLoads,
174
+ recentRuns,
175
+ incompleteSetupCount,
176
+ permissions: {
177
+ useAssistant: session?.permissions.useAssistant === true,
178
+ manageWorkflows: session?.permissions.manageWorkflows === true,
179
+ viewAudit: session?.permissions.viewAudit === true,
180
+ manageInstallation: session?.permissions.manageInstallation === true,
181
+ },
182
+ };
183
+ }
184
+
185
+ function publish(): void {
186
+ setCommandSnapshot(buildSnapshot());
187
+ }
188
+
189
+ async function loadCommandData(): Promise<void> {
190
+ const session = getShellSnapshot().session;
191
+ if (!session) {
192
+ reset();
193
+ publish();
194
+ return;
195
+ }
196
+ loading = true;
197
+ publish();
198
+ const tasks: Promise<void>[] = [
199
+ requestJson<ReportsResponse>("/v1/admin/reports?limit=100")
200
+ .then((response) => { reports = response.reports || []; })
201
+ .catch(() => { reports = []; }),
202
+ requestJson<QueuesResponse>(`/v1/admin/queues${session.permissions.manageConfiguration ? "?include_archived=true" : ""}`)
203
+ .then((response) => { queues = response.queues || []; })
204
+ .catch(() => { queues = []; }),
205
+ ];
206
+ if (session.permissions.readRuns) {
207
+ tasks.push(requestJson<WorkflowRunsResponse>("/v1/admin/workflow-runs?limit=50")
208
+ .then((response) => { runs = response.runs || []; })
209
+ .catch(() => { runs = []; }));
210
+ } else {
211
+ runs = [];
212
+ }
213
+ await Promise.all(tasks);
214
+ reportSignature = reportStoreSignature();
215
+ queueSignature = queueStoreSignature();
216
+ loading = false;
217
+ publish();
218
+ }
219
+
220
+ function scheduleRefresh(): void {
221
+ if (loading || refreshScheduled || !getShellSnapshot().session) return;
222
+ refreshScheduled = true;
223
+ queueMicrotask(() => {
224
+ refreshScheduled = false;
225
+ if (reportDataSignature() === reportStoreSignature() && queueDataSignature() === queueStoreSignature()) return;
226
+ void loadCommandData();
227
+ });
228
+ }
229
+
230
+ export function startCommandController(): void {
231
+ if (started) return;
232
+ started = true;
233
+ subscribeToShell(() => {
234
+ const session = getShellSnapshot().session;
235
+ const nextActorId = session?.actor.id || "";
236
+ if (nextActorId === actorId) {
237
+ publish();
238
+ return;
239
+ }
240
+ actorId = nextActorId;
241
+ reset();
242
+ publish();
243
+ if (session) void loadCommandData();
244
+ });
245
+ subscribeToOperationsWorkspace(publish);
246
+ subscribeToReportWorkspace(() => {
247
+ const snapshot = getReportWorkspaceSnapshot();
248
+ const signature = reportStoreSignature();
249
+ if (!snapshot.loading && signature !== reportSignature) {
250
+ reportSignature = signature;
251
+ scheduleRefresh();
252
+ }
253
+ });
254
+ subscribeToQueueWorkspace(() => {
255
+ const snapshot = getQueueWorkspaceSnapshot();
256
+ const signature = queueStoreSignature();
257
+ if (!snapshot.loading && signature !== queueSignature) {
258
+ queueSignature = signature;
259
+ scheduleRefresh();
260
+ }
261
+ });
262
+ window.addEventListener(settingsEvents.setupUpdated, (event) => {
263
+ if (!(event instanceof CustomEvent) || !event.detail?.setup) return;
264
+ setup = event.detail.setup as SetupStatus;
265
+ publish();
266
+ });
267
+ }
@@ -0,0 +1,19 @@
1
+ import type { CommandSnapshot } from "./types";
2
+
3
+ export const commandEvents = {
4
+ state: "safest:command-centre:state",
5
+ } as const;
6
+
7
+ export function isCommandSnapshot(value: unknown): value is CommandSnapshot {
8
+ if (!value || typeof value !== "object") return false;
9
+ const candidate = value as Partial<CommandSnapshot>;
10
+ return typeof candidate.loading === "boolean"
11
+ && typeof candidate.generatedAt === "string"
12
+ && typeof candidate.firstName === "string"
13
+ && typeof candidate.metrics === "object"
14
+ && Array.isArray(candidate.attention)
15
+ && Array.isArray(candidate.queueLoads)
16
+ && Array.isArray(candidate.recentRuns)
17
+ && typeof candidate.incompleteSetupCount === "number"
18
+ && typeof candidate.permissions === "object";
19
+ }
@@ -0,0 +1,36 @@
1
+ import { commandEvents, isCommandSnapshot } from "./events";
2
+ import type { CommandSnapshot } from "./types";
3
+
4
+ const subscribers = new Set<() => void>();
5
+
6
+ let snapshot: CommandSnapshot = {
7
+ loading: true,
8
+ generatedAt: new Date().toISOString(),
9
+ firstName: "there",
10
+ metrics: { openReports: 0, needsHuman: 0, activeRuns: 0, unresolvedIncidents: 0 },
11
+ attention: [],
12
+ queueLoads: [],
13
+ recentRuns: [],
14
+ incompleteSetupCount: 0,
15
+ permissions: { useAssistant: false, manageWorkflows: false, viewAudit: false, manageInstallation: false },
16
+ };
17
+
18
+ window.addEventListener(commandEvents.state, (event) => {
19
+ if (!(event instanceof CustomEvent) || !isCommandSnapshot(event.detail)) return;
20
+ snapshot = event.detail;
21
+ for (const subscriber of subscribers) subscriber();
22
+ });
23
+
24
+ export function getCommandSnapshot(): CommandSnapshot {
25
+ return snapshot;
26
+ }
27
+
28
+ export function setCommandSnapshot(next: CommandSnapshot | ((current: CommandSnapshot) => CommandSnapshot)): void {
29
+ snapshot = typeof next === "function" ? next(snapshot) : next;
30
+ for (const subscriber of subscribers) subscriber();
31
+ }
32
+
33
+ export function subscribeToCommand(callback: () => void): () => void {
34
+ subscribers.add(callback);
35
+ return () => subscribers.delete(callback);
36
+ }
@@ -0,0 +1,61 @@
1
+ import type { WorkspaceView } from "../shell/types";
2
+
3
+ export type CommandStatusTone = "neutral" | "warning" | "danger";
4
+
5
+ export type CommandAction =
6
+ | { type: "report"; reportId: string }
7
+ | { type: "view"; view: WorkspaceView };
8
+
9
+ export interface CommandMetricSnapshot {
10
+ openReports: number;
11
+ needsHuman: number;
12
+ activeRuns: number;
13
+ unresolvedIncidents: number;
14
+ }
15
+
16
+ export interface CommandAttentionItem {
17
+ id: string;
18
+ icon: string;
19
+ title: string;
20
+ detail: string;
21
+ meta: string;
22
+ urgent: boolean;
23
+ action: CommandAction;
24
+ }
25
+
26
+ export interface CommandQueueLoad {
27
+ id: string;
28
+ name: string;
29
+ targetMinutes: number;
30
+ openCount: number;
31
+ pressure: number;
32
+ }
33
+
34
+ export interface CommandRunSummary {
35
+ id: string;
36
+ title: string;
37
+ detail: string;
38
+ status: string;
39
+ tone: CommandStatusTone;
40
+ age: string;
41
+ action: CommandAction;
42
+ }
43
+
44
+ export interface CommandPermissions {
45
+ useAssistant: boolean;
46
+ manageWorkflows: boolean;
47
+ viewAudit: boolean;
48
+ manageInstallation: boolean;
49
+ }
50
+
51
+ export interface CommandSnapshot {
52
+ loading: boolean;
53
+ generatedAt: string;
54
+ firstName: string;
55
+ metrics: CommandMetricSnapshot;
56
+ attention: CommandAttentionItem[];
57
+ queueLoads: CommandQueueLoad[];
58
+ recentRuns: CommandRunSummary[];
59
+ incompleteSetupCount: number;
60
+ permissions: CommandPermissions;
61
+ }
@@ -0,0 +1,25 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ export interface AnalystIdentityData {
4
+ displayName: string;
5
+ avatarUrl?: string | null;
6
+ avatarColor?: string | null;
7
+ }
8
+
9
+ export function AnalystAvatar({ analyst, size = "normal" }: { analyst: AnalystIdentityData; size?: "normal" | "small" | "large" }) {
10
+ const style = { "--avatar-color": analyst.avatarColor || "#315d4d" } as CSSProperties;
11
+ return (
12
+ <span className={`analyst-avatar${size === "normal" ? "" : ` ${size}`}`} style={style}>
13
+ {analyst.avatarUrl ? <img src={analyst.avatarUrl} alt="" referrerPolicy="no-referrer" /> : analyst.displayName.trim().charAt(0).toUpperCase() || "?"}
14
+ </span>
15
+ );
16
+ }
17
+
18
+ export function AnalystIdentity({ analyst, detail }: { analyst: AnalystIdentityData; detail?: string }) {
19
+ return (
20
+ <span className="analyst-identity">
21
+ <AnalystAvatar analyst={analyst} />
22
+ <span><strong>{analyst.displayName || "Unknown analyst"}</strong>{detail ? <small>{detail}</small> : null}</span>
23
+ </span>
24
+ );
25
+ }
@@ -0,0 +1,27 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export function PageHeader({
4
+ title,
5
+ description,
6
+ titleId,
7
+ accessory,
8
+ className = "",
9
+ children,
10
+ }: {
11
+ title: ReactNode;
12
+ description?: ReactNode;
13
+ titleId?: string;
14
+ accessory?: ReactNode;
15
+ className?: string;
16
+ children?: ReactNode;
17
+ }) {
18
+ return (
19
+ <header className={`page-head console-page-head${className ? ` ${className}` : ""}`}>
20
+ <div className="page-title-group">
21
+ <div className="page-title-row"><h1 id={titleId}>{title}</h1>{accessory}</div>
22
+ {description ? <p>{description}</p> : null}
23
+ </div>
24
+ {children}
25
+ </header>
26
+ );
27
+ }
@@ -0,0 +1,200 @@
1
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore, type FormEvent } from "react";
2
+ import { ApiRequestError, errorMessage, mutationHeaders, requestJson } from "../lib/http";
3
+ import { getShellSnapshot, subscribeToShell } from "../shell/store";
4
+ import { configurationActionPath, configurationDefault, configurationEndpoint, configurationKeySlug, configurationLabel } from "./api";
5
+ import { announceConfigurationChanged, configurationEvents } from "./events";
6
+ import { editableReportForm, FormBuilder, reportFormContent, type EditableReportForm } from "./FormBuilder";
7
+ import type {
8
+ ConfigurationDetailResponse,
9
+ ConfigurationDraft,
10
+ ConfigurationPublishResponse,
11
+ ConfigurationSaveResponse,
12
+ ConfigurationType,
13
+ ConfigurationValidation,
14
+ ConfigurationValidationResponse,
15
+ ConfigurationVersion,
16
+ JsonObject,
17
+ QueueListResponse,
18
+ } from "./types";
19
+
20
+ interface EditorRequest { type: ConfigurationType; key: string; exists: boolean }
21
+ interface CheckedDraft { validation: ConfigurationValidation; saved: ConfigurationSaveResponse }
22
+
23
+ function jsonObject(value: string): JsonObject {
24
+ const parsed: unknown = JSON.parse(value);
25
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Configuration JSON must be an object.");
26
+ return parsed as JsonObject;
27
+ }
28
+
29
+ function invalidResponse(cause: unknown): ConfigurationValidation | null {
30
+ if (!(cause instanceof ApiRequestError) || cause.status !== 422 || !cause.body || typeof cause.body !== "object") return null;
31
+ const response = cause.body as ConfigurationValidationResponse;
32
+ return response.validation || null;
33
+ }
34
+
35
+ function ValidationSummary({ validation, message }: { validation: ConfigurationValidation | null; message: string }) {
36
+ const className = `validation-summary${validation ? validation.valid ? " valid" : " invalid" : ""}`;
37
+ if (!validation) return <div className={className}>{message || "Not checked in this session."}</div>;
38
+ const issues = [...(validation.errors || []), ...(validation.warnings || [])];
39
+ return <div className={className}><strong>{message || (validation.valid ? "Ready to publish" : "Changes needed")}</strong>{issues.length ? <ul>{issues.map((issue, index) => <li key={`${issue.code}-${issue.path || index}`}>{issue.path ? `${issue.path}: ` : ""}{issue.message}</li>)}</ul> : <p>{validation.valid ? `No blocking problems. ${validation.warnings?.length || 0} warnings.` : "Review the configuration and check it again."}</p>}</div>;
40
+ }
41
+
42
+ export function ConfigurationDialog() {
43
+ const shell = useSyncExternalStore(subscribeToShell, getShellSnapshot, getShellSnapshot);
44
+ const dialogRef = useRef<HTMLDialogElement>(null);
45
+ const keyRef = useRef<HTMLInputElement>(null);
46
+ const [editor, setEditor] = useState<EditorRequest | null>(null);
47
+ const [key, setKey] = useState("");
48
+ const [keyEdited, setKeyEdited] = useState(false);
49
+ const [keyLocked, setKeyLocked] = useState(false);
50
+ const [draft, setDraft] = useState<ConfigurationDraft | null>(null);
51
+ const [published, setPublished] = useState<ConfigurationVersion | null>(null);
52
+ const [form, setForm] = useState<EditableReportForm>(() => editableReportForm(configurationDefault("form")));
53
+ const [contentJson, setContentJson] = useState("{}");
54
+ const [baseline, setBaseline] = useState("");
55
+ const [validation, setValidation] = useState<ConfigurationValidation | null>(null);
56
+ const [validationMessage, setValidationMessage] = useState("");
57
+ const [loading, setLoading] = useState(false);
58
+ const [action, setAction] = useState<"" | "save" | "validate" | "publish">("");
59
+ const [error, setError] = useState("");
60
+ const busy = loading || Boolean(action);
61
+ const type = editor?.type || "form";
62
+ const visualForm = type === "form";
63
+ const currentContent = useMemo(() => {
64
+ try { return visualForm ? reportFormContent(form) as unknown as JsonObject : jsonObject(contentJson); }
65
+ catch { return null; }
66
+ }, [contentJson, form, visualForm]);
67
+ const serialized = currentContent ? JSON.stringify(currentContent) : "";
68
+ const dirty = Boolean(editor && !loading && (serialized !== baseline || (!keyLocked && key !== editor.key)));
69
+
70
+ const clear = (): void => {
71
+ setEditor(null); setKey(""); setKeyEdited(false); setKeyLocked(false); setDraft(null); setPublished(null);
72
+ setContentJson("{}"); setBaseline(""); setValidation(null); setValidationMessage(""); setLoading(false); setAction(""); setError("");
73
+ };
74
+ const close = (): void => {
75
+ if (busy) return;
76
+ if (dirty && !window.confirm("Discard the unsaved configuration changes?")) return;
77
+ clear();
78
+ };
79
+
80
+ useEffect(() => {
81
+ const open = (event: Event): void => {
82
+ if (!(event instanceof CustomEvent) || !["form", "routing_rules", "policy", "template"].includes(String(event.detail?.type))) return;
83
+ if (shell.session?.permissions.manageConfiguration !== true) return;
84
+ const nextType = event.detail.type as ConfigurationType;
85
+ setEditor({ type: nextType, key: typeof event.detail.key === "string" ? event.detail.key : "", exists: event.detail.exists === true });
86
+ };
87
+ window.addEventListener(configurationEvents.openEditor, open);
88
+ return () => window.removeEventListener(configurationEvents.openEditor, open);
89
+ }, [shell.session?.permissions.manageConfiguration]);
90
+ useEffect(() => {
91
+ const dialog = dialogRef.current;
92
+ if (!dialog) return;
93
+ if (editor && !dialog.open) { dialog.showModal(); requestAnimationFrame(() => keyRef.current?.focus()); }
94
+ if (!editor && dialog.open) dialog.close();
95
+ }, [editor]);
96
+ useEffect(() => {
97
+ if (!editor) return;
98
+ let active = true;
99
+ const load = async (): Promise<void> => {
100
+ setLoading(true); setError(""); setDraft(null); setPublished(null); setValidation(null); setValidationMessage(""); setBaseline("");
101
+ try {
102
+ const queueResponse = editor.type === "routing_rules" ? await requestJson<QueueListResponse>("/v1/admin/queues").catch(() => ({ queues: [] })) : { queues: [] };
103
+ const fallback = configurationDefault(editor.type, queueResponse.queues || []);
104
+ let content = fallback;
105
+ let nextDraft: ConfigurationDraft | null = null;
106
+ let nextPublished: ConfigurationVersion | null = null;
107
+ if (editor.exists) {
108
+ const result = await requestJson<ConfigurationDetailResponse>(configurationActionPath(editor.type, editor.key || "default", "draft"));
109
+ nextDraft = result.draft;
110
+ nextPublished = result.versions?.[0] || null;
111
+ content = nextDraft?.content || nextPublished?.content || fallback;
112
+ }
113
+ if (!active) return;
114
+ const resolvedKey = editor.type === "routing_rules" ? "default" : editor.key;
115
+ setKey(resolvedKey); setKeyEdited(Boolean(resolvedKey)); setKeyLocked(editor.type === "routing_rules" || editor.exists);
116
+ setDraft(nextDraft); setPublished(nextPublished); setContentJson(JSON.stringify(content, null, 2));
117
+ if (editor.type === "form") setForm(editableReportForm(content));
118
+ setBaseline(JSON.stringify(content));
119
+ if (nextDraft) {
120
+ setValidation(nextDraft.validation);
121
+ setValidationMessage(nextDraft.validation.valid ? "Stored check passed." : `${nextDraft.validation.errors?.length || 0} stored problems.`);
122
+ } else if (nextPublished) setValidationMessage(`Based on published version ${nextPublished.version}. Save to create an editable draft.`);
123
+ else setValidationMessage("Not saved yet.");
124
+ } catch (cause) { if (active) setError(errorMessage(cause, "The configuration draft could not be opened.")); }
125
+ finally { if (active) setLoading(false); }
126
+ };
127
+ void load();
128
+ return () => { active = false; };
129
+ }, [editor]);
130
+
131
+ const save = async (): Promise<ConfigurationSaveResponse | null> => {
132
+ if (!editor || !currentContent) { setError("Configuration JSON must be a valid object."); return null; }
133
+ const resolvedKey = type === "routing_rules" ? "default" : key.trim();
134
+ if (!resolvedKey) { setError("An internal name is required."); keyRef.current?.focus(); return null; }
135
+ if (draft && serialized === baseline) return { draftId: draft.id, revision: draft.revision, validation: draft.validation };
136
+ setAction("save"); setError("");
137
+ try {
138
+ const result = draft ? await requestJson<ConfigurationSaveResponse>(configurationActionPath(type, resolvedKey, "draft"), {
139
+ method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ expected_revision: draft.revision, content: currentContent }),
140
+ }) : await requestJson<ConfigurationSaveResponse>(`/v1/admin/${configurationEndpoint(type)}`, {
141
+ method: "POST", headers: mutationHeaders(), body: JSON.stringify({ key: resolvedKey, content: currentContent }),
142
+ });
143
+ setKey(resolvedKey); setKeyLocked(true); setBaseline(serialized);
144
+ setDraft((current) => ({
145
+ id: result.draftId || current?.id || "draft", configurationKey: resolvedKey, revision: result.revision,
146
+ validationStatus: result.validation.valid ? "valid" : "invalid", lastPublishedVersionId: current?.lastPublishedVersionId || null,
147
+ updatedAt: new Date().toISOString(), content: currentContent, validation: result.validation,
148
+ }));
149
+ setValidation(result.validation); setValidationMessage(`Draft revision ${result.revision} saved.`); announceConfigurationChanged();
150
+ return result;
151
+ } catch (cause) { setError(errorMessage(cause, "The configuration draft could not be saved.")); return null; }
152
+ finally { setAction(""); }
153
+ };
154
+
155
+ const validate = async (): Promise<CheckedDraft | null> => {
156
+ const saved = await save();
157
+ if (!saved || !editor) return null;
158
+ setAction("validate"); setError("");
159
+ try {
160
+ const result = await requestJson<ConfigurationValidationResponse>(configurationActionPath(type, type === "routing_rules" ? "default" : key, "validate"), { method: "POST" });
161
+ setValidation(result.validation); setValidationMessage(result.validation.valid ? "Ready to publish." : "Changes needed.");
162
+ setDraft((current) => current ? { ...current, validation: result.validation, validationStatus: result.validation.valid ? "valid" : "invalid" } : current);
163
+ return { validation: result.validation, saved };
164
+ } catch (cause) {
165
+ const invalid = invalidResponse(cause);
166
+ if (invalid) { setValidation(invalid); setValidationMessage("Changes needed."); return { validation: invalid, saved }; }
167
+ setError(errorMessage(cause, "The configuration check failed.")); return null;
168
+ } finally { setAction(""); }
169
+ };
170
+
171
+ const publish = async (): Promise<void> => {
172
+ const checked = await validate();
173
+ if (!checked?.validation.valid || !editor) return;
174
+ if (!window.confirm("Publish this immutable configuration version for future reports and workflow runs?")) return;
175
+ setAction("publish"); setError("");
176
+ try {
177
+ const result = await requestJson<ConfigurationPublishResponse>(configurationActionPath(type, type === "routing_rules" ? "default" : key, "publish"), { method: "POST", headers: mutationHeaders(), body: JSON.stringify({ expected_revision: checked.saved.revision }) });
178
+ setPublished({ id: result.versionId, version: result.version, content: currentContent || draft?.content || {}, publishedAt: new Date().toISOString() });
179
+ setValidationMessage(`Published version ${result.version}. Future reports and workflow runs can use it; historical work does not change.`);
180
+ announceConfigurationChanged();
181
+ } catch (cause) { setError(errorMessage(cause, "The configuration could not be published.")); }
182
+ finally { setAction(""); }
183
+ };
184
+
185
+ const updateForm = (next: EditableReportForm): void => {
186
+ if (!keyLocked && !keyEdited && next.title !== form.title) setKey(configurationKeySlug(next.title, ""));
187
+ setForm(next); setValidation(null); setValidationMessage("Unsaved changes.");
188
+ };
189
+ const title = `${editor?.exists ? "Edit" : "New"} ${configurationLabel(type).toLowerCase()}`;
190
+ const subtitle = loading ? "Loading configuration…" : `${key || "Unsaved draft"}${draft ? ` · Draft revision ${draft.revision}` : published ? ` · Based on published v${published.version}` : ""}`;
191
+ return <dialog ref={dialogRef} className="editor-dialog configuration-dialog" aria-labelledby="configuration-dialog-title" onCancel={(event) => { event.preventDefault(); close(); }} data-react-slice="configuration-dialog">
192
+ <div className="detail-head"><div><h2 id="configuration-dialog-title">{title}</h2><p>{subtitle}</p></div><button className="icon-button" type="button" aria-label="Close" disabled={busy} onClick={close}>×</button></div>
193
+ {loading ? <div className="configuration-dialog-loading"><div className="skeleton-block" role="status" aria-label="Loading configuration editor" /></div> : <form className="editor-form configuration-form" onSubmit={(event: FormEvent<HTMLFormElement>) => { event.preventDefault(); void save(); }}>
194
+ <label>Internal name <small>Used to identify this configuration in Safest.</small><input ref={keyRef} pattern="[a-z0-9]+(?:-[a-z0-9]+)*" required readOnly={keyLocked} disabled={busy} value={key} onChange={(event) => { setKeyEdited(true); setKey(event.currentTarget.value); }} /></label>
195
+ {visualForm ? <FormBuilder value={form} onChange={updateForm} disabled={busy} /> : <label>Configuration JSON<textarea className="code-input configuration-code" spellCheck={false} required disabled={busy} value={contentJson} onChange={(event) => { setContentJson(event.currentTarget.value); setValidation(null); setValidationMessage("Unsaved changes."); }} /></label>}
196
+ <ValidationSummary validation={validation} message={validationMessage} />
197
+ <div className="editor-footer"><p className="error" role="alert">{error}</p><div><button className="secondary" type="button" disabled={busy} onClick={close}>Cancel</button><button className="secondary" type="submit" disabled={busy || !dirty}>{action === "save" ? "Saving…" : "Save draft"}</button><button className="secondary" type="button" disabled={busy} onClick={() => void validate()}>{action === "validate" ? "Checking…" : "Check"}</button><button className="primary" type="button" disabled={busy} onClick={() => void publish()}>{action === "publish" ? "Publishing…" : "Publish"}</button></div></div>
198
+ </form>}
199
+ </dialog>;
200
+ }