aidevops 3.32.4 → 3.32.6

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.
@@ -1,5 +1,4 @@
1
1
  import type { GuiVaultCollectionSummary, GuiVaultStatusData } from "@aidevops/gui-shared";
2
- import type { KeyboardEvent, MouseEvent } from "react";
3
2
  import { FiLock, FiUnlock } from "react-icons/fi";
4
3
  import type { SurfaceId } from "./app-model";
5
4
  import { text } from "./app-model";
@@ -24,14 +23,38 @@ export function vaultCollectionTooltip(collection: GuiVaultCollectionSummary): s
24
23
  return text.vaultTooltip;
25
24
  }
26
25
 
27
- export type VaultDialogIntent = "setup" | "unlock" | "lock";
26
+ export type VaultDialogIntent = "setup" | "unlock" | "lock" | "recover" | "unavailable";
27
+
28
+ const vaultActionLabels: Record<VaultDialogIntent, string> = {
29
+ lock: "Lock Vault",
30
+ recover: "Review recovery",
31
+ setup: "Set up Vault",
32
+ unavailable: "Check Vault status",
33
+ unlock: "Unlock Vault",
34
+ };
35
+
36
+ const authoritativeStatusIntents: Partial<Record<GuiVaultStatusData["status"], VaultDialogIntent>> = {
37
+ locked: "unlock",
38
+ uninitialized: "setup",
39
+ };
28
40
 
29
41
  export function vaultDialogIntentForStatus(vault: GuiVaultStatusData): VaultDialogIntent {
42
+ let intent = authoritativeStatusIntents[vault.status] ?? "unavailable";
30
43
  if (vault.unlocked) {
31
- return "lock";
44
+ intent = "lock";
45
+ } else if (vault.status === "corrupted") {
46
+ intent = "recover";
47
+ } else if (vault.helper_status !== "available" || vault.status === "unknown") {
48
+ intent = "unavailable";
49
+ } else if (intent === "setup" && !vaultSetupIsRequired(vault)) {
50
+ intent = "unavailable";
32
51
  }
33
52
 
34
- return vault.readiness.setup_required || !vault.initialized ? "setup" : "unlock";
53
+ return intent;
54
+ }
55
+
56
+ export function vaultActionLabel(intent: VaultDialogIntent): string {
57
+ return vaultActionLabels[intent];
35
58
  }
36
59
 
