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