create-safest-tools 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +71 -0
- package/bin/create-safest-tools.mjs +12 -0
- package/package.json +34 -0
- package/src/cli.mjs +128 -0
- package/src/config.mjs +151 -0
- package/src/scaffold.mjs +30 -0
- package/template/LICENSE +201 -0
- package/template/README.md +35 -0
- package/template/console/README.md +28 -0
- package/template/console/analytics/AnalyticsWorkspace.tsx +55 -0
- package/template/console/analytics/controller.ts +133 -0
- package/template/console/analytics/events.ts +33 -0
- package/template/console/analytics/store.ts +33 -0
- package/template/console/analytics/types.ts +38 -0
- package/template/console/appeals/AppealWorkspace.tsx +73 -0
- package/template/console/appeals/controller.ts +125 -0
- package/template/console/appeals/events.ts +33 -0
- package/template/console/appeals/store.ts +35 -0
- package/template/console/appeals/types.ts +37 -0
- package/template/console/assistant/AssistantWorkspace.tsx +133 -0
- package/template/console/assistant/types.ts +34 -0
- package/template/console/auth/AccountJourney.tsx +69 -0
- package/template/console/auth/InvitationAcceptance.tsx +145 -0
- package/template/console/auth/OAuthButtons.tsx +78 -0
- package/template/console/auth/OperatorLogin.tsx +110 -0
- package/template/console/auth/PasswordRecovery.tsx +132 -0
- package/template/console/auth/account-route.ts +15 -0
- package/template/console/auth/browser-navigation.ts +7 -0
- package/template/console/auth/events.ts +15 -0
- package/template/console/command/CommandCentre.tsx +119 -0
- package/template/console/command/controller.ts +267 -0
- package/template/console/command/events.ts +19 -0
- package/template/console/command/store.ts +36 -0
- package/template/console/command/types.ts +61 -0
- package/template/console/components/AnalystIdentity.tsx +25 -0
- package/template/console/components/PageHeader.tsx +27 -0
- package/template/console/configuration/ConfigurationDialog.tsx +200 -0
- package/template/console/configuration/ConfigurationWorkspace.tsx +66 -0
- package/template/console/configuration/FormBuilder.tsx +143 -0
- package/template/console/configuration/api.ts +40 -0
- package/template/console/configuration/events.ts +14 -0
- package/template/console/configuration/types.ts +79 -0
- package/template/console/lib/format.ts +49 -0
- package/template/console/lib/http.ts +96 -0
- package/template/console/main.tsx +225 -0
- package/template/console/operations/OperationsWorkspace.tsx +126 -0
- package/template/console/operations/controller.ts +237 -0
- package/template/console/operations/events.ts +37 -0
- package/template/console/operations/store.ts +32 -0
- package/template/console/operations/types.ts +107 -0
- package/template/console/people/PeopleWorkspace.tsx +81 -0
- package/template/console/people/types.ts +25 -0
- package/template/console/profile/ProfileWorkspace.tsx +136 -0
- package/template/console/profile/events.ts +9 -0
- package/template/console/profile/types.ts +6 -0
- package/template/console/quality/QualityWorkspace.tsx +87 -0
- package/template/console/quality/controller.ts +154 -0
- package/template/console/quality/events.ts +34 -0
- package/template/console/quality/store.ts +36 -0
- package/template/console/quality/types.ts +70 -0
- package/template/console/queues/QueueEditor.tsx +173 -0
- package/template/console/queues/QueueWorkspace.tsx +109 -0
- package/template/console/queues/controller.ts +194 -0
- package/template/console/queues/events.ts +34 -0
- package/template/console/queues/store.ts +37 -0
- package/template/console/queues/types.ts +102 -0
- package/template/console/registry/RegistryDialog.tsx +198 -0
- package/template/console/registry/RegistryWorkspace.tsx +108 -0
- package/template/console/registry/events.ts +14 -0
- package/template/console/registry/types.ts +52 -0
- package/template/console/reports/ReportDrawer.tsx +159 -0
- package/template/console/reports/ReportWorkspace.tsx +90 -0
- package/template/console/reports/controller.ts +517 -0
- package/template/console/reports/events.ts +42 -0
- package/template/console/reports/store.ts +39 -0
- package/template/console/reports/types.ts +230 -0
- package/template/console/settings/BrandEditor.tsx +126 -0
- package/template/console/settings/ChannelDialog.tsx +241 -0
- package/template/console/settings/SettingsWorkspace.tsx +155 -0
- package/template/console/settings/events.ts +19 -0
- package/template/console/settings/handoff.ts +22 -0
- package/template/console/settings/types.ts +93 -0
- package/template/console/shell/WorkspaceShell.tsx +160 -0
- package/template/console/shell/controller.ts +182 -0
- package/template/console/shell/events.ts +25 -0
- package/template/console/shell/navigation.ts +61 -0
- package/template/console/shell/store.ts +56 -0
- package/template/console/shell/types.ts +86 -0
- package/template/console/workflows/CreateWorkflowDialog.tsx +87 -0
- package/template/console/workflows/WorkflowCanvas.tsx +42 -0
- package/template/console/workflows/WorkflowDialogs.tsx +6 -0
- package/template/console/workflows/WorkflowStudio.tsx +331 -0
- package/template/console/workflows/WorkflowWorkspace.tsx +82 -0
- package/template/console/workflows/events.ts +17 -0
- package/template/console/workflows/graph.ts +156 -0
- package/template/console/workflows/templates.ts +70 -0
- package/template/console/workflows/types.ts +201 -0
- package/template/gitignore.template +18 -0
- package/template/migrations/0001_reports_foundation.sql +444 -0
- package/template/migrations/0002_human_report_loop.sql +65 -0
- package/template/migrations/0003_delivery_reliability.sql +18 -0
- package/template/migrations/0004_public_intake.sql +14 -0
- package/template/migrations/0005_operations_visibility.sql +24 -0
- package/template/migrations/0006_ai_governance.sql +198 -0
- package/template/migrations/0007_ai_release_gates.sql +6 -0
- package/template/migrations/0008_retention_analytics_exports.sql +52 -0
- package/template/migrations/0009_retention_derived_copies.sql +14 -0
- package/template/migrations/0010_ai_quality_controls.sql +26 -0
- package/template/migrations/0011_analyst_presence.sql +28 -0
- package/template/migrations/0012_queue_policies.sql +87 -0
- package/template/migrations/0013_routing_agents.sql +63 -0
- package/template/migrations/0014_webhook_enrichments.sql +122 -0
- package/template/migrations/0015_queue_owned_ai.sql +55 -0
- package/template/migrations/0016_operator_accounts.sql +67 -0
- package/template/migrations/0017_operator_profiles_and_recovery.sql +33 -0
- package/template/migrations/0018_platform_configuration.sql +382 -0
- package/template/migrations/0019_workflow_authoring_runtime.sql +372 -0
- package/template/migrations/0020_tasks_findings_assistant_budgets.sql +427 -0
- package/template/migrations/0021_abuse_evidence_operations.sql +324 -0
- package/template/migrations/0022_workflow_dispatch_operations.sql +42 -0
- package/template/migrations/0023_component_connection_execution.sql +75 -0
- package/template/migrations/0024_access_runtime_integrity.sql +72 -0
- package/template/migrations/0025_installation_timezone.sql +11 -0
- package/template/migrations/0026_ai_and_egress_execution_controls.sql +49 -0
- package/template/migrations/0027_prompt_and_ai_registry.sql +37 -0
- package/template/migrations/0028_step_attempt_ai_provenance.sql +13 -0
- package/template/migrations/0029_evidence_fetch_transport.sql +5 -0
- package/template/migrations/0030_evidence_dlq_incidents.sql +45 -0
- package/template/migrations/0031_shadow_quality_integrity.sql +7 -0
- package/template/migrations/0032_action_delivery_outbox.sql +55 -0
- package/template/migrations/0033_configuration_and_assistant_drafts.sql +43 -0
- package/template/migrations/0034_installation_integrations.sql +31 -0
- package/template/migrations/0035_workspace_governance.sql +21 -0
- package/template/migrations/0036_published_routing_baseline.sql +15 -0
- package/template/migrations/0037_builtin_phishing_specialist.sql +71 -0
- package/template/migrations/0038_remove_deprecated_enrichment_runtime.sql +228 -0
- package/template/migrations/0039_secure_reporting_channels.sql +45 -0
- package/template/migrations/0040_notification_only_reporting.sql +24 -0
- package/template/migrations/0041_better_auth_credentials.sql +17 -0
- package/template/package.json +47 -0
- package/template/public/_headers +27 -0
- package/template/public/app-icon-192.png +0 -0
- package/template/public/app-icon-512.png +0 -0
- package/template/public/brand-icon.svg +7 -0
- package/template/public/brand-tokens.css +80 -0
- package/template/public/console/auth-shell.js +19340 -0
- package/template/public/customer-brand.js +58 -0
- package/template/public/embed/embed.css +139 -0
- package/template/public/embed/embed.js +408 -0
- package/template/public/embed/index.html +91 -0
- package/template/public/favicon.svg +7 -0
- package/template/public/fonts/Manrope-Variable.ttf +0 -0
- package/template/public/fonts/Newsreader-Italic-Variable.ttf +0 -0
- package/template/public/fonts/Newsreader-Variable.ttf +0 -0
- package/template/public/index.html +123 -0
- package/template/public/logo-primary.svg +7 -0
- package/template/public/logo-reversed.svg +7 -0
- package/template/public/manifest.webmanifest +21 -0
- package/template/public/public-report.js +184 -0
- package/template/public/report/index.html +39 -0
- package/template/public/report/public-report.css +20 -0
- package/template/public/social-card.png +0 -0
- package/template/public/styles.css +1521 -0
- package/template/public/widget.css +80 -0
- package/template/public/widget.js +220 -0
- package/template/reports.config.example.json +38 -0
- package/template/reports.schema.json +89 -0
- package/template/scripts/reports-auth-onboarding.mjs +180 -0
- package/template/scripts/reports-backup.mjs +276 -0
- package/template/scripts/reports-cloudflare-preflight.mjs +152 -0
- package/template/scripts/reports-deploy.mjs +173 -0
- package/template/scripts/reports-plan.mjs +226 -0
- package/template/scripts/reports-restore.mjs +180 -0
- package/template/scripts/reports-secrets.mjs +73 -0
- package/template/scripts/reports-uninstall-plan.mjs +32 -0
- package/template/src/agent-executor.ts +330 -0
- package/template/src/ai-observability.ts +163 -0
- package/template/src/ai-registry-validation.ts +244 -0
- package/template/src/ai-registry.ts +292 -0
- package/template/src/api-cursor.ts +84 -0
- package/template/src/assistant.ts +554 -0
- package/template/src/audit.ts +39 -0
- package/template/src/backup-service.ts +269 -0
- package/template/src/budget-control.ts +127 -0
- package/template/src/component-executor.ts +845 -0
- package/template/src/configuration-registry.ts +550 -0
- package/template/src/connection-egress.ts +430 -0
- package/template/src/connection-oauth.ts +368 -0
- package/template/src/evidence-service.ts +335 -0
- package/template/src/human-tasks.ts +316 -0
- package/template/src/index.ts +4114 -0
- package/template/src/installation-admin.ts +301 -0
- package/template/src/intake-abuse.ts +156 -0
- package/template/src/platform-registry-validation.ts +367 -0
- package/template/src/platform-registry.ts +753 -0
- package/template/src/report-admin.ts +342 -0
- package/template/src/report-ai-quality.ts +390 -0
- package/template/src/report-ai-validation.ts +80 -0
- package/template/src/report-ai.ts +857 -0
- package/template/src/report-auth.ts +292 -0
- package/template/src/report-better-auth.ts +583 -0
- package/template/src/report-context-schema.ts +132 -0
- package/template/src/report-context.ts +126 -0
- package/template/src/report-crypto.ts +142 -0
- package/template/src/report-delivery.ts +579 -0
- package/template/src/report-form-validation.ts +51 -0
- package/template/src/report-governance.ts +510 -0
- package/template/src/report-http.ts +90 -0
- package/template/src/report-operations.ts +685 -0
- package/template/src/report-operator-accounts.ts +810 -0
- package/template/src/report-presence.ts +398 -0
- package/template/src/report-queue-validation.ts +309 -0
- package/template/src/report-queues.ts +478 -0
- package/template/src/report-repository.ts +972 -0
- package/template/src/report-router-agent.ts +328 -0
- package/template/src/report-router-validation.ts +81 -0
- package/template/src/report-routing.ts +178 -0
- package/template/src/report-turnstile.ts +142 -0
- package/template/src/report-types.ts +224 -0
- package/template/src/report-validation.ts +279 -0
- package/template/src/report-workflow-validation.ts +119 -0
- package/template/src/report-workflow.ts +886 -0
- package/template/src/shadow-quality.ts +278 -0
- package/template/src/workflow-actions.ts +782 -0
- package/template/src/workflow-compiler.ts +230 -0
- package/template/src/workflow-dynamic-runtime.ts +613 -0
- package/template/src/workflow-effects.ts +316 -0
- package/template/src/workflow-expressions.ts +191 -0
- package/template/src/workflow-platform-types.ts +121 -0
- package/template/src/workflow-platform-validation.ts +540 -0
- package/template/src/workflow-repository.ts +916 -0
- package/template/src/workflow-runs.ts +686 -0
- package/template/src/workflow-simulator.ts +211 -0
- package/template/src/workspace-branding.ts +128 -0
- package/template/src/workspace-governance.ts +279 -0
- package/template/tsconfig.console.json +23 -0
- package/template/tsconfig.json +22 -0
- package/template/vite.console.config.ts +20 -0
- package/template/worker-configuration.d.ts +65 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState, useSyncExternalStore, type FormEvent } from "react";
|
|
2
|
+
import { AnalystAvatar } from "../components/AnalystIdentity";
|
|
3
|
+
import { dateTime, readable, relativeTime, safeCustomerUrl } from "../lib/format";
|
|
4
|
+
import {
|
|
5
|
+
dispatchClaimReport,
|
|
6
|
+
dispatchCloseReport,
|
|
7
|
+
dispatchHumanDecision,
|
|
8
|
+
dispatchReleaseReport,
|
|
9
|
+
dispatchReporterMessage,
|
|
10
|
+
} from "./events";
|
|
11
|
+
import { getReportWorkspaceSnapshot, subscribeToReportWorkspace } from "./store";
|
|
12
|
+
import type { ReportDetailRecord, TimelineEvent } from "./types";
|
|
13
|
+
|
|
14
|
+
function Fact({ label, children }: { label: string; children: React.ReactNode }) {
|
|
15
|
+
return <><dt>{label}</dt><dd>{children || "—"}</dd></>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function EmptyLine({ children }: { children: React.ReactNode }) {
|
|
19
|
+
return <p className="detail-empty">{children}</p>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function timelineDetail(event: TimelineEvent): string {
|
|
23
|
+
const details = event.details ?? {};
|
|
24
|
+
const value = (key: string): string => typeof details[key] === "string" || typeof details[key] === "number" ? String(details[key]) : "";
|
|
25
|
+
if (event.eventType === "state_transition") return `${readable(value("from_state") || "received")} → ${readable(value("to_state") || event.code)}`;
|
|
26
|
+
if (event.eventType === "finding") return value("summary") || readable(event.code);
|
|
27
|
+
if (event.eventType === "action") return `${readable(value("action_code") || "action")} · ${readable(event.code)}`;
|
|
28
|
+
if (event.eventType === "message") return `${readable(value("audience") || "reporter")} · ${readable(value("delivery_state") || event.code)}`;
|
|
29
|
+
return `${readable(event.code)}${value("error_code") ? ` · ${readable(value("error_code"))}` : ""}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function timelineMarker(eventType: string): string {
|
|
33
|
+
if (eventType === "finding") return "⬡";
|
|
34
|
+
if (eventType === "workflow_run") return "⌘";
|
|
35
|
+
if (eventType === "decision") return "✓";
|
|
36
|
+
if (eventType === "message") return "↗";
|
|
37
|
+
if (eventType === "action") return "!";
|
|
38
|
+
return "•";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function DetailContent({ report, actorId, generatedAt, actionCodes, claimReports, messageReporters, decideReports, pendingAction, messageMutationVersion, error }: {
|
|
42
|
+
report: ReportDetailRecord;
|
|
43
|
+
actorId: string;
|
|
44
|
+
generatedAt: string;
|
|
45
|
+
actionCodes: string[];
|
|
46
|
+
claimReports: boolean;
|
|
47
|
+
messageReporters: boolean;
|
|
48
|
+
decideReports: boolean;
|
|
49
|
+
pendingAction: "claim" | "release" | "message" | "decision" | null;
|
|
50
|
+
messageMutationVersion: number;
|
|
51
|
+
error: string;
|
|
52
|
+
}) {
|
|
53
|
+
const evidence = report.evidence ?? [];
|
|
54
|
+
const messages = report.messages ?? [];
|
|
55
|
+
const workflowRuns = report.workflowRuns ?? [];
|
|
56
|
+
const timeline = report.timeline ?? [];
|
|
57
|
+
const findings = report.findings ?? [];
|
|
58
|
+
const decisions = report.decisions ?? [];
|
|
59
|
+
const aiRuns = report.aiRuns ?? [];
|
|
60
|
+
const rules = report.policyRules ?? [];
|
|
61
|
+
const [selectedEvidence, setSelectedEvidence] = useState<Set<string>>(() => new Set(evidence.map((item) => item.reference)));
|
|
62
|
+
const [message, setMessage] = useState("");
|
|
63
|
+
const [awaitReporter, setAwaitReporter] = useState(true);
|
|
64
|
+
const [decisionCode, setDecisionCode] = useState("no_violation_found");
|
|
65
|
+
const [policyCode, setPolicyCode] = useState(rules[0]?.code ?? "");
|
|
66
|
+
const [rationale, setRationale] = useState("");
|
|
67
|
+
const [reporterNotice, setReporterNotice] = useState("");
|
|
68
|
+
const [affectedNotice, setAffectedNotice] = useState("");
|
|
69
|
+
const [actionCode, setActionCode] = useState("");
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
setSelectedEvidence(new Set(evidence.map((item) => item.reference)));
|
|
73
|
+
setMessage("");
|
|
74
|
+
setAwaitReporter(true);
|
|
75
|
+
setDecisionCode("no_violation_found");
|
|
76
|
+
setPolicyCode(rules[0]?.code ?? "");
|
|
77
|
+
setRationale("");
|
|
78
|
+
setReporterNotice("");
|
|
79
|
+
setAffectedNotice("");
|
|
80
|
+
setActionCode("");
|
|
81
|
+
}, [report.id]);
|
|
82
|
+
useEffect(() => setMessage(""), [messageMutationVersion]);
|
|
83
|
+
|
|
84
|
+
const deliveries = useMemo(() => [
|
|
85
|
+
...(report.actions ?? []).map((action) => ({ id: `action-${action.id ?? action.actionCode}`, title: `Application action · ${readable(action.actionCode)}`, state: action.status, detail: `${action.attemptCount} attempt${action.attemptCount === 1 ? "" : "s"}${action.errorCode ? ` · ${readable(action.errorCode)}` : ""}` })),
|
|
86
|
+
...(report.notices ?? []).map((notice) => ({ id: `notice-${notice.id ?? notice.audience}`, title: `${readable(notice.audience)} notice`, state: notice.deliveryState, detail: `Updated ${relativeTime(notice.updatedAt)}` })),
|
|
87
|
+
...(report.workflowActions ?? []).filter((action) => action.recordKind === "proposal").map((action) => ({ id: `workflow-${action.id ?? action.actionCode}`, title: `Workflow action · ${readable(action.actionCode)}`, state: action.state, detail: `${readable(action.authorityMode || action.recordKind || "proposal")} · ${action.attemptCount || 0} attempt${action.attemptCount === 1 ? "" : "s"}${action.errorCode ? ` · ${readable(action.errorCode)}` : ""}` })),
|
|
88
|
+
], [report]);
|
|
89
|
+
const customerUrl = safeCustomerUrl(report.customerUrl);
|
|
90
|
+
const activeClaim = Boolean(report.claim && Date.parse(report.claim.expiresAt) > Date.parse(generatedAt));
|
|
91
|
+
const mine = Boolean(activeClaim && report.claim?.reviewerId === actorId);
|
|
92
|
+
const claimable = claimReports && ["human_review", "awaiting_reporter", "human_review_requested"].includes(report.state);
|
|
93
|
+
const canMessage = mine && messageReporters;
|
|
94
|
+
const canDecide = mine && decideReports && rules.length > 0;
|
|
95
|
+
const changingClaim = pendingAction === "claim" || pendingAction === "release";
|
|
96
|
+
|
|
97
|
+
const sendMessage = (event: FormEvent<HTMLFormElement>): void => {
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
dispatchReporterMessage({ body: message, awaitReporter });
|
|
100
|
+
};
|
|
101
|
+
const recordDecision = (event: FormEvent<HTMLFormElement>): void => {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
dispatchHumanDecision({ decisionCode, policyCode, rationale, evidenceReferences: [...selectedEvidence], reporterNotice, affectedUserNotice: affectedNotice, actionCode });
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
<div className="detail-grid">
|
|
109
|
+
<section><h3>Allegation and target</h3><dl>
|
|
110
|
+
<Fact label="Allegation">{readable(report.reasonCode)}</Fact>
|
|
111
|
+
<Fact label="Target type">{readable(report.targetType)}</Fact>
|
|
112
|
+
<Fact label="Target reference">{report.targetReference}</Fact>
|
|
113
|
+
<Fact label="Customer content">{customerUrl ? <a className="text-button" href={customerUrl} target="_blank" rel="noopener noreferrer" referrerPolicy="no-referrer">Open in customer application ↗</a> : "Not provided"}</Fact>
|
|
114
|
+
<Fact label="Owner reference">{report.ownerReference || "—"}</Fact>
|
|
115
|
+
<Fact label="Reporter reference">{report.reporterReference || "—"}</Fact>
|
|
116
|
+
<Fact label="Adjudication policy">{report.policyTitle || "Policy"} · {report.policyVersionId || "unavailable"}</Fact>
|
|
117
|
+
<Fact label="Queue policy version">{report.queuePolicyVersionId || "—"}</Fact>
|
|
118
|
+
<Fact label="Reviewer policy">{report.reviewerInstructions || "—"}</Fact>
|
|
119
|
+
<Fact label="Received">{dateTime(report.receivedAt)}</Fact>
|
|
120
|
+
</dl></section>
|
|
121
|
+
<section><h3>Evidence references considered</h3><div className="evidence-list">
|
|
122
|
+
{evidence.length ? evidence.map((item) => <label className="evidence-item checkbox" key={item.reference}><input type="checkbox" checked={selectedEvidence.has(item.reference)} onChange={(event) => setSelectedEvidence((current) => { const next = new Set(current); if (event.currentTarget.checked) next.add(item.reference); else next.delete(item.reference); return next; })} /><span>{item.reference} · {readable(item.kind)} · {readable(item.availability)}</span></label>) : <EmptyLine>No evidence references were recorded.</EmptyLine>}
|
|
123
|
+
</div></section>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{workflowRuns.length || timeline.length ? <section><h3>Workflow & 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
|
+
|
|
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>
|
|
129
|
+
|
|
130
|
+
{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
|
+
|
|
132
|
+
{decisions.length ? <section><h3>Decisions & 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
|
+
|
|
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}
|
|
135
|
+
|
|
136
|
+
{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
|
+
|
|
138
|
+
<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
|
+
|
|
140
|
+
<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>
|
|
142
|
+
<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
|
+
</div>
|
|
144
|
+
<p className="error" role="alert">{error}</p>
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function ReportDrawer() {
|
|
150
|
+
const snapshot = useSyncExternalStore(subscribeToReportWorkspace, getReportWorkspaceSnapshot, getReportWorkspaceSnapshot);
|
|
151
|
+
const detail = snapshot.detail;
|
|
152
|
+
const open = Boolean(detail.reportId);
|
|
153
|
+
return (
|
|
154
|
+
<dialog className="detail-dialog report-drawer" open={open} aria-labelledby="report-detail-title" onCancel={(event) => { event.preventDefault(); dispatchCloseReport(); }} data-react-slice="report-drawer">
|
|
155
|
+
<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>
|
|
157
|
+
</dialog>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useSyncExternalStore, type ChangeEvent, type MouseEvent } from "react";
|
|
2
|
+
import { AnalystIdentity } from "../components/AnalystIdentity";
|
|
3
|
+
import { PageHeader } from "../components/PageHeader";
|
|
4
|
+
import { readable, relativeTime } from "../lib/format";
|
|
5
|
+
import { dispatchOpenReport, dispatchReportFilters, dispatchReportRefresh } from "./events";
|
|
6
|
+
import { getReportWorkspaceSnapshot, subscribeToReportWorkspace } from "./store";
|
|
7
|
+
import type { PresenceAnalyst, ReportFilters, ReportPriorityFilter, ReportSort, ReportSummary } from "./types";
|
|
8
|
+
|
|
9
|
+
function priorityName(priority: number): "high" | "medium" | "low" {
|
|
10
|
+
if (priority >= 70) return "high";
|
|
11
|
+
if (priority >= 40) return "medium";
|
|
12
|
+
return "low";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Presence({ analysts }: { analysts: PresenceAnalyst[] }) {
|
|
16
|
+
return (
|
|
17
|
+
<section className="active-analysts" aria-labelledby="active-analysts-title">
|
|
18
|
+
<div><strong id="active-analysts-title">Live now</strong><span>{analysts.length}</span></div>
|
|
19
|
+
<div className="active-analyst-list">
|
|
20
|
+
{analysts.length ? analysts.map((analyst) => (
|
|
21
|
+
<div className="active-analyst" key={analyst.actorId}>
|
|
22
|
+
<AnalystIdentity analyst={analyst} detail={analyst.activityLabel} />
|
|
23
|
+
<span className={`presence-dot ${analyst.activity}`} title={analyst.activityLabel} />
|
|
24
|
+
</div>
|
|
25
|
+
)) : <p className="active-analyst-empty">No active sessions</p>}
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function Owner({ report }: { report: ReportSummary }) {
|
|
32
|
+
if (!report.owner) return <span className="unclaimed">Unclaimed</span>;
|
|
33
|
+
return <AnalystIdentity analyst={report.owner} detail={report.owner.mine ? "You" : "Claimed"} />;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ReportRow({ report }: { report: ReportSummary }) {
|
|
37
|
+
const open = (): void => dispatchOpenReport(report.id);
|
|
38
|
+
const openFromButton = (event: MouseEvent<HTMLButtonElement>): void => {
|
|
39
|
+
event.stopPropagation();
|
|
40
|
+
open();
|
|
41
|
+
};
|
|
42
|
+
const priority = priorityName(report.priority);
|
|
43
|
+
return (
|
|
44
|
+
<tr onClick={open}>
|
|
45
|
+
<td><div className="report-identity"><span className="report-avatar" aria-hidden="true">{String(report.targetReference || report.reference).slice(0, 2).toUpperCase()}</span><span><button className="report-link" type="button" onClick={openFromButton}>{report.reference}</button><small>{report.targetType}: {report.targetReference} · {readable(report.reasonCode)}</small></span></div></td>
|
|
46
|
+
<td><strong>{report.queueName}</strong><small>{readable(report.state)}</small></td>
|
|
47
|
+
<td><span className={`priority-badge ${priority}`}>{readable(priority)}</span></td>
|
|
48
|
+
<td><strong>{relativeTime(report.receivedAt).replace(" ago", "")}</strong><small>{new Date(report.receivedAt).toLocaleString()}</small></td>
|
|
49
|
+
<td><Owner report={report} /></td>
|
|
50
|
+
</tr>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ReportWorkspace() {
|
|
55
|
+
const snapshot = useSyncExternalStore(subscribeToReportWorkspace, getReportWorkspaceSnapshot, getReportWorkspaceSnapshot);
|
|
56
|
+
const updateFilter = <TKey extends keyof ReportFilters>(key: TKey, value: ReportFilters[TKey]): void => {
|
|
57
|
+
dispatchReportFilters({ ...snapshot.filters, [key]: value });
|
|
58
|
+
};
|
|
59
|
+
const search = (event: ChangeEvent<HTMLInputElement>): void => updateFilter("search", event.currentTarget.value);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div data-react-slice="report-workspace">
|
|
63
|
+
<PageHeader title="Reports" description="Review incoming reports and assign ownership." accessory={<span className="live-label"><i />Live queue</span>}>
|
|
64
|
+
<Presence analysts={snapshot.presence} />
|
|
65
|
+
</PageHeader>
|
|
66
|
+
|
|
67
|
+
<section className="surface report-surface" aria-labelledby="inbox-title">
|
|
68
|
+
<h2 id="inbox-title" className="visually-hidden">Accepted reports</h2>
|
|
69
|
+
<div className="surface-head">
|
|
70
|
+
<div className="queue-toolbar">
|
|
71
|
+
<label className="search-field"><span aria-hidden="true">⌕</span><input type="search" placeholder="Search reports" aria-label="Search reports" value={snapshot.filters.search} onChange={search} /></label>
|
|
72
|
+
<label>Queue<select value={snapshot.filters.queueId} onChange={(event) => updateFilter("queueId", event.currentTarget.value)}><option value="">All queues</option>{snapshot.queues.map((queue) => <option value={queue.id} key={queue.id}>{queue.name}</option>)}</select></label>
|
|
73
|
+
<label>Priority<select value={snapshot.filters.priority} onChange={(event) => updateFilter("priority", event.currentTarget.value as ReportPriorityFilter)}><option value="">All priorities</option><option value="high">High</option><option value="medium">Medium</option><option value="low">Low</option></select></label>
|
|
74
|
+
<label>Age<select value={snapshot.filters.age} onChange={(event) => updateFilter("age", event.currentTarget.value as ReportFilters["age"])}><option value="">Any age</option><option value="hour">Under 1 hour</option><option value="day">Under 24 hours</option><option value="older">Older than 24 hours</option></select></label>
|
|
75
|
+
<label>State<select value={snapshot.filters.state} onChange={(event) => updateFilter("state", event.currentTarget.value)}><option value="">All states</option><option value="received">Received</option><option value="human_review">Human review</option><option value="awaiting_reporter">Awaiting reporter</option><option value="resolved_by_human">Resolved by human</option></select></label>
|
|
76
|
+
<label>Sort<select value={snapshot.filters.sort} onChange={(event) => updateFilter("sort", event.currentTarget.value as ReportSort)}><option value="oldest">Oldest first</option><option value="newest">Newest first</option><option value="priority">Priority first</option></select></label>
|
|
77
|
+
</div>
|
|
78
|
+
<button className="icon-button refresh-button" type="button" aria-label="Refresh reports" disabled={snapshot.refreshing} onClick={dispatchReportRefresh}>{snapshot.refreshing ? "…" : "↻"}</button>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="table-wrap" aria-busy={snapshot.loading || snapshot.refreshing}>
|
|
81
|
+
<table><thead><tr><th>Report</th><th>Queue</th><th>Priority</th><th>Age</th><th>Owner</th></tr></thead><tbody>
|
|
82
|
+
{snapshot.loading ? <tr><td colSpan={5}><div className="skeleton-block" /></td></tr> : snapshot.reports.map((report) => <ReportRow report={report} key={report.id} />)}
|
|
83
|
+
</tbody></table>
|
|
84
|
+
{!snapshot.loading && snapshot.reports.length === 0 ? <div className="empty"><span aria-hidden="true">✓</span><h3>No reports in this view</h3><p>New reports will appear here after intake and routing.</p></div> : null}
|
|
85
|
+
</div>
|
|
86
|
+
<p className="error surface-error" role="alert">{snapshot.error}</p>
|
|
87
|
+
</section>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|