create-safest-tools 0.6.0 → 0.6.1

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 (46) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/update.mjs +10 -1
  4. package/template/README.md +1 -1
  5. package/template/console/README.md +2 -2
  6. package/template/console/command/CommandCentre.tsx +94 -63
  7. package/template/console/command/controller.ts +71 -65
  8. package/template/console/command/events.ts +1 -2
  9. package/template/console/command/presentation.ts +34 -0
  10. package/template/console/command/store.ts +3 -4
  11. package/template/console/command/types.ts +25 -21
  12. package/template/console/registry/RegistryDetailsDialog.tsx +1 -1
  13. package/template/console/registry/RegistryDialog.tsx +22 -23
  14. package/template/console/reports/EnrichmentFindings.tsx +148 -0
  15. package/template/console/reports/ReportDrawer.tsx +19 -6
  16. package/template/console/reports/ReportWorkspace.tsx +104 -14
  17. package/template/console/reports/controller.ts +31 -13
  18. package/template/console/reports/presence-presentation.ts +52 -0
  19. package/template/console/reports/store.ts +1 -1
  20. package/template/console/reports/types.ts +9 -0
  21. package/template/console/shell/controller.ts +1 -8
  22. package/template/migrations/0001_safest_resolve_baseline.sql +0 -1
  23. package/template/package.json +8 -7
  24. package/template/public/console/auth-shell.js +9353 -8950
  25. package/template/public/styles.css +271 -74
  26. package/template/src/component-executor.ts +2 -2
  27. package/template/src/connection-oauth.ts +12 -3
  28. package/template/src/database-readiness.ts +9 -0
  29. package/template/src/index.ts +13 -11
  30. package/template/src/platform-registry-validation.ts +25 -2
  31. package/template/src/platform-registry.ts +10 -7
  32. package/template/src/report-better-auth.ts +18 -6
  33. package/template/src/report-delivery.ts +20 -26
  34. package/template/src/report-email-inbound.ts +33 -22
  35. package/template/src/report-email.ts +7 -30
  36. package/template/src/report-http.ts +37 -2
  37. package/template/src/report-operations.ts +15 -5
  38. package/template/src/report-operator-accounts.ts +7 -6
  39. package/template/src/report-presence.ts +15 -9
  40. package/template/src/report-turnstile.ts +7 -3
  41. package/template/src/report-workflow.ts +34 -7
  42. package/template/src/workflow-dynamic-runtime.ts +2 -2
  43. package/template/src/workflow-effects.ts +22 -21
  44. package/template/src/workflow-expressions.ts +30 -0
  45. package/template/src/workflow-loader-code.ts +21 -0
  46. package/template/src/workflow-runs.ts +52 -9
package/README.md CHANGED
@@ -18,7 +18,7 @@ npx create-safest-tools safest-resolve \
18
18
  --email-from reports@example.com
