create-safest-tools 0.3.1 → 0.4.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 (71) hide show
  1. package/README.md +8 -4
  2. package/package.json +1 -1
  3. package/src/cli.mjs +52 -7
  4. package/src/config.mjs +26 -5
  5. package/src/scaffold.mjs +4 -1
  6. package/src/update.mjs +295 -0
  7. package/template/README.md +7 -5
  8. package/template/console/reports/ReportDrawer.tsx +51 -6
  9. package/template/console/reports/controller.ts +11 -5
  10. package/template/console/reports/store.ts +1 -1
  11. package/template/console/reports/types.ts +23 -0
  12. package/template/console/shell/WorkspaceShell.tsx +0 -4
  13. package/template/migrations/0001_safest_resolve_baseline.sql +2446 -0
  14. package/template/package.json +4 -3
  15. package/template/public/console/auth-shell.js +4333 -4243
  16. package/template/public/styles.css +19 -17
  17. package/template/reports.config.example.json +2 -1
  18. package/template/reports.schema.json +2 -1
  19. package/template/scripts/reports-auth-onboarding.mjs +6 -5
  20. package/template/scripts/reports-plan.mjs +14 -5
  21. package/template/src/index.ts +12 -3
  22. package/template/src/installation-admin.ts +2 -1
  23. package/template/src/report-ai.ts +30 -5
  24. package/template/src/report-better-auth.ts +5 -14
  25. package/template/src/report-delivery.ts +4 -3
  26. package/template/src/report-email.ts +106 -0
  27. package/template/src/report-workflow.ts +103 -21
  28. package/template/src/workflow-effects.ts +4 -3
  29. package/template/worker-configuration.d.ts +5 -3
  30. package/template/migrations/0001_reports_foundation.sql +0 -444
  31. package/template/migrations/0002_human_report_loop.sql +0 -65
  32. package/template/migrations/0003_delivery_reliability.sql +0 -18
  33. package/template/migrations/0004_public_intake.sql +0 -14
  34. package/template/migrations/0005_operations_visibility.sql +0 -24
  35. package/template/migrations/0006_ai_governance.sql +0 -198
  36. package/template/migrations/0007_ai_release_gates.sql +0 -6
  37. package/template/migrations/0008_retention_analytics_exports.sql +0 -52
  38. package/template/migrations/0009_retention_derived_copies.sql +0 -14
  39. package/template/migrations/0010_ai_quality_controls.sql +0 -26
  40. package/template/migrations/0011_analyst_presence.sql +0 -28
  41. package/template/migrations/0012_queue_policies.sql +0 -87
  42. package/template/migrations/0013_routing_agents.sql +0 -63
  43. package/template/migrations/0014_webhook_enrichments.sql +0 -122
  44. package/template/migrations/0015_queue_owned_ai.sql +0 -55
  45. package/template/migrations/0016_operator_accounts.sql +0 -67
  46. package/template/migrations/0017_operator_profiles_and_recovery.sql +0 -33
  47. package/template/migrations/0018_platform_configuration.sql +0 -382
  48. package/template/migrations/0019_workflow_authoring_runtime.sql +0 -372
  49. package/template/migrations/0020_tasks_findings_assistant_budgets.sql +0 -427
  50. package/template/migrations/0021_abuse_evidence_operations.sql +0 -324
  51. package/template/migrations/0022_workflow_dispatch_operations.sql +0 -42
  52. package/template/migrations/0023_component_connection_execution.sql +0 -75
  53. package/template/migrations/0024_access_runtime_integrity.sql +0 -72
  54. package/template/migrations/0025_installation_timezone.sql +0 -11
  55. package/template/migrations/0026_ai_and_egress_execution_controls.sql +0 -49
  56. package/template/migrations/0027_prompt_and_ai_registry.sql +0 -37
  57. package/template/migrations/0028_step_attempt_ai_provenance.sql +0 -13
  58. package/template/migrations/0029_evidence_fetch_transport.sql +0 -5
  59. package/template/migrations/0030_evidence_dlq_incidents.sql +0 -45
  60. package/template/migrations/0031_shadow_quality_integrity.sql +0 -7
  61. package/template/migrations/0032_action_delivery_outbox.sql +0 -55
  62. package/template/migrations/0033_configuration_and_assistant_drafts.sql +0 -43
  63. package/template/migrations/0034_installation_integrations.sql +0 -31
  64. package/template/migrations/0035_workspace_governance.sql +0 -21
  65. package/template/migrations/0036_published_routing_baseline.sql +0 -15
  66. package/template/migrations/0037_builtin_phishing_specialist.sql +0 -71
  67. package/template/migrations/0038_remove_deprecated_enrichment_runtime.sql +0 -228
  68. package/template/migrations/0039_secure_reporting_channels.sql +0 -45
  69. package/template/migrations/0040_notification_only_reporting.sql +0 -24
  70. package/template/migrations/0041_better_auth_credentials.sql +0 -17
  71. package/template/migrations/0042_owner_setup_tokens.sql +0 -29