37
60
  export function VaultPadlock({ collection, compact = false, onActivate, vault }: {
@@ -40,41 +63,78 @@ export function VaultPadlock({ collection, compact = false, onActivate, vault }:
40
63
  onActivate?: (intent: VaultDialogIntent) => void;
41
64
  vault: GuiVaultStatusData;
42
65
  }) {
43
- const locked = collection.state !== "unlocked" || !vault.unlocked;
44
- const stateLabel = locked ? "Locked" : "Unlocked";
45
- const Icon = locked ? FiLock : FiUnlock;
46
- const tooltip = `${stateLabel}: ${vaultCollectionTooltip(collection)}`;
47
- const className = compact ? "vault-padlock compact" : "vault-padlock";
48
-
49
- const activate = (event: MouseEvent<HTMLSpanElement> | KeyboardEvent<HTMLSpanElement>) => {
50
- if (onActivate === undefined) {
51
- return;
52
- }
53
-
54
- event.preventDefault();
55
- event.stopPropagation();
56
- onActivate(vaultDialogIntentForStatus(vault));
57
- };
66
+ const intent = vaultDialogIntentForStatus(vault);
67
+ const presentation = vaultPadlockPresentation(collection, compact, onActivate !== undefined, intent, vault);
68
+ const content = <PadlockContent presentation={presentation} />;
58
69
 
59
- const handleKeyDown = (event: KeyboardEvent<HTMLSpanElement>) => {
60
- if (event.key === "Enter" || event.key === " ") {
61
- activate(event);
62
- }
63
- };
70
+ if (onActivate === undefined) {
71
+ return <span aria-label={presentation.tooltip} className={presentation.className} data-vault-state={presentation.state} role="img" title={presentation.tooltip}>{content}</span>;
72
+ }
64
73
 
65
74
  return (
66
- <span
67
- aria-label={tooltip}
68
- className={onActivate ? `${className} interactive` : className}
69
- data-vault-state={locked ? "locked" : "unlocked"}
70
- onClick={activate}
71
- onKeyDown={handleKeyDown}
72
- role={onActivate ? "button" : undefined}
73
- tabIndex={onActivate ? 0 : undefined}
74
- title={tooltip}
75
+ <button
76
+ aria-label={presentation.tooltip}
77
+ className={presentation.className}
78
+ data-vault-state={presentation.state}
79
+ onClick={(event) => {
80
+ event.preventDefault();
81
+ event.stopPropagation();
82
+ onActivate(intent);
83
+ }}
84
+ title={presentation.tooltip}
85
+ type="button"
75
86
  >
76
- <Icon aria-hidden="true" focusable="false" />
77
- <span>{compact ? stateLabel : `${stateLabel} by Vault`}</span>
78
- </span>
87
+ {content}
88
+ </button>
79
89
  );
80
90
  }
91
+
92
+ interface VaultPadlockPresentation {
93
+ className: string;
94
+ label: string;
95
+ locked: boolean;
96
+ state: "locked" | "unlocked";
97
+ tooltip: string;
98
+ }
99
+
100
+ function vaultPadlockPresentation(collection: GuiVaultCollectionSummary, compact: boolean, interactive: boolean, intent: VaultDialogIntent, vault: GuiVaultStatusData): VaultPadlockPresentation {
101
+ const locked = collection.state !== "unlocked" || !vault.unlocked;
102
+ const stateLabel = vaultStateLabel(intent, locked);
103
+ const compactLabel = compact || intent === "recover" || intent === "unavailable";
104
+ const classNames = ["vault-padlock"];
105
+ if (compact) classNames.push("compact");
106
+ if (interactive) classNames.push("interactive");
107
+
108
+ return {
109
+ className: classNames.join(" "),
110
+ label: compactLabel ? stateLabel : `${stateLabel} by Vault`,
111
+ locked,
112
+ state: locked ? "locked" : "unlocked",
113
+ tooltip: vaultStateTooltip(intent, stateLabel, collection),
114
+ };
115
+ }
116
+
117
+ function PadlockContent({ presentation }: { presentation: VaultPadlockPresentation }) {
118
+ const Icon = presentation.locked ? FiLock : FiUnlock;
119
+ return <><Icon aria-hidden="true" focusable="false" /><span>{presentation.label}</span></>;
120
+ }
121
+
122
+ function vaultStateLabel(intent: VaultDialogIntent, locked: boolean): string {
123
+ const intentLabels: Partial<Record<VaultDialogIntent, string>> = {
124
+ recover: "Recovery required",
125
+ unavailable: "Status unavailable",
126
+ };
127
+ return intentLabels[intent] ?? (locked ? "Locked" : "Unlocked");
128
+ }
129
+
130
+ function vaultStateTooltip(intent: VaultDialogIntent, stateLabel: string, collection: GuiVaultCollectionSummary): string {
131
+ const intentTooltips: Partial<Record<VaultDialogIntent, string>> = {
132
+ recover: "Recovery required: Vault metadata is damaged; preserve encrypted data.",
133
+ unavailable: "Status unavailable: protected content remains hidden until Vault state is authoritative.",
134
+ };
135
+ return intentTooltips[intent] ?? `${stateLabel}: ${vaultCollectionTooltip(collection)}`;
136
+ }
137
+
138
+ function vaultSetupIsRequired(vault: GuiVaultStatusData): boolean {
139
+ return vault.status === "uninitialized" && vault.setup_state === "uninitialized" && vault.readiness.setup_required;
140
+ }
@@ -0,0 +1,282 @@
1
+ import type { GuiStatusData, GuiVaultCollectionSummary, GuiVaultStatusData } from "@aidevops/gui-shared";
2
+ import type { ReactElement } from "react";
3
+ import { text } from "./app-model";
4
+ import { type VaultDialogIntent, VaultPadlock, vaultActionLabel, vaultDialogIntentForStatus } from "./VaultBadges";
5
+
6
+ interface VaultAvailability {
7
+ custodyDetail: string;
8
+ custodyValue: string;
9
+ readinessUnknown: boolean;
10
+ }
11
+
12
+ interface VaultSurfaceProps {
13
+ onVaultRequest: (intent: VaultDialogIntent) => void;
14
+ status: GuiStatusData;
15
+ }
16
+
17
+ export function VaultSurface({ onVaultRequest, status }: VaultSurfaceProps): ReactElement {
18
+ const vault = status.vault;
19
+ const availability = vaultAvailability(vault);
20
+
21
+ return (
22
+ <section className="surface-page vault-surface" aria-label={text.vault}>
23
+ <VaultHero availability={availability} onVaultRequest={onVaultRequest} vault={vault} />
24
+ <VaultNotice availability={availability} vault={vault} />
25
+ <VaultFeatureGrid availability={availability} vault={vault} />
26
+ <VaultSetupPanel availability={availability} onVaultRequest={onVaultRequest} vault={vault} />
27
+ <VaultCollectionsPanel onVaultRequest={onVaultRequest} vault={vault} />
28
+ <VaultDevicesPanel vault={vault} />
29
+ </section>
30
+ );
31
+ }
32
+
33
+ export function LockedVaultGate({ collection, label, onVaultRequest, vault }: {
34
+ collection: GuiVaultCollectionSummary;
35
+ label: string;
36
+ onVaultRequest: (intent: VaultDialogIntent) => void;
37
+ vault: GuiVaultStatusData;
38
+ }): ReactElement {
39
+ const intent = vaultDialogIntentForStatus(vault);
40
+ const copy = lockedGateCopy(intent, label, vault);
41
+
42
+ return (
43
+ <section className="panel vault-locked-gate" aria-label={copy.heading}>
44
+ <div className="section-heading split-heading">
45
+ <div>
46
+ <p className="eyebrow">{collection.data_class}</p>
47
+ <h2>{copy.heading}</h2>
48
+ <p>{copy.summary}</p>
49
+ </div>
50
+ <VaultPadlock collection={collection} onActivate={onVaultRequest} vault={vault} />
51
+ </div>
52
+ <div className="notice compact-notice" role="note">{copy.detail}</div>
53
+ <button className="secondary-action vault-cta" onClick={() => onVaultRequest(intent)} title={copy.detail} type="button">{vaultActionLabel(intent)}</button>
54
+ </section>
55
+ );
56
+ }
57
+
58
+ function VaultHero({ availability, onVaultRequest, vault }: { availability: VaultAvailability; onVaultRequest: (intent: VaultDialogIntent) => void; vault: GuiVaultStatusData }): ReactElement {
59
+ const vaultCollection = vault.collections.find((collection) => collection.surface_ids.includes("vault")) ?? vault.collections[0];
60
+ return (
61
+ <div className="hero-panel vault-hero">
62
+ <div className="section-heading split-heading">
63
+ <div>
64
+ <p className="eyebrow">{vault.value_policy}</p>
65
+ <h2>{text.vault}</h2>
66
+ <p>{text.vaultIntro}</p>
67
+ </div>
68
+ {vaultCollection ? <VaultPadlock collection={vaultCollection} onActivate={onVaultRequest} vault={vault} /> : null}
69
+ </div>
70
+ <ul aria-label="Vault readiness" className="vault-readiness-strip">
71
+ {vaultReadiness(vault, availability.readinessUnknown).map((item) => <li key={item.label}><strong>{item.label}</strong>{item.value}</li>)}
72
+ </ul>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ function VaultNotice({ availability, vault }: { availability: VaultAvailability; vault: GuiVaultStatusData }): ReactElement {
78
+ const notice = vaultNoticeCopy(vault, availability.readinessUnknown);
79
+ return <div className={`notice compact-notice${notice.warning ? " warning-notice" : ""}`} role="note">{notice.detail}</div>;
80
+ }
81
+
82
+ function VaultFeatureGrid({ availability, vault }: { availability: VaultAvailability; vault: GuiVaultStatusData }): ReactElement {
83
+ return (
84
+ <div className="vault-card-grid">
85
+ {vaultFeatureCards(vault, availability).map((card) => <VaultFeatureCard detail={card.detail} key={card.label} label={card.label} value={card.value} />)}
86
+ </div>
87
+ );
88
+ }
89
+
90
+ function VaultFeatureCard({ detail, label, value }: { detail: string; label: string; value: string }): ReactElement {
91
+ return <article className="vault-feature-card"><span>{label}</span><strong>{value}</strong><small>{detail}</small></article>;
92
+ }
93
+
94
+ function VaultSetupPanel({ availability, onVaultRequest, vault }: { availability: VaultAvailability; onVaultRequest: (intent: VaultDialogIntent) => void; vault: GuiVaultStatusData }): ReactElement {
95
+ const intent = vaultDialogIntentForStatus(vault);
96
+ const detail = availability.readinessUnknown ? availability.custodyDetail : vault.setup_hint;
97
+ return (
98
+ <section className="panel vault-setup-panel" aria-label={text.vaultSetup}>
99
+ <div className="section-heading split-heading">
100
+ <div>
101
+ <p className="eyebrow">{text.vaultSetup}</p>
102
+ <h2>{vaultSetupHeading(vault, availability.readinessUnknown)}</h2>
103
+ <p>{detail}</p>
104
+ </div>
105
+ <button className="secondary-action vault-cta" onClick={() => onVaultRequest(intent)} title={availability.readinessUnknown ? availability.custodyDetail : vault.unlock_hint} type="button">{vaultActionLabel(intent)}</button>
106
+ </div>
107
+ {availability.readinessUnknown ? <VaultUnavailableSetupNotice vault={vault} /> : <VaultSetupSteps unlockHint={vault.unlock_hint} />}
108
+ </section>
109
+ );
110
+ }
111
+
112
+ function VaultUnavailableSetupNotice({ vault }: { vault: GuiVaultStatusData }): ReactElement {
113
+ const detail = vault.status === "corrupted"
114
+ ? "Use recovery guidance only. Preserve the current Vault directory and do not run initialization commands."
115
+ : "Retry authoritative status before following setup or unlock instructions.";
116
+ return <p className="empty-state">{detail}</p>;
117
+ }
118
+
119
+ function VaultSetupSteps({ unlockHint }: { unlockHint: string }): ReactElement {
120
+ return (
121
+ <>
122
+ <ol className="vault-step-list">
123
+ <li>Initialize locally with the hidden-prompt helper.</li>
124
+ <li>Verify the harmless restart test before migrating real data.</li>
125
+ <li>Keep passphrases, recovery material, and private keys out of chat, arguments, environment variables, logs, issues, and fixtures.</li>
126
+ </ol>
127
+ <code>{unlockHint}</code>
128
+ </>
129
+ );
130
+ }
131
+
132
+ function VaultCollectionsPanel({ onVaultRequest, vault }: { onVaultRequest: (intent: VaultDialogIntent) => void; vault: GuiVaultStatusData }): ReactElement {
133
+ return (
134
+ <section className="panel" aria-label="Vault encrypted collections">
135
+ <div className="section-heading">
136
+ <p className="eyebrow">{text.vaultStatus}</p>
137
+ <h2>Encrypted collections</h2>
138
+ <p>{text.vaultCollectionIntro}</p>
139
+ </div>
140
+ <ul className="object-list vault-collection-list">
141
+ {vault.collections.map((collection) => <VaultCollectionRow collection={collection} key={collection.id} onVaultRequest={onVaultRequest} vault={vault} />)}
142
+ </ul>
143
+ </section>
144
+ );
145
+ }
146
+
147
+ function VaultCollectionRow({ collection, onVaultRequest, vault }: { collection: GuiVaultCollectionSummary; onVaultRequest: (intent: VaultDialogIntent) => void; vault: GuiVaultStatusData }): ReactElement {
148
+ return (
149
+ <li>
150
+ <strong>{collection.label}</strong>
151
+ <VaultPadlock collection={collection} compact onActivate={onVaultRequest} vault={vault} />
152
+ <span>{collection.preview_policy}</span>
153
+ <small>{collection.labels.join(", ")}</small>
154
+ <small>{collection.surface_ids.join(", ")}</small>
155
+ </li>
156
+ );
157
+ }
158
+
159
+ function VaultDevicesPanel({ vault }: { vault: GuiVaultStatusData }): ReactElement {
160
+ return (
161
+ <section className="panel" aria-label="Vault devices and audit">
162
+ <div className="section-heading split-heading">
163
+ <div>
164
+ <p className="eyebrow">{text.vaultDevices}</p>
165
+ <h2>Devices, sync, messages, backups, and audit</h2>
166
+ <p>These placeholders expose readiness and redacted metadata only. Git, object storage, messaging, SSH, VPNs, and VPS disks remain untrusted transports.</p>
167
+ </div>
168
+ <span className="count-pill">{vault.sync.transport_policy}</span>
169
+ </div>
170
+ <div className="vault-device-grid">
171
+ {vault.devices.map((device) => (
172
+ <article className="vault-device-card" key={device.id_ref}>
173
+ <p className="eyebrow">{device.trust_state}</p>
174
+ <h3>{device.label}</h3>
175
+ <Detail label="device" value={device.id_ref} />
176
+ <Detail label="last seen" value={device.last_seen} />
177
+ <Detail label="audit head" value={device.audit_head_ref} />
178
+ </article>
179
+ ))}
180
+ </div>
181
+ </section>
182
+ );
183
+ }
184
+
185
+ function Detail({ label, value }: { label: string; value: string }): ReactElement {
186
+ return <span><small>{label}</small><strong>{value}</strong></span>;
187
+ }
188
+
189
+ function vaultAvailability(vault: GuiVaultStatusData): VaultAvailability {
190
+ const readinessUnknown = vault.helper_status !== "available" || vault.status === "unknown" || vault.status === "corrupted";
191
+ let custodyDetail = vault.unlock_hint;
192
+ let custodyValue = vault.locked ? "locked" : "unlocked";
193
+ if (readinessUnknown) {
194
+ custodyDetail = "The local helper did not return authoritative lock metadata.";
195
+ custodyValue = "unavailable";
196
+ }
197
+ if (vault.status === "corrupted") {
198
+ custodyDetail = "Metadata is damaged. Preserve encrypted data and use the recovery guidance.";
199
+ custodyValue = "recovery";
200
+ }
201
+ return { custodyDetail, custodyValue, readinessUnknown };
202
+ }
203
+
204
+ function vaultReadiness(vault: GuiVaultStatusData, readinessUnknown: boolean): Array<{ label: string; value: string }> {
205
+ return [
206
+ { label: "migration", value: migrationReadiness(vault, readinessUnknown) },
207
+ { label: "setup", value: setupReadiness(vault, readinessUnknown) },
208
+ { label: "restart test", value: restartReadiness(vault, readinessUnknown) },
209
+ { label: "remote unlock", value: vault.readiness.remote_unlock_enabled ? "enabled" : "disabled" },
210
+ ];
211
+ }
212
+
213
+ function migrationReadiness(vault: GuiVaultStatusData, readinessUnknown: boolean): string {
214
+ return readinessUnknown ? "unknown" : vault.readiness.migration_allowed ? "ready" : "blocked";
215
+ }
216
+
217
+ function setupReadiness(vault: GuiVaultStatusData, readinessUnknown: boolean): string {
218
+ let value = vault.setup_state === "migration-ready" ? "complete" : "in progress";
219
+ if (vault.readiness.setup_required) value = "required";
220
+ if (readinessUnknown) value = "unknown";
221
+ return value;
222
+ }
223
+
224
+ function restartReadiness(vault: GuiVaultStatusData, readinessUnknown: boolean): string {
225
+ let value = vault.setup_state === "migration-ready" ? "verified" : "pending";
226
+ if (vault.readiness.restart_test_required) value = "required";
227
+ if (vault.status === "uninitialized") value = "not started";
228
+ if (readinessUnknown) value = "unknown";
229
+ return value;
230
+ }
231
+
232
+ function vaultFeatureCards(vault: GuiVaultStatusData, availability: VaultAvailability): Array<{ detail: string; label: string; value: string }> {
233
+ return [
234
+ { label: text.vaultStatus, value: vault.status, detail: "Metadata-only lock state from the local helper." },
235
+ { label: text.vaultSetup, value: vault.setup_state, detail: availability.readinessUnknown ? availability.custodyDetail : vault.setup_hint },
236
+ { label: text.vaultLockUnlock, value: availability.custodyValue, detail: availability.custodyDetail },
237
+ { label: text.vaultDevices, value: `${vault.devices.length} device`, detail: "Device trust metadata only; private keys are never exposed." },
238
+ { label: text.vaultSync, value: vault.sync.status, detail: "Encrypted bundles and signed manifests over untrusted transports." },
239
+ { label: text.vaultMessages, value: vault.secure_messages.status, detail: "Secure message placeholders keep payloads hidden while locked." },
240
+ { label: text.vaultBackups, value: vault.backups.status, detail: "Encrypted backups and recovery flows are metadata-only here." },
241
+ { label: text.vaultAudit, value: vault.audit.status, detail: `${vault.audit.event_count} redacted audit events; ${vault.audit.latest_event_ref}.` },
242
+ ];
243
+ }
244
+
245
+ function vaultNoticeCopy(vault: GuiVaultStatusData, readinessUnknown: boolean): { detail: string; warning: boolean } {
246
+ let detail = vault.locked
247
+ ? `${text.vaultLockedPreview} ${vault.unlock_hint}`
248
+ : "Vault is unlocked for this local session. Protected actions remain read-only until audited write routes are implemented.";
249
+ let warning = false;
250
+ if (readinessUnknown) {
251
+ detail = "Vault lock state is unavailable. Setup and unlock guidance remains disabled until status is authoritative.";
252
+ warning = true;
253
+ }
254
+ if (vault.status === "corrupted") {
255
+ detail = "Vault metadata needs recovery. Preserve existing encrypted data and do not initialise over it.";
256
+ }
257
+ return { detail, warning };
258
+ }
259
+
260
+ function vaultSetupHeading(vault: GuiVaultStatusData, readinessUnknown: boolean): string {
261
+ let heading = vault.readiness.setup_required ? "Setup required" : "Setup metadata";
262
+ if (readinessUnknown) heading = "Setup status unavailable";
263
+ if (vault.status === "corrupted") heading = "Recovery required";
264
+ return heading;
265
+ }
266
+
267
+ function lockedGateCopy(intent: VaultDialogIntent, label: string, vault: GuiVaultStatusData): { detail: string; heading: string; summary: string } {
268
+ let detail = `${text.vaultTooltip} ${vault.unlock_hint}`;
269
+ let heading = `${label} is locked`;
270
+ let summary: string = text.vaultLockedPreview;
271
+ if (intent === "unavailable") {
272
+ detail = "Protected content remains hidden until the local helper returns authoritative status.";
273
+ heading = `${label} Vault status is unavailable`;
274
+ summary = detail;
275
+ }
276
+ if (intent === "recover") {
277
+ detail = "Protected content remains hidden while damaged Vault metadata is reviewed.";
278
+ heading = `${label} needs Vault recovery`;
279
+ summary = detail;
280
+ }
281
+ return { detail, heading, summary };
282
+ }
@@ -6,6 +6,7 @@ import {
6
6
  type GuiFileRootId,
7
7
  type GuiResponseEnvelope,
8
8
  type GuiStatusData,
9
+ type GuiVaultStatusData,
9
10
  statusFixture,
10
11
  } from "../../gui-shared/src";
11
12
 
@@ -28,10 +29,15 @@ export function mockedStatus(): GuiResponseEnvelope<GuiStatusData> {
28
29
  authority: "aidevops helpers",
29
30
  path_refs: ["~/.aidevops/agents", "~/.config/aidevops/settings.json"],
30
31
  },
31
- data: statusFixture,
32
+ data: { ...statusFixture, secrets: [] },
32
33
  });
33
34
  }