19
19
  ```
20
20
 
21
- The `--email-from` address is a real, receivable mailbox used for receipts, follow-ups, outcomes, and replies from reporters or affected users. Use a short base address such as `reports@example.com` on a domain onboarded under Cloudflare Email Service → Email Sending, then enable Email Routing for that domain. Setup creates an exact Worker route for this address, enables plus-addressing for signed case replies, and does not enable catch-all mail. Account verification, invitations, and password recovery use a distinct `accounts@` address on the same domain by default; pass `--auth-email-from` to choose another address. Every customer-facing email automatically uses the organization name, logo, colors, type, and spacing saved under Configuration → Branding.
21
+ The `--email-from` address is a real, receivable mailbox used for receipts, follow-ups, outcomes, and replies from reporters or affected users. Use a short base address such as `reports@example.com` on a domain onboarded under Cloudflare Email Service → Email Sending, then enable Email Routing for that domain. Setup creates an exact Worker route for this address, enables plus-addressing for signed case replies, and does not enable catch-all mail. Account verification, invitations, and password recovery use a distinct `accounts@` address on the same domain by default; pass `--auth-email-from` to choose another address. Every customer-facing email automatically uses the organization name, logo, colors, type, and spacing saved under Settings → Branding.
22
22
 
23
23
  The command creates a local project and prints a read-only infrastructure plan. It does not change Cloudflare unless `--deploy` is supplied or the generated project’s `npm run setup` command is run and explicitly confirmed.
24
24
 
@@ -46,7 +46,7 @@ npm run setup
46
46
 
47
47
  `setup:plan` lists exact resource names and the reporter email route, then exits without modifying Cloudflare. `setup` opens Wrangler's Cloudflare login when needed, lets you choose the owning account, and verifies Workers Paid from the account's Workers usage model. Standard accounts need no separate billing token; only legacy or ambiguous account models use the temporary Billing Read fallback. It then guides Google, GitHub, and Cloudflare OAuth configuration with exact callback URLs and masked secret input. Nothing is provisioned until the exact `DEPLOY <installation-id>` confirmation. After confirmation it verifies Email Routing, enables subaddressing, and deploys only the exact reporter-address route. If Cloudflare interrupts the deployment, run `npm run setup` again; it detects the unfinished installation, reuses the saved authentication choices and exact resources, suppresses expected already-exists errors, and continues through the owner-link step.
48
48
 
49
- A new installation applies one current D1 schema baseline. Later releases add only forward-compatible upgrade migrations.
49
+ A new 0.6.1 installation applies one current D1 schema baseline. Later releases add only forward-compatible upgrade migrations. Because 0.6.1 replaces the earlier development migration chain with that baseline, the updater refuses to modify a deployed pre-0.6.1 project in place. Back up that deployment and create a separately named installation instead. Undeployed scaffolds may cross the boundary normally.
50
50
 
51
51
  After deployment, setup prints a 256-bit, single-use owner link that expires after 15 minutes. The raw token stays in the URL fragment, D1 stores only its digest, and the installer never writes it to disk. Open it to create the Safest owner account. If it expires, run `npm run owner:setup`. For later break-glass recovery, authenticate Wrangler and run `npm run owner:recover`.
52
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-safest-tools",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Create customer-owned abuse-reporting infrastructure on Cloudflare",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/src/update.mjs CHANGED
@@ -6,6 +6,7 @@ import { buildWranglerConfiguration } from "./config.mjs";
6
6
 
7
7
  const MANIFEST_PATH = ".safest-managed.json";
8
8
  const MANIFEST_SCHEMA_VERSION = 1;
9
+ const FRESH_SCHEMA_BASELINE_VERSION = "0.6.1";
9
10
  const REBUILDABLE_PATHS = new Set(["public/console/auth-shell.js"]);
10
11
  const CUSTOMER_RUNTIME_VAR_KEYS = new Set([
11
12
  "ALLOWED_ACTION_CODES",
@@ -185,6 +186,15 @@ export async function planProjectUpdate({ directory, templateDirectory }) {
185
186
  if (compareVersions(targetVersion, manifest.installedVersion) < 0) {
186
187
  throw new Error(`refusing to downgrade Safest Resolve from ${manifest.installedVersion} to ${targetVersion}`);
187
188
  }
189
+ const deployed = (currentWrangler.d1_databases ?? []).some(({ database_id: id }) => typeof id === "string" && id.length > 0);
190
+ if (deployed
191
+ && compareVersions(manifest.installedVersion, FRESH_SCHEMA_BASELINE_VERSION) < 0
192
+ && compareVersions(targetVersion, FRESH_SCHEMA_BASELINE_VERSION) >= 0) {
193
+ throw new Error(
194
+ `Safest Resolve ${FRESH_SCHEMA_BASELINE_VERSION} starts a new fresh-install schema lineage. `
195
+ + `Back up this ${manifest.installedVersion} deployment, then create and verify a separately named installation instead of updating it in place.`,
196
+ );
197
+ }
188
198
 
189
199
  const added = [];
190
200
  const updated = [];
@@ -205,7 +215,6 @@ export async function planProjectUpdate({ directory, templateDirectory }) {
205
215
  if (!wranglerMatchesManifest(currentWrangler, manifest.wranglerHash)) conflicts.push("wrangler.jsonc");
206
216
 
207
217
  const desiredWrangler = mergeWranglerRuntimeState(buildWranglerConfiguration(reportsConfiguration), currentWrangler);
208
- const deployed = (currentWrangler.d1_databases ?? []).some(({ database_id: id }) => typeof id === "string" && id.length > 0);
209
218
  const uniqueConflicts = [...new Set(conflicts)].sort();
210
219
  return {
211
220
  action: "update",
@@ -14,7 +14,7 @@ Before deployment:
14
14
  4. Type the exact installation confirmation when setup requests it. After deployment, setup prints a 256-bit, single-use owner link that expires after 15 minutes and is never written to disk.
15
15
  5. Open the owner link, create the Safest owner account, then invite administrators and analysts from People. Nobody needs a Cloudflare account to sign in. Names and workspace roles are always displayed separately.
16
16
 
17
- Setup creates D1, four private R2 buckets, and three Queues, applies one current D1 schema baseline, and deploys the Worker with Dynamic Workflows. Later releases add only forward-compatible upgrade migrations. After owner setup, verify the `database`, `workflow_runtime`, `workers_ai`, `object_storage`, and `queues` setup features, then require an authenticated `GET /ready` response of 200. Public `GET /health` is liveness only. Better Auth owns password hashing, OAuth accounts, sessions, and one-time password-reset tokens in D1. Upgrades always back up D1 before migrations and deployment.
17
+ Setup creates D1, four private R2 buckets, and three Queues, applies the current fresh-install D1 baseline, and deploys the Worker with Dynamic Workflows. Version 0.6.1 begins this schema lineage; the updater blocks deployed pre-0.6.1 projects from crossing it in place and directs operators to a verified backup plus separately named installation. Later releases add only forward-compatible migrations. After owner setup, verify the `database`, `workflow_runtime`, `workers_ai`, `object_storage`, and `queues` setup features, then require an authenticated `GET /ready` response of 200. Public `GET /health` is liveness only. Better Auth owns password hashing, OAuth accounts, sessions, and one-time password-reset tokens in D1. Upgrades always back up D1 before migrations and deployment.
18
18
 
19
19
  If the setup link expires, run `npm run owner:setup`. If the owner loses access later, authenticate Wrangler as the Cloudflare account owner and run `npm run owner:recover`; the resulting single-use link creates a 12-hour break-glass session and is recorded in the audit log.
20
20
 
@@ -6,10 +6,10 @@ The console uses React, TypeScript, and Vite. Each product area keeps its DOM, t
6
6
 
7
7
  - **Account access:** sign-in, infrastructure-owner bootstrap, password reset, invitation acceptance, and optional profile-picture upload.
8
8
  - **Workspace shell:** header, responsive navigation, current route, health, analyst identity, and permission-aware navigation.
9
- - **Command centre:** metrics, work that needs attention, queue pressure, recent workflow runs, and setup actions.
9
+ - **Command centre:** an SLA-aware review queue, reviewer availability, queue health, operational health, and the next best action.
10
10
  - **Reports and appeals:** queue filters, presence, claims, participant messages, decisions, delivery history, workflow evidence, and separate appeal ownership.
11
11
  - **Queues and workflows:** queue policy publication, routing rollout, workflow drafts, graph editing, checks, fixture tests, publishing, and queue pins.
12
- - **Components and configuration:** component and connection records, outbound allowlists, intake forms, routing rules, decision policies, message templates, and the visual form builder.
12
+ - **Tools, connections, and configuration:** reusable workflow capabilities, governed outbound routes, intake forms, routing rules, decision policies, message templates, and the visual form builder. Runtime APIs and database tables retain the stable `component` name for backward compatibility.
13
13
  - **Operations and quality:** AI samples, reviewer corrections, incidents, retries, dead-letter evidence, analytics, audit verification, and CSV export.
14
14
  - **People and settings:** invitations, password-reset emails, roles, personal profiles, deployment checks, branding, and reporting channels.
15
15
  - **Ask Safest:** scoped conversations, citations, read-only activity, and links back to reports.
@@ -1,71 +1,86 @@
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
3
  import { dispatchOpenReport } from "../reports/events";
6
4
  import { dispatchNavigation } from "../shell/events";
5
+ import { compactDuration } from "./presentation";
6
+ import { getCommandSnapshot, subscribeToCommand } from "./store";
7
+ import type {
8
+ CommandAction,
9
+ CommandMetricSnapshot,
10
+ CommandQueueLoad,
11
+ CommandSystemHealth,
12
+ CommandStatusTone,
13
+ } from "./types";
7
14
 
8
15
  function performAction(action: CommandAction): void {
9
16
  if (action.type === "report") dispatchOpenReport(action.reportId);
10
17
  else dispatchNavigation(action.view);
11
18
  }
12
19
 
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>;
20
+ function EmptyReviewQueue() {
21
+ return (
22
+ <div className="command-empty">
23
+ <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>
24
+ <div><strong>Review queue is clear</strong><small>New reports will appear here as they are routed.</small></div>
25
+ </div>
26
+ );
27
27
  }
28
28
 
29
29
  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 },
30
+ const values: Array<{ label: string; value: number; detail: string; tone: CommandStatusTone }> = [
31
+ { label: "To review", value: metrics.awaitingReview, detail: "Ready for a decision", tone: metrics.awaitingReview ? "warning" : "positive" },
32
+ { label: "Urgent", value: metrics.urgent, detail: "Highest priority", tone: metrics.urgent ? "danger" : "positive" },
33
+ { label: "Unassigned", value: metrics.unassigned, detail: "Needs an owner", tone: metrics.unassigned ? "warning" : "positive" },
34
+ { label: "Reviewers online", value: metrics.onlineReviewers, detail: "Available now", tone: "neutral" },
35
35
  ];
36
36
  return (
37
- <section className="metrics command-metrics" aria-label="Workspace summary" aria-busy={loading}>
37
+ <section className="command-signals" aria-label="Live review summary" aria-busy={loading}>
38
38
  {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>
39
+ <button className={`command-signal ${item.tone}`} type="button" key={item.label} onClick={() => dispatchNavigation("reports")}>
40
+ <span>{item.label}</span><strong>{loading ? "—" : item.value}</strong><small>{item.detail}</small>
41
+ </button>
40
42
  ))}
41
43
  </section>
42
44
  );
43
45
  }
44
46
 
45
47
  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);
48
+ const width = Math.max(queue.openCount ? 7 : 0, queue.pressure);
49
+ const target = queue.targetMinutes ? `${compactDuration(queue.targetMinutes)} response target` : "No response target";
50
+ const risk = queue.breachedCount
51
+ ? `${queue.breachedCount} overdue`
52
+ : queue.openCount ? `Oldest waiting ${compactDuration(queue.oldestMinutes)}` : "Queue is clear";
48
53
  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}%` }} />