@@ -9,7 +9,7 @@ import {
9
9
  dispatchReporterMessage,
10
10
  } from "./events";
11
11
  import { getReportWorkspaceSnapshot, subscribeToReportWorkspace } from "./store";
12
- import type { ReportDetailRecord, TimelineEvent } from "./types";
12
+ import type { ReportAnswer, ReportDetailRecord, TimelineEvent } from "./types";
13
13
 
14
14
  function Fact({ label, children }: { label: string; children: React.ReactNode }) {
15
15
  return <><dt>{label}</dt><dd>{children || "—"}</dd></>;
@@ -19,6 +19,40 @@ function EmptyLine({ children }: { children: React.ReactNode }) {
19
19
  return <p className="detail-empty">{children}</p>;
20
20
  }
21
21
 
22
+ function answerOptionLabel(answer: ReportAnswer, value: string): string {
23
+ return answer.options?.find((option) => option.value === value)?.label || readable(value);
24
+ }
25
+
26
+ function AnswerValue({ answer }: { answer: ReportAnswer }) {
27
+ if (!answer.answered) return <span className="answer-empty">Not answered</span>;
28
+ if (typeof answer.value === "boolean") return <span>{answer.value ? "Yes" : "No"}</span>;
29
+ if (Array.isArray(answer.value)) {
30
+ const values = answer.value.map((value) => answerOptionLabel(answer, String(value)));
31
+ return <span>{values.length ? values.join(", ") : "None selected"}</span>;
32
+ }
33
+ if (answer.value === null || answer.value === undefined || answer.value === "") return <span className="answer-empty">Not answered</span>;
34
+ const value = String(answer.value);
35
+ if (answer.type === "select") return <span>{answerOptionLabel(answer, value)}</span>;
36
+ const url = answer.type === "url" ? safeCustomerUrl(value) : null;
37
+ return url
38
+ ? <a className="text-button" href={url} target="_blank" rel="noopener noreferrer" referrerPolicy="no-referrer">{value} ↗</a>
39
+ : <span className="answer-text">{value}</span>;
40
+ }
41
+
42
+ function deliveryLabel(state: string): string {
43
+ if (state === "suppressed") return "Not sent";
44
+ if (state === "queued" || state === "pending" || state === "sending" || state === "delivering") return "Queued";
45
+ return readable(state);
46
+ }
47
+
48
+ function deliveryExplanation(state: string): string {
49
+ if (state === "suppressed") return "No eligible participant delivery channel was available when this message was created.";
50
+ if (state === "queued" || state === "pending" || state === "sending" || state === "delivering") return "Waiting for the delivery worker.";
51
+ if (state === "delivered") return "Successfully handed to the configured delivery provider.";
52
+ if (state === "failed" || state === "rejected" || state === "bounced") return "Delivery needs attention in Operations.";
53
+ return "";
54
+ }
55
+
22
56
  function timelineDetail(event: TimelineEvent): string {
23
57
  const details = event.details ?? {};
24
58
  const value = (key: string): string => typeof details[key] === "string" || typeof details[key] === "number" ? String(details[key]) : "";
@@ -38,7 +72,7 @@ function timelineMarker(eventType: string): string {
38
72
  return "•";
39
73
  }
40
74
 
41
- function DetailContent({ report, actorId, generatedAt, actionCodes, claimReports, messageReporters, decideReports, pendingAction, messageMutationVersion, error }: {
75
+ function DetailContent({ report, actorId, generatedAt, actionCodes, claimReports, messageReporters, decideReports, pendingAction, messageMutationVersion, statusMessage, error }: {
42
76
  report: ReportDetailRecord;
43
77
  actorId: string;
44
78
  generatedAt: string;
@@ -48,9 +82,11 @@ function DetailContent({ report, actorId, generatedAt, actionCodes, claimReports
48
82
  decideReports: boolean;
49
83
  pendingAction: "claim" | "release" | "message" | "decision" | null;
50
84
  messageMutationVersion: number;
85
+ statusMessage: string;
51
86
  error: string;
52
87
  }) {
53
88
  const evidence = report.evidence ?? [];
89
+ const answers = report.answers ?? [];
54
90
  const messages = report.messages ?? [];
55
91
  const workflowRuns = report.workflowRuns ?? [];
56
92
  const timeline = report.timeline ?? [];
@@ -106,6 +142,14 @@ function DetailContent({ report, actorId, generatedAt, actionCodes, claimReports
106
142
  return (
107
143
  <>
108
144
  <div className="detail-grid">
145
+ <section className="report-submission"><h3>Reporter submission</h3><dl className="submission-meta">
146
+ <Fact label="Form">{report.formTitle || report.formVersionId || "Unknown form"}</Fact>
147
+ <Fact label="Channel">{readable(report.source || "unknown")}</Fact>
148
+ <Fact label="Submitted">{dateTime(report.submittedAt || report.receivedAt)}</Fact>
149
+ <Fact label="Language">{report.locale || "—"}</Fact>
150
+ </dl><div className="answer-list">
151
+ {answers.length ? answers.map((answer) => <article className="answer-record" key={answer.fieldKey}><div><strong>{answer.label || readable(answer.fieldKey)}</strong>{answer.required ? <small>Required question</small> : null}</div><AnswerValue answer={answer} /></article>) : <EmptyLine>This form did not contain additional questions.</EmptyLine>}
152
+ </div></section>
109
153
  <section><h3>Allegation and target</h3><dl>
110
154
  <Fact label="Allegation">{readable(report.reasonCode)}</Fact>
111
155
  <Fact label="Target type">{readable(report.targetType)}</Fact>
@@ -125,22 +169,23 @@ function DetailContent({ report, actorId, generatedAt, actionCodes, claimReports
125
169
 
126
170
  {workflowRuns.length || timeline.length ? <section><h3>Workflow &amp; case timeline</h3><div className="report-run-list">{workflowRuns.map((run) => <article className="report-run-record" key={run.id}><div><strong>{run.workflowName || run.workflowKey || `Workflow ${String(run.workflowVersionId || run.id).slice(0, 12)}`}</strong><small>{readable(run.runKind || "primary")} · {readable(run.authorityMode || "assist")} · immutable version {run.workflowVersion || String(run.workflowVersionId || "").slice(0, 12)}</small></div><span className={`badge${["failed", "repair_required"].includes(run.state) ? " urgent" : ""}`}>{readable(run.state)}</span></article>)}</div><div className="report-timeline">{timeline.map((item, index) => <article className="timeline-event" key={item.id ?? `${item.eventType}-${item.createdAt}-${index}`}><span className={`timeline-marker ${item.eventType || "event"}`}>{timelineMarker(item.eventType)}</span><div><strong>{readable(item.eventType || "activity")}</strong><span>{timelineDetail(item)}</span><small>{relativeTime(item.createdAt)} · {readable(item.actorType || "system")}</small></div></article>)}</div></section> : null}
127
171
 
128
- <section><h3>Case messages</h3><div className="message-list">{messages.length ? messages.map((item, index) => <article className={`case-message ${item.direction}`} key={item.id ?? `${item.createdAt}-${index}`}><div>{item.body}</div><small>{readable(item.senderType)} · {dateTime(item.createdAt)} · {readable(item.deliveryState)}</small></article>) : <EmptyLine>No case messages yet.</EmptyLine>}</div></section>
172
+ <section><h3>Case messages</h3><div className="message-list">{messages.length ? messages.map((item, index) => <article className={`case-message ${item.direction}`} key={item.id ?? `${item.createdAt}-${index}`}><div>{item.body}</div><small>{readable(item.senderType)} · {dateTime(item.createdAt)} · {deliveryLabel(item.deliveryState)}{deliveryExplanation(item.deliveryState) ? ` — ${deliveryExplanation(item.deliveryState)}` : ""}</small></article>) : <EmptyLine>No case messages yet.</EmptyLine>}</div></section>
129
173
 
130
174
  {findings.length ? <section><h3>Component findings</h3><div className="finding-result-list">{findings.map((finding, index) => <article key={finding.id ?? `${finding.findingType}-${index}`}><div><strong>{finding.findingType}</strong><small>{finding.summary}{finding.componentVersionId ? ` · component ${finding.componentVersionId}` : ""}{finding.expiresAt ? ` · expires ${dateTime(finding.expiresAt)}` : ""}</small>{finding.data ? <pre>{JSON.stringify(finding.data, null, 2)}</pre> : null}</div><span className={`badge${finding.freshness !== "available" ? " urgent" : ""}`}>{readable(finding.freshness || finding.status)}</span></article>)}</div></section> : null}
131
175
 
132
176
  {decisions.length ? <section><h3>Decisions &amp; rationale</h3><div className="decision-record-list">{decisions.map((decision, index) => <article className="decision-record" key={decision.id ?? `${decision.createdAt}-${index}`}><div><strong>{readable(decision.decisionCode)}</strong><span className="badge">{decision.policyCode || "Policy decision"}</span></div><p>{decision.rationale || "No rationale recorded."}</p><small>{readable(decision.makerType)} · {dateTime(decision.createdAt)} · {(decision.evidenceReferences || []).length} evidence reference(s)</small></article>)}</div></section> : null}
133
177
 
134
- {deliveries.length ? <section><h3>Decision and delivery</h3><div className="delivery-list">{deliveries.map((delivery) => <article key={delivery.id}><div><strong>{delivery.title}</strong><small>{delivery.detail}</small></div><span className={`badge${["failed", "rejected"].includes(delivery.state) ? " urgent" : ""}`}>{readable(delivery.state)}</span></article>)}</div></section> : null}
178
+ {deliveries.length ? <section><h3>Decision and delivery</h3><div className="delivery-list">{deliveries.map((delivery) => <article key={delivery.id}><div><strong>{delivery.title}</strong><small>{delivery.detail}{deliveryExplanation(delivery.state) ? ` · ${deliveryExplanation(delivery.state)}` : ""}</small></div><span className={`badge${["failed", "rejected", "bounced", "suppressed"].includes(delivery.state) ? " urgent" : ""}`}>{deliveryLabel(delivery.state)}</span></article>)}</div></section> : null}
135
179
 
136
180
  {aiRuns.length ? <section><h3>Bounded AI record</h3><div className="ai-run-list">{aiRuns.map((run, index) => <article key={run.id ?? `${run.model}-${index}`}><div><strong>{readable(run.mode)} · {readable(run.status)}</strong><small>{run.model} · {run.promptVersion} · {run.configVersionId}</small></div><p>{run.summary || readable(run.errorCode || "No validated result")}</p><small>{readable(run.outcome || "no outcome")} · {run.policyCode || "no policy"} · {run.estimatedCostMicrousd || 0} µUSD{run.uncertain ? " · escalated for uncertainty" : ""}</small></article>)}</div></section> : null}
137
181
 
138
182
  <div className="claim-bar"><p>{activeClaim && report.claim ? <><AnalystAvatar analyst={report.claim.analyst ?? { displayName: "Another reviewer" }} size="small" />{mine ? "You hold" : `${report.claim.analyst?.displayName || "Another reviewer"} holds`} this report until {new Date(report.claim.expiresAt).toLocaleTimeString()}.</> : "This report must be claimed before a reviewer changes it."}</p><div>{mine ? <button className="secondary" type="button" disabled={changingClaim} onClick={dispatchReleaseReport}>{pendingAction === "release" ? "Releasing…" : "Release"}</button> : null}{claimable && !activeClaim ? <button className="primary" type="button" disabled={changingClaim} onClick={dispatchClaimReport}>{pendingAction === "claim" ? "Claiming…" : "Claim report"}</button> : null}</div></div>
139
183
 
140
184
  <div className="detail-actions">
141
- <form onSubmit={sendMessage}><h3>Ask the reporter</h3><label>Message<textarea maxLength={4000} required value={message} onChange={(event) => setMessage(event.currentTarget.value)} disabled={!canMessage || pendingAction === "message"} /></label><label className="checkbox"><input type="checkbox" checked={awaitReporter} onChange={(event) => setAwaitReporter(event.currentTarget.checked)} disabled={!canMessage || pendingAction === "message"} />Move to awaiting reporter</label><button className="secondary" type="submit" disabled={!canMessage || pendingAction === "message"}>{pendingAction === "message" ? "Sending…" : "Send participant update"}</button></form>
185
+ <form onSubmit={sendMessage}><h3>Ask the reporter</h3><p className="form-help">The update is emailed when the reporter supplied an address. Otherwise, Resolve uses the configured signed customer notification channel.</p><label>Message<textarea maxLength={4000} required value={message} onChange={(event) => setMessage(event.currentTarget.value)} disabled={!canMessage || pendingAction === "message"} /></label><label className="checkbox"><input type="checkbox" checked={awaitReporter} onChange={(event) => setAwaitReporter(event.currentTarget.checked)} disabled={!canMessage || pendingAction === "message"} />Move to awaiting reporter</label><button className="secondary" type="submit" disabled={!canMessage || pendingAction === "message"}>{pendingAction === "message" ? "Queueing…" : "Send participant update"}</button></form>
142
186
  <form onSubmit={recordDecision}><h3>Record a human decision</h3><label>Decision code<input value={decisionCode} onChange={(event) => setDecisionCode(event.currentTarget.value)} disabled={!canDecide || pendingAction === "decision"} required /></label><label>Policy code<select value={policyCode} onChange={(event) => setPolicyCode(event.currentTarget.value)} disabled={!canDecide || pendingAction === "decision"} required>{rules.map((rule) => <option value={rule.code} key={rule.code}>{rule.title ? `${rule.code} · ${rule.title}` : rule.code}</option>)}</select></label><label>Internal rationale<textarea maxLength={8000} value={rationale} onChange={(event) => setRationale(event.currentTarget.value)} disabled={!canDecide || pendingAction === "decision"} required /></label><label>Reporter outcome<textarea maxLength={4000} value={reporterNotice} onChange={(event) => setReporterNotice(event.currentTarget.value)} disabled={!canDecide || pendingAction === "decision"} required /></label><label>Affected-user notice <small>Optional; sent through a separate audience channel.</small><textarea maxLength={4000} value={affectedNotice} onChange={(event) => setAffectedNotice(event.currentTarget.value)} disabled={!canDecide || pendingAction === "decision"} /></label>{actionCodes.length ? <label>Application action <small>Optional. Only deployment-allowlisted actions are shown.</small><select value={actionCode} onChange={(event) => setActionCode(event.currentTarget.value)} disabled={!canDecide || pendingAction === "decision"}><option value="">No application action</option>{actionCodes.map((code) => <option value={code} key={code}>{readable(code)}</option>)}</select></label> : null}<button className="primary" type="submit" disabled={!canDecide || pendingAction === "decision"}>{pendingAction === "decision" ? "Recording…" : "Complete human review"}</button></form>
143
187
  </div>
188
+ <p className="mutation-status" role="status">{statusMessage}</p>
144
189
  <p className="error" role="alert">{error}</p>
145
190
  </>
146
191
  );
@@ -153,7 +198,7 @@ export function ReportDrawer() {
153
198
  return (
154
199
  <dialog className="detail-dialog report-drawer" open={open} aria-labelledby="report-detail-title" onCancel={(event) => { event.preventDefault(); dispatchCloseReport(); }} data-react-slice="report-drawer">
155
200
  <div className="detail-head"><div><h2 id="report-detail-title">{detail.report?.reference || "Report"}</h2><p>{detail.report ? `${readable(detail.report.state)} · ${readable(detail.report.queueId || "unassigned")}` : "Loading report…"}</p></div><button className="icon-button" type="button" aria-label="Close report detail" onClick={dispatchCloseReport}>×</button></div>
156
- <div className="report-detail-content">{detail.loading && !detail.report ? <div className="skeleton-block" role="status" aria-label="Loading report detail" /> : detail.report ? <DetailContent report={detail.report} actorId={snapshot.actorId} generatedAt={snapshot.generatedAt} actionCodes={snapshot.actionCodes} claimReports={snapshot.permissions.claimReports} messageReporters={snapshot.permissions.messageReporters} decideReports={snapshot.permissions.decideReports} pendingAction={detail.pendingAction} messageMutationVersion={detail.messageMutationVersion} error={detail.error} /> : detail.error ? <p className="error surface-error" role="alert">{detail.error}</p> : null}</div>
201
+ <div className="report-detail-content">{detail.loading && !detail.report ? <div className="skeleton-block" role="status" aria-label="Loading report detail" /> : detail.report ? <DetailContent report={detail.report} actorId={snapshot.actorId} generatedAt={snapshot.generatedAt} actionCodes={snapshot.actionCodes} claimReports={snapshot.permissions.claimReports} messageReporters={snapshot.permissions.messageReporters} decideReports={snapshot.permissions.decideReports} pendingAction={detail.pendingAction} messageMutationVersion={detail.messageMutationVersion} statusMessage={detail.statusMessage} error={detail.error} /> : detail.error ? <p className="error surface-error" role="alert">{detail.error}</p> : null}</div>
157
202
  </dialog>
158
203
  );
159
204
  }
@@ -62,7 +62,7 @@ const initialSnapshot = (): ReportWorkspaceSnapshot => ({
62
62
  presence: [],
63
63
  actionCodes: [],
64
64
  permissions: { claimReports: false, messageReporters: false, decideReports: false, readRuns: false },
65
- detail: { reportId: null, loading: false, error: "", pendingAction: null, messageMutationVersion: 0, report: null },
65
+ detail: { reportId: null, loading: false, error: "", statusMessage: "", pendingAction: null, messageMutationVersion: 0, report: null },
66
66
  });
67
67
 
68
68
  let actorId = "";
@@ -223,6 +223,7 @@ async function openReport(reportId: string): Promise<void> {
223
223
  reportId,
224
224
  loading: true,
225
225
  error: "",
226
+ statusMessage: snapshot.detail.reportId === reportId ? snapshot.detail.statusMessage : "",
226
227
  report: snapshot.detail.report?.id === reportId ? snapshot.detail.report : null,
227
228
  },
228
229
  });
@@ -262,7 +263,7 @@ function closeReport(): void {
262
263
  detailRequestVersion += 1;
263
264
  const messageMutationVersion = getReportWorkspaceSnapshot().detail.messageMutationVersion;
264
265
  document.body.classList.remove("report-open");
265
- publish({ detail: { reportId: null, loading: false, error: "", pendingAction: null, messageMutationVersion, report: null } });
266
+ publish({ detail: { reportId: null, loading: false, error: "", statusMessage: "", pendingAction: null, messageMutationVersion, report: null } });
266
267
  sendPresenceActivity();
267
268
  }
268
269
 
@@ -301,15 +302,20 @@ async function sendMessage(input: ReporterMessageInput): Promise<void> {
301
302
  const reportId = snapshot.detail.report?.id;
302
303
  if (!reportId || !snapshot.permissions.messageReporters || snapshot.detail.pendingAction) return;
303
304
  const idempotencyKey = crypto.randomUUID();
304
- publish({ detail: { ...snapshot.detail, error: "", pendingAction: "message" } });
305
+ publish({ detail: { ...snapshot.detail, error: "", statusMessage: "", pendingAction: "message" } });
305
306
  try {
306
- await requestJson(`/v1/admin/reports/${encodeURIComponent(reportId)}/messages`, {
307
+ const response = await requestJson<{ delivery_state?: string; delivery_channel?: string | null }>(`/v1/admin/reports/${encodeURIComponent(reportId)}/messages`, {
307
308
  method: "POST",
308
309
  headers: { "content-type": "application/json", "idempotency-key": idempotencyKey },
309
310
  body: JSON.stringify({ idempotency_key: idempotencyKey, body: input.body, await_reporter: input.awaitReporter }),
310
311
  });
311
312
  const version = getReportWorkspaceSnapshot().detail.messageMutationVersion + 1;
312
- publish({ detail: { ...getReportWorkspaceSnapshot().detail, messageMutationVersion: version } });
313
+ const statusMessage = response.delivery_state === "queued"
314
+ ? response.delivery_channel === "email"
315
+ ? "Participant update queued for email delivery."
316
+ : "Participant update queued for the configured customer notification channel."
317
+ : "The update was saved but not sent because this report has no available participant delivery channel.";
318
+ publish({ detail: { ...getReportWorkspaceSnapshot().detail, messageMutationVersion: version, statusMessage } });
313
319
  await Promise.all([openReport(reportId), loadReports()]);
314
320
  } catch (cause) {
315
321
  publish({ detail: { ...getReportWorkspaceSnapshot().detail, error: errorMessage(cause, "The message could not be created.") } });
@@ -15,7 +15,7 @@ let snapshot: ReportWorkspaceSnapshot = {
15
15
  presence: [],
16
16
  actionCodes: [],
17
17
  permissions: { claimReports: false, messageReporters: false, decideReports: false, readRuns: false },
18
- detail: { reportId: null, loading: false, error: "", pendingAction: null, messageMutationVersion: 0, report: null },
18
+ detail: { reportId: null, loading: false, error: "", statusMessage: "", pendingAction: null, messageMutationVersion: 0, report: null },
19
19
  };
20
20
 
21
21
  window.addEventListener(reportEvents.state, (event) => {
@@ -151,12 +151,33 @@ export interface ReportClaim {
151
151
  analyst?: AnalystIdentityData;
152
152
  }
153
153
 
154
+ export interface ReportAnswerOption {
155
+ value: string;
156
+ label: string;
157
+ }
158
+
159
+ export interface ReportAnswer {
160
+ fieldKey: string;
161
+ label: string;
162
+ type: string;
163
+ required: boolean;
164
+ answered: boolean;
165
+ value: unknown;
166
+ options?: ReportAnswerOption[];
167
+ }
168
+
154
169
  export interface ReportDetailRecord {
155
170
  id: string;
156
171
  reference: string;
157
172
  state: string;
158
173
  queueId?: string;
159
174
  reasonCode: string;
175
+ source?: string;
176
+ locale?: string;
177
+ submittedAt?: string;
178
+ formVersionId?: string;
179
+ formTitle?: string;
180
+ consentNotice?: string;
160
181
  targetType: string;
161
182
  targetReference: string;
162
183
  customerUrl?: string;
@@ -168,6 +189,7 @@ export interface ReportDetailRecord {
168
189
  reviewerInstructions?: string;
169
190
  receivedAt: string;
170
191
  policyRules?: PolicyRule[];
192
+ answers?: ReportAnswer[];
171
193
  evidence?: EvidenceReference[];
172
194
  messages?: CaseMessage[];
173
195
  workflowRuns?: WorkflowRunRecord[];
@@ -189,6 +211,7 @@ export interface ReportDetailSnapshot {
189
211
  error: string;
190
212
  pendingAction: ReportPendingAction;
191
213
  messageMutationVersion: number;
214
+ statusMessage: string;
192
215
  report: ReportDetailRecord | null;
193
216
  }
194
217
 
@@ -94,10 +94,6 @@ function Navigation({ session, currentView, reportCount, open, onClose }: {
94
94
  </Fragment>
95
95
  ))}
96
96
  </nav>
97
- <div className="principle">
98
- <span className="principle-dot" aria-hidden="true" />
99
- <div><strong>Customer-owned records</strong><p>Reports and decisions stay in this deployment.</p></div>
100
- </div>
101
97
  </aside>
102
98
  <button className="nav-scrim" type="button" aria-label="Close navigation" hidden={!open} onClick={onClose} />
103
99
  </>