@gmickel/gno 1.27.1 → 1.29.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.
Files changed (195) hide show
  1. package/README.md +46 -20
  2. package/assets/skill/SKILL.md +59 -1
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/chunk-b2zm0jjd.js +50 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/browser-extension/dist/preview.html +1 -1
  8. package/browser-extension/dist/service-worker.js +6 -6
  9. package/package.json +2 -2
  10. package/spec/cli.md +82 -5
  11. package/spec/db/schema.sql +31 -0
  12. package/spec/mcp.md +71 -0
  13. package/spec/output-schemas/ask.schema.json +156 -1
  14. package/spec/output-schemas/browser-clip-preview.schema.json +30 -0
  15. package/spec/output-schemas/collection-egress-check.schema.json +91 -0
  16. package/spec/output-schemas/collection-egress-policy-set.schema.json +56 -0
  17. package/spec/output-schemas/collection-egress-policy.schema.json +41 -0
  18. package/spec/output-schemas/context-capsule-v1.schema.json +183 -3
  19. package/spec/output-schemas/context-capsule-verification.schema.json +1 -1
  20. package/spec/output-schemas/egress-audit-management.schema.json +88 -0
  21. package/spec/output-schemas/get.schema.json +94 -0
  22. package/spec/output-schemas/mcp-http-error.schema.json +113 -2
  23. package/spec/output-schemas/mcp-job-status.schema.json +28 -0
  24. package/spec/output-schemas/multi-get.schema.json +128 -0
  25. package/spec/output-schemas/publish-artifact.schema.json +32 -0
  26. package/spec/output-schemas/record-import.schema.json +223 -0
  27. package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
  28. package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
  29. package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
  30. package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
  31. package/spec/output-schemas/search-result.schema.json +94 -0
  32. package/spec/output-schemas/search-results.schema.json +155 -0
  33. package/spec/project-profile.schema.json +66 -0
  34. package/src/app/context-format.ts +1 -1
  35. package/src/app/context-runtime-contract.ts +16 -7
  36. package/src/cli/commands/ask.ts +7 -0
  37. package/src/cli/commands/collection/index.ts +5 -0
  38. package/src/cli/commands/collection/policy.ts +126 -0
  39. package/src/cli/commands/context-build.ts +5 -0
  40. package/src/cli/commands/daemon.ts +45 -3
  41. package/src/cli/commands/egress-audit.ts +68 -0
  42. package/src/cli/commands/embed.ts +2 -0
  43. package/src/cli/commands/get.ts +12 -2
  44. package/src/cli/commands/index-cmd.ts +13 -0
  45. package/src/cli/commands/multi-get.ts +9 -2
  46. package/src/cli/commands/query.ts +12 -0
  47. package/src/cli/commands/replay.ts +13 -3
  48. package/src/cli/commands/shared.ts +28 -0
  49. package/src/cli/commands/update.ts +5 -0
  50. package/src/cli/commands/vsearch.ts +3 -1
  51. package/src/cli/program.ts +127 -0
  52. package/src/config/index.ts +9 -0
  53. package/src/config/project-profile.ts +2 -0
  54. package/src/config/types.ts +66 -0
  55. package/src/converters/adapters/browser-export/adapter.ts +199 -0
  56. package/src/converters/adapters/browser-export/formats.ts +358 -0
  57. package/src/converters/adapters/email/adapter.ts +429 -0
  58. package/src/converters/adapters/email/html.ts +162 -0
  59. package/src/converters/adapters/email/mime.ts +454 -0
  60. package/src/converters/adapters/email/parameters.ts +77 -0
  61. package/src/converters/adapters/ical/adapter.ts +475 -0
  62. package/src/converters/adapters/ical/recurrence.ts +186 -0
  63. package/src/converters/adapters/jsonl/adapter.ts +238 -0
  64. package/src/converters/adapters/jsonl/config.ts +105 -0
  65. package/src/converters/adapters/shared/html-text.ts +165 -0
  66. package/src/converters/adapters/shared/record-utils.ts +79 -0
  67. package/src/converters/adapters/shared/utf8-lines.ts +141 -0
  68. package/src/converters/adapters/transcript/adapter.ts +295 -0
  69. package/src/converters/adapters/transcript/json.ts +184 -0
  70. package/src/converters/adapters/transcript/model.ts +171 -0
  71. package/src/converters/adapters/transcript/text.ts +58 -0
  72. package/src/converters/adapters/transcript/timed.ts +152 -0
  73. package/src/converters/index.ts +11 -1
  74. package/src/converters/mime.ts +8 -0
  75. package/src/converters/pipeline.ts +15 -1
  76. package/src/converters/registry.ts +37 -1
  77. package/src/converters/types.ts +136 -0
  78. package/src/core/browser-clip.ts +16 -1
  79. package/src/core/collection-egress-policy-projection.ts +28 -0
  80. package/src/core/collection-egress-policy-service.ts +443 -0
  81. package/src/core/collection-egress-policy-validation.ts +242 -0
  82. package/src/core/config-mutation.ts +31 -19
  83. package/src/core/context-capsule-schema.ts +198 -1
  84. package/src/core/context-capsule-validation.ts +4 -4
  85. package/src/core/context-capsule-verification.ts +5 -1
  86. package/src/core/context-capsule.ts +177 -112
  87. package/src/core/context-evidence.ts +93 -15
  88. package/src/core/destination-classifier.ts +402 -0
  89. package/src/core/document-capabilities.ts +12 -0
  90. package/src/core/egress-audit.ts +239 -0
  91. package/src/core/egress-authorization.ts +50 -0
  92. package/src/core/egress-enforcement.ts +264 -0
  93. package/src/core/egress-policy.ts +440 -0
  94. package/src/core/egress-provenance.ts +336 -0
  95. package/src/core/job-manager.ts +37 -0
  96. package/src/core/network-boundary-inventory.ts +261 -0
  97. package/src/core/project-profile-apply-state.ts +5 -0
  98. package/src/core/project-profile.ts +4 -0
  99. package/src/core/record-metadata.ts +49 -0
  100. package/src/core/retrieval-qrels.ts +6 -0
  101. package/src/core/retrieval-replay.ts +18 -4
  102. package/src/core/retrieval-trace-export.ts +23 -4
  103. package/src/core/retrieval-trace-management-helpers.ts +1 -0
  104. package/src/core/retrieval-trace-management-types.ts +3 -0
  105. package/src/core/retrieval-trace-management.ts +9 -1
  106. package/src/core/retrieval-trace-session.ts +94 -0
  107. package/src/core/retrieval-trace.ts +22 -0
  108. package/src/ingestion/record-adapter-canonical.ts +433 -0
  109. package/src/ingestion/record-adapter.ts +448 -0
  110. package/src/ingestion/record-container.ts +704 -0
  111. package/src/ingestion/record-path.ts +20 -0
  112. package/src/ingestion/record-sync.ts +70 -0
  113. package/src/ingestion/sync.ts +243 -36
  114. package/src/ingestion/types.ts +71 -1
  115. package/src/ingestion/walker.ts +31 -11
  116. package/src/llm/errors.ts +10 -0
  117. package/src/llm/http-inference.ts +175 -0
  118. package/src/llm/http-policy.ts +537 -0
  119. package/src/llm/httpEmbedding.ts +47 -28
  120. package/src/llm/httpGeneration.ts +31 -18
  121. package/src/llm/httpRerank.ts +30 -18
  122. package/src/llm/index.ts +1 -0
  123. package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
  124. package/src/llm/pinned-http-connection.ts +177 -0
  125. package/src/mcp/context.ts +53 -1
  126. package/src/mcp/http-egress.ts +220 -0
  127. package/src/mcp/http-security.ts +32 -21
  128. package/src/mcp/http-session.ts +13 -2
  129. package/src/mcp/http-transport.ts +96 -6
  130. package/src/mcp/sync-egress.ts +24 -0
  131. package/src/mcp/tools/add-collection.ts +4 -0
  132. package/src/mcp/tools/ask.ts +1 -1
  133. package/src/mcp/tools/context.ts +9 -2
  134. package/src/mcp/tools/egress.ts +247 -0
  135. package/src/mcp/tools/embed.ts +1 -0
  136. package/src/mcp/tools/get.ts +10 -2
  137. package/src/mcp/tools/index-cmd.ts +7 -0
  138. package/src/mcp/tools/index.ts +98 -0
  139. package/src/mcp/tools/multi-get.ts +9 -2
  140. package/src/mcp/tools/query.ts +10 -0
  141. package/src/mcp/tools/remove-collection.ts +4 -0
  142. package/src/mcp/tools/sync.ts +6 -0
  143. package/src/mcp/tools/trace.ts +7 -1
  144. package/src/mcp/tools/vsearch.ts +1 -0
  145. package/src/mcp/tools/workspace-write.ts +2 -0
  146. package/src/pipeline/egress-lineage.ts +124 -0
  147. package/src/pipeline/filters.ts +1 -1
  148. package/src/pipeline/graph-retrieval.ts +7 -4
  149. package/src/pipeline/hybrid.ts +21 -4
  150. package/src/pipeline/result-context.ts +12 -4
  151. package/src/pipeline/search.ts +23 -4
  152. package/src/pipeline/types.ts +6 -0
  153. package/src/pipeline/vsearch.ts +67 -28
  154. package/src/publish/artifact.ts +31 -3
  155. package/src/publish/export-service.ts +25 -0
  156. package/src/sdk/client.ts +119 -2
  157. package/src/sdk/documents.ts +13 -5
  158. package/src/sdk/embed.ts +1 -0
  159. package/src/sdk/types.ts +33 -0
  160. package/src/serve/browse-tree.ts +4 -2
  161. package/src/serve/clipper-capture.ts +5 -1
  162. package/src/serve/closed-json.ts +61 -0
  163. package/src/serve/config-sync.ts +4 -1
  164. package/src/serve/context.ts +1 -0
  165. package/src/serve/doc-events.ts +110 -39
  166. package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
  167. package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
  168. package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
  169. package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
  170. package/src/serve/public/globals.built.css +1 -1
  171. package/src/serve/public/pages/Collections.tsx +3 -0
  172. package/src/serve/resident-request.ts +80 -3
  173. package/src/serve/resident-runtime.ts +63 -1
  174. package/src/serve/routes/api.ts +237 -65
  175. package/src/serve/routes/clipper.ts +64 -1
  176. package/src/serve/routes/mcp.ts +6 -0
  177. package/src/serve/routes/traces.ts +27 -4
  178. package/src/serve/server.ts +118 -7
  179. package/src/store/migrations/022-record-export-lineage.ts +42 -0
  180. package/src/store/migrations/023-collection-egress-policy.ts +61 -0
  181. package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
  182. package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
  183. package/src/store/migrations/index.ts +8 -0
  184. package/src/store/retrieval-trace-codec.ts +15 -0
  185. package/src/store/sqlite/adapter.ts +284 -14
  186. package/src/store/sqlite/change-journal-store.ts +41 -2
  187. package/src/store/sqlite/egress-audit-store.ts +485 -0
  188. package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
  189. package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
  190. package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
  191. package/src/store/sqlite/retrieval-trace-store.ts +104 -5
  192. package/src/store/types.ts +166 -1
  193. package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip +0 -0
  194. package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
  195. package/browser-extension/dist/chunk-vn5f663b.js +0 -50