54
+ <article className={`queue-health-row ${queue.tone}`}>
55
+ <div className="queue-health-title"><strong>{queue.name}</strong><span>{queue.openCount} to review</span></div>
56
+ <div className="queue-health-meta"><span>{risk}</span><span>{queue.unassignedCount} unassigned</span></div>
57
+ <span className="queue-health-track" role="progressbar" aria-label={`${queue.name} response target usage`} aria-valuemin={0} aria-valuemax={100} aria-valuenow={queue.pressure}>
58
+ <i style={{ width: `${width}%` }} />
53
59
  </span>
54
- <strong className="queue-load-count">{queue.openCount} open</strong>
60
+ <small>{target}</small>
55
61
  </article>
56
62
  );
57
63
  }
58
64
 
59
- function RunRow({ run }: { run: CommandRunSummary }) {
60
- const tone = run.tone === "neutral" ? "" : ` ${run.tone}`;
65
+ function SystemHealth({ health, canOpen }: { health: CommandSystemHealth; canOpen: boolean }) {
66
+ const healthy = health.issueCount === 0;
61
67
  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>
68
+ <section className="surface command-panel system-health-panel">
69
+ <div className="surface-head">
70
+ <div><h2>System health</h2><p>Delivery and automation requiring intervention.</p></div>
71
+ {canOpen ? <button className="text-button" type="button" onClick={() => dispatchNavigation("operations")}>Open operations</button> : null}
72
+ </div>
73
+ <div className={`system-health-summary${healthy ? " healthy" : " danger"}`}>
74
+ <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>
75
+ <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>
76
+ </div>
77
+ <dl className="system-health-facts">
78
+ <div><dt>Incidents</dt><dd>{health.incidentCount}</dd></div>
79
+ <div><dt>Delivery failures</dt><dd>{health.deliveryFailureCount}</dd></div>
80
+ <div><dt>Workflow repairs</dt><dd>{health.workflowIssueCount}</dd></div>
81
+ <div><dt>Active workflows</dt><dd>{health.activeRunCount}</dd></div>
82
+ </dl>
83
+ </section>
69
84
  );
70
85
  }
