create-safest-tools 0.6.0 → 0.7.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 (172) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/src/config.mjs +2 -2
  4. package/src/update.mjs +10 -1
  5. package/template/README.md +2 -2
  6. package/template/console/README.md +2 -2
  7. package/template/console/analytics/AnalyticsWorkspace.tsx +49 -36
  8. package/template/console/analytics/controller.ts +10 -2
  9. package/template/console/analytics/types.ts +7 -0
  10. package/template/console/appeals/AppealFollowup.tsx +171 -0
  11. package/template/console/appeals/AppealWorkspace.tsx +33 -4
  12. package/template/console/appeals/RecordAppealDialog.tsx +42 -0
  13. package/template/console/appeals/appeal-corrections.css +29 -0
  14. package/template/console/appeals/controller.ts +40 -14
  15. package/template/console/appeals/events.ts +1 -0
  16. package/template/console/appeals/types.ts +6 -0
  17. package/template/console/appeals/useAppealDraftGuard.ts +12 -0
  18. package/template/console/assistant/AssistantWorkspace.tsx +28 -5
  19. package/template/console/command/CommandCentre.tsx +101 -64
  20. package/template/console/command/controller.ts +80 -70
  21. package/template/console/command/events.ts +1 -2
  22. package/template/console/command/presentation.ts +29 -0
  23. package/template/console/command/store.ts +4 -4
  24. package/template/console/command/types.ts +26 -21
  25. package/template/console/configuration/ConfigurationBuilders.tsx +101 -0
  26. package/template/console/configuration/ConfigurationDialog.tsx +28 -13
  27. package/template/console/configuration/ConfigurationWorkspace.tsx +21 -8
  28. package/template/console/configuration/types.ts +1 -1
  29. package/template/console/lib/priority.ts +13 -0
  30. package/template/console/operations/CaseEscalations.tsx +142 -0
  31. package/template/console/operations/OperationsWorkspace.tsx +2 -0
  32. package/template/console/operations/case-escalations.css +20 -0
  33. package/template/console/people/PeopleWorkspace.tsx +8 -4
  34. package/template/console/people/PersonAccessDialog.tsx +40 -0
  35. package/template/console/people/types.ts +1 -0
  36. package/template/console/profile/ProfileWorkspace.tsx +1 -1
  37. package/template/console/registry/IntegrationSetup.tsx +41 -0
  38. package/template/console/registry/IntegrationVerification.tsx +54 -0
  39. package/template/console/registry/RegistryDetailsDialog.tsx +2 -2
  40. package/template/console/registry/RegistryDialog.tsx +56 -27
  41. package/template/console/registry/RegistryWorkspace.tsx +10 -5
  42. package/template/console/registry/integration-verification.css +25 -0
  43. package/template/console/reports/CaseActions.tsx +81 -0
  44. package/template/console/reports/CaseContinuity.tsx +27 -0
  45. package/template/console/reports/CaseNotices.tsx +48 -0
  46. package/template/console/reports/CaseOwnership.tsx +78 -0
  47. package/template/console/reports/CaseReviewTasks.tsx +148 -0
  48. package/template/console/reports/EnrichmentFindings.tsx +148 -0
  49. package/template/console/reports/InvestigationBrief.tsx +29 -0
  50. package/template/console/reports/ReportDrawer.tsx +122 -67
  51. package/template/console/reports/ReportWorkspace.tsx +112 -23
  52. package/template/console/reports/controller.ts +125 -60
  53. package/template/console/reports/events.ts +9 -2
  54. package/template/console/reports/load-inbox.ts +17 -0
  55. package/template/console/reports/presence-presentation.ts +52 -0
  56. package/template/console/reports/private-drafts.ts +32 -0
  57. package/template/console/reports/store.ts +2 -2
  58. package/template/console/reports/types.ts +50 -2
  59. package/template/console/reports/use-editing-access.ts +13 -0
  60. package/template/console/settings/SettingsWorkspace.tsx +1 -1
  61. package/template/console/shell/WorkspaceShell.tsx +55 -29
  62. package/template/console/shell/WorkspaceUpdateNotice.tsx +48 -0
  63. package/template/console/shell/controller.ts +23 -16
  64. package/template/console/shell/navigation.ts +30 -39
  65. package/template/console/workflows/CreatePlaybookDialog.tsx +73 -0
  66. package/template/console/workflows/PlaybookExamples.tsx +160 -0
  67. package/template/console/workflows/PlaybookFields.tsx +109 -0
  68. package/template/console/workflows/PlaybookGuideEditor.tsx +125 -0
  69. package/template/console/workflows/PlaybookHistory.tsx +106 -0
  70. package/template/console/workflows/WorkflowDialogs.tsx +2 -1
  71. package/template/console/workflows/WorkflowStudio.tsx +108 -39
  72. package/template/console/workflows/WorkflowWorkspace.tsx +9 -7
  73. package/template/console/workflows/templates.ts +3 -3
  74. package/template/console/workflows/types.ts +11 -0
  75. package/template/migrations/0001_safest_resolve_baseline.sql +0 -1
  76. package/template/migrations/0002_playbook_publication_evidence.sql +36 -0
  77. package/template/migrations/0003_action_confirmation.sql +24 -0
  78. package/template/migrations/0004_notice_recovery.sql +22 -0
  79. package/template/migrations/0005_appeal_continuity.sql +17 -0
  80. package/template/migrations/0006_case_ownership.sql +23 -0
  81. package/template/migrations/0007_integration_verification.sql +21 -0
  82. package/template/migrations/0008_case_escalations.sql +135 -0
  83. package/template/migrations/0009_participant_continuation.sql +61 -0
  84. package/template/migrations/0010_appeal_corrections.sql +41 -0
  85. package/template/npm-shrinkwrap.json +8036 -0
  86. package/template/package.json +31 -22
  87. package/template/public/_headers +16 -1
  88. package/template/public/console/auth-shell.js +22151 -14847
  89. package/template/public/console/release.json +1 -0
  90. package/template/public/console/workspace-features.css +2 -0
  91. package/template/public/embed/embed.css +3 -0
  92. package/template/public/embed/embed.js +19 -1
  93. package/template/public/embed/index.html +2 -0
  94. package/template/public/index.html +9 -6
  95. package/template/public/participant.css +2 -0
  96. package/template/public/participant.html +13 -0
  97. package/template/public/participant.js +111 -0
  98. package/template/public/public-report.js +21 -1
  99. package/template/public/report/index.html +1 -1
  100. package/template/public/styles.css +599 -83
  101. package/template/public/workspace.css +124 -0
  102. package/template/scripts/reports-plan.mjs +1 -1
  103. package/template/shared/case-escalations.ts +31 -0
  104. package/template/shared/integration-progress.ts +27 -0
  105. package/template/shared/notice-inputs.ts +3 -0
  106. package/template/shared/playbook-resources.ts +14 -0
  107. package/template/shared/playbooks.ts +249 -0
  108. package/template/src/agent-executor.ts +25 -8
  109. package/template/src/ai-observability.ts +14 -13
  110. package/template/src/ai-registry-validation.ts +1 -1
  111. package/template/src/appeal-continuity.ts +75 -0
  112. package/template/src/appeal-correction-state.ts +46 -0
  113. package/template/src/appeal-corrections.ts +112 -0
  114. package/template/src/appeal-decisions.ts +85 -0
  115. package/template/src/assistant.ts +3 -4
  116. package/template/src/case-escalations.ts +223 -0
  117. package/template/src/case-investigator.ts +179 -0
  118. package/template/src/component-executor.ts +5 -4
  119. package/template/src/configuration-registry.ts +4 -1
  120. package/template/src/connection-oauth.ts +12 -3
  121. package/template/src/database-readiness.ts +14 -0
  122. package/template/src/human-tasks.ts +152 -55
  123. package/template/src/index.ts +191 -29
  124. package/template/src/installation-admin.ts +2 -1
  125. package/template/src/integration-progress.ts +45 -0
  126. package/template/src/integration-verification.ts +249 -0
  127. package/template/src/investigation-evidence.ts +95 -0
  128. package/template/src/investigator-fixture.ts +30 -0
  129. package/template/src/notice-delivery.ts +19 -0
  130. package/template/src/notice-recovery.ts +81 -0
  131. package/template/src/operator-scope.ts +31 -0
  132. package/template/src/participant-access.ts +163 -0
  133. package/template/src/participant-case.ts +87 -0
  134. package/template/src/participant-customer-access.ts +93 -0
  135. package/template/src/participant-recovery.ts +43 -0
  136. package/template/src/participant-routes.ts +67 -0
  137. package/template/src/platform-registry-validation.ts +25 -2
  138. package/template/src/platform-registry.ts +10 -7
  139. package/template/src/playbook-resources.ts +58 -0
  140. package/template/src/report-admin.ts +9 -2
  141. package/template/src/report-auth.ts +1 -28
  142. package/template/src/report-better-auth.ts +18 -6
  143. package/template/src/report-delivery.ts +149 -148
  144. package/template/src/report-email-inbound.ts +33 -22
  145. package/template/src/report-email-template.ts +5 -2
  146. package/template/src/report-email.ts +7 -30
  147. package/template/src/report-governance.ts +42 -45
  148. package/template/src/report-http.ts +37 -2
  149. package/template/src/report-operations.ts +30 -67
  150. package/template/src/report-operator-accounts.ts +7 -6
  151. package/template/src/report-ownership.ts +64 -0
  152. package/template/src/report-presence.ts +15 -9
  153. package/template/src/report-repository.ts +16 -7
  154. package/template/src/report-turnstile.ts +7 -3
  155. package/template/src/report-types.ts +1 -0
  156. package/template/src/report-workflow.ts +183 -249
  157. package/template/src/workflow-actions.ts +248 -156
  158. package/template/src/workflow-compiler.ts +63 -12
  159. package/template/src/workflow-dynamic-runtime.ts +20 -9
  160. package/template/src/workflow-effects.ts +117 -39
  161. package/template/src/workflow-event-transport.ts +16 -0
  162. package/template/src/workflow-expressions.ts +30 -0
  163. package/template/src/workflow-loader-code.ts +21 -0
  164. package/template/src/workflow-platform-types.ts +3 -0
  165. package/template/src/workflow-platform-validation.ts +23 -1
  166. package/template/src/workflow-repository.ts +131 -28
  167. package/template/src/workflow-runs.ts +61 -13
  168. package/template/src/workflow-simulator.ts +7 -4
  169. package/template/src/workspace-governance.ts +6 -4
  170. package/template/tsconfig.json +1 -0
  171. package/template/vite.console.config.ts +30 -2
  172. package/template/worker-configuration.d.ts +3 -2