@@ -8,6 +8,12 @@ import {
8
8
  import { useEffect, useMemo, useState } from "react";
9
9
 
10
10
  import { apiFetch } from "../hooks/use-api";
11
+ import { CollectionEgressCheckPanel } from "./CollectionEgressCheckPanel";
12
+ import {
13
+ CollectionEgressPolicyEditor,
14
+ type CollectionEgressPolicy,
15
+ } from "./CollectionEgressPolicyEditor";
16
+ import { EgressAuditPanel } from "./EgressAuditPanel";
11
17
  import { Button } from "./ui/button";
12
18
  import {
13
19
  Dialog,
@@ -50,6 +56,15 @@ export interface CollectionModelDetails {
50
56
  name: string;
51
57
  path: string;
52
58
  pattern?: string;
59
+ egressPolicy?: {
60
+ schemaVersion: "1.0";
61
+ collection: string;
62
+ configuredPolicy: "local_only" | "lan" | "remote" | null;
63
+ effectivePolicy: "local_only" | "lan" | "remote";
64
+ source: "explicit" | "config_default";
65
+ revision: number;
66
+ version: string;
67
+ } | null;
53
68
  }
54
69
 
55
70
  interface UpdateCollectionResponse {
@@ -58,6 +73,7 @@ interface UpdateCollectionResponse {
58
73
  }
59
74
 
60
75
  interface CollectionModelDialogProps {
76
+ availableCollections?: readonly string[];
61
77
  collection: CollectionModelDetails | null;
62
78
  onOpenChange: (open: boolean) => void;
63
79
  onSaved: () => void;
@@ -119,6 +135,7 @@ function collectionLooksCodeHeavy(collection: CollectionModelDetails): boolean {
119
135
  }
120
136
 
121
137
  export function CollectionModelDialog({
138
+ availableCollections = [],
122
139
  collection,
123
140
  onOpenChange,
124
141
  onSaved,
@@ -132,6 +149,9 @@ export function CollectionModelDialog({
132
149
  });
133
150
  const [error, setError] = useState<string | null>(null);
134
151
  const [saving, setSaving] = useState(false);
152
+ const [policyDraft, setPolicyDraft] =
153
+ useState<CollectionEgressPolicy>("local_only");
154
+ const [relaxationConfirmed, setRelaxationConfirmed] = useState(false);
135
155
  const showCodeRecommendation =
136
156
  collection !== null &&
137
157
  collectionLooksCodeHeavy(collection) &&
@@ -150,6 +170,8 @@ export function CollectionModelDialog({
150
170
  });
151
171
  setError(null);
152
172
  setSaving(false);
173
+ setPolicyDraft(collection.egressPolicy?.effectivePolicy ?? "local_only");
174
+ setRelaxationConfirmed(false);
153
175
  }, [collection, open]);
154
176
 
155
177
  const patch = useMemo(() => {
@@ -170,7 +192,13 @@ export function CollectionModelDialog({
170
192
  return nextPatch;
171
193
  }, [collection, draft]);
172
194
 
173
- const hasChanges = Object.keys(patch).length > 0;
195
+ const originalPolicy =
196
+ collection?.egressPolicy?.effectivePolicy ?? "local_only";
197
+ const policyChanged = policyDraft !== originalPolicy;
198
+ const policyOrder = { local_only: 0, lan: 1, remote: 2 } as const;
199
+ const policyRelaxed =
200
+ policyChanged && policyOrder[policyDraft] > policyOrder[originalPolicy];
201
+ const hasChanges = Object.keys(patch).length > 0 || policyChanged;
174
202
  const embedChanged = Object.hasOwn(patch, "embed");
175
203
 
176
204
  const handleSave = async () => {
@@ -181,13 +209,46 @@ export function CollectionModelDialog({
181
209
  setSaving(true);
182
210
  setError(null);
183
211
 
184
- const { error: requestError } = await apiFetch<UpdateCollectionResponse>(
185
- `/api/collections/${encodeURIComponent(collection.name)}`,
186
- {
187
- method: "PATCH",
188
- body: JSON.stringify({ models: patch }),
212
+ if (policyChanged) {
213
+ const state = collection.egressPolicy;
214
+ const { error: policyError } = await apiFetch(
215
+ `/api/collections/${encodeURIComponent(collection.name)}/egress-policy`,
216
+ {
217
+ method: "PUT",
218
+ body: JSON.stringify({
219
+ policy: policyDraft,
220
+ confirmation:
221
+ policyRelaxed && state && relaxationConfirmed
222
+ ? {
223
+ collection: state.collection,
224
+ currentPolicy: state.effectivePolicy,
225
+ currentRevision: state.revision,
226
+ targetPolicy: policyDraft,
227
+ acknowledged: true,
228
+ }
229
+ : undefined,
230
+ }),
231
+ }
232
+ );
233
+ if (policyError) {
234
+ setSaving(false);
235
+ setError(policyError);
236
+ return;
189
237
  }
190
- );
238
+ }
239
+
240
+ const requestError =
241
+ Object.keys(patch).length > 0
242
+ ? (
243
+ await apiFetch<UpdateCollectionResponse>(
244
+ `/api/collections/${encodeURIComponent(collection.name)}`,
245
+ {
246
+ method: "PATCH",
247
+ body: JSON.stringify({ models: patch }),
248
+ }
249
+ )
250
+ ).error
251
+ : null;
191
252
 
192
253
  setSaving(false);
193
254
 
@@ -240,6 +301,27 @@ export function CollectionModelDialog({
240
301
  {/* Scrollable model roles */}
241
302
  <div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden">
242
303
  <div className="divide-y divide-border/15">
304
+ <CollectionEgressPolicyEditor
305
+ confirmed={relaxationConfirmed}
306
+ onConfirmedChange={setRelaxationConfirmed}
307
+ onPolicyChange={(policy) => {
308
+ setPolicyDraft(policy);
309
+ setRelaxationConfirmed(false);
310
+ }}
311
+ policy={policyDraft}
312
+ relaxed={policyRelaxed}
313
+ revision={collection?.egressPolicy?.revision ?? 0}
314
+ source={collection?.egressPolicy?.source ?? "config_default"}
315
+ />
316
+ {collection ? (
317
+ <CollectionEgressCheckPanel
318
+ availableCollections={availableCollections}
319
+ collection={collection.name}
320
+ effectivePolicy={originalPolicy}
321
+ source={collection.egressPolicy?.source ?? "config_default"}
322
+ />
323
+ ) : null}
324
+ <EgressAuditPanel />
243
325
  {MODEL_ROLES.map((role) => {
244
326
  const source = collection?.modelSources?.[role] ?? "preset";
245
327
  const effectiveValue = collection?.effectiveModels?.[role] ?? "";
@@ -385,12 +467,14 @@ export function CollectionModelDialog({
385
467
  </Button>
386
468
  <Button
387
469
  className="text-xs"
388
- disabled={!hasChanges || saving}
470
+ disabled={
471
+ !hasChanges || saving || (policyRelaxed && !relaxationConfirmed)
472
+ }
389
473
  onClick={() => void handleSave()}
390
474
  size="sm"
391
475
  >
392
476
  {saving ? <Loader2Icon className="size-3.5 animate-spin" /> : null}
393
- Save model settings
477
+ Save collection settings
394
478
  </Button>
395
479
  </DialogFooter>
396
480
  </DialogContent>
@@ -0,0 +1,278 @@
1
+ import {
2
+ ChevronRightIcon,
3
+ Loader2Icon,
4
+ ReceiptTextIcon,
5
+ Trash2Icon,
6
+ } from "lucide-react";
7
+ import { useCallback, useEffect, useState } from "react";
8
+
9
+ import { apiFetch } from "../hooks/use-api";
10
+ import { Button } from "./ui/button";
11
+
12
+ interface AuditReceipt {
13
+ action: string;
14
+ auditId: string;
15
+ byteSize: number;
16
+ contentClass: string;
17
+ createdAtMs: number;
18
+ decision: "allow" | "deny";
19
+ destinationZone: string;
20
+ effectivePolicy: string;
21
+ reasonCode: string;
22
+ }
23
+
24
+ interface AuditPage {
25
+ nextCursor: string | null;
26
+ receipts: AuditReceipt[];
27
+ }
28
+
29
+ interface AuditStatus {
30
+ bytes: number;
31
+ receipts: number;
32
+ retention: {
33
+ maxAgeDays: number;
34
+ maxBytes: number;
35
+ maxReceipts: number;
36
+ };
37
+ }
38
+
39
+ interface CleanupResult {
40
+ auditId?: string;
41
+ checkpointedFrames: number;
42
+ deleted: number;
43
+ physicalCleanup: string;
44
+ remainingWalFrames: number;
45
+ }
46
+
47
+ export function EgressAuditPanel() {
48
+ const [receipts, setReceipts] = useState<AuditReceipt[]>([]);
49
+ const [nextCursor, setNextCursor] = useState<string | null>(null);
50
+ const [status, setStatus] = useState<AuditStatus | null>(null);
51
+ const [selected, setSelected] = useState<AuditReceipt | null>(null);
52
+ const [pendingDelete, setPendingDelete] = useState<string | null>(null);
53
+ const [purgeConfirmed, setPurgeConfirmed] = useState(false);
54
+ const [cleanup, setCleanup] = useState<CleanupResult | null>(null);
55
+ const [loading, setLoading] = useState(false);
56
+ const [error, setError] = useState<string | null>(null);
57
+
58
+ const loadStatus = useCallback(async () => {
59
+ const response = await apiFetch<AuditStatus>("/api/egress/audits/status");
60
+ if (response.data) setStatus(response.data);
61
+ else if (response.error) setError(response.error);
62
+ }, []);
63
+
64
+ const loadPage = useCallback(async (cursor?: string) => {
65
+ setLoading(true);
66
+ setError(null);
67
+ const suffix = cursor
68
+ ? `?limit=10&cursor=${encodeURIComponent(cursor)}`
69
+ : "?limit=10";
70
+ const response = await apiFetch<AuditPage>(`/api/egress/audits${suffix}`);
71
+ setLoading(false);
72
+ if (response.error || !response.data) {
73
+ setError(response.error ?? "Audit list failed");
74
+ return;
75
+ }
76
+ const page = response.data;
77
+ setReceipts((current) =>
78
+ cursor ? [...current, ...page.receipts] : page.receipts
79
+ );
80
+ setNextCursor(page.nextCursor);
81
+ }, []);
82
+
83
+ useEffect(() => {
84
+ void Promise.all([loadPage(), loadStatus()]);
85
+ }, [loadPage, loadStatus]);
86
+
87
+ const show = async (auditId: string) => {
88
+ setError(null);
89
+ const response = await apiFetch<{ receipt: AuditReceipt }>(
90
+ `/api/egress/audits/${encodeURIComponent(auditId)}`
91
+ );
92
+ if (response.data) setSelected(response.data.receipt);
93
+ else setError(response.error ?? "Audit receipt unavailable");
94
+ };
95
+
96
+ const remove = async (auditId: string) => {
97
+ setError(null);
98
+ const response = await apiFetch<CleanupResult>(
99
+ `/api/egress/audits/${encodeURIComponent(auditId)}`,
100
+ { method: "DELETE" }
101
+ );
102
+ setPendingDelete(null);
103
+ if (response.error || !response.data) {
104
+ setError(response.error ?? "Audit deletion failed");
105
+ return;
106
+ }
107
+ setCleanup(response.data);
108
+ setSelected((current) => (current?.auditId === auditId ? null : current));
109
+ setReceipts((current) =>
110
+ current.filter((receipt) => receipt.auditId !== auditId)
111
+ );
112
+ await loadStatus();
113
+ };
114
+
115
+ const purge = async () => {
116
+ setError(null);
117
+ const response = await apiFetch<CleanupResult>("/api/egress/audits", {
118
+ method: "DELETE",
119
+ });
120
+ setPurgeConfirmed(false);
121
+ if (response.error || !response.data) {
122
+ setError(response.error ?? "Audit purge failed");
123
+ return;
124
+ }
125
+ setCleanup(response.data);
126
+ setReceipts([]);
127
+ setNextCursor(null);
128
+ setSelected(null);
129
+ await loadStatus();
130
+ };
131
+
132
+ return (
133
+ <section
134
+ aria-labelledby="egress-audit-title"
135
+ className="space-y-3 border-border/15 border-t px-6 py-5"
136
+ >
137
+ <div className="flex items-start justify-between gap-4">
138
+ <div>
139
+ <h3
140
+ className="flex items-center gap-2 font-medium text-[13px]"
141
+ id="egress-audit-title"
142
+ >
143
+ <ReceiptTextIcon className="size-3.5 text-secondary/70" />
144
+ Local audit receipts
145
+ </h3>
146
+ <p className="mt-1 text-muted-foreground/50 text-xs">
147
+ Content-free decisions; newest first; retained locally.
148
+ </p>
149
+ </div>
150
+ <p className="font-mono text-[10px] text-muted-foreground/50">
151
+ {status
152
+ ? `${status.receipts} receipts · ${status.bytes} bytes`
153
+ : "loading status"}
154
+ </p>
155
+ </div>
156
+
157
+ {error ? (
158
+ <p className="text-destructive text-xs" role="alert">
159
+ {error}
160
+ </p>
161
+ ) : null}
162
+
163
+ <div className="max-h-44 overflow-y-auto rounded-md border border-border/20">
164
+ {receipts.length === 0 && !loading ? (
165
+ <p className="p-3 text-muted-foreground/50 text-xs">
166
+ No audit receipts yet.
167
+ </p>
168
+ ) : null}
169
+ {receipts.map((receipt) => (
170
+ <div
171
+ className="flex items-center gap-2 border-border/15 border-b px-3 py-2 last:border-0"
172
+ key={receipt.auditId}
173
+ >
174
+ <button
175
+ className="min-w-0 flex-1 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-secondary"
176
+ onClick={() => void show(receipt.auditId)}
177
+ type="button"
178
+ >
179
+ <span className="block truncate font-mono text-[10px]">
180
+ {receipt.decision} · {receipt.reasonCode}
181
+ </span>
182
+ <span className="block text-[10px] text-muted-foreground/45">
183
+ {receipt.action} / {receipt.destinationZone} /{" "}
184
+ {receipt.contentClass}
185
+ </span>
186
+ </button>
187
+ {pendingDelete === receipt.auditId ? (
188
+ <div className="flex gap-1">
189
+ <Button
190
+ onClick={() => void remove(receipt.auditId)}
191
+ size="sm"
192
+ type="button"
193
+ variant="destructive"
194
+ >
195
+ Confirm
196
+ </Button>
197
+ <Button
198
+ onClick={() => setPendingDelete(null)}
199
+ size="sm"
200
+ type="button"
201
+ variant="ghost"
202
+ >
203
+ Cancel
204
+ </Button>
205
+ </div>
206
+ ) : (
207
+ <Button
208
+ aria-label="Delete audit receipt"
209
+ onClick={() => setPendingDelete(receipt.auditId)}
210
+ size="icon-sm"
211
+ type="button"
212
+ variant="ghost"
213
+ >
214
+ <Trash2Icon className="size-3" />
215
+ </Button>
216
+ )}
217
+ </div>
218
+ ))}
219
+ </div>
220
+
221
+ {nextCursor ? (
222
+ <Button
223
+ disabled={loading}
224
+ onClick={() => void loadPage(nextCursor)}
225
+ size="sm"
226
+ type="button"
227
+ variant="outline"
228
+ >
229
+ {loading ? <Loader2Icon className="size-3 animate-spin" /> : null}
230
+ Load older
231
+ <ChevronRightIcon className="size-3" />
232
+ </Button>
233
+ ) : null}
234
+
235
+ {selected ? (
236
+ <output className="block rounded-md border border-border/20 bg-muted/10 p-3 font-mono text-[10px]">
237
+ <span className="block">{selected.auditId}</span>
238
+ <span className="block text-muted-foreground/60">
239
+ {selected.effectivePolicy} · {selected.reasonCode} ·{" "}
240
+ {selected.byteSize} bytes
241
+ </span>
242
+ </output>
243
+ ) : null}
244
+
245
+ {cleanup ? (
246
+ <output
247
+ aria-live="polite"
248
+ className="block text-[10px] text-muted-foreground/60"
249
+ >
250
+ Deleted {cleanup.deleted}; cleanup {cleanup.physicalCleanup};
251
+ checkpointed {cleanup.checkpointedFrames}; WAL remaining{" "}
252
+ {cleanup.remainingWalFrames}.
253
+ </output>
254
+ ) : null}
255
+
256
+ <div className="flex flex-wrap items-center gap-3 border-border/15 border-t pt-3">
257
+ <label className="flex items-center gap-2 text-xs">
258
+ <input
259
+ checked={purgeConfirmed}
260
+ onChange={(event) => setPurgeConfirmed(event.target.checked)}
261
+ type="checkbox"
262
+ />
263
+ Confirm purge of all local audit receipts
264
+ </label>
265
+ <Button
266
+ className="ml-auto"
267
+ disabled={!purgeConfirmed}
268
+ onClick={() => void purge()}
269
+ size="sm"
270
+ type="button"
271
+ variant="destructive"
272
+ >
273
+ Purge audits
274
+ </Button>
275
+ </div>
276
+ </section>
277
+ );
278
+ }