34
35
 
36
+ export function unavailableStatus(): GuiResponseEnvelope<GuiStatusData> {
37
+ const envelope = mockedStatus();
38
+ return { ...envelope, data: { ...envelope.data, vault: unavailableVault("error") } };
39
+ }
40
+
35
41
  export async function fetchFileExplorer(
36
42
  rootId: GuiFileRootId,
37
43
  relativePath = "",
@@ -68,6 +74,7 @@ export function mockedFileExplorer(rootId: GuiFileRootId): GuiResponseEnvelope<G
68
74
  function normalizeStatusEnvelope(envelope: GuiResponseEnvelope<Partial<GuiStatusData>>): GuiResponseEnvelope<GuiStatusData> {
69
75
  const data = envelope.data ?? {};
70
76
 
77
+ const vault = normalizeVault(data.vault);
71
78
  return {
72
79
  ...envelope,
73
80
  data: {
@@ -88,31 +95,72 @@ function normalizeStatusEnvelope(envelope: GuiResponseEnvelope<Partial<GuiStatus
88
95
  ai_apps: data.ai_apps ?? statusFixture.ai_apps,
89
96
  managed_apps: data.managed_apps ?? statusFixture.managed_apps,
90
97
  notifications: data.notifications ?? statusFixture.notifications,
91
- vault: {
92
- ...statusFixture.vault,
93
- ...data.vault,
94
- readiness: { ...statusFixture.vault.readiness, ...data.vault?.readiness },
95
- collections: data.vault?.collections ?? statusFixture.vault.collections,
96
- devices: data.vault?.devices ?? statusFixture.vault.devices,
97
- sync: { ...statusFixture.vault.sync, ...data.vault?.sync },
98
- secure_messages: { ...statusFixture.vault.secure_messages, ...data.vault?.secure_messages },
99
- backups: { ...statusFixture.vault.backups, ...data.vault?.backups },
100
- audit: { ...statusFixture.vault.audit, ...data.vault?.audit },
101
- },
102
- pulse_workers: {
103
- ...statusFixture.pulse_workers,
104
- ...data.pulse_workers,
105
- kpis: data.pulse_workers?.kpis ?? statusFixture.pulse_workers.kpis,
106
- attention: data.pulse_workers?.attention ?? statusFixture.pulse_workers.attention,
107
- insights: data.pulse_workers?.insights ?? statusFixture.pulse_workers.insights,
108
- filters: { ...statusFixture.pulse_workers.filters, ...data.pulse_workers?.filters },
109
- charts: data.pulse_workers?.charts ?? statusFixture.pulse_workers.charts,
110
- events: data.pulse_workers?.events ?? statusFixture.pulse_workers.events,
111
- actions: data.pulse_workers?.actions ?? statusFixture.pulse_workers.actions,
112
- },
98
+ vault,
99
+ pulse_workers: normalizePulseWorkers(data.pulse_workers),
113
100
  capabilities: data.capabilities ?? statusFixture.capabilities,
114
- secrets: data.secrets ?? statusFixture.secrets,
101
+ secrets: visibleSecrets(vault, data.secrets),
115
102
  placeholders: data.placeholders ?? statusFixture.placeholders,
116
103
  },
117
104
  };
118
105
  }
106
+
107
+ function normalizePulseWorkers(
108
+ pulseWorkers: GuiStatusData["pulse_workers"] | undefined,
109
+ ): GuiStatusData["pulse_workers"] {
110
+ return {
111
+ ...statusFixture.pulse_workers,
112
+ ...pulseWorkers,
113
+ kpis: pulseWorkers?.kpis ?? statusFixture.pulse_workers.kpis,
114
+ attention: pulseWorkers?.attention ?? statusFixture.pulse_workers.attention,
115
+ insights: pulseWorkers?.insights ?? statusFixture.pulse_workers.insights,
116
+ filters: { ...statusFixture.pulse_workers.filters, ...pulseWorkers?.filters },
117
+ charts: pulseWorkers?.charts ?? statusFixture.pulse_workers.charts,
118
+ events: pulseWorkers?.events ?? statusFixture.pulse_workers.events,
119
+ actions: pulseWorkers?.actions ?? statusFixture.pulse_workers.actions,
120
+ };
121
+ }
122
+
123
+ function visibleSecrets(
124
+ vault: GuiVaultStatusData,
125
+ secrets: GuiStatusData["secrets"] | undefined,
126
+ ): GuiStatusData["secrets"] {
127
+ return vault.status === "unlocked" && vault.unlocked ? secrets ?? [] : [];
128
+ }
129
+
130
+ function normalizeVault(vault: GuiStatusData["vault"] | undefined): GuiVaultStatusData {
131
+ if (vault === undefined || vault.status === undefined || vault.setup_state === undefined) {
132
+ return unavailableVault("unchecked");
133
+ }
134
+ return {
135
+ ...statusFixture.vault,
136
+ ...vault,
137
+ readiness: { ...statusFixture.vault.readiness, ...vault.readiness },
138
+ collections: vault.collections ?? statusFixture.vault.collections,
139
+ devices: vault.devices ?? statusFixture.vault.devices,
140
+ sync: { ...statusFixture.vault.sync, ...vault.sync },
141
+ secure_messages: { ...statusFixture.vault.secure_messages, ...vault.secure_messages },
142
+ backups: { ...statusFixture.vault.backups, ...vault.backups },
143
+ audit: { ...statusFixture.vault.audit, ...vault.audit },
144
+ };
145
+ }
146
+
147
+ function unavailableVault(helperStatus: GuiVaultStatusData["helper_status"]): GuiVaultStatusData {
148
+ return {
149
+ ...statusFixture.vault,
150
+ status: "unknown",
151
+ setup_state: "unknown",
152
+ initialized: false,
153
+ locked: true,
154
+ unlocked: false,
155
+ available: false,
156
+ helper_status: helperStatus,
157
+ readiness: {
158
+ ...statusFixture.vault.readiness,
159
+ migration_allowed: false,
160
+ setup_required: false,
161
+ restart_test_required: false,
162
+ locked_content_hidden: true,
163
+ },
164
+ collections: statusFixture.vault.collections.map((collection) => ({ ...collection, state: collection.state === "planned" ? "planned" : "unknown" })),
165
+ };
166
+ }