@@ -1,71 +1,91 @@
1
1
  import { useSyncExternalStore } from "react";
2
2
  import { PageHeader } from "../components/PageHeader";
3
- import { getCommandSnapshot, subscribeToCommand } from "./store";
4
- import type { CommandAction, CommandMetricSnapshot, CommandQueueLoad, CommandRunSummary } from "./types";
5
- import { dispatchOpenReport } from "../reports/events";
3
+ import { CaseEscalations } from '../operations/CaseEscalations';
4
+ import { dispatchOpenReport, dispatchReportFilters } from "../reports/events";
6
5
  import { dispatchNavigation } from "../shell/events";
6
+ import { compactDuration } from "./presentation";
7
+ import { getCommandSnapshot, subscribeToCommand } from "./store";
8
+ import type {
9
+ CommandAction,
10
+ CommandMetricSnapshot,
11
+ CommandQueueLoad,
12
+ CommandSystemHealth,
13
+ CommandStatusTone,
14
+ } from "./types";
7
15
 
8
16
  function performAction(action: CommandAction): void {
9
17
  if (action.type === "report") dispatchOpenReport(action.reportId);
10
18
  else dispatchNavigation(action.view);
11
19
  }
12
20
 
13
- function formattedDate(value: string): string {
14
- const date = new Date(value);
15
- if (!Number.isFinite(date.getTime())) return "Today";
16
- return new Intl.DateTimeFormat(undefined, { weekday: "long", day: "numeric", month: "long" }).format(date);
17
- }
18
-
19
- function greeting(value: string, firstName: string): string {
20
- const hour = new Date(value).getHours();
21
- const timeOfDay = hour < 12 ? "morning" : hour < 18 ? "afternoon" : "evening";
22
- return `Good ${timeOfDay}, ${firstName || "there"}.`;
23
- }
24
-
25
- function EmptyProduct({ icon, title, description }: { icon: string; title: string; description: string }) {
26
- return <div className="empty-product"><span aria-hidden="true">{icon}</span><h2>{title}</h2><p>{description}</p></div>;
21
+ function EmptyReviewQueue() {
22
+ return (
23
+ <div className="command-empty">
24
+ <span aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" /><path d="m8 12 2.5 2.5L16 9" /></svg></span>
25
+ <div><strong>Review queue is clear</strong><small>New reports will appear here as they are routed.</small></div>
26
+ </div>
27
+ );
27
28
  }
28
29
 
29
30
  function Metrics({ metrics, loading }: { metrics: CommandMetricSnapshot; loading: boolean }) {
30
- const values = [
31
- { label: "Open reports", detail: "Accepted allegations", icon: "i-inbox", value: metrics.openReports },
32
- { label: "Needs a person", detail: "Unclaimed or waiting", icon: "i-user", value: metrics.needsHuman },
33
- { label: "Workflow runs", detail: "Running or waiting", icon: "i-workflow", value: metrics.activeRuns },
34
- { label: "Unresolved incidents", detail: "Waiting for repair", icon: "i-activity", value: metrics.unresolvedIncidents },
31
+ const openSummary = (label: string): void => {
32
+ dispatchReportFilters({ search: "", queueId: "", priority: label === "Urgent" ? "urgent" : "", age: "", state: "needs_review", sort: "priority", ownership: label === "Unassigned" ? "unassigned" : "" });
33
+ dispatchNavigation("reports");
34
+ };
35
+ const values: Array<{ label: string; value: number; detail: string; tone: CommandStatusTone }> = [
36
+ { label: "To review", value: metrics.awaitingReview, detail: "Ready for a decision", tone: metrics.awaitingReview ? "warning" : "positive" },
37
+ { label: "Urgent", value: metrics.urgent, detail: "Highest priority", tone: metrics.urgent ? "danger" : "positive" },
38
+ { label: "Unassigned", value: metrics.unassigned, detail: "Needs an owner", tone: metrics.unassigned ? "warning" : "positive" },
39
+ { label: "Reviewers online", value: metrics.onlineReviewers, detail: "Available now", tone: "neutral" },
35
40
  ];
36
41
  return (
37
- <section className="metrics command-metrics" aria-label="Workspace summary" aria-busy={loading}>
42
+ <section className="command-signals" aria-label="Live review summary" aria-busy={loading}>
38
43
  {values.map((item) => (
39
- <article key={item.label}><span>{item.label} <svg aria-hidden="true"><use href={`#${item.icon}`} /></svg></span><strong>{loading ? "—" : item.value}</strong><small>{item.detail}</small></article>
44
+ item.label === "Reviewers online" ? <article className={`command-signal ${item.tone}`} key={item.label}><span>{item.label}</span><strong>{loading ? "—" : item.value}</strong><small>{item.detail}</small></article> : <button className={`command-signal ${item.tone}`} type="button" key={item.label} onClick={() => openSummary(item.label)}>
45
+ <span>{item.label}</span><strong>{loading ? "—" : item.value}</strong><small>{item.detail}</small>
46
+ </button>
40
47
  ))}
41
48
  </section>
42
49
  );
43
50
  }
44
51
 
45
52
  function QueueLoadRow({ queue }: { queue: CommandQueueLoad }) {
46
- const tone = queue.pressure >= 80 ? " danger" : queue.pressure >= 55 ? " warning" : "";
47
- const width = Math.max(queue.openCount ? 8 : 0, queue.pressure);
53
+ const width = Math.max(queue.openCount ? 7 : 0, queue.pressure);
54
+ const target = queue.targetMinutes ? `${compactDuration(queue.targetMinutes)} response target` : "No response target";
55
+ const risk = queue.breachedCount
56
+ ? `${queue.breachedCount} overdue`
57
+ : queue.openCount ? `Oldest waiting ${compactDuration(queue.oldestMinutes)}` : "Queue is clear";
48
58
  return (
49
- <article className="queue-load-row">
50
- <div className="queue-load-label"><strong>{queue.name}</strong><small>{queue.targetMinutes}m first-response target</small></div>
51
- <span className="queue-load-track" role="progressbar" aria-label={`${queue.name} response pressure`} aria-valuemin={0} aria-valuemax={100} aria-valuenow={queue.pressure}>
52
- <i className={`queue-load-fill${tone}`} style={{ width: `${width}%` }} />
59
+ <article className={`queue-health-row ${queue.tone}`}>
60
+ <div className="queue-health-title"><strong>{queue.name}</strong><span>{queue.openCount} to review</span></div>
61
+ <div className="queue-health-meta"><span>{risk}</span><span>{queue.unassignedCount} unassigned</span></div>
62
+ <span className="queue-health-track" role="progressbar" aria-label={`${queue.name} response target usage`} aria-valuemin={0} aria-valuemax={100} aria-valuenow={queue.pressure}>
63
+ <i style={{ width: `${width}%` }} />
53
64
  </span>
54
- <strong className="queue-load-count">{queue.openCount} open</strong>
65
+ <small>{target}</small>
55
66
  </article>
56
67
  );
57
68
  }
58
69
 
59
- function RunRow({ run }: { run: CommandRunSummary }) {
60
- const tone = run.tone === "neutral" ? "" : ` ${run.tone}`;
70
+ function SystemHealth({ health, canOpen }: { health: CommandSystemHealth; canOpen: boolean }) {
71
+ const healthy = health.issueCount === 0;
61
72
  return (
62
- <article className="compact-run-row">
63
- <span className="run-glyph" aria-hidden="true">⌘</span>
64
- <div><strong>{run.title}</strong><small>{run.detail}</small></div>
65
- <span className={`run-status${tone}`}>{run.status}</span>
66
- <span className="run-age">{run.age}</span>
67
- <button className="text-button run-open" type="button" onClick={() => performAction(run.action)}>Open</button>
68
- </article>
73
+ <section className="surface command-panel system-health-panel">
74
+ <div className="surface-head">
75
+ <div><h2>System health</h2><p>Delivery and automation requiring intervention.</p></div>
76
+ {canOpen ? <button className="text-button" type="button" onClick={() => dispatchNavigation("operations")}>Open operations</button> : null}
77
+ </div>
78
+ <div className={`system-health-summary${healthy ? " healthy" : " danger"}`}>
79
+ <span aria-hidden="true"><svg viewBox="0 0 24 24">{healthy ? <><circle cx="12" cy="12" r="9" /><path d="m8 12 2.5 2.5L16 9" /></> : <><path d="M10.3 3.6 2.4 18a2 2 0 0 0 1.8 3h15.6a2 2 0 0 0 1.8-3L13.7 3.6a2 2 0 0 0-3.4 0z" /><path d="M12 9v4M12 17h.01" /></>}</svg></span>
80
+ <div><strong>{healthy ? "All systems healthy" : `${health.issueCount} system area${health.issueCount === 1 ? "" : "s"} need attention`}</strong><small>{healthy ? "No incidents, failed deliveries, or workflow repairs." : "Open Operations to acknowledge and recover affected work."}</small></div>
81
+ </div>
82
+ <dl className="system-health-facts">
83
+ <div><dt>Incidents</dt><dd>{health.incidentCount}</dd></div>
84
+ <div><dt>Delivery failures</dt><dd>{health.deliveryFailureCount}</dd></div>
85
+ <div><dt>Workflow repairs</dt><dd>{health.workflowIssueCount}</dd></div>
86
+ <div><dt>Active workflows</dt><dd>{health.activeRunCount}</dd></div>
87
+ </dl>
88
+ </section>
69
89
  );
70
90
  }
71
91
 
@@ -75,21 +95,20 @@ function LoadingRows() {
75
95
 
76
96
  export function CommandCentre() {
77
97
  const snapshot = useSyncExternalStore(subscribeToCommand, getCommandSnapshot, getCommandSnapshot);
78
- const welcome = greeting(snapshot.generatedAt, snapshot.firstName);
79
- const attentionSummary = snapshot.metrics.needsHuman
80
- ? `${snapshot.metrics.needsHuman} item${snapshot.metrics.needsHuman === 1 ? "" : "s"} need attention.`
81
- : "No reports or workflow issues need attention.";
98
+ const reviewCount = snapshot.metrics.awaitingReview;
99
+ const summary = snapshot.loading
100
+ ? "Loading the live review queue."
101
+ : reviewCount
102
+ ? `${reviewCount} case${reviewCount === 1 ? "" : "s"} awaiting review · oldest waiting ${compactDuration(snapshot.metrics.oldestWaitMinutes)}.`
103
+ : "The review queue is clear. New work will appear here automatically.";
104
+ const nextReport = snapshot.nextReview;
82
105
 
83
106
  return (
84
107
  <div data-react-slice="command-centre">
85
- <PageHeader
86
- className="command-head"
87
- title="Command centre"
88
- description={`${formattedDate(snapshot.generatedAt)} · ${welcome} ${snapshot.loading ? "Loading workspace status." : attentionSummary}`}
89
- >
90
- <div className="surface-buttons">
108
+ <PageHeader className="command-head" title="Today" description={summary}>
109
+ <div className="surface-buttons command-actions">
110
+ {nextReport ? <button className="primary" type="button" onClick={() => dispatchOpenReport(nextReport.reportId, true)}><svg aria-hidden="true"><use href="#i-inbox" /></svg>{nextReport.mine ? "Continue review" : "Review next"}</button> : null}
91
111
  {snapshot.permissions.useAssistant ? <button className="secondary command-assistant-action" type="button" onClick={() => dispatchNavigation("assistant")}><svg aria-hidden="true"><use href="#i-spark" /></svg>Ask Safest</button> : null}
92
- {snapshot.permissions.manageWorkflows ? <button className="primary" type="button" onClick={() => dispatchNavigation("workflows")}><svg aria-hidden="true"><use href="#i-plus" /></svg>New workflow</button> : null}
93
112
  </div>
94
113
  </PageHeader>
95
114
 
@@ -98,22 +117,40 @@ export function CommandCentre() {
98
117
  ) : null}
99
118
 
100
119
  <Metrics metrics={snapshot.metrics} loading={snapshot.loading} />
120
+ <CaseEscalations compact />
101
121
 
102
- <div className="command-grid">
103
- <section className="surface command-panel"><div className="surface-head"><h2>Needs attention</h2><button className="text-button" type="button" onClick={() => dispatchNavigation("reports")}>View all</button></div><div className="attention-list" aria-busy={snapshot.loading}>
104
- {snapshot.loading ? <LoadingRows /> : snapshot.attention.length ? snapshot.attention.map((item) => (
105
- <button className="attention-row" type="button" key={item.id} onClick={() => performAction(item.action)}><span className={`attention-glyph${item.urgent ? " danger" : ""}`} aria-hidden="true">{item.icon}</span><span><strong>{item.title}</strong><small>{item.detail}</small></span><span className="attention-meta">{item.meta}</span></button>
106
- )) : <EmptyProduct icon="✓" title="You’re all caught up" description="No reports or workflow issues need attention." />}
107
- </div></section>
122
+ <div className="command-workspace-grid">
123
+ <section className="surface command-panel review-priority-panel">
124
+ <div className="surface-head">
125
+ <div><h2>Review queue</h2><p>Ordered by overdue target, priority, ownership, then age.</p></div>
126
+ <div className="surface-head-actions"><span className="live-label"><i />Live</span><button className="text-button" type="button" onClick={() => dispatchNavigation("reports")}>View all</button></div>
127
+ </div>
128
+ <div className="review-priority-list" aria-busy={snapshot.loading}>
129
+ {snapshot.loading ? <LoadingRows /> : snapshot.attention.length ? snapshot.attention.map((item) => (
130
+ <button className={`review-priority-row ${item.tone}`} type="button" key={item.id} onClick={() => performAction(item.action)}>
131
+ <i className="review-priority-rail" aria-hidden="true" />
132
+ <span className="review-priority-main">
133
+ <span className="review-priority-title"><strong>{item.title}</strong><b className={`priority-chip ${item.tone}`}>{item.priorityLabel}</b></span>
134
+ <small>{item.reference} · {item.queueName}</small>
135
+ <span className="review-priority-meta"><span>{item.ownership}</span><span>Received {item.age}</span></span>
136
+ </span>
137
+ <span className={`sla-chip ${item.tone}`}>{item.slaLabel}</span>
138
+ <svg className="review-row-arrow" aria-hidden="true" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6" /></svg>
139
+ </button>
140
+ )) : <EmptyReviewQueue />}
141
+ </div>
142
+ </section>
108
143
 
109
- <section className="surface command-panel queue-load-panel"><div className="surface-head"><h2>Queue load</h2><span className="live-label"><i />Live</span></div><div className="queue-load-list" aria-busy={snapshot.loading}>
110
- {snapshot.loading ? <LoadingRows /> : snapshot.queueLoads.length ? snapshot.queueLoads.map((queue) => <QueueLoadRow queue={queue} key={queue.id} />) : <EmptyProduct icon="≋" title="Queue load unavailable" description="Your role does not include queue load data." />}
111
- </div></section>
144
+ <div className="command-side-stack">
145
+ <section className="surface command-panel queue-health-panel">
146
+ <div className="surface-head"><div><h2>Queue health</h2><p>Current wait against each first-response target.</p></div></div>
147
+ <div className="queue-health-list" aria-busy={snapshot.loading}>
148
+ {snapshot.loading ? <LoadingRows /> : snapshot.queueLoads.length ? snapshot.queueLoads.map((queue) => <QueueLoadRow queue={queue} key={queue.id} />) : <div className="command-empty compact"><div><strong>No active queues</strong><small>Published queues will appear here.</small></div></div>}
149
+ </div>
150
+ </section>
151
+ <SystemHealth health={snapshot.systemHealth} canOpen={snapshot.permissions.viewAudit} />
152
+ </div>
112
153
  </div>
113
-
114
- <section className="surface command-panel recent-runs-panel"><div className="surface-head"><h2>Recent workflow runs</h2>{snapshot.permissions.viewAudit ? <button className="text-button" type="button" onClick={() => dispatchNavigation("operations")}>View runs</button> : null}</div><div className="compact-run-list" aria-busy={snapshot.loading}>
115
- {snapshot.loading ? <LoadingRows /> : snapshot.recentRuns.length ? snapshot.recentRuns.map((run) => <RunRow run={run} key={run.id} />) : <EmptyProduct icon="⌘" title="No recent runs" description="Runs appear here when a report starts a workflow." />}
116
- </div></section>
117
154
  </div>
118
155
  );
119
156
  }
@@ -1,4 +1,6 @@
1
+ import { loadInbox } from "../reports/load-inbox";
1
2
  import { requestJson } from "../lib/http";
3
+ import { readable, relativeTime } from "../lib/format";
2
4
  import { getOperationsWorkspaceSnapshot, subscribeToOperationsWorkspace } from "../operations/store";
3
5
  import { getQueueWorkspaceSnapshot, subscribeToQueueWorkspace } from "../queues/store";
4
6
  import type { QueueRecord } from "../queues/types";
@@ -6,8 +8,9 @@ import { getReportWorkspaceSnapshot, subscribeToReportWorkspace } from "../repor
6
8
  import { settingsEvents } from "../settings/events";
7
9
  import type { SetupStatus } from "../settings/types";
8
10
  import { getShellSnapshot, subscribeToShell } from "../shell/store";
11
+ import { priorityLabel, slaStatus } from "./presentation";
9
12
  import { setCommandSnapshot } from "./store";
10
- import type { CommandAttentionItem, CommandQueueLoad, CommandRunSummary, CommandSnapshot } from "./types";
13
+ import type { CommandAttentionItem, CommandQueueLoad, CommandSnapshot } from "./types";
11
14
 
12
15
  interface CommandReport {
13
16
  id: string;
@@ -17,7 +20,7 @@ interface CommandReport {
17
20
  state: string;
18
21
  priority: number;
19
22
  receivedAt: string;
20
- claim?: { reviewerId?: string } | null;
23
+ assignment?: { reviewerId?: string; analyst?: { displayName?: string } } | null;
21
24
  }
22
25
 
23
26
  interface WorkflowRun {
@@ -32,12 +35,13 @@ interface WorkflowRun {
32
35
  createdAt: string;
33
36
  }
34
37
 
35
- interface ReportsResponse { reports?: CommandReport[] }
36
38
  interface QueuesResponse { queues?: QueueRecord[] }
37
39
  interface WorkflowRunsResponse { runs?: WorkflowRun[] }
38
40
 
39
41
  const closedReportStates = new Set(["resolved_by_human", "resolved_by_ai", "closed"]);
42
+ const reviewReportStates = new Set(["human_review", "human_review_requested"]);
40
43
  const activeRunStates = new Set(["created", "dispatch_pending", "queued", "running", "waiting", "paused"]);
44
+ const unhealthyRunStates = new Set(["failed", "repair_required"]);
41
45
 
42
46
  let actorId = "";
43
47
  let reports: CommandReport[] = [];
@@ -51,7 +55,7 @@ let refreshScheduled = false;
51
55
  let started = false;
52
56
 
53
57
  function reportDataSignature(): string {
54
- return reports.map((report) => `${report.id}:${report.state}:${report.priority}:${report.claim?.reviewerId || ""}`).sort().join("|");
58
+ return reports.map((report) => `${report.id}:${report.state}:${report.priority}:${report.assignment?.reviewerId || ""}`).sort().join("|");
55
59
  }
56
60
 
57
61
  function reportStoreSignature(): string {
@@ -72,23 +76,9 @@ function queueStoreSignature(): string {
72
76
  .join("|");
73
77
  }
74
78
 
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`;
79
+ function reportAgeMinutes(report: CommandReport): number {
80
+ const receivedAt = Date.parse(report.receivedAt);
81
+ return Number.isFinite(receivedAt) ? Math.max(0, (Date.now() - receivedAt) / 60_000) : 0;
92
82
  }
93
83
 
94
84
  function reset(): void {
@@ -104,78 +94,97 @@ function reset(): void {
104
94
  function buildSnapshot(): CommandSnapshot {
105
95
  const session = getShellSnapshot().session;
106
96
  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));
97
+ const needsHuman = openReports.filter((report) => reviewReportStates.has(report.state));
108
98
  const activeRuns = runs.filter((run) => activeRunStates.has(run.state));
99
+ const workflowIssueCount = runs.filter((run) => unhealthyRunStates.has(run.state)).length;
109
100
  const operations = getOperationsWorkspaceSnapshot().operations;
110
101
  const incidentCount = Number(operations?.incidents.open || 0) + Number(operations?.incidents.acknowledged || 0);
111
- const displayName = session?.displayName || "there";
102
+ const deliveryFailureCount = Number(operations?.routing.failed || 0)
103
+ + Number(operations?.routing.deadLetter || 0)
104
+ + Number(operations?.actions.terminalFailed || 0)
105
+ + Number(operations?.notifications.terminalFailed || 0)
106
+ + Number(operations?.emails.terminalFailed || 0);
107
+ const queueById = new Map(queues.map((queue) => [queue.id, queue]));
108
+ const sortedAttention = [...needsHuman].sort((left, right) => {
109
+ const leftTarget = queueById.get(left.queueId || "")?.activePolicy?.responseSlaMinutes ?? null;
110
+ const rightTarget = queueById.get(right.queueId || "")?.activePolicy?.responseSlaMinutes ?? null;
111
+ const leftBreached = slaStatus(reportAgeMinutes(left), leftTarget).breached;
112
+ const rightBreached = slaStatus(reportAgeMinutes(right), rightTarget).breached;
113
+ if (leftBreached !== rightBreached) return leftBreached ? -1 : 1;
114
+ if (left.priority !== right.priority) return right.priority - left.priority;
115
+ if (Boolean(left.assignment) !== Boolean(right.assignment)) return left.assignment ? 1 : -1;
116
+ return Date.parse(left.receivedAt) - Date.parse(right.receivedAt);
117
+ });
112
118
 
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" },
119
+ const attention: CommandAttentionItem[] = sortedAttention.slice(0, 6).map((report) => {
120
+ const queue = queueById.get(report.queueId || "");
121
+ const sla = slaStatus(reportAgeMinutes(report), queue?.activePolicy?.responseSlaMinutes ?? null);
122
+ return {
123
+ id: `report-${report.id}`,
124
+ title: `${report.reasonCode ? readable(report.reasonCode) : "Safety"} report`,
125
+ reference: report.reference,
126
+ queueName: queue?.name || "Unrouted",
127
+ age: relativeTime(report.receivedAt),
128
+ ownership: report.assignment ? (report.assignment.reviewerId === actorId ? "Assigned to you" : `Assigned to ${report.assignment.analyst?.displayName || "another reviewer"}`) : "Unassigned",
129
+ priorityLabel: priorityLabel(report.priority),
130
+ slaLabel: sla.label,
131
+ tone: report.priority >= 80 ? "danger" : sla.tone,
132
+ action: { type: "report", reportId: report.id },
133
+ };
130
134
  });
131
135
 
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;
136
+ const queueLoads: CommandQueueLoad[] = queues.filter((queue) => queue.status === "active").map((queue): CommandQueueLoad => {
137
+ const queuedReports = needsHuman.filter((report) => report.queueId === queue.id);
138
+ const targetMinutes = queue.activePolicy?.responseSlaMinutes ?? null;
139
+ const ages = queuedReports.map(reportAgeMinutes);
140
+ const oldestMinutes = ages.length ? Math.max(...ages) : 0;
141
+ const breachedCount = ages.filter((age) => slaStatus(age, targetMinutes).breached).length;
142
+ const pressure = targetMinutes ? Math.min(100, Math.round((oldestMinutes / Math.max(1, targetMinutes)) * 100)) : 0;
137
143
  return {
138
144
  id: queue.id,
139
145
  name: queue.name,
140
146
  targetMinutes,
141
147
  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" },
148
+ unassignedCount: queuedReports.filter((report) => !report.assignment).length,
149
+ oldestMinutes,
150
+ breachedCount,
151
+ pressure,
152
+ tone: breachedCount ? "danger" : pressure >= 75 ? "warning" : "positive",
157
153
  };
158
- });
154
+ }).sort((left, right) => right.breachedCount - left.breachedCount || right.pressure - left.pressure || right.openCount - left.openCount).slice(0, 5);
159
155
 
160
156
  const optionalSetup = new Set(setup?.optional || []);
161
157
  const incompleteSetupCount = setup?.features.filter((feature) => feature.state !== "ready" && !optionalSetup.has(feature.featureKey)).length || 0;
158
+ const unassigned = needsHuman.filter((report) => !report.assignment).length;
159
+ const urgent = needsHuman.filter((report) => report.priority >= 80).length;
160
+ const oldestWaitMinutes = needsHuman.length ? Math.max(...needsHuman.map(reportAgeMinutes)) : 0;
161
+ const systemIssueCount = Number(incidentCount > 0) + Number(workflowIssueCount > 0) + Number(deliveryFailureCount > 0);
162
162
  return {
163
163
  loading,
164
164
  generatedAt: new Date().toISOString(),
165
- firstName: displayName.split(" ")[0] || "there",
166
165
  metrics: {
167
- openReports: openReports.length,
168
- needsHuman: needsHuman.length,
169
- activeRuns: activeRuns.length,
170
- unresolvedIncidents: incidentCount,
166
+ awaitingReview: needsHuman.length,
167
+ urgent,
168
+ unassigned,
169
+ onlineReviewers: getReportWorkspaceSnapshot().presence.length,
170
+ oldestWaitMinutes,
171
171
  },
172
172
  attention,
173
+ nextReview: session?.permissions.claimReports ? (() => {
174
+ const next = sortedAttention.find(report => report.assignment?.reviewerId === actorId) || sortedAttention.find(report => !report.assignment);
175
+ return next ? { reportId: next.id, mine: next.assignment?.reviewerId === actorId } : null;
176
+ })() : null,
173
177
  queueLoads,
174
- recentRuns,
178
+ systemHealth: {
179
+ issueCount: systemIssueCount,
180
+ incidentCount,
181
+ workflowIssueCount,
182
+ deliveryFailureCount,
183
+ activeRunCount: activeRuns.length,
184
+ },
175
185
  incompleteSetupCount,
176
186
  permissions: {
177
187
  useAssistant: session?.permissions.useAssistant === true,
178
- manageWorkflows: session?.permissions.manageWorkflows === true,
179
188
  viewAudit: session?.permissions.viewAudit === true,
180
189
  manageInstallation: session?.permissions.manageInstallation === true,
181
190
  },
@@ -196,8 +205,8 @@ async function loadCommandData(): Promise<void> {
196
205
  loading = true;
197
206
  publish();
198
207
  const tasks: Promise<void>[] = [
199
- requestJson<ReportsResponse>("/v1/admin/reports?limit=100")
200
- .then((response) => { reports = response.reports || []; })
208
+ loadInbox<CommandReport>()
209
+ .then((response) => { reports = response; })
201
210
  .catch(() => { reports = []; }),
202
211
  requestJson<QueuesResponse>(`/v1/admin/queues${session.permissions.manageConfiguration ? "?include_archived=true" : ""}`)
203
212
  .then((response) => { queues = response.queues || []; })
@@ -250,6 +259,7 @@ export function startCommandController(): void {
250
259
  reportSignature = signature;
251
260
  scheduleRefresh();
252
261
  }
262
+ publish();
253
263
  });
254
264
  subscribeToQueueWorkspace(() => {
255
265
  const snapshot = getQueueWorkspaceSnapshot();
@@ -9,11 +9,10 @@ export function isCommandSnapshot(value: unknown): value is CommandSnapshot {
9
9
  const candidate = value as Partial<CommandSnapshot>;
10
10
  return typeof candidate.loading === "boolean"
11
11
  && typeof candidate.generatedAt === "string"
12
- && typeof candidate.firstName === "string"
13
12
  && typeof candidate.metrics === "object"
14
13
  && Array.isArray(candidate.attention)
15
14
  && Array.isArray(candidate.queueLoads)
16
- && Array.isArray(candidate.recentRuns)
15
+ && typeof candidate.systemHealth === "object"
17
16
  && typeof candidate.incompleteSetupCount === "number"
18
17
  && typeof candidate.permissions === "object";
19
18
  }
@@ -0,0 +1,29 @@
1
+ import type { CommandStatusTone } from "./types";
2
+ export { priorityLabel } from "../lib/priority";
3
+
4
+ export function compactDuration(minutes: number): string {
5
+ const rounded = Math.max(0, Math.floor(minutes));
6
+ if (rounded < 1) return "now";
7
+ if (rounded < 60) return `${rounded}m`;
8
+ const hours = Math.floor(rounded / 60);
9
+ if (hours < 48) return `${hours}h`;
10
+ return `${Math.floor(hours / 24)}d`;
11
+ }
12
+
13
+ export function slaStatus(ageMinutes: number, targetMinutes: number | null): {
14
+ label: string;
15
+ tone: CommandStatusTone;
16
+ breached: boolean;
17
+ } {
18
+ if (!targetMinutes || targetMinutes <= 0) {
19
+ return { label: `${compactDuration(ageMinutes)} waiting`, tone: "neutral", breached: false };
20
+ }
21
+ const remaining = targetMinutes - ageMinutes;
22
+ if (remaining <= 0) {
23
+ return { label: `${compactDuration(Math.abs(remaining))} overdue`, tone: "danger", breached: true };
24
+ }
25
+ if (remaining <= Math.max(15, targetMinutes * .25)) {
26
+ return { label: `${compactDuration(remaining)} to target`, tone: "warning", breached: false };
27
+ }
28
+ return { label: `${compactDuration(remaining)} to target`, tone: "positive", breached: false };
29
+ }
@@ -6,13 +6,13 @@ const subscribers = new Set<() => void>();
6
6
  let snapshot: CommandSnapshot = {
7
7
  loading: true,
8
8
  generatedAt: new Date().toISOString(),
9
- firstName: "there",
10
- metrics: { openReports: 0, needsHuman: 0, activeRuns: 0, unresolvedIncidents: 0 },
9
+ metrics: { awaitingReview: 0, urgent: 0, unassigned: 0, onlineReviewers: 0, oldestWaitMinutes: 0 },
11
10
  attention: [],
11
+ nextReview: null,
12
12
  queueLoads: [],
13
- recentRuns: [],
13
+ systemHealth: { issueCount: 0, incidentCount: 0, workflowIssueCount: 0, deliveryFailureCount: 0, activeRunCount: 0 },
14
14
  incompleteSetupCount: 0,
15
- permissions: { useAssistant: false, manageWorkflows: false, viewAudit: false, manageInstallation: false },
15
+ permissions: { useAssistant: false, viewAudit: false, manageInstallation: false },
16
16
  };
17
17
 
18
18
  window.addEventListener(commandEvents.state, (event) => {
@@ -1,61 +1,66 @@
1
1
  import type { WorkspaceView } from "../shell/types";
2
2
 
3
- export type CommandStatusTone = "neutral" | "warning" | "danger";
3
+ export type CommandStatusTone = "neutral" | "positive" | "warning" | "danger";
4
4
 
5
5
  export type CommandAction =
6
6
  | { type: "report"; reportId: string }
7
7
  | { type: "view"; view: WorkspaceView };
8
8
 
9
9
  export interface CommandMetricSnapshot {
10
- openReports: number;
11
- needsHuman: number;
12
- activeRuns: number;
13
- unresolvedIncidents: number;
10
+ awaitingReview: number;
11
+ urgent: number;
12
+ unassigned: number;
13
+ onlineReviewers: number;
14
+ oldestWaitMinutes: number;
14
15
  }
15
16
 
16
17
  export interface CommandAttentionItem {
17
18
  id: string;
18
- icon: string;
19
19
  title: string;
20
- detail: string;
21
- meta: string;
22
- urgent: boolean;
20
+ reference: string;
21
+ queueName: string;
22
+ age: string;
23
+ ownership: string;
24
+ priorityLabel: string;
25
+ slaLabel: string;
26
+ tone: CommandStatusTone;
23
27
  action: CommandAction;
24
28
  }
25
29
 
26
30
  export interface CommandQueueLoad {
27
31
  id: string;
28
32
  name: string;
29
- targetMinutes: number;
33
+ targetMinutes: number | null;
30
34
  openCount: number;
35
+ unassignedCount: number;
36
+ oldestMinutes: number;
37
+ breachedCount: number;
31
38
  pressure: number;
39
+ tone: CommandStatusTone;
32
40
  }
33
41
 
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
+ export interface CommandSystemHealth {
43
+ issueCount: number;
44
+ incidentCount: number;
45
+ workflowIssueCount: number;
46
+ deliveryFailureCount: number;
47
+ activeRunCount: number;
42
48
  }
43
49
 
44
50
  export interface CommandPermissions {
45
51
  useAssistant: boolean;
46
- manageWorkflows: boolean;
47
52
  viewAudit: boolean;
48
53
  manageInstallation: boolean;
49
54
  }
50
55
 
51
56
  export interface CommandSnapshot {
57
+ nextReview: { reportId: string; mine: boolean } | null;
52
58
  loading: boolean;
53
59
  generatedAt: string;
54
- firstName: string;
55
60
  metrics: CommandMetricSnapshot;
56
61
  attention: CommandAttentionItem[];
57
62
  queueLoads: CommandQueueLoad[];
58
- recentRuns: CommandRunSummary[];
63
+ systemHealth: CommandSystemHealth;
59
64
  incompleteSetupCount: number;
60
65
  permissions: CommandPermissions;
61
66
  }