71
86
 
@@ -75,21 +90,20 @@ function LoadingRows() {
75
90
 
76
91
  export function CommandCentre() {
77
92
  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.";
93
+ const reviewCount = snapshot.metrics.awaitingReview;
94
+ const summary = snapshot.loading
95
+ ? "Loading the live review queue."
96
+ : reviewCount
97
+ ? `${reviewCount} report${reviewCount === 1 ? "" : "s"} awaiting review · oldest waiting ${compactDuration(snapshot.metrics.oldestWaitMinutes)}.`
98
+ : "The review queue is clear. New work will appear here automatically.";
99
+ const nextReport = snapshot.attention[0];
82
100
 
83
101
  return (
84
102
  <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">
103
+ <PageHeader className="command-head" title="Command centre" description={summary}>
104
+ <div className="surface-buttons command-actions">
105
+ {nextReport ? <button className="primary" type="button" onClick={() => performAction(nextReport.action)}><svg aria-hidden="true"><use href="#i-inbox" /></svg>Review next</button> : null}
91
106
  {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
107
  </div>
94
108
  </PageHeader>
95
109
 
@@ -99,21 +113,38 @@ export function CommandCentre() {
99
113
 
100
114
  <Metrics metrics={snapshot.metrics} loading={snapshot.loading} />
101
115
 
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>
116
+ <div className="command-workspace-grid">
117
+ <section className="surface command-panel review-priority-panel">
118
+ <div className="surface-head">
119
+ <div><h2>Review queue</h2><p>Ordered by overdue target, priority, ownership, then age.</p></div>
120
+ <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>
121
+ </div>
122
+ <div className="review-priority-list" aria-busy={snapshot.loading}>
123
+ {snapshot.loading ? <LoadingRows /> : snapshot.attention.length ? snapshot.attention.map((item) => (
124
+ <button className={`review-priority-row ${item.tone}`} type="button" key={item.id} onClick={() => performAction(item.action)}>
125
+ <i className="review-priority-rail" aria-hidden="true" />
126
+ <span className="review-priority-main">
127
+ <span className="review-priority-title"><strong>{item.title}</strong><b className={`priority-chip ${item.tone}`}>{item.priorityLabel}</b></span>
128
+ <small>{item.reference} · {item.queueName}</small>
129
+ <span className="review-priority-meta"><span>{item.ownership}</span><span>Received {item.age}</span></span>
130
+ </span>
131
+ <span className={`sla-chip ${item.tone}`}>{item.slaLabel}</span>
132
+ <svg className="review-row-arrow" aria-hidden="true" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6" /></svg>
133
+ </button>
134
+ )) : <EmptyReviewQueue />}
135
+ </div>
136
+ </section>
108
137
 
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>
138
+ <div className="command-side-stack">
139
+ <section className="surface command-panel queue-health-panel">
140
+ <div className="surface-head"><div><h2>Queue health</h2><p>Current wait against each first-response target.</p></div></div>
141
+ <div className="queue-health-list" aria-busy={snapshot.loading}>
142
+ {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>}
143
+ </div>
144
+ </section>
145
+ <SystemHealth health={snapshot.systemHealth} canOpen={snapshot.permissions.viewAudit} />
146
+ </div>
112
147
  </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
148
  </div>
118
149
  );
119
150
  }
@@ -1,4 +1,5 @@
1
1
  import { requestJson } from "../lib/http";
2
+ import { readable, relativeTime } from "../lib/format";
2
3
  import { getOperationsWorkspaceSnapshot, subscribeToOperationsWorkspace } from "../operations/store";
3
4
  import { getQueueWorkspaceSnapshot, subscribeToQueueWorkspace } from "../queues/store";
4
5
  import type { QueueRecord } from "../queues/types";
@@ -6,8 +7,9 @@ import { getReportWorkspaceSnapshot, subscribeToReportWorkspace } from "../repor
6
7
  import { settingsEvents } from "../settings/events";
7
8
  import type { SetupStatus } from "../settings/types";
8
9
  import { getShellSnapshot, subscribeToShell } from "../shell/store";
10
+ import { priorityLabel, slaStatus } from "./presentation";
9
11
  import { setCommandSnapshot } from "./store";
10
- import type { CommandAttentionItem, CommandQueueLoad, CommandRunSummary, CommandSnapshot } from "./types";
12
+ import type { CommandAttentionItem, CommandQueueLoad, CommandSnapshot } from "./types";
11
13
 
12
14
  interface CommandReport {
13
15
  id: string;
@@ -37,7 +39,9 @@ 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[] = [];
@@ -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,93 @@ 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.claim) !== Boolean(right.claim)) return left.claim ? 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.claim ? "Assigned" : "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.claim).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.claim).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
173
  queueLoads,
174
- recentRuns,
174
+ systemHealth: {
175
+ issueCount: systemIssueCount,
176
+ incidentCount,
177
+ workflowIssueCount,
178
+ deliveryFailureCount,
179
+ activeRunCount: activeRuns.length,
180
+ },
175
181
  incompleteSetupCount,
176
182
  permissions: {
177
183
  useAssistant: session?.permissions.useAssistant === true,
178
- manageWorkflows: session?.permissions.manageWorkflows === true,
179
184
  viewAudit: session?.permissions.viewAudit === true,
180
185
  manageInstallation: session?.permissions.manageInstallation === true,
181
186
  },
@@ -250,6 +255,7 @@ export function startCommandController(): void {
250
255
  reportSignature = signature;
251
256
  scheduleRefresh();
252
257
  }
258
+ publish();
253
259
  });
254
260
  subscribeToQueueWorkspace(() => {
255
261
  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,34 @@
1
+ import type { CommandStatusTone } from "./types";
2
+
3
+ export function compactDuration(minutes: number): string {
4
+ const rounded = Math.max(0, Math.floor(minutes));
5
+ if (rounded < 1) return "now";
6
+ if (rounded < 60) return `${rounded}m`;
7
+ const hours = Math.floor(rounded / 60);
8
+ if (hours < 48) return `${hours}h`;
9
+ return `${Math.floor(hours / 24)}d`;
10
+ }
11
+
12
+ export function priorityLabel(priority: number): string {
13
+ if (priority >= 80) return "Urgent";
14
+ if (priority >= 60) return "High";
15
+ return "Standard";
16
+ }
17
+
18
+ export function slaStatus(ageMinutes: number, targetMinutes: number | null): {
19
+ label: string;
20
+ tone: CommandStatusTone;
21
+ breached: boolean;
22
+ } {
23
+ if (!targetMinutes || targetMinutes <= 0) {
24
+ return { label: `${compactDuration(ageMinutes)} waiting`, tone: "neutral", breached: false };
25
+ }
26
+ const remaining = targetMinutes - ageMinutes;
27
+ if (remaining <= 0) {
28
+ return { label: `${compactDuration(Math.abs(remaining))} overdue`, tone: "danger", breached: true };
29
+ }
30
+ if (remaining <= Math.max(15, targetMinutes * .25)) {
31
+ return { label: `${compactDuration(remaining)} to target`, tone: "warning", breached: false };
32
+ }
33
+ return { label: `${compactDuration(remaining)} to target`, tone: "positive", breached: false };
34
+ }
@@ -6,13 +6,12 @@ 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: [],
12
11
  queueLoads: [],
13
- recentRuns: [],
12
+ systemHealth: { issueCount: 0, incidentCount: 0, workflowIssueCount: 0, deliveryFailureCount: 0, activeRunCount: 0 },
14
13
  incompleteSetupCount: 0,
15
- permissions: { useAssistant: false, manageWorkflows: false, viewAudit: false, manageInstallation: false },
14
+ permissions: { useAssistant: false, viewAudit: false, manageInstallation: false },
16
15
  };
17
16
 
18
17
  window.addEventListener(commandEvents.state, (event) => {