@s-gw/s-gw 0.1.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 (123) hide show
  1. package/.codex-plugin/plugin.json +35 -0
  2. package/.mcp.json +16 -0
  3. package/LICENSE +201 -0
  4. package/NOTICE +7 -0
  5. package/README.md +197 -0
  6. package/TRADEMARKS.md +9 -0
  7. package/assets/icons/aws-ec2.png +0 -0
  8. package/assets/icons/lucide/bot.svg +8 -0
  9. package/assets/icons/lucide/monitor.svg +5 -0
  10. package/assets/icons/lucide/server.svg +6 -0
  11. package/assets/icons/lucide/terminal.svg +4 -0
  12. package/assets/icons/s-gw-128.png +0 -0
  13. package/assets/icons/s-gw-16.png +0 -0
  14. package/assets/icons/s-gw-180.png +0 -0
  15. package/assets/icons/s-gw-192.png +0 -0
  16. package/assets/icons/s-gw-32.png +0 -0
  17. package/assets/icons/s-gw-64.png +0 -0
  18. package/assets/icons/s-gw-menu-bar-template.png +0 -0
  19. package/dist/agent-context.d.ts +17 -0
  20. package/dist/agent-context.js +207 -0
  21. package/dist/agents.d.ts +64 -0
  22. package/dist/agents.js +763 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +1385 -0
  25. package/dist/command-suggest.d.ts +3 -0
  26. package/dist/command-suggest.js +131 -0
  27. package/dist/console-server.d.ts +16 -0
  28. package/dist/console-server.js +978 -0
  29. package/dist/console-ui/assets/codex-DYTPdPxi.png +0 -0
  30. package/dist/console-ui/assets/cursor-CBrUTJD-.png +0 -0
  31. package/dist/console-ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  32. package/dist/console-ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  33. package/dist/console-ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  34. package/dist/console-ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  35. package/dist/console-ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  36. package/dist/console-ui/assets/hermes-B8hNbJPm.png +0 -0
  37. package/dist/console-ui/assets/index-BxUf0Sye.js +96 -0
  38. package/dist/console-ui/assets/index-CmTiBR_w.css +2 -0
  39. package/dist/console-ui/assets/omnigent-Cxa4p2Mq.png +0 -0
  40. package/dist/console-ui/assets/openclaw-C5wL4ZVW.png +0 -0
  41. package/dist/console-ui/assets/opencode-D_wFATSC.png +0 -0
  42. package/dist/console-ui/assets/openhands-DnrlGgev.svg +9 -0
  43. package/dist/console-ui/assets/s-gw-64-ByMUGQ3K.png +0 -0
  44. package/dist/console-ui/assets/vscode-Bdtr9eyf.png +0 -0
  45. package/dist/console-ui/assets/zeptoclaw-DztQW8Sw.png +0 -0
  46. package/dist/console-ui/index.html +13 -0
  47. package/dist/crypto.d.ts +6 -0
  48. package/dist/crypto.js +53 -0
  49. package/dist/executor.d.ts +7 -0
  50. package/dist/executor.js +297 -0
  51. package/dist/gateway.d.ts +31 -0
  52. package/dist/gateway.js +114 -0
  53. package/dist/guard.d.ts +61 -0
  54. package/dist/guard.js +247 -0
  55. package/dist/install.d.ts +146 -0
  56. package/dist/install.js +629 -0
  57. package/dist/mcp-server.d.ts +2 -0
  58. package/dist/mcp-server.js +119 -0
  59. package/dist/native/s-gw-core +0 -0
  60. package/dist/native/s-gw-keychain-helper +0 -0
  61. package/dist/onepassword.d.ts +48 -0
  62. package/dist/onepassword.js +412 -0
  63. package/dist/paths.d.ts +4 -0
  64. package/dist/paths.js +22 -0
  65. package/dist/s-gw Menu Bar.app/Contents/Info.plist +28 -0
  66. package/dist/s-gw Menu Bar.app/Contents/MacOS/s-gw-menu-bar-helper +0 -0
  67. package/dist/s-gw Menu Bar.app/Contents/Resources/AppIcon.icns +0 -0
  68. package/dist/s-gw Menu Bar.app/Contents/Resources/AwsEc2.png +0 -0
  69. package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-bot.svg +8 -0
  70. package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-monitor.svg +5 -0
  71. package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-server.svg +6 -0
  72. package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-terminal.svg +4 -0
  73. package/dist/s-gw Menu Bar.app/Contents/Resources/MenuBarTemplate.png +0 -0
  74. package/dist/s-gw Menu Bar.app/Contents/_CodeSignature/CodeResources +194 -0
  75. package/dist/s-gw.app/Contents/Info.plist +28 -0
  76. package/dist/s-gw.app/Contents/MacOS/s-gw +0 -0
  77. package/dist/s-gw.app/Contents/Resources/AppIcon.icns +0 -0
  78. package/dist/s-gw.app/Contents/Resources/MenuBarTemplate.png +0 -0
  79. package/dist/s-gw.app/Contents/_CodeSignature/CodeResources +139 -0
  80. package/dist/scanner.d.ts +9 -0
  81. package/dist/scanner.js +437 -0
  82. package/dist/ssh.d.ts +31 -0
  83. package/dist/ssh.js +286 -0
  84. package/dist/store.d.ts +131 -0
  85. package/dist/store.js +1611 -0
  86. package/dist/types.d.ts +196 -0
  87. package/dist/types.js +2 -0
  88. package/dist/unlock.d.ts +29 -0
  89. package/dist/unlock.js +274 -0
  90. package/dist/windows/VERSION.txt +1 -0
  91. package/dist/windows/s-gw-client.cmd +4 -0
  92. package/dist/windows/s-gw-client.ps1 +106 -0
  93. package/dist/windows/s-gw-credential.cmd +4 -0
  94. package/dist/windows/s-gw-credential.ps1 +167 -0
  95. package/dist/windows/s-gw-helper.cmd +4 -0
  96. package/dist/windows/s-gw-helper.ps1 +180 -0
  97. package/docs/README.md +23 -0
  98. package/docs/agents.md +160 -0
  99. package/docs/architecture.md +72 -0
  100. package/docs/deployment.md +447 -0
  101. package/docs/detection.md +44 -0
  102. package/docs/images/s-gw-overview.png +0 -0
  103. package/docs/integrations.md +195 -0
  104. package/docs/keychain.md +39 -0
  105. package/docs/onepassword.md +84 -0
  106. package/docs/quickstart.md +104 -0
  107. package/docs/threat-model.md +100 -0
  108. package/docs/ui/THIRD_PARTY_NOTICES.md +111 -0
  109. package/docs/ui/apple-touch-icon.png +0 -0
  110. package/docs/ui/favicon-32.png +0 -0
  111. package/docs/ui/local-console.html +4477 -0
  112. package/docs/ui/vendor/d3-sankey/d3-array.LICENSE.txt +27 -0
  113. package/docs/ui/vendor/d3-sankey/d3-array.min.js +2 -0
  114. package/docs/ui/vendor/d3-sankey/d3-path.LICENSE.txt +27 -0
  115. package/docs/ui/vendor/d3-sankey/d3-path.min.js +2 -0
  116. package/docs/ui/vendor/d3-sankey/d3-sankey.LICENSE.txt +27 -0
  117. package/docs/ui/vendor/d3-sankey/d3-sankey.min.js +2 -0
  118. package/docs/ui/vendor/d3-sankey/d3-shape.LICENSE.txt +27 -0
  119. package/docs/ui/vendor/d3-sankey/d3-shape.min.js +2 -0
  120. package/docs/ui/vendor/sankeymatic/LICENSE.txt +17 -0
  121. package/docs/ui/vendor/sankeymatic/sankey.js +897 -0
  122. package/package.json +117 -0
  123. package/skills/s-gw/SKILL.md +19 -0
@@ -0,0 +1,4477 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>s-gw Local Console</title>
7
+ <link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png">
8
+ <link rel="apple-touch-icon" href="apple-touch-icon.png">
9
+ <script>
10
+ (function () {
11
+ var params = new URLSearchParams(window.location.search);
12
+ var prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
13
+ var fallback = prefersDark ? "dark" : "light";
14
+
15
+ try {
16
+ document.documentElement.dataset.theme = localStorage.getItem("sgw-theme") || fallback;
17
+ } catch (err) {
18
+ document.documentElement.dataset.theme = fallback;
19
+ }
20
+
21
+ var embed = params.get("embed");
22
+ if (embed) {
23
+ document.documentElement.dataset.embed = embed;
24
+ }
25
+
26
+ var embedDensity = params.get("compact") === "1" ? "compact" : "";
27
+ if (embedDensity) {
28
+ document.documentElement.dataset.embedDensity = embedDensity;
29
+ }
30
+ }());
31
+ </script>
32
+ <script src="vendor/d3-sankey/d3-array.min.js"></script>
33
+ <script src="vendor/d3-sankey/d3-path.min.js"></script>
34
+ <script src="vendor/d3-sankey/d3-shape.min.js"></script>
35
+ <script src="vendor/d3-sankey/d3-sankey.min.js"></script>
36
+ <style>
37
+ :root {
38
+ color-scheme: light;
39
+ --bg: #f8fafc;
40
+ --surface: #ffffff;
41
+ --surface-soft: #f8fafc;
42
+ --surface-tint: #eefafa;
43
+ --text: #101827;
44
+ --muted: #5d6979;
45
+ --faint: #7a8797;
46
+ --border: #dce3eb;
47
+ --border-strong: #cad4df;
48
+ --sidebar: #fbfdff;
49
+ --sidebar-active: #e5f7f3;
50
+ --topbar: rgba(255, 255, 255, .92);
51
+ --input: #ffffff;
52
+ --accent: #0b8f83;
53
+ --accent-strong: #08786f;
54
+ --green: #14965c;
55
+ --blue: #1167c4;
56
+ --red: #d92d20;
57
+ --orange: #b76b00;
58
+ --purple: #6658d3;
59
+ --shadow: 0 18px 42px rgba(15, 23, 42, .05);
60
+ --radius: 8px;
61
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
62
+ }
63
+
64
+ :root[data-theme="dark"] {
65
+ color-scheme: dark;
66
+ --bg: #0c1219;
67
+ --surface: #121b24;
68
+ --surface-soft: #0f1720;
69
+ --surface-tint: #112d2c;
70
+ --text: #edf4f7;
71
+ --muted: #a3b1be;
72
+ --faint: #7f91a1;
73
+ --border: #273745;
74
+ --border-strong: #34485a;
75
+ --sidebar: #0a1118;
76
+ --sidebar-active: #123632;
77
+ --topbar: rgba(12, 18, 25, .94);
78
+ --input: #0e1720;
79
+ --accent: #27b7aa;
80
+ --accent-strong: #35d0c2;
81
+ --green: #42c783;
82
+ --blue: #72aaff;
83
+ --red: #ff8178;
84
+ --orange: #ffbf5a;
85
+ --purple: #9b94ff;
86
+ --shadow: 0 22px 54px rgba(0, 0, 0, .28);
87
+ }
88
+
89
+ * {
90
+ box-sizing: border-box;
91
+ }
92
+
93
+ body {
94
+ margin: 0;
95
+ min-height: 100vh;
96
+ color: var(--text);
97
+ background: var(--bg);
98
+ letter-spacing: 0;
99
+ }
100
+
101
+ button,
102
+ input,
103
+ select {
104
+ font: inherit;
105
+ }
106
+
107
+ button {
108
+ cursor: pointer;
109
+ }
110
+
111
+ svg {
112
+ display: block;
113
+ width: 18px;
114
+ height: 18px;
115
+ stroke: currentColor;
116
+ stroke-width: 2;
117
+ stroke-linecap: round;
118
+ stroke-linejoin: round;
119
+ fill: none;
120
+ }
121
+
122
+ .app-shell {
123
+ display: grid;
124
+ grid-template-columns: 248px minmax(0, 1fr);
125
+ min-height: 100vh;
126
+ }
127
+
128
+ :root[data-embed="usage-flow"] body {
129
+ min-height: 0;
130
+ background: transparent;
131
+ }
132
+
133
+ :root[data-embed="usage-flow"] .app-shell {
134
+ display: block;
135
+ min-height: 0;
136
+ }
137
+
138
+ :root[data-embed="usage-flow"] .sidebar,
139
+ :root[data-embed="usage-flow"] .topbar,
140
+ :root[data-embed="usage-flow"] .content > :not(#usage-flow) {
141
+ display: none;
142
+ }
143
+
144
+ :root[data-embed="usage-flow"] .content {
145
+ padding: 0;
146
+ }
147
+
148
+ :root[data-embed="usage-flow"] .flow-panel {
149
+ margin: 0;
150
+ border: 0;
151
+ border-radius: 0;
152
+ box-shadow: none;
153
+ background: transparent;
154
+ }
155
+
156
+ :root[data-embed="usage-flow"] .flow-panel .panel-head,
157
+ :root[data-embed="usage-flow"] .flow-detail {
158
+ display: none;
159
+ }
160
+
161
+ :root[data-embed="usage-flow"] .flow-layout {
162
+ display: block;
163
+ padding: 0;
164
+ position: relative;
165
+ }
166
+
167
+ :root[data-embed="usage-flow"] .sankey-wrap {
168
+ min-height: 100vh;
169
+ border: 0;
170
+ border-radius: 0;
171
+ background: transparent;
172
+ }
173
+
174
+ :root[data-embed="usage-flow"] .sankey-wrap svg {
175
+ min-width: 760px;
176
+ width: max(100%, 760px);
177
+ }
178
+
179
+ :root[data-embed="usage-flow"][data-embed-density="compact"] .flow-legend,
180
+ :root[data-embed="usage-flow"][data-embed-density="compact"] .flow-drill {
181
+ display: none;
182
+ }
183
+
184
+ :root[data-embed="usage-flow"][data-embed-density="compact"],
185
+ :root[data-embed="usage-flow"][data-embed-density="compact"] body,
186
+ :root[data-embed="usage-flow"][data-embed-density="compact"] .app-shell,
187
+ :root[data-embed="usage-flow"][data-embed-density="compact"] .content,
188
+ :root[data-embed="usage-flow"][data-embed-density="compact"] .flow-panel,
189
+ :root[data-embed="usage-flow"][data-embed-density="compact"] .flow-layout,
190
+ :root[data-embed="usage-flow"][data-embed-density="compact"] .sankey-wrap {
191
+ height: 100vh;
192
+ min-height: 0;
193
+ overflow: hidden;
194
+ }
195
+
196
+ :root[data-embed="usage-flow"][data-embed-density="compact"] .sankey-wrap svg {
197
+ min-width: 0;
198
+ width: 100%;
199
+ }
200
+
201
+ :root[data-embed="usage-flow"][data-embed-density="compact"] .sankey-wrap {
202
+ overflow: hidden;
203
+ }
204
+
205
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-detail {
206
+ position: absolute;
207
+ top: 12px;
208
+ left: auto;
209
+ right: 12px;
210
+ z-index: 5;
211
+ display: block;
212
+ width: min(760px, calc(100vw - 24px));
213
+ min-width: min(380px, calc(100vw - 24px));
214
+ height: min(520px, calc(100vh - 24px));
215
+ min-height: min(260px, calc(100vh - 24px));
216
+ max-width: calc(100vw - 24px);
217
+ max-height: calc(100vh - 24px);
218
+ resize: none;
219
+ background: var(--surface);
220
+ box-shadow: 0 18px 48px rgba(15, 23, 42, .18);
221
+ padding-left: 0;
222
+ transform: translateX(0);
223
+ transform-origin: right center;
224
+ transition: transform .2s cubic-bezier(.2, .8, .2, 1), opacity .16s ease;
225
+ animation: flow-detail-slide-in .22s cubic-bezier(.2, .8, .2, 1);
226
+ will-change: transform, opacity;
227
+ }
228
+
229
+ :root[data-embed="usage-flow"][data-flow-closing="true"] .flow-detail {
230
+ opacity: 0;
231
+ pointer-events: none;
232
+ transform: translateX(calc(100% + 24px));
233
+ }
234
+
235
+ @keyframes flow-detail-slide-in {
236
+ from {
237
+ opacity: 0;
238
+ transform: translateX(calc(100% + 24px));
239
+ }
240
+ to {
241
+ opacity: 1;
242
+ transform: translateX(0);
243
+ }
244
+ }
245
+
246
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-detail table {
247
+ min-width: 700px;
248
+ }
249
+
250
+ :root[data-flow-resizing="true"] {
251
+ cursor: ew-resize;
252
+ user-select: none;
253
+ }
254
+
255
+ .flow-resize-grip {
256
+ appearance: none;
257
+ display: none;
258
+ }
259
+
260
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-resize-grip {
261
+ position: absolute;
262
+ top: 0;
263
+ bottom: 0;
264
+ left: 0;
265
+ z-index: 8;
266
+ display: flex;
267
+ width: 16px;
268
+ align-items: center;
269
+ justify-content: center;
270
+ padding: 0;
271
+ border: 0;
272
+ background: transparent;
273
+ cursor: ew-resize;
274
+ opacity: .82;
275
+ transform: translateX(-50%);
276
+ }
277
+
278
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-resize-grip:hover,
279
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-resize-grip:focus-visible,
280
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-detail[data-flow-resizing="true"] .flow-resize-grip {
281
+ opacity: 1;
282
+ outline: none;
283
+ }
284
+
285
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-resize-grip:focus-visible .flow-resize-handle {
286
+ outline: 2px solid color-mix(in srgb, var(--teal) 58%, transparent);
287
+ outline-offset: 2px;
288
+ }
289
+
290
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-resize-grip::before {
291
+ content: "";
292
+ position: absolute;
293
+ top: 0;
294
+ bottom: 0;
295
+ left: 50%;
296
+ width: 1px;
297
+ transform: translateX(-.5px);
298
+ pointer-events: none;
299
+ background: color-mix(in srgb, var(--border) 88%, transparent);
300
+ }
301
+
302
+ .flow-resize-handle {
303
+ position: relative;
304
+ z-index: 1;
305
+ display: grid;
306
+ width: 10px;
307
+ height: 24px;
308
+ place-items: center;
309
+ border: 1px solid color-mix(in srgb, var(--border) 90%, transparent);
310
+ border-radius: 4px;
311
+ background: var(--surface);
312
+ color: var(--muted);
313
+ box-shadow: 0 2px 8px rgba(15, 23, 42, .12);
314
+ }
315
+
316
+ .flow-resize-handle::before {
317
+ content: "";
318
+ display: block;
319
+ width: 7px;
320
+ height: 14px;
321
+ background-image: radial-gradient(circle, currentColor 1.1px, transparent 1.2px);
322
+ background-position: 0 0;
323
+ background-size: 4px 5px;
324
+ opacity: .8;
325
+ }
326
+
327
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-resize-grip:hover .flow-resize-handle,
328
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-detail[data-flow-resizing="true"] .flow-resize-handle {
329
+ border-color: color-mix(in srgb, var(--teal) 46%, var(--border));
330
+ color: color-mix(in srgb, var(--teal) 80%, var(--text));
331
+ box-shadow: 0 2px 10px rgba(20, 184, 166, .18);
332
+ }
333
+
334
+ @media (max-width: 560px) {
335
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-detail {
336
+ left: 12px;
337
+ right: 12px;
338
+ width: auto;
339
+ resize: none;
340
+ }
341
+ }
342
+
343
+ @media (prefers-reduced-motion: reduce) {
344
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-detail {
345
+ animation: none;
346
+ transition: none;
347
+ }
348
+ }
349
+
350
+ .sidebar {
351
+ position: sticky;
352
+ top: 0;
353
+ display: flex;
354
+ flex-direction: column;
355
+ gap: 24px;
356
+ height: 100vh;
357
+ padding: 26px 16px 22px;
358
+ border-right: 1px solid var(--border);
359
+ background: var(--sidebar);
360
+ }
361
+
362
+ .brand {
363
+ display: flex;
364
+ align-items: center;
365
+ gap: 11px;
366
+ padding: 0 10px 16px;
367
+ color: var(--text);
368
+ }
369
+
370
+ .brand-mark {
371
+ display: grid;
372
+ place-items: center;
373
+ width: 30px;
374
+ height: 30px;
375
+ border-radius: 8px;
376
+ color: var(--accent);
377
+ background: #e5f7f3;
378
+ flex: 0 0 auto;
379
+ }
380
+
381
+ :root[data-theme="dark"] .brand-mark {
382
+ background: #123632;
383
+ }
384
+
385
+ .brand strong {
386
+ font-size: 19px;
387
+ line-height: 1;
388
+ font-weight: 800;
389
+ white-space: nowrap;
390
+ }
391
+
392
+ .nav {
393
+ display: grid;
394
+ gap: 7px;
395
+ }
396
+
397
+ .nav button {
398
+ display: grid;
399
+ grid-template-columns: 24px 1fr auto;
400
+ align-items: center;
401
+ gap: 11px;
402
+ width: 100%;
403
+ min-height: 40px;
404
+ padding: 0 13px;
405
+ border: 0;
406
+ border-radius: var(--radius);
407
+ background: transparent;
408
+ color: var(--text);
409
+ text-align: left;
410
+ font-size: 14px;
411
+ font-weight: 500;
412
+ }
413
+
414
+ .nav button.active {
415
+ background: var(--sidebar-active);
416
+ color: var(--accent-strong);
417
+ font-weight: 700;
418
+ }
419
+
420
+ .nav .bubble,
421
+ .queue-count {
422
+ display: inline-flex;
423
+ align-items: center;
424
+ justify-content: center;
425
+ min-width: 22px;
426
+ height: 22px;
427
+ padding: 0 7px;
428
+ border-radius: 99px;
429
+ background: #1265c7;
430
+ color: #fff;
431
+ font-size: 12px;
432
+ font-weight: 800;
433
+ }
434
+
435
+ .profile-card {
436
+ margin-top: auto;
437
+ border: 1px solid var(--border);
438
+ border-radius: var(--radius);
439
+ background: var(--surface);
440
+ overflow: hidden;
441
+ }
442
+
443
+ .profile-button {
444
+ display: grid;
445
+ grid-template-columns: 20px 1fr 16px;
446
+ align-items: center;
447
+ gap: 10px;
448
+ width: 100%;
449
+ min-height: 49px;
450
+ padding: 0 14px;
451
+ border: 0;
452
+ border-bottom: 1px solid var(--border);
453
+ background: transparent;
454
+ color: var(--text);
455
+ text-align: left;
456
+ font-size: 14px;
457
+ font-weight: 600;
458
+ }
459
+
460
+ .version {
461
+ display: flex;
462
+ align-items: center;
463
+ gap: 9px;
464
+ min-height: 44px;
465
+ padding: 0 16px;
466
+ color: var(--muted);
467
+ font-size: 13px;
468
+ font-weight: 600;
469
+ }
470
+
471
+ .dot {
472
+ width: 9px;
473
+ height: 9px;
474
+ border-radius: 99px;
475
+ background: var(--green);
476
+ flex: 0 0 auto;
477
+ }
478
+
479
+ .main {
480
+ min-width: 0;
481
+ }
482
+
483
+ .topbar {
484
+ position: sticky;
485
+ top: 0;
486
+ z-index: 10;
487
+ display: grid;
488
+ grid-template-columns: minmax(360px, 1fr) auto;
489
+ align-items: center;
490
+ gap: 22px;
491
+ min-height: 80px;
492
+ padding: 0 20px;
493
+ border-bottom: 1px solid var(--border);
494
+ background: var(--topbar);
495
+ backdrop-filter: blur(18px);
496
+ }
497
+
498
+ .mobile-brand {
499
+ display: none;
500
+ }
501
+
502
+ .status-strip {
503
+ display: flex;
504
+ align-items: center;
505
+ gap: 22px;
506
+ min-width: 0;
507
+ color: var(--text);
508
+ font-size: 14px;
509
+ font-weight: 600;
510
+ }
511
+
512
+ .status-item {
513
+ display: inline-flex;
514
+ align-items: center;
515
+ gap: 10px;
516
+ white-space: nowrap;
517
+ }
518
+
519
+ .status-item svg {
520
+ width: 17px;
521
+ height: 17px;
522
+ color: #334155;
523
+ }
524
+
525
+ :root[data-theme="dark"] .status-item svg {
526
+ color: #c8d4df;
527
+ }
528
+
529
+ .divider {
530
+ width: 1px;
531
+ height: 24px;
532
+ background: var(--border);
533
+ }
534
+
535
+ .top-actions {
536
+ display: flex;
537
+ align-items: center;
538
+ justify-content: flex-end;
539
+ gap: 12px;
540
+ min-width: 0;
541
+ }
542
+
543
+ .search {
544
+ position: relative;
545
+ width: min(282px, 24vw);
546
+ min-width: 220px;
547
+ }
548
+
549
+ .search svg {
550
+ position: absolute;
551
+ left: 13px;
552
+ top: 50%;
553
+ width: 18px;
554
+ height: 18px;
555
+ color: var(--muted);
556
+ transform: translateY(-50%);
557
+ pointer-events: none;
558
+ }
559
+
560
+ .search input {
561
+ width: 100%;
562
+ height: 42px;
563
+ padding: 0 48px 0 40px;
564
+ border: 1px solid var(--border);
565
+ border-radius: var(--radius);
566
+ background: var(--input);
567
+ color: var(--text);
568
+ outline: none;
569
+ font-size: 14px;
570
+ }
571
+
572
+ .search input:focus {
573
+ border-color: var(--accent);
574
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
575
+ }
576
+
577
+ .kbd {
578
+ position: absolute;
579
+ right: 11px;
580
+ top: 50%;
581
+ padding: 2px 7px;
582
+ border: 1px solid var(--border);
583
+ border-radius: 6px;
584
+ color: var(--muted);
585
+ background: var(--surface-soft);
586
+ font-size: 12px;
587
+ font-weight: 700;
588
+ transform: translateY(-50%);
589
+ }
590
+
591
+ .pill-button,
592
+ .icon-button {
593
+ display: inline-flex;
594
+ align-items: center;
595
+ justify-content: center;
596
+ gap: 9px;
597
+ min-height: 42px;
598
+ border: 1px solid var(--border);
599
+ border-radius: var(--radius);
600
+ background: var(--surface);
601
+ color: var(--text);
602
+ font-size: 14px;
603
+ font-weight: 700;
604
+ }
605
+
606
+ .pill-button {
607
+ padding: 0 14px;
608
+ }
609
+
610
+ .icon-button {
611
+ width: 42px;
612
+ padding: 0;
613
+ }
614
+
615
+ .theme-toggle .sun {
616
+ display: none;
617
+ }
618
+
619
+ :root[data-theme="dark"] .theme-toggle .moon {
620
+ display: none;
621
+ }
622
+
623
+ :root[data-theme="dark"] .theme-toggle .sun {
624
+ display: block;
625
+ }
626
+
627
+ .queue-button {
628
+ padding-right: 12px;
629
+ white-space: nowrap;
630
+ }
631
+
632
+ .content {
633
+ padding: 22px 18px 30px;
634
+ }
635
+
636
+ .metrics {
637
+ display: grid;
638
+ grid-template-columns: repeat(4, minmax(0, 1fr));
639
+ gap: 14px;
640
+ margin-bottom: 18px;
641
+ }
642
+
643
+ .metric {
644
+ display: grid;
645
+ grid-template-columns: 48px minmax(0, 1fr);
646
+ gap: 16px;
647
+ align-items: center;
648
+ min-height: 118px;
649
+ padding: 20px;
650
+ border: 1px solid var(--border);
651
+ border-radius: var(--radius);
652
+ background: var(--surface);
653
+ box-shadow: var(--shadow);
654
+ }
655
+
656
+ .metric-icon {
657
+ display: grid;
658
+ place-items: center;
659
+ width: 46px;
660
+ height: 46px;
661
+ border-radius: 10px;
662
+ color: var(--accent);
663
+ background: #e2f5f4;
664
+ }
665
+
666
+ .metric-icon.blue {
667
+ color: var(--blue);
668
+ background: #eaf2ff;
669
+ }
670
+
671
+ .metric-icon.red {
672
+ color: var(--red);
673
+ background: #ffeceb;
674
+ }
675
+
676
+ :root[data-theme="dark"] .metric-icon {
677
+ background: #123632;
678
+ }
679
+
680
+ :root[data-theme="dark"] .metric-icon.blue {
681
+ background: #172942;
682
+ }
683
+
684
+ :root[data-theme="dark"] .metric-icon.red {
685
+ background: #34191a;
686
+ }
687
+
688
+ .metric-label {
689
+ color: var(--text);
690
+ font-size: 14px;
691
+ font-weight: 800;
692
+ line-height: 1.2;
693
+ }
694
+
695
+ .metric-value {
696
+ margin-top: 7px;
697
+ color: var(--text);
698
+ font-size: 27px;
699
+ line-height: 1;
700
+ font-weight: 850;
701
+ }
702
+
703
+ .metric-note {
704
+ margin-top: 13px;
705
+ color: var(--muted);
706
+ font-size: 13px;
707
+ line-height: 1.25;
708
+ font-weight: 500;
709
+ }
710
+
711
+ .metric-note.attention {
712
+ color: var(--red);
713
+ }
714
+
715
+ .panel-grid {
716
+ display: grid;
717
+ grid-template-columns: minmax(0, 1.15fr) minmax(470px, .85fr);
718
+ gap: 18px;
719
+ margin-bottom: 18px;
720
+ }
721
+
722
+ .panel {
723
+ border: 1px solid var(--border);
724
+ border-radius: var(--radius);
725
+ background: var(--surface);
726
+ box-shadow: var(--shadow);
727
+ overflow: hidden;
728
+ }
729
+
730
+ .panel-head {
731
+ display: flex;
732
+ align-items: center;
733
+ justify-content: space-between;
734
+ gap: 14px;
735
+ min-height: 60px;
736
+ padding: 0 20px;
737
+ border-bottom: 1px solid var(--border);
738
+ }
739
+
740
+ .panel-title {
741
+ display: flex;
742
+ align-items: center;
743
+ gap: 12px;
744
+ min-width: 0;
745
+ color: var(--text);
746
+ font-size: 18px;
747
+ line-height: 1.1;
748
+ font-weight: 850;
749
+ }
750
+
751
+ .title-count {
752
+ display: inline-flex;
753
+ align-items: center;
754
+ justify-content: center;
755
+ min-width: 25px;
756
+ height: 25px;
757
+ border-radius: 99px;
758
+ background: #eaf2ff;
759
+ color: var(--blue);
760
+ font-size: 13px;
761
+ font-weight: 850;
762
+ }
763
+
764
+ :root[data-theme="dark"] .title-count {
765
+ background: #172942;
766
+ }
767
+
768
+ .table-wrap {
769
+ width: 100%;
770
+ overflow-x: auto;
771
+ }
772
+
773
+ table {
774
+ width: 100%;
775
+ min-width: 620px;
776
+ border-collapse: collapse;
777
+ }
778
+
779
+ .approval-panel {
780
+ display: flex;
781
+ flex-direction: column;
782
+ }
783
+
784
+ .approval-panel .table-wrap {
785
+ flex: 1;
786
+ }
787
+
788
+ .approval-panel table {
789
+ min-width: 820px;
790
+ table-layout: fixed;
791
+ }
792
+
793
+ .approval-panel th {
794
+ white-space: normal;
795
+ line-height: 1.2;
796
+ }
797
+
798
+ .approval-panel th:nth-child(1),
799
+ .approval-panel td:nth-child(1) {
800
+ width: 94px;
801
+ }
802
+
803
+ .approval-panel th:nth-child(2),
804
+ .approval-panel td:nth-child(2) {
805
+ width: 150px;
806
+ }
807
+
808
+ .approval-panel th:nth-child(3),
809
+ .approval-panel td:nth-child(3) {
810
+ width: 112px;
811
+ }
812
+
813
+ .approval-panel th:nth-child(4),
814
+ .approval-panel td:nth-child(4) {
815
+ width: 160px;
816
+ }
817
+
818
+ .approval-panel th:nth-child(5),
819
+ .approval-panel td:nth-child(5) {
820
+ width: 96px;
821
+ }
822
+
823
+ .approval-panel th:nth-child(6),
824
+ .approval-panel td:nth-child(6) {
825
+ width: 78px;
826
+ }
827
+
828
+ .approval-panel th:nth-child(7),
829
+ .approval-panel td:nth-child(7) {
830
+ width: 130px;
831
+ }
832
+
833
+ .approval-panel td {
834
+ white-space: normal;
835
+ }
836
+
837
+ .approval-panel td:nth-child(2),
838
+ .approval-panel td:nth-child(3),
839
+ .approval-panel td:nth-child(4) {
840
+ overflow-wrap: anywhere;
841
+ }
842
+
843
+ .approval-panel td:nth-child(1),
844
+ .approval-panel td:nth-child(5),
845
+ .approval-panel td:nth-child(6),
846
+ .approval-panel td:nth-child(7) {
847
+ overflow-wrap: normal;
848
+ white-space: nowrap;
849
+ }
850
+
851
+ th,
852
+ td {
853
+ padding: 12px 14px;
854
+ border-bottom: 1px solid var(--border);
855
+ text-align: left;
856
+ vertical-align: middle;
857
+ font-size: 13px;
858
+ line-height: 1.35;
859
+ white-space: nowrap;
860
+ }
861
+
862
+ th {
863
+ color: var(--text);
864
+ background: var(--surface-soft);
865
+ font-size: 12px;
866
+ font-weight: 800;
867
+ }
868
+
869
+ tbody tr {
870
+ transition: background .12s ease;
871
+ }
872
+
873
+ tbody tr:hover {
874
+ background: color-mix(in srgb, var(--accent) 5%, transparent);
875
+ }
876
+
877
+ tr.hidden-row {
878
+ display: none;
879
+ }
880
+
881
+ .agent-cell {
882
+ display: grid;
883
+ grid-template-columns: 30px minmax(0, 1fr);
884
+ gap: 10px;
885
+ align-items: center;
886
+ white-space: normal;
887
+ }
888
+
889
+ .agent-badge {
890
+ display: grid;
891
+ place-items: center;
892
+ width: 30px;
893
+ height: 30px;
894
+ border-radius: 7px;
895
+ color: #2e2a85;
896
+ background: #e6e1ff;
897
+ font-weight: 850;
898
+ }
899
+
900
+ :root[data-theme="dark"] .agent-badge {
901
+ color: #d6d2ff;
902
+ background: #282559;
903
+ }
904
+
905
+ .cell-sub {
906
+ display: block;
907
+ margin-top: 3px;
908
+ color: var(--muted);
909
+ font-size: 12px;
910
+ line-height: 1.25;
911
+ white-space: normal;
912
+ }
913
+
914
+ .risk {
915
+ display: inline-flex;
916
+ align-items: center;
917
+ min-height: 26px;
918
+ padding: 0 10px;
919
+ border: 1px solid;
920
+ border-radius: 6px;
921
+ font-size: 13px;
922
+ font-weight: 700;
923
+ }
924
+
925
+ .risk.low {
926
+ color: #087a55;
927
+ border-color: #9bd8c3;
928
+ background: #e8f8f2;
929
+ }
930
+
931
+ .risk.medium {
932
+ color: var(--orange);
933
+ border-color: #ffbf63;
934
+ background: #fff7e8;
935
+ }
936
+
937
+ .risk.high {
938
+ color: var(--red);
939
+ border-color: #ff9d99;
940
+ background: #fff0ef;
941
+ }
942
+
943
+ .risk.critical {
944
+ color: var(--red);
945
+ border-color: #ff9d99;
946
+ background: #fff0ef;
947
+ }
948
+
949
+ :root[data-theme="dark"] .risk.low {
950
+ color: #63dbad;
951
+ border-color: #226b54;
952
+ background: #102d24;
953
+ }
954
+
955
+ :root[data-theme="dark"] .risk.medium {
956
+ color: #ffc56b;
957
+ border-color: #7b561f;
958
+ background: #2d2312;
959
+ }
960
+
961
+ :root[data-theme="dark"] .risk.high {
962
+ color: #ff9b93;
963
+ border-color: #7c3434;
964
+ background: #321819;
965
+ }
966
+
967
+ :root[data-theme="dark"] .risk.critical {
968
+ color: #ff9b93;
969
+ border-color: #7c3434;
970
+ background: #321819;
971
+ }
972
+
973
+ .action-group {
974
+ display: flex;
975
+ gap: 6px;
976
+ justify-content: flex-end;
977
+ white-space: nowrap;
978
+ }
979
+
980
+ .action-button {
981
+ display: inline-flex;
982
+ align-items: center;
983
+ justify-content: center;
984
+ min-width: 30px;
985
+ width: 30px;
986
+ height: 28px;
987
+ padding: 0;
988
+ border: 1px solid var(--border);
989
+ border-radius: 6px;
990
+ background: var(--surface);
991
+ color: var(--text);
992
+ font-size: 12px;
993
+ font-weight: 800;
994
+ }
995
+
996
+ .action-button svg {
997
+ width: 15px;
998
+ height: 15px;
999
+ }
1000
+
1001
+ .action-button.approve {
1002
+ color: #087a55;
1003
+ border-color: #9bd8c3;
1004
+ background: #e8f8f2;
1005
+ }
1006
+
1007
+ .action-button.deny {
1008
+ color: var(--red);
1009
+ border-color: #ffb4b1;
1010
+ background: #fff4f3;
1011
+ }
1012
+
1013
+ .action-button.busy,
1014
+ .action-button:disabled {
1015
+ opacity: .5;
1016
+ cursor: progress;
1017
+ pointer-events: none;
1018
+ }
1019
+
1020
+ :root[data-theme="dark"] .action-button.approve {
1021
+ color: #63dbad;
1022
+ border-color: #226b54;
1023
+ background: #102d24;
1024
+ }
1025
+
1026
+ :root[data-theme="dark"] .action-button.deny {
1027
+ color: #ff9b93;
1028
+ border-color: #7c3434;
1029
+ background: #321819;
1030
+ }
1031
+
1032
+ .row-menu {
1033
+ display: inline-grid;
1034
+ place-items: center;
1035
+ width: 28px;
1036
+ height: 28px;
1037
+ border: 1px solid var(--border);
1038
+ border-radius: 6px;
1039
+ background: var(--surface);
1040
+ color: var(--text);
1041
+ }
1042
+
1043
+ .row-menu svg {
1044
+ width: 16px;
1045
+ height: 16px;
1046
+ }
1047
+
1048
+ .provider-cell {
1049
+ display: flex;
1050
+ align-items: center;
1051
+ gap: 10px;
1052
+ font-weight: 650;
1053
+ }
1054
+
1055
+ .provider-icon {
1056
+ display: grid;
1057
+ place-items: center;
1058
+ width: 22px;
1059
+ height: 22px;
1060
+ color: var(--text);
1061
+ flex: 0 0 auto;
1062
+ }
1063
+
1064
+ .provider-icon.text-mark {
1065
+ font-size: 10px;
1066
+ font-weight: 850;
1067
+ }
1068
+
1069
+ code {
1070
+ display: inline-block;
1071
+ max-width: 210px;
1072
+ padding: 4px 8px;
1073
+ border: 1px solid var(--border);
1074
+ border-radius: 6px;
1075
+ background: var(--surface-soft);
1076
+ color: var(--text);
1077
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
1078
+ font-size: 12px;
1079
+ line-height: 1.2;
1080
+ overflow: hidden;
1081
+ text-overflow: ellipsis;
1082
+ vertical-align: middle;
1083
+ }
1084
+
1085
+ .table-footer {
1086
+ display: flex;
1087
+ align-items: center;
1088
+ justify-content: center;
1089
+ min-height: 40px;
1090
+ padding: 0 16px;
1091
+ border-top: 1px solid var(--border);
1092
+ color: var(--muted);
1093
+ font-size: 13px;
1094
+ }
1095
+
1096
+ .panel-head > .table-footer {
1097
+ min-height: auto;
1098
+ padding: 0;
1099
+ border-top: 0;
1100
+ justify-content: flex-end;
1101
+ white-space: nowrap;
1102
+ }
1103
+
1104
+ .audit table {
1105
+ min-width: 980px;
1106
+ }
1107
+
1108
+ .audit td {
1109
+ white-space: normal;
1110
+ }
1111
+
1112
+ .audit td:first-child,
1113
+ .audit td:nth-child(5),
1114
+ .audit td:nth-child(6) {
1115
+ white-space: nowrap;
1116
+ }
1117
+
1118
+ .actor {
1119
+ display: grid;
1120
+ grid-template-columns: 28px minmax(0, 1fr);
1121
+ gap: 10px;
1122
+ align-items: center;
1123
+ }
1124
+
1125
+ .event-kind {
1126
+ display: flex;
1127
+ align-items: center;
1128
+ gap: 10px;
1129
+ color: var(--text);
1130
+ }
1131
+
1132
+ .event-kind svg {
1133
+ width: 19px;
1134
+ height: 19px;
1135
+ }
1136
+
1137
+ .audit-details {
1138
+ color: var(--text);
1139
+ white-space: normal;
1140
+ min-width: 360px;
1141
+ }
1142
+
1143
+ .audit-details span {
1144
+ display: inline-block;
1145
+ margin-right: 14px;
1146
+ }
1147
+
1148
+ .token {
1149
+ min-width: 190px;
1150
+ text-align: center;
1151
+ }
1152
+
1153
+ .chevron-cell {
1154
+ width: 28px;
1155
+ text-align: right;
1156
+ }
1157
+
1158
+ .toast {
1159
+ position: fixed;
1160
+ right: 22px;
1161
+ bottom: 22px;
1162
+ z-index: 30;
1163
+ max-width: min(360px, calc(100vw - 44px));
1164
+ padding: 12px 14px;
1165
+ border: 1px solid var(--border);
1166
+ border-radius: var(--radius);
1167
+ background: var(--surface);
1168
+ color: var(--text);
1169
+ box-shadow: var(--shadow);
1170
+ font-size: 13px;
1171
+ font-weight: 650;
1172
+ opacity: 0;
1173
+ transform: translateY(8px);
1174
+ pointer-events: none;
1175
+ transition: opacity .18s ease, transform .18s ease;
1176
+ }
1177
+
1178
+ .toast.show {
1179
+ opacity: 1;
1180
+ transform: translateY(0);
1181
+ }
1182
+
1183
+ .menu-popover {
1184
+ position: fixed;
1185
+ z-index: 45;
1186
+ display: none;
1187
+ min-width: 210px;
1188
+ max-width: min(280px, calc(100vw - 24px));
1189
+ padding: 6px;
1190
+ border: 1px solid var(--border);
1191
+ border-radius: 8px;
1192
+ background: var(--surface);
1193
+ color: var(--text);
1194
+ box-shadow: var(--shadow);
1195
+ }
1196
+
1197
+ .menu-popover.show {
1198
+ display: block;
1199
+ }
1200
+
1201
+ .menu-item {
1202
+ display: grid;
1203
+ grid-template-columns: minmax(0, 1fr);
1204
+ width: 100%;
1205
+ padding: 9px 10px;
1206
+ border: 0;
1207
+ border-radius: 6px;
1208
+ background: transparent;
1209
+ color: var(--text);
1210
+ text-align: left;
1211
+ font: inherit;
1212
+ font-size: 13px;
1213
+ line-height: 1.25;
1214
+ }
1215
+
1216
+ .menu-item:hover,
1217
+ .menu-item:focus-visible {
1218
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
1219
+ outline: none;
1220
+ }
1221
+
1222
+ .menu-item.danger {
1223
+ color: var(--red);
1224
+ }
1225
+
1226
+ .menu-item[disabled] {
1227
+ color: var(--muted);
1228
+ cursor: not-allowed;
1229
+ }
1230
+
1231
+ .menu-item span {
1232
+ display: block;
1233
+ margin-top: 3px;
1234
+ color: var(--muted);
1235
+ font-size: 12px;
1236
+ overflow-wrap: anywhere;
1237
+ }
1238
+
1239
+ .banner {
1240
+ display: none;
1241
+ align-items: flex-start;
1242
+ gap: 12px;
1243
+ padding: 14px 16px;
1244
+ margin-bottom: 18px;
1245
+ border-radius: 12px;
1246
+ border: 1px solid transparent;
1247
+ font-size: 14px;
1248
+ }
1249
+
1250
+ .banner.show {
1251
+ display: flex;
1252
+ }
1253
+
1254
+ .banner-warn {
1255
+ background: rgba(234, 179, 8, .12);
1256
+ border-color: rgba(234, 179, 8, .4);
1257
+ color: var(--text);
1258
+ }
1259
+
1260
+ .banner-error {
1261
+ background: rgba(239, 68, 68, .12);
1262
+ border-color: rgba(239, 68, 68, .45);
1263
+ color: var(--text);
1264
+ }
1265
+
1266
+ .banner-approval {
1267
+ background: rgba(245, 158, 11, .15);
1268
+ border-color: rgba(245, 158, 11, .48);
1269
+ color: var(--text);
1270
+ }
1271
+
1272
+ .banner-icon svg {
1273
+ width: 20px;
1274
+ height: 20px;
1275
+ stroke: currentColor;
1276
+ flex: none;
1277
+ }
1278
+
1279
+ .banner-warn .banner-icon svg {
1280
+ stroke: #b08400;
1281
+ }
1282
+
1283
+ .banner-error .banner-icon svg {
1284
+ stroke: #dc2626;
1285
+ }
1286
+
1287
+ .banner-approval .banner-icon svg {
1288
+ stroke: #d97706;
1289
+ }
1290
+
1291
+ .banner-body {
1292
+ flex: 1;
1293
+ min-width: 0;
1294
+ }
1295
+
1296
+ .banner-body strong {
1297
+ display: block;
1298
+ font-weight: 750;
1299
+ }
1300
+
1301
+ .banner-body p {
1302
+ margin: 4px 0 0;
1303
+ color: var(--muted);
1304
+ }
1305
+
1306
+ .banner-list {
1307
+ margin: 6px 0 0;
1308
+ padding-left: 18px;
1309
+ color: var(--muted);
1310
+ }
1311
+
1312
+ .banner-list li {
1313
+ margin-top: 3px;
1314
+ }
1315
+
1316
+ .banner-list code,
1317
+ .banner-body code {
1318
+ background: rgba(127, 127, 127, .16);
1319
+ padding: 1px 5px;
1320
+ border-radius: 5px;
1321
+ font-size: 12.5px;
1322
+ }
1323
+
1324
+ .banner-action {
1325
+ flex: none;
1326
+ align-self: center;
1327
+ border: 1px solid rgba(239, 68, 68, .5);
1328
+ background: transparent;
1329
+ color: var(--text);
1330
+ font-weight: 650;
1331
+ font-size: 13px;
1332
+ padding: 7px 14px;
1333
+ border-radius: 9px;
1334
+ cursor: pointer;
1335
+ }
1336
+
1337
+ .banner-action:hover {
1338
+ background: rgba(239, 68, 68, .14);
1339
+ }
1340
+
1341
+ .metric[data-high-risk-jump] {
1342
+ cursor: pointer;
1343
+ }
1344
+
1345
+ .metric[data-high-risk-jump]:hover {
1346
+ border-color: color-mix(in srgb, var(--red) 45%, var(--border));
1347
+ }
1348
+
1349
+ .empty-state {
1350
+ display: none;
1351
+ min-height: 72px;
1352
+ padding: 24px;
1353
+ color: var(--muted);
1354
+ text-align: center;
1355
+ font-size: 14px;
1356
+ }
1357
+
1358
+ .empty-state.show {
1359
+ display: block;
1360
+ }
1361
+
1362
+ .approval-panel tbody {
1363
+ min-height: 210px;
1364
+ }
1365
+
1366
+ .grants-panel {
1367
+ margin-bottom: 18px;
1368
+ }
1369
+
1370
+ .grants-panel table {
1371
+ min-width: 820px;
1372
+ }
1373
+
1374
+ .grants-panel td:nth-child(3) {
1375
+ overflow-wrap: anywhere;
1376
+ white-space: normal;
1377
+ }
1378
+
1379
+ .flow-panel {
1380
+ margin-bottom: 18px;
1381
+ }
1382
+
1383
+ .flow-layout {
1384
+ display: grid;
1385
+ grid-template-columns: 1fr;
1386
+ gap: 16px;
1387
+ padding: 16px;
1388
+ }
1389
+
1390
+ .flow-legend {
1391
+ display: flex;
1392
+ flex-wrap: wrap;
1393
+ gap: 8px;
1394
+ padding: 0 16px 14px;
1395
+ }
1396
+
1397
+ .flow-legend span {
1398
+ display: inline-flex;
1399
+ min-height: 24px;
1400
+ align-items: center;
1401
+ gap: 7px;
1402
+ padding: 0 9px;
1403
+ border: 1px solid var(--border);
1404
+ border-radius: 6px;
1405
+ background: var(--surface-soft);
1406
+ color: var(--muted);
1407
+ font-size: 11px;
1408
+ font-weight: 750;
1409
+ }
1410
+
1411
+ .flow-legend i {
1412
+ width: 8px;
1413
+ height: 8px;
1414
+ border-radius: 2px;
1415
+ background: var(--blue);
1416
+ }
1417
+
1418
+ .flow-legend .agent i { background: #7567e8; }
1419
+ .flow-legend .auth i { background: #0b9f91; }
1420
+ .flow-legend .target i { background: #1d73d4; }
1421
+
1422
+ .sankey-wrap {
1423
+ position: relative;
1424
+ min-height: 500px;
1425
+ border: 1px solid var(--border);
1426
+ border-radius: var(--radius);
1427
+ background: linear-gradient(180deg, var(--surface-soft), var(--surface));
1428
+ overflow: auto;
1429
+ }
1430
+
1431
+ .sankey-wrap svg {
1432
+ display: block;
1433
+ width: max(100%, 1100px);
1434
+ min-width: 1100px;
1435
+ height: 500px;
1436
+ }
1437
+
1438
+ .sankey-link {
1439
+ fill: none;
1440
+ cursor: pointer;
1441
+ stroke-linecap: butt;
1442
+ stroke-opacity: .30;
1443
+ transition: stroke-opacity .15s ease, filter .15s ease;
1444
+ pointer-events: stroke;
1445
+ }
1446
+
1447
+ :root[data-theme="dark"] .sankey-link {
1448
+ stroke-opacity: .32;
1449
+ }
1450
+
1451
+ .sankey-link:hover {
1452
+ stroke-opacity: .52;
1453
+ }
1454
+
1455
+ .sankey-link.is-selected {
1456
+ stroke-opacity: .68;
1457
+ }
1458
+
1459
+ .sankey-link.is-muted,
1460
+ .sankey-node.is-muted {
1461
+ opacity: .28;
1462
+ }
1463
+
1464
+ .sankey-node rect {
1465
+ cursor: pointer;
1466
+ stroke: var(--surface);
1467
+ stroke: color-mix(in srgb, var(--surface) 72%, transparent);
1468
+ stroke-width: 1;
1469
+ shape-rendering: geometricPrecision;
1470
+ }
1471
+
1472
+ .sankey-node.agent rect {
1473
+ fill: #7567e8;
1474
+ }
1475
+
1476
+ .sankey-node.auth rect {
1477
+ fill: #0b9f91;
1478
+ }
1479
+
1480
+ .sankey-node.target rect {
1481
+ fill: #1d73d4;
1482
+ }
1483
+
1484
+ .sankey-node.is-selected rect {
1485
+ stroke: var(--text);
1486
+ stroke-width: 1.5;
1487
+ filter: drop-shadow(0 2px 7px rgba(29, 115, 212, .26));
1488
+ }
1489
+
1490
+ .sankey-heading {
1491
+ fill: var(--text);
1492
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1493
+ font-size: 18px;
1494
+ font-weight: 850;
1495
+ letter-spacing: 0;
1496
+ text-transform: none;
1497
+ }
1498
+
1499
+ .sankey-heading.is-compact-heading {
1500
+ font-size: 13px;
1501
+ font-weight: 850;
1502
+ }
1503
+
1504
+ .sankey-label {
1505
+ fill: var(--text);
1506
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1507
+ font-size: 13.5px;
1508
+ font-synthesis-weight: none;
1509
+ font-weight: 400;
1510
+ paint-order: normal;
1511
+ pointer-events: none;
1512
+ stroke: transparent;
1513
+ stroke-linejoin: round;
1514
+ stroke-opacity: 0;
1515
+ stroke-width: 0;
1516
+ transition: opacity .12s ease;
1517
+ }
1518
+
1519
+ .sankey-value {
1520
+ fill: var(--muted);
1521
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1522
+ font-size: 12px;
1523
+ font-synthesis-weight: none;
1524
+ font-weight: 400;
1525
+ paint-order: normal;
1526
+ pointer-events: none;
1527
+ stroke: transparent;
1528
+ stroke-linejoin: round;
1529
+ stroke-opacity: 0;
1530
+ stroke-width: 0;
1531
+ transition: opacity .12s ease;
1532
+ }
1533
+
1534
+ .sankey-label.is-compact-label {
1535
+ font-size: 11px;
1536
+ }
1537
+
1538
+ .sankey-label.is-secondary-label,
1539
+ .sankey-value.is-secondary-label {
1540
+ opacity: 0;
1541
+ }
1542
+
1543
+ .sankey-node:hover .is-secondary-label,
1544
+ .sankey-node:focus .is-secondary-label,
1545
+ .sankey-node.is-selected .is-secondary-label {
1546
+ opacity: 1;
1547
+ }
1548
+
1549
+ .flow-empty {
1550
+ display: none;
1551
+ position: absolute;
1552
+ inset: 0;
1553
+ place-items: center;
1554
+ padding: 28px;
1555
+ color: var(--muted);
1556
+ text-align: center;
1557
+ font-size: 14px;
1558
+ }
1559
+
1560
+ .flow-empty.show {
1561
+ display: grid;
1562
+ }
1563
+
1564
+ .flow-detail {
1565
+ min-width: 0;
1566
+ border: 1px solid var(--border);
1567
+ border-radius: var(--radius);
1568
+ overflow: auto;
1569
+ }
1570
+
1571
+ .flow-drill {
1572
+ display: flex;
1573
+ align-items: flex-start;
1574
+ justify-content: space-between;
1575
+ gap: 12px;
1576
+ margin: 0 16px;
1577
+ padding: 12px 14px;
1578
+ border: 1px solid var(--border);
1579
+ border-radius: var(--radius);
1580
+ background: var(--surface-soft);
1581
+ }
1582
+
1583
+ .flow-drill-kicker {
1584
+ color: var(--muted);
1585
+ font-size: 11px;
1586
+ font-weight: 800;
1587
+ letter-spacing: 0;
1588
+ text-transform: uppercase;
1589
+ }
1590
+
1591
+ .flow-drill strong {
1592
+ display: block;
1593
+ margin-top: 3px;
1594
+ color: var(--text);
1595
+ font-size: 14px;
1596
+ }
1597
+
1598
+ .flow-drill p {
1599
+ margin: 4px 0 0;
1600
+ color: var(--muted);
1601
+ font-size: 12px;
1602
+ line-height: 1.35;
1603
+ }
1604
+
1605
+ .flow-detail table {
1606
+ width: 100%;
1607
+ min-width: 700px;
1608
+ table-layout: fixed;
1609
+ }
1610
+
1611
+ .flow-detail th,
1612
+ .flow-detail td {
1613
+ white-space: normal;
1614
+ vertical-align: top;
1615
+ overflow-wrap: anywhere;
1616
+ }
1617
+
1618
+ .flow-detail th:nth-child(1),
1619
+ .flow-detail td:nth-child(1) {
1620
+ width: 20%;
1621
+ }
1622
+
1623
+ .flow-detail th:nth-child(2),
1624
+ .flow-detail td:nth-child(2) {
1625
+ width: 32%;
1626
+ }
1627
+
1628
+ .flow-detail th:nth-child(3),
1629
+ .flow-detail td:nth-child(3) {
1630
+ width: 36%;
1631
+ }
1632
+
1633
+ .flow-detail th:nth-child(4),
1634
+ .flow-detail td:nth-child(4) {
1635
+ width: 12%;
1636
+ overflow-wrap: normal;
1637
+ white-space: nowrap;
1638
+ }
1639
+
1640
+ .flow-detail[data-flow-detail-width="medium"] table,
1641
+ .flow-detail[data-flow-detail-width="narrow"] table,
1642
+ .flow-detail[data-flow-detail-width="compact"] table {
1643
+ min-width: 0;
1644
+ }
1645
+
1646
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-detail[data-flow-detail-width="medium"] table,
1647
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-detail[data-flow-detail-width="narrow"] table,
1648
+ :root[data-embed="usage-flow"][data-flow-selected="true"] .flow-detail[data-flow-detail-width="compact"] table {
1649
+ min-width: 0;
1650
+ }
1651
+
1652
+ .flow-detail[data-flow-detail-width="medium"] .cell-sub,
1653
+ .flow-detail[data-flow-detail-width="medium"] .flow-state,
1654
+ .flow-detail[data-flow-detail-width="narrow"] .cell-sub,
1655
+ .flow-detail[data-flow-detail-width="narrow"] .flow-state,
1656
+ .flow-detail[data-flow-detail-width="compact"] .cell-sub,
1657
+ .flow-detail[data-flow-detail-width="compact"] .flow-state {
1658
+ display: none;
1659
+ }
1660
+
1661
+ .flow-detail[data-flow-detail-width="narrow"] th:nth-child(1),
1662
+ .flow-detail[data-flow-detail-width="narrow"] td:nth-child(1),
1663
+ .flow-detail[data-flow-detail-width="compact"] th:nth-child(1),
1664
+ .flow-detail[data-flow-detail-width="compact"] td:nth-child(1) {
1665
+ display: none;
1666
+ }
1667
+
1668
+ .flow-detail[data-flow-detail-width="narrow"] th:nth-child(2),
1669
+ .flow-detail[data-flow-detail-width="narrow"] td:nth-child(2) {
1670
+ width: 48%;
1671
+ }
1672
+
1673
+ .flow-detail[data-flow-detail-width="narrow"] th:nth-child(3),
1674
+ .flow-detail[data-flow-detail-width="narrow"] td:nth-child(3) {
1675
+ width: 38%;
1676
+ }
1677
+
1678
+ .flow-detail[data-flow-detail-width="narrow"] th:nth-child(4),
1679
+ .flow-detail[data-flow-detail-width="narrow"] td:nth-child(4) {
1680
+ width: 74px;
1681
+ }
1682
+
1683
+ .flow-detail[data-flow-detail-width="compact"] th:nth-child(3),
1684
+ .flow-detail[data-flow-detail-width="compact"] td:nth-child(3),
1685
+ .flow-detail[data-flow-detail-width="compact"] th:nth-child(4),
1686
+ .flow-detail[data-flow-detail-width="compact"] td:nth-child(4) {
1687
+ display: none;
1688
+ }
1689
+
1690
+ .flow-detail[data-flow-detail-width="compact"] th:nth-child(2),
1691
+ .flow-detail[data-flow-detail-width="compact"] td:nth-child(2) {
1692
+ width: 100%;
1693
+ }
1694
+
1695
+ .flow-detail[data-flow-detail-width="compact"] th:nth-child(2)::after {
1696
+ content: " / target";
1697
+ color: var(--muted);
1698
+ font-weight: 700;
1699
+ }
1700
+
1701
+ .flow-detail[data-flow-detail-width="compact"] td:nth-child(2)::after {
1702
+ content: attr(data-compact-target);
1703
+ display: block;
1704
+ margin-top: 4px;
1705
+ color: var(--muted);
1706
+ font-size: 12px;
1707
+ line-height: 1.3;
1708
+ }
1709
+
1710
+ .flow-detail .agent-cell {
1711
+ align-items: start;
1712
+ }
1713
+
1714
+ .flow-state {
1715
+ display: flex;
1716
+ flex-wrap: wrap;
1717
+ gap: 4px;
1718
+ margin-top: 5px;
1719
+ }
1720
+
1721
+ .state-chip {
1722
+ display: inline-flex;
1723
+ min-height: 20px;
1724
+ align-items: center;
1725
+ padding: 0 6px;
1726
+ border-radius: 5px;
1727
+ background: var(--surface-soft);
1728
+ color: var(--muted);
1729
+ font-size: 11px;
1730
+ font-weight: 700;
1731
+ }
1732
+
1733
+ @media (max-width: 1180px) {
1734
+ .app-shell {
1735
+ grid-template-columns: 1fr;
1736
+ }
1737
+
1738
+ .sidebar {
1739
+ display: none;
1740
+ }
1741
+
1742
+ .mobile-brand {
1743
+ display: flex;
1744
+ align-items: center;
1745
+ gap: 10px;
1746
+ font-weight: 850;
1747
+ }
1748
+
1749
+ .topbar {
1750
+ grid-template-columns: 1fr;
1751
+ min-height: auto;
1752
+ padding: 14px 18px;
1753
+ }
1754
+
1755
+ .status-strip {
1756
+ justify-content: space-between;
1757
+ }
1758
+
1759
+ .top-actions {
1760
+ justify-content: flex-start;
1761
+ flex-wrap: wrap;
1762
+ }
1763
+
1764
+ .search {
1765
+ width: min(520px, 100%);
1766
+ }
1767
+
1768
+ .metrics,
1769
+ .panel-grid {
1770
+ grid-template-columns: 1fr 1fr;
1771
+ }
1772
+
1773
+ .flow-layout {
1774
+ grid-template-columns: 1fr;
1775
+ }
1776
+
1777
+ .panel-grid .panel {
1778
+ min-width: 0;
1779
+ }
1780
+ }
1781
+
1782
+ @media (max-width: 760px) {
1783
+ .status-strip {
1784
+ align-items: flex-start;
1785
+ flex-direction: column;
1786
+ gap: 10px;
1787
+ }
1788
+
1789
+ .divider {
1790
+ display: none;
1791
+ }
1792
+
1793
+ .content {
1794
+ padding: 16px;
1795
+ }
1796
+
1797
+ .metrics,
1798
+ .panel-grid {
1799
+ grid-template-columns: 1fr;
1800
+ }
1801
+
1802
+ .metric {
1803
+ min-height: 104px;
1804
+ }
1805
+
1806
+ .panel-head {
1807
+ align-items: flex-start;
1808
+ flex-direction: column;
1809
+ padding: 14px;
1810
+ }
1811
+
1812
+ .panel-head .pill-button {
1813
+ width: 100%;
1814
+ }
1815
+
1816
+ .search {
1817
+ min-width: 100%;
1818
+ }
1819
+
1820
+ .queue-button {
1821
+ flex: 1 1 190px;
1822
+ }
1823
+
1824
+ .theme-toggle {
1825
+ flex: 0 0 42px;
1826
+ }
1827
+ }
1828
+ </style>
1829
+ </head>
1830
+ <body>
1831
+ <svg width="0" height="0" aria-hidden="true" focusable="false" style="position:absolute">
1832
+ <!-- UI icons sourced from Lucide Static 1.17.0; GitHub mark sourced from Simple Icons 16.22.0. See THIRD_PARTY_NOTICES.md. -->
1833
+ <symbol id="i-shield-lock" viewBox="0 0 24 24"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
1834
+ <path d="m9 12 2 2 4-4" /></symbol>
1835
+ <symbol id="i-home" viewBox="0 0 24 24"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
1836
+ <path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /></symbol>
1837
+ <symbol id="i-key" viewBox="0 0 24 24"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" />
1838
+ <circle cx="16.5" cy="7.5" r=".5" fill="currentColor" /></symbol>
1839
+ <symbol id="i-clipboard" viewBox="0 0 24 24"><rect width="8" height="4" x="8" y="2" rx="1" ry="1" />
1840
+ <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
1841
+ <path d="M12 11h4" />
1842
+ <path d="M12 16h4" />
1843
+ <path d="M8 11h.01" />
1844
+ <path d="M8 16h.01" /></symbol>
1845
+ <symbol id="i-bot" viewBox="0 0 24 24"><path d="M12 8V4H8" />
1846
+ <rect width="16" height="12" x="4" y="8" rx="2" />
1847
+ <path d="M2 14h2" />
1848
+ <path d="M20 14h2" />
1849
+ <path d="M15 13v2" />
1850
+ <path d="M9 13v2" /></symbol>
1851
+ <symbol id="i-settings" viewBox="0 0 24 24"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
1852
+ <circle cx="12" cy="12" r="3" /></symbol>
1853
+ <symbol id="i-monitor" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="3" rx="2" />
1854
+ <line x1="8" x2="16" y1="21" y2="21" />
1855
+ <line x1="12" x2="12" y1="17" y2="21" /></symbol>
1856
+ <symbol id="i-lock" viewBox="0 0 24 24"><circle cx="12" cy="16" r="1" />
1857
+ <rect x="3" y="10" width="18" height="12" rx="2" />
1858
+ <path d="M7 10V7a5 5 0 0 1 10 0v3" /></symbol>
1859
+ <symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" />
1860
+ <path d="M12 6v6l4 2" /></symbol>
1861
+ <symbol id="i-users" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
1862
+ <path d="M16 3.128a4 4 0 0 1 0 7.744" />
1863
+ <path d="M22 21v-2a4 4 0 0 0-3-3.87" />
1864
+ <circle cx="9" cy="7" r="4" /></symbol>
1865
+ <symbol id="i-alert" viewBox="0 0 24 24"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
1866
+ <path d="M12 8v4" />
1867
+ <path d="M12 16h.01" /></symbol>
1868
+ <symbol id="i-search" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34" />
1869
+ <circle cx="11" cy="11" r="8" /></symbol>
1870
+ <symbol id="i-external" viewBox="0 0 24 24"><path d="M15 3h6v6" />
1871
+ <path d="M10 14 21 3" />
1872
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
1873
+ <symbol id="i-more" viewBox="0 0 24 24"><circle cx="12" cy="12" r="1" />
1874
+ <circle cx="12" cy="5" r="1" />
1875
+ <circle cx="12" cy="19" r="1" /></symbol>
1876
+ <symbol id="i-filter" viewBox="0 0 24 24"><path d="M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z" /></symbol>
1877
+ <symbol id="i-download" viewBox="0 0 24 24"><path d="M12 15V3" />
1878
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
1879
+ <path d="m7 10 5 5 5-5" /></symbol>
1880
+ <symbol id="i-check" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" />
1881
+ <path d="m9 12 2 2 4-4" /></symbol>
1882
+ <symbol id="i-terminal" viewBox="0 0 24 24"><path d="m7 11 2-2-2-2" />
1883
+ <path d="M11 13h4" />
1884
+ <rect width="18" height="18" x="3" y="3" rx="2" ry="2" /></symbol>
1885
+ <symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" />
1886
+ <path d="M12 16v-4" />
1887
+ <path d="M12 8h.01" /></symbol>
1888
+ <symbol id="i-chevron" viewBox="0 0 24 24"><path d="m9 18 6-6-6-6" /></symbol>
1889
+ <symbol id="i-chevron-down" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6" /></symbol>
1890
+ <symbol id="i-moon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" /></symbol>
1891
+ <symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4" />
1892
+ <path d="M12 2v2" />
1893
+ <path d="M12 20v2" />
1894
+ <path d="m4.93 4.93 1.41 1.41" />
1895
+ <path d="m17.66 17.66 1.41 1.41" />
1896
+ <path d="M2 12h2" />
1897
+ <path d="M20 12h2" />
1898
+ <path d="m6.34 17.66-1.41 1.41" />
1899
+ <path d="m19.07 4.93-1.41 1.41" /></symbol>
1900
+ <symbol id="i-github" viewBox="0 0 24 24"><path fill="currentColor" stroke="none" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" /></symbol>
1901
+ <symbol id="i-box" viewBox="0 0 24 24"><path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
1902
+ <path d="M12 22V12" />
1903
+ <polyline points="3.29 7 12 12 20.71 7" />
1904
+ <path d="m7.5 4.27 9 5.15" /></symbol>
1905
+ </svg>
1906
+
1907
+ <div class="app-shell">
1908
+ <aside class="sidebar">
1909
+ <div class="brand">
1910
+ <span class="brand-mark"><svg><use href="#i-shield-lock"></use></svg></span>
1911
+ <strong>s-gw</strong>
1912
+ </div>
1913
+
1914
+ <nav class="nav" aria-label="Primary">
1915
+ <button class="active" type="button" data-nav="overview"><svg><use href="#i-home"></use></svg><span>Overview</span></button>
1916
+ <button type="button" data-nav="flow"><svg><use href="#i-users"></use></svg><span>Usage Flow</span></button>
1917
+ <button type="button" data-nav="credentials"><svg><use href="#i-key"></use></svg><span>Credentials</span></button>
1918
+ <button type="button" data-nav="requests"><svg><use href="#i-clipboard"></use></svg><span>Requests</span><span class="bubble">2</span></button>
1919
+ <button type="button" data-nav="agents"><svg><use href="#i-bot"></use></svg><span>Agents</span></button>
1920
+ <button type="button" data-nav="audit"><svg><use href="#i-alert"></use></svg><span>Audit</span></button>
1921
+ <button type="button" data-nav="settings"><svg><use href="#i-settings"></use></svg><span>Settings</span></button>
1922
+ </nav>
1923
+
1924
+ <div class="profile-card">
1925
+ <button class="profile-button" type="button" data-menu="profile" aria-haspopup="menu">
1926
+ <svg><use href="#i-monitor"></use></svg>
1927
+ <span>Local profile</span>
1928
+ <svg><use href="#i-chevron-down"></use></svg>
1929
+ </button>
1930
+ <div class="version"><span class="dot"></span><span>v1.2.3</span></div>
1931
+ </div>
1932
+ </aside>
1933
+
1934
+ <main class="main">
1935
+ <header class="topbar">
1936
+ <div class="status-strip">
1937
+ <div class="mobile-brand">
1938
+ <span class="brand-mark"><svg><use href="#i-shield-lock"></use></svg></span>
1939
+ <span>s-gw</span>
1940
+ </div>
1941
+ <span class="status-item"><span class="dot"></span>Local daemon running <svg><use href="#i-monitor"></use></svg></span>
1942
+ <span class="divider"></span>
1943
+ <span class="status-item"><span class="dot"></span>macOS Keychain unlocked <svg><use href="#i-key"></use></svg></span>
1944
+ </div>
1945
+
1946
+ <div class="top-actions">
1947
+ <label class="search">
1948
+ <svg><use href="#i-search"></use></svg>
1949
+ <input type="search" data-search placeholder="Search..." aria-label="Search console rows">
1950
+ <span class="kbd">Cmd K</span>
1951
+ </label>
1952
+ <button class="icon-button theme-toggle" type="button" data-theme-toggle aria-label="Switch to dark mode" title="Switch theme">
1953
+ <svg class="moon"><use href="#i-moon"></use></svg>
1954
+ <svg class="sun"><use href="#i-sun"></use></svg>
1955
+ </button>
1956
+ <button class="pill-button queue-button" type="button" data-jump="#approvals">
1957
+ <svg><use href="#i-users"></use></svg>
1958
+ <span>Approve Queue</span>
1959
+ <span class="queue-count">2</span>
1960
+ </button>
1961
+ </div>
1962
+ </header>
1963
+
1964
+ <section class="content">
1965
+ <div class="banner banner-error" data-connection-banner role="alert">
1966
+ <span class="banner-icon"><svg><use href="#i-info"></use></svg></span>
1967
+ <div class="banner-body">
1968
+ <strong data-connection-title>Can't reach the local daemon</strong>
1969
+ <p data-connection-text>The console lost contact with <code>s-gw console</code>. Approvals and live state are paused.</p>
1970
+ </div>
1971
+ <button class="banner-action" type="button" data-connection-retry>Retry</button>
1972
+ </div>
1973
+
1974
+ <div class="banner banner-warn" data-readiness-banner role="status">
1975
+ <span class="banner-icon"><svg><use href="#i-key"></use></svg></span>
1976
+ <div class="banner-body">
1977
+ <strong data-readiness-title>s-gw is not ready yet</strong>
1978
+ <ul class="banner-list" data-readiness-blockers></ul>
1979
+ </div>
1980
+ </div>
1981
+
1982
+ <div class="banner banner-approval" data-approval-alert role="alert">
1983
+ <span class="banner-icon"><svg><use href="#i-clock"></use></svg></span>
1984
+ <div class="banner-body">
1985
+ <strong data-approval-alert-title>Approval needed now</strong>
1986
+ <p data-approval-alert-text>An agent is waiting for permission to use a local credential.</p>
1987
+ </div>
1988
+ <button class="banner-action" type="button" data-jump="#approvals">Review</button>
1989
+ </div>
1990
+
1991
+ <section class="metrics" aria-label="Summary">
1992
+ <article class="metric" data-searchable="local secrets handles stored securely">
1993
+ <span class="metric-icon"><svg><use href="#i-lock"></use></svg></span>
1994
+ <div>
1995
+ <div class="metric-label">Local secrets</div>
1996
+ <div class="metric-value">28</div>
1997
+ <div class="metric-note">Handles stored securely</div>
1998
+ </div>
1999
+ </article>
2000
+ <article class="metric" data-searchable="pending approvals review">
2001
+ <span class="metric-icon blue"><svg><use href="#i-clock"></use></svg></span>
2002
+ <div>
2003
+ <div class="metric-label">Pending approvals</div>
2004
+ <div class="metric-value">2</div>
2005
+ <div class="metric-note">Requires your review</div>
2006
+ </div>
2007
+ </article>
2008
+ <article class="metric" data-searchable="active agents connected idle">
2009
+ <span class="metric-icon"><svg><use href="#i-users"></use></svg></span>
2010
+ <div>
2011
+ <div class="metric-label">Active agents</div>
2012
+ <div class="metric-value">3</div>
2013
+ <div class="metric-note">Connected and idle</div>
2014
+ </div>
2015
+ </article>
2016
+ <article class="metric" data-searchable="high risk findings attention" data-high-risk-jump>
2017
+ <span class="metric-icon red"><svg><use href="#i-alert"></use></svg></span>
2018
+ <div>
2019
+ <div class="metric-label">High risk findings</div>
2020
+ <div class="metric-value">1</div>
2021
+ <div class="metric-note attention">Requires attention</div>
2022
+ </div>
2023
+ </article>
2024
+ </section>
2025
+
2026
+ <section class="panel flow-panel" id="usage-flow">
2027
+ <div class="panel-head">
2028
+ <h2 class="panel-title"><svg><use href="#i-users"></use></svg>Agent credential flow</h2>
2029
+ <span class="table-footer" data-flow-summary>0 requests mapped</span>
2030
+ </div>
2031
+ <div class="flow-legend" aria-label="Usage flow stages">
2032
+ <span class="agent"><i></i>Agent</span>
2033
+ <span class="auth"><i></i>Authentication type</span>
2034
+ <span class="target"><i></i>Target type</span>
2035
+ </div>
2036
+ <div class="flow-drill" data-flow-drill>
2037
+ <div>
2038
+ <div class="flow-drill-kicker" data-flow-drill-kicker>All routes</div>
2039
+ <strong data-flow-drill-title>All credential-use routes</strong>
2040
+ <p data-flow-drill-detail>Grouped by agent, authentication type, and target type.</p>
2041
+ </div>
2042
+ </div>
2043
+ <div class="flow-layout">
2044
+ <div class="sankey-wrap">
2045
+ <svg data-sankey role="img" aria-label="Agent to authentication type to target type flow"></svg>
2046
+ <div class="flow-empty" data-flow-empty>No credential-use requests yet.</div>
2047
+ </div>
2048
+ <div class="flow-detail">
2049
+ <button
2050
+ type="button"
2051
+ class="flow-resize-grip"
2052
+ data-flow-resize-grip
2053
+ role="separator"
2054
+ aria-label="Resize details panel"
2055
+ aria-orientation="vertical"
2056
+ title="Resize details panel"
2057
+ >
2058
+ <span class="flow-resize-handle" aria-hidden="true"></span>
2059
+ </button>
2060
+ <table>
2061
+ <thead>
2062
+ <tr>
2063
+ <th>Agent</th>
2064
+ <th>Authentication type</th>
2065
+ <th>Target type</th>
2066
+ <th>Requests</th>
2067
+ </tr>
2068
+ </thead>
2069
+ <tbody data-flow-rows></tbody>
2070
+ </table>
2071
+ </div>
2072
+ </div>
2073
+ </section>
2074
+
2075
+ <section class="panel-grid">
2076
+ <article class="panel approval-panel" id="approvals">
2077
+ <div class="panel-head">
2078
+ <h1 class="panel-title">Pending approvals <span class="title-count">2</span></h1>
2079
+ <button class="pill-button" type="button" data-menu="requests-panel" aria-haspopup="menu">
2080
+ <span>View all requests</span>
2081
+ <svg><use href="#i-external"></use></svg>
2082
+ </button>
2083
+ </div>
2084
+ <div class="table-wrap">
2085
+ <table>
2086
+ <thead>
2087
+ <tr>
2088
+ <th>Agent</th>
2089
+ <th>Requested command</th>
2090
+ <th>Target</th>
2091
+ <th>Handle</th>
2092
+ <th>Requested</th>
2093
+ <th>Risk</th>
2094
+ <th></th>
2095
+ </tr>
2096
+ </thead>
2097
+ <tbody>
2098
+ <tr data-searchable="codex /usr/bin/ssh prod-bastion ed25519 private key medium">
2099
+ <td><span class="agent-cell"><span class="agent-badge">C</span><span>Codex<span class="cell-sub">v1.0.1</span></span></span></td>
2100
+ <td>/usr/bin/ssh</td>
2101
+ <td>prod-bastion<span class="cell-sub">10.0.1.12</span></td>
2102
+ <td>s-gw:private-key:<span class="cell-sub">ed25519:prod-bastion</span></td>
2103
+ <td>2m ago<span class="cell-sub">12:42:18</span></td>
2104
+ <td><span class="risk medium">Medium</span></td>
2105
+ <td><button class="row-menu" type="button" data-menu="request" aria-haspopup="menu"><svg><use href="#i-more"></use></svg></button></td>
2106
+ </tr>
2107
+ <tr data-searchable="codex /usr/bin/ssh prod-bastion rsa private key high">
2108
+ <td><span class="agent-cell"><span class="agent-badge">C</span><span>Codex<span class="cell-sub">v1.0.1</span></span></span></td>
2109
+ <td>/usr/bin/ssh</td>
2110
+ <td>prod-bastion<span class="cell-sub">10.0.1.12</span></td>
2111
+ <td>s-gw:private-key:<span class="cell-sub">rsa:prod-bastion</span></td>
2112
+ <td>1m ago<span class="cell-sub">12:43:02</span></td>
2113
+ <td><span class="risk high">High</span></td>
2114
+ <td><button class="row-menu" type="button" data-menu="request" aria-haspopup="menu"><svg><use href="#i-more"></use></svg></button></td>
2115
+ </tr>
2116
+ </tbody>
2117
+ </table>
2118
+ </div>
2119
+ <div class="empty-state" data-empty="approvals">No approvals match this search.</div>
2120
+ <div class="table-footer">Showing 1-2 of 2</div>
2121
+ </article>
2122
+
2123
+ <article class="panel" id="credentials-panel">
2124
+ <div class="panel-head">
2125
+ <h2 class="panel-title">Credential handles</h2>
2126
+ <button class="pill-button" type="button" data-menu="credentials-panel" aria-haspopup="menu">
2127
+ <span>View all credentials</span>
2128
+ <svg><use href="#i-external"></use></svg>
2129
+ </button>
2130
+ </div>
2131
+ <div class="table-wrap">
2132
+ <table>
2133
+ <thead>
2134
+ <tr>
2135
+ <th>Provider</th>
2136
+ <th>Handle prefix</th>
2137
+ <th>Secrets</th>
2138
+ <th>Severity</th>
2139
+ <th>Last used</th>
2140
+ <th></th>
2141
+ </tr>
2142
+ </thead>
2143
+ <tbody>
2144
+ <tr data-searchable="github s-gw:github low">
2145
+ <td><span class="provider-cell"><span class="provider-icon"><svg><use href="#i-github"></use></svg></span>GitHub</span></td>
2146
+ <td>s-gw:github</td>
2147
+ <td>6</td>
2148
+ <td><span class="risk low">Low</span></td>
2149
+ <td>2h ago<span class="cell-sub">10:21:33</span></td>
2150
+ <td><button class="row-menu" type="button" data-menu="credential" aria-haspopup="menu"><svg><use href="#i-more"></use></svg></button></td>
2151
+ </tr>
2152
+ <tr data-searchable="aws s-gw:aws medium">
2153
+ <td><span class="provider-cell"><span class="provider-icon text-mark">aws</span>AWS</span></td>
2154
+ <td>s-gw:aws</td>
2155
+ <td>9</td>
2156
+ <td><span class="risk medium">Medium</span></td>
2157
+ <td>15m ago<span class="cell-sub">12:29:04</span></td>
2158
+ <td><button class="row-menu" type="button" data-menu="credential" aria-haspopup="menu"><svg><use href="#i-more"></use></svg></button></td>
2159
+ </tr>
2160
+ <tr data-searchable="openai s-gw:openai low">
2161
+ <td><span class="provider-cell"><span class="provider-icon text-mark">AI</span>OpenAI</span></td>
2162
+ <td>s-gw:openai</td>
2163
+ <td>3</td>
2164
+ <td><span class="risk low">Low</span></td>
2165
+ <td>3h ago<span class="cell-sub">09:15:11</span></td>
2166
+ <td><button class="row-menu" type="button" data-menu="credential" aria-haspopup="menu"><svg><use href="#i-more"></use></svg></button></td>
2167
+ </tr>
2168
+ <tr data-searchable="ssh s-gw:private-key high">
2169
+ <td><span class="provider-cell"><span class="provider-icon"><svg><use href="#i-terminal"></use></svg></span>SSH</span></td>
2170
+ <td>s-gw:private-key</td>
2171
+ <td>10</td>
2172
+ <td><span class="risk high">High</span></td>
2173
+ <td>5m ago<span class="cell-sub">12:39:48</span></td>
2174
+ <td><button class="row-menu" type="button" data-menu="credential" aria-haspopup="menu"><svg><use href="#i-more"></use></svg></button></td>
2175
+ </tr>
2176
+ <tr data-searchable="generic s-gw:generic low">
2177
+ <td><span class="provider-cell"><span class="provider-icon"><svg><use href="#i-box"></use></svg></span>Generic</span></td>
2178
+ <td>s-gw:generic</td>
2179
+ <td>0</td>
2180
+ <td><span class="risk low">Low</span></td>
2181
+ <td>-</td>
2182
+ <td><button class="row-menu" type="button" data-menu="credential" aria-haspopup="menu"><svg><use href="#i-more"></use></svg></button></td>
2183
+ </tr>
2184
+ </tbody>
2185
+ </table>
2186
+ </div>
2187
+ <div class="empty-state" data-empty="credentials">No credential handles match this search.</div>
2188
+ <div class="table-footer">Showing 1-5 of 5</div>
2189
+ </article>
2190
+ </section>
2191
+
2192
+ <section class="panel grants-panel" id="approval-grants">
2193
+ <div class="panel-head">
2194
+ <h2 class="panel-title"><svg><use href="#i-clock"></use></svg>Reusable approvals</h2>
2195
+ <button class="pill-button" type="button" data-revoke-all-grants>
2196
+ <span>Revoke all</span>
2197
+ </button>
2198
+ </div>
2199
+ <div class="table-wrap">
2200
+ <table>
2201
+ <thead>
2202
+ <tr>
2203
+ <th>Agent</th>
2204
+ <th>Scope</th>
2205
+ <th>Handle</th>
2206
+ <th>Mode</th>
2207
+ <th>Expires</th>
2208
+ <th></th>
2209
+ </tr>
2210
+ </thead>
2211
+ <tbody></tbody>
2212
+ </table>
2213
+ </div>
2214
+ <div class="empty-state" data-empty="grants">No reusable approvals are active.</div>
2215
+ <div class="table-footer">Showing 0 of 0</div>
2216
+ </section>
2217
+
2218
+ <section class="panel audit" id="audit">
2219
+ <div class="panel-head">
2220
+ <h2 class="panel-title"><svg><use href="#i-alert"></use></svg>Audit timeline</h2>
2221
+ <div class="top-actions">
2222
+ <button class="pill-button" type="button" data-menu="audit-filter" aria-haspopup="menu">
2223
+ <svg><use href="#i-filter"></use></svg>
2224
+ <span>All events</span>
2225
+ <svg><use href="#i-chevron-down"></use></svg>
2226
+ </button>
2227
+ <button class="pill-button" type="button" data-download>
2228
+ <svg><use href="#i-download"></use></svg>
2229
+ <span>Download</span>
2230
+ </button>
2231
+ </div>
2232
+ </div>
2233
+ <div class="table-wrap">
2234
+ <table>
2235
+ <thead>
2236
+ <tr>
2237
+ <th>Time</th>
2238
+ <th>Actor</th>
2239
+ <th>Event</th>
2240
+ <th>Details</th>
2241
+ <th></th>
2242
+ <th></th>
2243
+ </tr>
2244
+ </thead>
2245
+ <tbody>
2246
+ <tr data-searchable="codex approval requested /usr/bin/ssh prod-bastion ed25519">
2247
+ <td>12:43:02</td>
2248
+ <td><span class="actor"><span class="agent-badge">C</span><span>Codex (agent)<span class="cell-sub">192.168.1.42</span></span></span></td>
2249
+ <td><span class="event-kind"><svg><use href="#i-alert"></use></svg>Approval requested</span></td>
2250
+ <td class="audit-details"><span>Command: /usr/bin/ssh</span><span>Target: prod-bastion (10.0.1.12)</span><span>Handle: s-gw:private-key:rsa:prod-bastion</span><span>Purpose: connect</span></td>
2251
+ <td><code class="token">&lt;&lt;SGW_SECRET:8f2b...a1d4&gt;&gt;</code></td>
2252
+ <td class="chevron-cell"><svg><use href="#i-chevron"></use></svg></td>
2253
+ </tr>
2254
+ <tr data-searchable="codex approval requested /usr/bin/ssh prod-bastion ed25519">
2255
+ <td>12:42:18</td>
2256
+ <td><span class="actor"><span class="agent-badge">C</span><span>Codex (agent)<span class="cell-sub">192.168.1.42</span></span></span></td>
2257
+ <td><span class="event-kind"><svg><use href="#i-alert"></use></svg>Approval requested</span></td>
2258
+ <td class="audit-details"><span>Command: /usr/bin/ssh</span><span>Target: prod-bastion (10.0.1.12)</span><span>Handle: s-gw:private-key:ed25519:prod-bastion</span><span>Purpose: connect</span></td>
2259
+ <td><code class="token">&lt;&lt;SGW_SECRET:3c9e...7b21&gt;&gt;</code></td>
2260
+ <td class="chevron-cell"><svg><use href="#i-chevron"></use></svg></td>
2261
+ </tr>
2262
+ <tr data-searchable="you approved codex">
2263
+ <td>12:41:05</td>
2264
+ <td><span class="actor"><span class="provider-icon"><svg><use href="#i-users"></use></svg></span><span>You (user)<span class="cell-sub">macbook-pro.local</span></span></span></td>
2265
+ <td><span class="event-kind"><svg style="color:var(--green)"><use href="#i-check"></use></svg>Approved</span></td>
2266
+ <td class="audit-details"><span>Approved request for Codex (agent)</span><span>Handle: s-gw:private-key:ed25519:prod-bastion</span></td>
2267
+ <td><code class="token">&lt;&lt;SGW_SECRET:3c9e...7b21&gt;&gt;</code></td>
2268
+ <td class="chevron-cell"><svg><use href="#i-chevron"></use></svg></td>
2269
+ </tr>
2270
+ <tr data-searchable="codex secret accessed /usr/bin/ssh">
2271
+ <td>12:40:58</td>
2272
+ <td><span class="actor"><span class="agent-badge">C</span><span>Codex (agent)<span class="cell-sub">192.168.1.42</span></span></span></td>
2273
+ <td><span class="event-kind"><svg><use href="#i-terminal"></use></svg>Secret accessed</span></td>
2274
+ <td class="audit-details"><span>Accessed via /usr/bin/ssh</span><span>Handle: s-gw:private-key:ed25519:prod-bastion</span></td>
2275
+ <td><code class="token">&lt;&lt;SGW_SECRET:3c9e...7b21&gt;&gt;</code></td>
2276
+ <td class="chevron-cell"><svg><use href="#i-chevron"></use></svg></td>
2277
+ </tr>
2278
+ <tr data-searchable="system credential rotated rsa private key">
2279
+ <td>12:39:48</td>
2280
+ <td><span class="actor"><span class="provider-icon"><svg><use href="#i-settings"></use></svg></span><span>s-gw (system)</span></span></td>
2281
+ <td><span class="event-kind"><svg><use href="#i-info"></use></svg>Credential rotated</span></td>
2282
+ <td class="audit-details"><span>Handle: s-gw:private-key:rsa:prod-bastion</span><span>New version stored</span></td>
2283
+ <td><code class="token">&lt;&lt;SGW_SECRET:9a7d...c4f2&gt;&gt;</code></td>
2284
+ <td class="chevron-cell"><svg><use href="#i-chevron"></use></svg></td>
2285
+ </tr>
2286
+ </tbody>
2287
+ </table>
2288
+ </div>
2289
+ <div class="empty-state" data-empty="audit">No audit events match this search.</div>
2290
+ <div class="table-footer">Showing 1-5 of 256</div>
2291
+ </section>
2292
+ </section>
2293
+ </main>
2294
+ </div>
2295
+
2296
+ <div class="toast" data-toast-box role="status" aria-live="polite"></div>
2297
+ <div class="menu-popover" data-menu-popover role="menu" aria-hidden="true"></div>
2298
+
2299
+ <script>
2300
+ (function () {
2301
+ var root = document.documentElement;
2302
+ var toggle = document.querySelector("[data-theme-toggle]");
2303
+ var search = document.querySelector("[data-search]");
2304
+ var toastBox = document.querySelector("[data-toast-box]");
2305
+ var menuBox = document.querySelector("[data-menu-popover]");
2306
+ var activeMenuActions = {};
2307
+ var apiToken = window.SGW_CONSOLE_TOKEN || "";
2308
+ var liveMode = Boolean(window.SGW_CONSOLE_LIVE && apiToken);
2309
+ var currentState = null;
2310
+ var usageFlowFilter = null;
2311
+ var usageFlowCloseTimer = null;
2312
+ var usageFlowDetailObserver = null;
2313
+ var usageFlowWindowResizeBound = false;
2314
+ var usageFlowResize = null;
2315
+ // Requests with an approve/deny POST in flight. A credential gate must not
2316
+ // fire a second decision on the same row mid-round-trip — a double-clicked
2317
+ // approve would 409 ("already approved") and toast an error on a request
2318
+ // that actually succeeded; an approve-then-deny race is worse.
2319
+ var pendingActions = {};
2320
+
2321
+ function saveTheme(theme) {
2322
+ try {
2323
+ localStorage.setItem("sgw-theme", theme);
2324
+ } catch (err) {
2325
+ return;
2326
+ }
2327
+ }
2328
+
2329
+ function setTheme(theme, shouldSave) {
2330
+ root.dataset.theme = theme;
2331
+
2332
+ if (toggle) {
2333
+ var next = theme === "dark" ? "light" : "dark";
2334
+ toggle.setAttribute("aria-label", "Switch to " + next + " mode");
2335
+ toggle.title = "Switch to " + next + " mode";
2336
+ }
2337
+
2338
+ if (shouldSave) {
2339
+ saveTheme(theme);
2340
+ }
2341
+ }
2342
+
2343
+ function showToast(message) {
2344
+ if (!toastBox) {
2345
+ return;
2346
+ }
2347
+
2348
+ toastBox.textContent = message;
2349
+ toastBox.classList.add("show");
2350
+ window.clearTimeout(showToast.timer);
2351
+ showToast.timer = window.setTimeout(function () {
2352
+ toastBox.classList.remove("show");
2353
+ }, 1800);
2354
+ }
2355
+
2356
+ function showMenu(button, items) {
2357
+ if (!menuBox || !items.length) {
2358
+ return;
2359
+ }
2360
+
2361
+ activeMenuActions = {};
2362
+ menuBox.innerHTML = items.map(function (item, index) {
2363
+ var id = "m" + index;
2364
+ activeMenuActions[id] = item.action || function () {};
2365
+ return '<button class="menu-item' + (item.danger ? " danger" : "") + '" type="button" role="menuitem" data-menu-item="' + id + '"' +
2366
+ (item.disabled ? " disabled" : "") + ">" +
2367
+ escapeHtml(item.label) +
2368
+ (item.sub ? "<span>" + escapeHtml(item.sub) + "</span>" : "") +
2369
+ "</button>";
2370
+ }).join("");
2371
+ menuBox.classList.add("show");
2372
+ menuBox.setAttribute("aria-hidden", "false");
2373
+ positionMenu(button);
2374
+ }
2375
+
2376
+ function hideMenu() {
2377
+ if (!menuBox) {
2378
+ return;
2379
+ }
2380
+
2381
+ menuBox.classList.remove("show");
2382
+ menuBox.setAttribute("aria-hidden", "true");
2383
+ }
2384
+
2385
+ function positionMenu(button) {
2386
+ if (!menuBox) {
2387
+ return;
2388
+ }
2389
+
2390
+ var rect = button.getBoundingClientRect();
2391
+ var menuRect = menuBox.getBoundingClientRect();
2392
+ var maxLeft = window.innerWidth - menuRect.width - 12;
2393
+ var opensFromSidebar = Boolean(button.closest(".sidebar"));
2394
+ var preferredLeft = opensFromSidebar && rect.right + menuRect.width + 20 <= window.innerWidth
2395
+ ? rect.right + 8
2396
+ : rect.right - menuRect.width;
2397
+ var left = Math.max(12, Math.min(maxLeft, preferredLeft));
2398
+ var top = rect.bottom + 8;
2399
+ if (top + menuRect.height > window.innerHeight - 12) {
2400
+ top = Math.max(12, rect.top - menuRect.height - 8);
2401
+ }
2402
+
2403
+ menuBox.style.left = left + "px";
2404
+ menuBox.style.top = top + "px";
2405
+ }
2406
+
2407
+ function openMenuFor(button) {
2408
+ var kind = button.dataset.menu;
2409
+ if (kind === "profile") {
2410
+ showMenu(button, profileMenuItems());
2411
+ return;
2412
+ }
2413
+ if (kind === "credential") {
2414
+ showMenu(button, credentialMenuItems(button));
2415
+ return;
2416
+ }
2417
+ if (kind === "request") {
2418
+ showMenu(button, requestMenuItems(button));
2419
+ return;
2420
+ }
2421
+ if (kind === "audit-filter") {
2422
+ showMenu(button, auditFilterItems());
2423
+ return;
2424
+ }
2425
+ if (kind === "credentials-panel") {
2426
+ showMenu(button, credentialsPanelItems());
2427
+ return;
2428
+ }
2429
+ if (kind === "requests-panel") {
2430
+ showMenu(button, requestsPanelItems());
2431
+ return;
2432
+ }
2433
+ if (kind === "agents-nav") {
2434
+ showMenu(button, agentMenuItems());
2435
+ return;
2436
+ }
2437
+ if (kind === "settings-nav") {
2438
+ showMenu(button, settingsMenuItems());
2439
+ }
2440
+ }
2441
+
2442
+ function apiJson(path, options) {
2443
+ if (!liveMode) {
2444
+ return Promise.reject(new Error("Console API is not available from the static file."));
2445
+ }
2446
+
2447
+ var init = options || {};
2448
+ init.headers = Object.assign({
2449
+ "X-SGW-Console-Token": apiToken,
2450
+ "Content-Type": "application/json"
2451
+ }, init.headers || {});
2452
+
2453
+ return fetch(path, init).then(function (response) {
2454
+ return response.json().catch(function () {
2455
+ return {};
2456
+ }).then(function (payload) {
2457
+ if (!response.ok) {
2458
+ throw new Error(payload.error || "Console API request failed.");
2459
+ }
2460
+
2461
+ return payload;
2462
+ });
2463
+ });
2464
+ }
2465
+
2466
+ function apiText(path) {
2467
+ if (!liveMode) {
2468
+ return Promise.reject(new Error("Console API is not available from the static file."));
2469
+ }
2470
+
2471
+ return fetch(path, {
2472
+ headers: {
2473
+ "X-SGW-Console-Token": apiToken
2474
+ }
2475
+ }).then(function (response) {
2476
+ if (!response.ok) {
2477
+ throw new Error("Download failed.");
2478
+ }
2479
+
2480
+ return response.text();
2481
+ });
2482
+ }
2483
+
2484
+ function loadState(message, quiet) {
2485
+ if (!liveMode) {
2486
+ renderUsageFlow(mockUsageFlowState());
2487
+ updateSearch();
2488
+ return Promise.resolve();
2489
+ }
2490
+
2491
+ return apiJson("/api/state").then(function (state) {
2492
+ currentState = state;
2493
+ setConnectionError(false);
2494
+ renderState(state);
2495
+ updateSearch();
2496
+ if (message) {
2497
+ showToast(message);
2498
+ }
2499
+ }).catch(function (error) {
2500
+ setConnectionError(true, error && error.message);
2501
+ // Background polls show the persistent banner but skip the toast so a
2502
+ // downed daemon doesn't fire an alert every refresh tick.
2503
+ if (!quiet) {
2504
+ showToast(error.message || "Lost contact with the local daemon");
2505
+ }
2506
+ });
2507
+ }
2508
+
2509
+ function setConnectionError(failed, detail) {
2510
+ var banner = document.querySelector("[data-connection-banner]");
2511
+ if (!banner) {
2512
+ return;
2513
+ }
2514
+ // Only meaningful in live mode; the static file never had a daemon to lose.
2515
+ banner.classList.toggle("show", Boolean(failed) && liveMode);
2516
+ if (failed && detail) {
2517
+ var text = banner.querySelector("[data-connection-text]");
2518
+ if (text) {
2519
+ text.textContent = detail;
2520
+ }
2521
+ }
2522
+ }
2523
+
2524
+ function renderState(state) {
2525
+ renderReadiness(state);
2526
+ renderApprovalAlert(state);
2527
+ renderStatus(state);
2528
+ renderMetrics(state);
2529
+ renderUsageFlow(state);
2530
+ renderApprovals(state);
2531
+ renderCredentials(state);
2532
+ renderApprovalGrants(state);
2533
+ renderAudit(state);
2534
+ }
2535
+
2536
+ function renderApprovalAlert(state) {
2537
+ var banner = document.querySelector("[data-approval-alert]");
2538
+ if (!banner) {
2539
+ return;
2540
+ }
2541
+ var pending = state.pendingRequests || [];
2542
+ banner.classList.toggle("show", pending.length > 0);
2543
+ if (!pending.length) {
2544
+ return;
2545
+ }
2546
+
2547
+ var first = pending[0] || {};
2548
+ var title = banner.querySelector("[data-approval-alert-title]");
2549
+ var text = banner.querySelector("[data-approval-alert-text]");
2550
+ var agent = requestAgentName(first);
2551
+ var command = first.action && first.action.command ? shortPath(first.action.command) : "local command";
2552
+ if (title) {
2553
+ title.textContent = pending.length === 1 ? "Approval needed now" : pending.length + " approvals need review";
2554
+ }
2555
+ if (text) {
2556
+ text.textContent = agent + " is waiting to run " + command + " with " + shortHandle(first.handle || "s-gw handle") + ". This only approves local credential use; host-agent command prompts are separate.";
2557
+ }
2558
+ }
2559
+
2560
+ function renderReadiness(state) {
2561
+ var banner = document.querySelector("[data-readiness-banner]");
2562
+ if (!banner) {
2563
+ return;
2564
+ }
2565
+
2566
+ // Older daemons that predate the readiness verdict omit the field; treat that
2567
+ // as "no opinion" and keep the banner hidden rather than nagging falsely.
2568
+ var hasVerdict = state && typeof state.ready === "boolean";
2569
+ var ready = !hasVerdict || state.ready;
2570
+ banner.classList.toggle("show", liveMode && hasVerdict && !ready);
2571
+ if (ready) {
2572
+ return;
2573
+ }
2574
+
2575
+ var readiness = state.readiness || {};
2576
+ var title = banner.querySelector("[data-readiness-title]");
2577
+ if (title) {
2578
+ title.textContent = readiness.summary || "s-gw is not ready yet";
2579
+ }
2580
+
2581
+ var list = banner.querySelector("[data-readiness-blockers]");
2582
+ if (list) {
2583
+ var blockers = readiness.blockers && readiness.blockers.length
2584
+ ? readiness.blockers
2585
+ : ["Run s-gw setup to configure a local unlock source."];
2586
+ list.innerHTML = blockers.map(function (line) {
2587
+ return "<li>" + withCode(line) + "</li>";
2588
+ }).join("");
2589
+ }
2590
+ }
2591
+
2592
+ // Render `like this` spans as <code> without trusting the daemon text as HTML.
2593
+ function withCode(text) {
2594
+ return escapeHtml(String(text)).replace(/`([^`]+)`/g, function (_match, inner) {
2595
+ return "<code>" + inner + "</code>";
2596
+ });
2597
+ }
2598
+
2599
+ function renderStatus(state) {
2600
+ var version = document.querySelector(".version span:last-child");
2601
+ if (version) {
2602
+ version.textContent = "v" + state.version;
2603
+ }
2604
+
2605
+ var statusItems = document.querySelectorAll(".status-item");
2606
+ if (statusItems[0]) {
2607
+ statusItems[0].innerHTML = '<span class="dot"></span>Local daemon running <svg><use href="#i-monitor"></use></svg>';
2608
+ }
2609
+ if (statusItems[1]) {
2610
+ var unlock = state.status && state.status.unlock ? state.status.unlock : {};
2611
+ var label = unlock.activeSource === "macos-keychain" ? "macOS Keychain unlocked" :
2612
+ unlock.activeSource === "env" ? "Env unlock active" : "Unlock required";
2613
+ statusItems[1].innerHTML = '<span class="dot"></span>' + escapeHtml(label) + ' <svg><use href="#i-key"></use></svg>';
2614
+ }
2615
+ }
2616
+
2617
+ function renderMetrics(state) {
2618
+ var metrics = state.metrics || {};
2619
+ var cards = document.querySelectorAll(".metric");
2620
+ setMetric(cards[0], "Local secrets", metrics.localSecrets || 0, "Handles stored securely");
2621
+ setMetric(cards[1], "Pending approvals", metrics.pendingApprovals || 0, "Requires your review");
2622
+ setMetric(cards[2], "Supported agents", metrics.activeAgents || 0, "Local MCP profiles");
2623
+ setMetric(cards[3], "High risk findings", metrics.highRiskFindings || 0,
2624
+ metrics.highRiskFindings ? "Requires attention" : "No high risk findings");
2625
+
2626
+ document.querySelectorAll(".queue-count, .bubble, .title-count").forEach(function (item) {
2627
+ item.textContent = String(metrics.pendingApprovals || 0);
2628
+ });
2629
+ }
2630
+
2631
+ function setMetric(card, label, value, note) {
2632
+ if (!card) {
2633
+ return;
2634
+ }
2635
+
2636
+ var labelNode = card.querySelector(".metric-label");
2637
+ var valueNode = card.querySelector(".metric-value");
2638
+ var noteNode = card.querySelector(".metric-note");
2639
+ if (labelNode) {
2640
+ labelNode.textContent = label;
2641
+ }
2642
+ if (valueNode) {
2643
+ valueNode.textContent = String(value);
2644
+ }
2645
+ if (noteNode) {
2646
+ noteNode.textContent = note;
2647
+ noteNode.classList.toggle("attention", label === "High risk findings" && value > 0);
2648
+ }
2649
+ }
2650
+
2651
+ function renderUsageFlow(state) {
2652
+ var flow = state.usageFlow || { nodes: [], links: [], rows: [], totalRequests: 0 };
2653
+ var svg = document.querySelector("[data-sankey]");
2654
+ var rowsBody = document.querySelector("[data-flow-rows]");
2655
+ var empty = document.querySelector("[data-flow-empty]");
2656
+ var summary = document.querySelector("[data-flow-summary]");
2657
+ if (!svg || !rowsBody) {
2658
+ return;
2659
+ }
2660
+
2661
+ var allRows = flow.rows || [];
2662
+ var filteredRows = allRows.filter(function (row) {
2663
+ return flowRowMatchesFilter(row, usageFlowFilter);
2664
+ });
2665
+ var rows = filteredRows.slice(0, 12);
2666
+ var nodes = flow.nodes || [];
2667
+ var links = flow.links || [];
2668
+ if (summary) {
2669
+ summary.textContent = usageFlowFilter
2670
+ ? rowsRequestCount(filteredRows) + " matching requests"
2671
+ : (flow.totalRequests || 0) + " requests mapped";
2672
+ }
2673
+
2674
+ if (empty) {
2675
+ empty.classList.toggle("show", allRows.length === 0);
2676
+ }
2677
+ renderUsageFlowDrill(flow, filteredRows);
2678
+ rowsBody.innerHTML = rows.map(function (row) {
2679
+ var searchable = [row.agent, row.authType, row.targetType, row.credential, row.handle, row.action, row.command, row.target].join(" ");
2680
+ return '<tr data-searchable="' + attr(searchable) + '">' +
2681
+ '<td><span class="agent-cell"><span class="agent-badge">' + escapeHtml(row.agent.slice(0, 1) || "A") + '</span><span>' +
2682
+ escapeHtml(row.agent) + '<span class="cell-sub">' + escapeHtml(timeAgo(row.lastSeen)) + '</span></span></span></td>' +
2683
+ '<td data-compact-target="' + attr(row.targetType || "Other target") + '">' + escapeHtml(row.authType || "Unknown credential") + '<span class="cell-sub">' + escapeHtml(flowExamples(row.credentials, row.credential || row.handle)) + '</span></td>' +
2684
+ '<td>' + escapeHtml(row.targetType || "Other target") + '<span class="cell-sub">' + escapeHtml(flowExamples(row.targets, row.target || row.action || row.command)) + '</span></td>' +
2685
+ '<td>' + escapeHtml(String(row.count)) + flowStateMarkup(row.states) + '</td>' +
2686
+ '</tr>';
2687
+ }).join("");
2688
+ syncUsageFlowDetailWidth();
2689
+
2690
+ var compactEmbed = isCompactUsageFlowEmbed();
2691
+ var wrap = svg.closest(".sankey-wrap");
2692
+ var viewportHeight = Math.max(1, Math.floor(window.innerHeight || document.documentElement.clientHeight || 300));
2693
+ var containerWidth = wrap ? wrap.clientWidth : svg.getBoundingClientRect().width;
2694
+ var containerHeight = compactEmbed ? viewportHeight : (wrap ? wrap.clientHeight : viewportHeight);
2695
+ var maxColumn = sankeyMaxColumnSize(nodes);
2696
+ var width = compactEmbed
2697
+ ? Math.max(320, Math.floor(containerWidth || 360))
2698
+ : Math.max(1280, Math.floor(containerWidth || 1280), Math.min(1700, 960 + (nodes.length || 1) * 10));
2699
+ var height = compactEmbed
2700
+ ? Math.max(260, Math.floor(containerHeight || 300))
2701
+ : Math.max(640, Math.min(1800, 360 + maxColumn * 18 + links.length * 3));
2702
+ svg.style.minWidth = width + "px";
2703
+ svg.style.width = compactEmbed ? "100%" : "max(100%, " + width + "px)";
2704
+ svg.style.height = height + "px";
2705
+ svg.setAttribute("viewBox", "0 0 " + width + " " + height);
2706
+ try {
2707
+ svg.innerHTML = d3SankeySvg(nodes, links, width, height);
2708
+ svg.setAttribute("data-engine", "d3-sankey");
2709
+ svg.removeAttribute("data-render-error");
2710
+ updateUsageFlowSelection(svg);
2711
+ } catch (error) {
2712
+ console.error(error);
2713
+ svg.innerHTML = sankeyErrorSvg(width, height, error);
2714
+ svg.setAttribute("data-engine", "unavailable");
2715
+ svg.setAttribute("data-render-error", error && error.message ? error.message : String(error));
2716
+ }
2717
+ }
2718
+
2719
+ function isCompactUsageFlowEmbed() {
2720
+ return root.dataset.embed === "usage-flow" && root.dataset.embedDensity === "compact";
2721
+ }
2722
+
2723
+ function renderUsageFlowDrill(flow, rows) {
2724
+ root.dataset.flowSelected = usageFlowFilter ? "true" : "false";
2725
+ if (!usageFlowFilter) {
2726
+ delete root.dataset.flowClosing;
2727
+ }
2728
+ var box = document.querySelector("[data-flow-drill]");
2729
+ if (!box) {
2730
+ return;
2731
+ }
2732
+ var kicker = box.querySelector("[data-flow-drill-kicker]");
2733
+ var title = box.querySelector("[data-flow-drill-title]");
2734
+ var detail = box.querySelector("[data-flow-drill-detail]");
2735
+ var total = rowsRequestCount(rows);
2736
+ if (!usageFlowFilter) {
2737
+ if (kicker) {
2738
+ kicker.textContent = "All routes";
2739
+ }
2740
+ if (title) {
2741
+ title.textContent = "All credential-use routes";
2742
+ }
2743
+ if (detail) {
2744
+ detail.textContent = (flow.totalRequests || 0) + " requests across " + ((flow.rows || []).length) + " grouped agent/authentication/target routes.";
2745
+ }
2746
+ return;
2747
+ }
2748
+
2749
+ if (kicker) {
2750
+ kicker.textContent = usageFlowFilter.type === "link" ? "Selected link" : flowKindLabel(usageFlowFilter.kind);
2751
+ }
2752
+ if (title) {
2753
+ title.textContent = usageFlowFilter.label || "Selected route";
2754
+ }
2755
+ if (detail) {
2756
+ detail.textContent = total + " matching request" + (total === 1 ? "" : "s") + " across " + rows.length + " grouped route" + (rows.length === 1 ? "" : "s") + ".";
2757
+ }
2758
+ }
2759
+
2760
+ function rowsRequestCount(rows) {
2761
+ return rows.reduce(function (sum, row) {
2762
+ return sum + (Number(row.count) || 0);
2763
+ }, 0);
2764
+ }
2765
+
2766
+ function flowExamples(values, fallback) {
2767
+ var list = Array.isArray(values) ? values.filter(Boolean) : [];
2768
+ if (!list.length && fallback) {
2769
+ list = [fallback];
2770
+ }
2771
+ if (!list.length) {
2772
+ return "";
2773
+ }
2774
+ var shown = list.slice(0, 3);
2775
+ var more = list.length > shown.length ? " +" + (list.length - shown.length) + " more" : "";
2776
+ return shown.join(", ") + more;
2777
+ }
2778
+
2779
+ function flowRowMatchesFilter(row, filter) {
2780
+ if (!filter) {
2781
+ return true;
2782
+ }
2783
+
2784
+ if (filter.type === "node") {
2785
+ return row.agentId === filter.id ||
2786
+ row.authTypeId === filter.id ||
2787
+ row.targetTypeId === filter.id;
2788
+ }
2789
+
2790
+ if (filter.type === "link") {
2791
+ return flowRowHasLink(row, filter.source, filter.target);
2792
+ }
2793
+
2794
+ return true;
2795
+ }
2796
+
2797
+ function flowRowHasLink(row, source, target) {
2798
+ var pairs = [
2799
+ [row.agentId, row.authTypeId],
2800
+ [row.authTypeId, row.targetTypeId]
2801
+ ];
2802
+ return pairs.some(function (pair) {
2803
+ return pair[0] === source && pair[1] === target;
2804
+ });
2805
+ }
2806
+
2807
+ function usageFlowNodeById(id) {
2808
+ var flow = currentUsageFlow();
2809
+ return (flow.nodes || []).find(function (node) {
2810
+ return node.id === id;
2811
+ }) || null;
2812
+ }
2813
+
2814
+ function usageFlowLinkByPair(source, target) {
2815
+ var flow = currentUsageFlow();
2816
+ return (flow.links || []).find(function (link) {
2817
+ return link.source === source && link.target === target;
2818
+ }) || null;
2819
+ }
2820
+
2821
+ function currentUsageFlow() {
2822
+ if (currentState && currentState.usageFlow) {
2823
+ return currentState.usageFlow;
2824
+ }
2825
+ return mockUsageFlowState().usageFlow;
2826
+ }
2827
+
2828
+ function selectUsageFlowNode(id) {
2829
+ var node = usageFlowNodeById(id);
2830
+ if (!node) {
2831
+ return;
2832
+ }
2833
+ cancelUsageFlowClose();
2834
+ usageFlowFilter = {
2835
+ type: "node",
2836
+ id: id,
2837
+ kind: node.kind,
2838
+ label: node.label,
2839
+ detail: node.detail || ""
2840
+ };
2841
+ renderUsageFlow(currentState || mockUsageFlowState());
2842
+ }
2843
+
2844
+ function selectUsageFlowLink(source, target) {
2845
+ var link = usageFlowLinkByPair(source, target);
2846
+ var sourceNode = usageFlowNodeById(source);
2847
+ var targetNode = usageFlowNodeById(target);
2848
+ if (!link || !sourceNode || !targetNode) {
2849
+ return;
2850
+ }
2851
+ cancelUsageFlowClose();
2852
+ usageFlowFilter = {
2853
+ type: "link",
2854
+ source: source,
2855
+ target: target,
2856
+ label: sourceNode.label + " -> " + targetNode.label,
2857
+ detail: flowCountLabel(link.value)
2858
+ };
2859
+ renderUsageFlow(currentState || mockUsageFlowState());
2860
+ }
2861
+
2862
+ function clearUsageFlowFilter() {
2863
+ if (!usageFlowFilter) {
2864
+ cancelUsageFlowClose();
2865
+ root.dataset.flowSelected = "false";
2866
+ return;
2867
+ }
2868
+
2869
+ if (root.dataset.embed === "usage-flow") {
2870
+ if (root.dataset.flowClosing === "true") {
2871
+ return;
2872
+ }
2873
+ root.dataset.flowClosing = "true";
2874
+ usageFlowCloseTimer = window.setTimeout(function () {
2875
+ usageFlowCloseTimer = null;
2876
+ usageFlowFilter = null;
2877
+ delete root.dataset.flowClosing;
2878
+ renderUsageFlow(currentState || mockUsageFlowState());
2879
+ }, 220);
2880
+ return;
2881
+ }
2882
+
2883
+ usageFlowFilter = null;
2884
+ delete root.dataset.flowClosing;
2885
+ renderUsageFlow(currentState || mockUsageFlowState());
2886
+ }
2887
+
2888
+ function cancelUsageFlowClose() {
2889
+ if (usageFlowCloseTimer) {
2890
+ window.clearTimeout(usageFlowCloseTimer);
2891
+ usageFlowCloseTimer = null;
2892
+ }
2893
+ delete root.dataset.flowClosing;
2894
+ }
2895
+
2896
+ function dismissUsageFlowFilterForClick(target) {
2897
+ if (!usageFlowFilter || !target || !target.closest) {
2898
+ return;
2899
+ }
2900
+ if (target.closest(".flow-detail") || target.closest("[data-flow-drill]") || target.closest("[data-flow-resize-grip]")) {
2901
+ return;
2902
+ }
2903
+ clearUsageFlowFilter();
2904
+ }
2905
+
2906
+ function usageFlowDetailWidthRange() {
2907
+ var max = Math.max(380, window.innerWidth - 24);
2908
+ return {
2909
+ min: Math.min(380, max),
2910
+ max: max
2911
+ };
2912
+ }
2913
+
2914
+ function clampUsageFlowDetailWidth(width) {
2915
+ var range = usageFlowDetailWidthRange();
2916
+ return Math.max(range.min, Math.min(range.max, Math.round(width)));
2917
+ }
2918
+
2919
+ function startUsageFlowDetailResize(event) {
2920
+ var grip = event.target && event.target.closest ? event.target.closest("[data-flow-resize-grip]") : null;
2921
+ if (!grip) {
2922
+ return;
2923
+ }
2924
+ var detail = grip.closest(".flow-detail");
2925
+ if (!detail) {
2926
+ return;
2927
+ }
2928
+
2929
+ event.preventDefault();
2930
+ event.stopPropagation();
2931
+ cancelUsageFlowClose();
2932
+ usageFlowResize = {
2933
+ pointerId: event.pointerId,
2934
+ startX: event.clientX,
2935
+ startWidth: detail.getBoundingClientRect().width,
2936
+ detail: detail
2937
+ };
2938
+ root.dataset.flowResizing = "true";
2939
+ detail.dataset.flowResizing = "true";
2940
+ if (grip.setPointerCapture && event.pointerId !== undefined) {
2941
+ try {
2942
+ grip.setPointerCapture(event.pointerId);
2943
+ } catch (err) {
2944
+ // Synthetic pointer events in tests do not always have capture state.
2945
+ }
2946
+ }
2947
+ window.addEventListener("pointermove", resizeUsageFlowDetail);
2948
+ window.addEventListener("pointerup", finishUsageFlowDetailResize);
2949
+ window.addEventListener("pointercancel", finishUsageFlowDetailResize);
2950
+ }
2951
+
2952
+ function resizeUsageFlowDetailBy(delta) {
2953
+ var detail = document.querySelector(".flow-detail");
2954
+ if (!detail) {
2955
+ return;
2956
+ }
2957
+ var width = detail.getBoundingClientRect().width;
2958
+ detail.style.width = clampUsageFlowDetailWidth(width + delta) + "px";
2959
+ syncUsageFlowDetailWidth();
2960
+ }
2961
+
2962
+ function handleUsageFlowResizeKeydown(event) {
2963
+ var grip = event.target && event.target.closest ? event.target.closest("[data-flow-resize-grip]") : null;
2964
+ if (!grip) {
2965
+ return;
2966
+ }
2967
+
2968
+ var range = usageFlowDetailWidthRange();
2969
+ var step = event.shiftKey ? 80 : 24;
2970
+ if (event.key === "ArrowLeft") {
2971
+ event.preventDefault();
2972
+ cancelUsageFlowClose();
2973
+ resizeUsageFlowDetailBy(step);
2974
+ return;
2975
+ }
2976
+ if (event.key === "ArrowRight") {
2977
+ event.preventDefault();
2978
+ cancelUsageFlowClose();
2979
+ resizeUsageFlowDetailBy(-step);
2980
+ return;
2981
+ }
2982
+ if (event.key === "Home") {
2983
+ event.preventDefault();
2984
+ cancelUsageFlowClose();
2985
+ var detail = grip.closest(".flow-detail");
2986
+ if (detail) {
2987
+ detail.style.width = range.min + "px";
2988
+ syncUsageFlowDetailWidth();
2989
+ }
2990
+ return;
2991
+ }
2992
+ if (event.key === "End") {
2993
+ event.preventDefault();
2994
+ cancelUsageFlowClose();
2995
+ var endDetail = grip.closest(".flow-detail");
2996
+ if (endDetail) {
2997
+ endDetail.style.width = range.max + "px";
2998
+ syncUsageFlowDetailWidth();
2999
+ }
3000
+ }
3001
+ }
3002
+
3003
+ function resizeUsageFlowDetail(event) {
3004
+ if (!usageFlowResize) {
3005
+ return;
3006
+ }
3007
+ event.preventDefault();
3008
+ var nextWidth = usageFlowResize.startWidth + usageFlowResize.startX - event.clientX;
3009
+ usageFlowResize.detail.style.width = clampUsageFlowDetailWidth(nextWidth) + "px";
3010
+ syncUsageFlowDetailWidth();
3011
+ }
3012
+
3013
+ function finishUsageFlowDetailResize() {
3014
+ if (usageFlowResize && usageFlowResize.detail) {
3015
+ delete usageFlowResize.detail.dataset.flowResizing;
3016
+ }
3017
+ usageFlowResize = null;
3018
+ delete root.dataset.flowResizing;
3019
+ window.removeEventListener("pointermove", resizeUsageFlowDetail);
3020
+ window.removeEventListener("pointerup", finishUsageFlowDetailResize);
3021
+ window.removeEventListener("pointercancel", finishUsageFlowDetailResize);
3022
+ syncUsageFlowDetailWidth();
3023
+ }
3024
+
3025
+ function syncUsageFlowDetailWidth() {
3026
+ var detail = document.querySelector(".flow-detail");
3027
+ if (!detail) {
3028
+ return;
3029
+ }
3030
+
3031
+ if (detail.style.width) {
3032
+ detail.style.width = clampUsageFlowDetailWidth(parseFloat(detail.style.width)) + "px";
3033
+ }
3034
+
3035
+ var width = detail.getBoundingClientRect().width;
3036
+ var tier = "wide";
3037
+ if (width > 0 && width < 500) {
3038
+ tier = "compact";
3039
+ } else if (width > 0 && width < 620) {
3040
+ tier = "narrow";
3041
+ } else if (width > 0 && width < 820) {
3042
+ tier = "medium";
3043
+ }
3044
+ detail.dataset.flowDetailWidth = tier;
3045
+ updateUsageFlowResizeHandleState(detail);
3046
+ }
3047
+
3048
+ function updateUsageFlowResizeHandleState(detail) {
3049
+ var grip = detail.querySelector("[data-flow-resize-grip]");
3050
+ if (!grip) {
3051
+ return;
3052
+ }
3053
+ var range = usageFlowDetailWidthRange();
3054
+ grip.setAttribute("aria-valuemin", String(range.min));
3055
+ grip.setAttribute("aria-valuemax", String(range.max));
3056
+ grip.setAttribute("aria-valuenow", String(Math.round(detail.getBoundingClientRect().width)));
3057
+ }
3058
+
3059
+ function observeUsageFlowDetailWidth() {
3060
+ var detail = document.querySelector(".flow-detail");
3061
+ if (!detail) {
3062
+ return;
3063
+ }
3064
+
3065
+ if (window.ResizeObserver && !usageFlowDetailObserver) {
3066
+ usageFlowDetailObserver = new ResizeObserver(syncUsageFlowDetailWidth);
3067
+ usageFlowDetailObserver.observe(detail);
3068
+ }
3069
+ if (!window.ResizeObserver && !usageFlowWindowResizeBound) {
3070
+ usageFlowWindowResizeBound = true;
3071
+ window.addEventListener("resize", syncUsageFlowDetailWidth);
3072
+ }
3073
+ syncUsageFlowDetailWidth();
3074
+ }
3075
+
3076
+ function updateUsageFlowSelection(svg) {
3077
+ var hasFilter = Boolean(usageFlowFilter);
3078
+ svg.querySelectorAll("[data-flow-node]").forEach(function (node) {
3079
+ var selected = hasFilter && usageFlowFilter.type === "node" && node.dataset.flowNode === usageFlowFilter.id;
3080
+ var related = hasFilter && flowFilterTouchesNode(node.dataset.flowNode);
3081
+ node.classList.toggle("is-selected", Boolean(selected));
3082
+ node.classList.toggle("is-muted", hasFilter && !related);
3083
+ });
3084
+ svg.querySelectorAll("[data-flow-link-source]").forEach(function (link) {
3085
+ var selected = hasFilter && usageFlowFilter.type === "link" &&
3086
+ link.dataset.flowLinkSource === usageFlowFilter.source &&
3087
+ link.dataset.flowLinkTarget === usageFlowFilter.target;
3088
+ var related = hasFilter && flowFilterTouchesLink(link.dataset.flowLinkSource, link.dataset.flowLinkTarget);
3089
+ link.classList.toggle("is-selected", Boolean(selected));
3090
+ link.classList.toggle("is-muted", hasFilter && !related);
3091
+ });
3092
+ }
3093
+
3094
+ function flowFilterTouchesNode(id) {
3095
+ if (!usageFlowFilter || !id) {
3096
+ return true;
3097
+ }
3098
+ if (usageFlowFilter.type === "node") {
3099
+ return usageFlowFilter.id === id;
3100
+ }
3101
+ return usageFlowFilter.source === id || usageFlowFilter.target === id;
3102
+ }
3103
+
3104
+ function flowFilterTouchesLink(source, target) {
3105
+ if (!usageFlowFilter) {
3106
+ return true;
3107
+ }
3108
+ if (usageFlowFilter.type === "link") {
3109
+ return usageFlowFilter.source === source && usageFlowFilter.target === target;
3110
+ }
3111
+ return usageFlowFilter.id === source || usageFlowFilter.id === target;
3112
+ }
3113
+
3114
+ function mockUsageFlowState() {
3115
+ return {
3116
+ usageFlow: {
3117
+ totalRequests: 4,
3118
+ nodes: [
3119
+ { id: "agent:Codex", kind: "agent", label: "Codex", detail: "Local agent", count: 3 },
3120
+ { id: "agent:Claude", kind: "agent", label: "Claude", detail: "Local agent", count: 1 },
3121
+ { id: "auth:ssh-private-key", kind: "auth", label: "SSH private key", detail: "high risk / stored in macOS Keychain", count: 3 },
3122
+ { id: "auth:github-token", kind: "auth", label: "GitHub token", detail: "low risk / stored locally", count: 1 },
3123
+ { id: "target:ssh-server", kind: "target", label: "SSH server", detail: "prod-bastion", count: 3 },
3124
+ { id: "target:github-repository", kind: "target", label: "GitHub repository", detail: "repo", count: 1 }
3125
+ ],
3126
+ links: [
3127
+ { source: "agent:Codex", target: "auth:ssh-private-key", value: 2 },
3128
+ { source: "agent:Claude", target: "auth:ssh-private-key", value: 1 },
3129
+ { source: "agent:Codex", target: "auth:github-token", value: 1 },
3130
+ { source: "auth:ssh-private-key", target: "target:ssh-server", value: 3 },
3131
+ { source: "auth:github-token", target: "target:github-repository", value: 1 }
3132
+ ],
3133
+ rows: [
3134
+ {
3135
+ agentId: "agent:Codex",
3136
+ agent: "Codex",
3137
+ authTypeId: "auth:ssh-private-key",
3138
+ authType: "SSH private key",
3139
+ targetTypeId: "target:ssh-server",
3140
+ targetType: "SSH server",
3141
+ handle: "s-gw:private-key:prod-bastion",
3142
+ credential: "Prod bastion key",
3143
+ action: "ssh -> prod-bastion",
3144
+ command: "ssh",
3145
+ target: "prod-bastion",
3146
+ handles: ["s-gw:private-key:prod-bastion"],
3147
+ credentials: ["Prod bastion key"],
3148
+ actions: ["ssh -> prod-bastion"],
3149
+ targets: ["prod-bastion"],
3150
+ count: 2,
3151
+ lastSeen: new Date(Date.now() - 120000).toISOString(),
3152
+ states: { pending: 1, approved: 0, executing: 0, executed: 1, denied: 0, failed: 0 }
3153
+ },
3154
+ {
3155
+ agentId: "agent:Claude",
3156
+ agent: "Claude",
3157
+ authTypeId: "auth:ssh-private-key",
3158
+ authType: "SSH private key",
3159
+ targetTypeId: "target:ssh-server",
3160
+ targetType: "SSH server",
3161
+ handle: "s-gw:private-key:prod-bastion",
3162
+ credential: "Prod bastion key",
3163
+ action: "ssh -> prod-bastion",
3164
+ command: "ssh",
3165
+ target: "prod-bastion",
3166
+ handles: ["s-gw:private-key:prod-bastion"],
3167
+ credentials: ["Prod bastion key"],
3168
+ actions: ["ssh -> prod-bastion"],
3169
+ targets: ["prod-bastion"],
3170
+ count: 1,
3171
+ lastSeen: new Date(Date.now() - 360000).toISOString(),
3172
+ states: { pending: 0, approved: 0, executing: 0, executed: 0, denied: 1, failed: 0 }
3173
+ },
3174
+ {
3175
+ agentId: "agent:Codex",
3176
+ agent: "Codex",
3177
+ authTypeId: "auth:github-token",
3178
+ authType: "GitHub token",
3179
+ targetTypeId: "target:github-repository",
3180
+ targetType: "GitHub repository",
3181
+ handle: "s-gw:api-token:github",
3182
+ credential: "GitHub API token",
3183
+ action: "gh -> repo",
3184
+ command: "gh",
3185
+ target: "repo",
3186
+ handles: ["s-gw:api-token:github"],
3187
+ credentials: ["GitHub API token"],
3188
+ actions: ["gh -> repo"],
3189
+ targets: ["repo"],
3190
+ count: 1,
3191
+ lastSeen: new Date(Date.now() - 600000).toISOString(),
3192
+ states: { pending: 0, approved: 0, executing: 0, executed: 1, denied: 0, failed: 0 }
3193
+ }
3194
+ ]
3195
+ }
3196
+ };
3197
+ }
3198
+
3199
+ function d3SankeySvg(nodes, links, width, height) {
3200
+ if (!nodes.length || !links.length) {
3201
+ return "";
3202
+ }
3203
+ if (!window.d3 || typeof window.d3.sankey !== "function" || typeof window.d3.sankeyLinkHorizontal !== "function") {
3204
+ throw new Error("d3-sankey renderer is not available.");
3205
+ }
3206
+
3207
+ var nodeMap = {};
3208
+ for (var i = 0; i < nodes.length; i++) {
3209
+ nodeMap[nodes[i].id] = nodes[i];
3210
+ }
3211
+
3212
+ var liveLinks = links.filter(function (link) {
3213
+ return nodeMap[link.source] && nodeMap[link.target] && Number(link.value) > 0;
3214
+ });
3215
+ if (!liveLinks.length) {
3216
+ return "";
3217
+ }
3218
+
3219
+ var linkedNodeIds = new Set();
3220
+ liveLinks.forEach(function (link) {
3221
+ linkedNodeIds.add(link.source);
3222
+ linkedNodeIds.add(link.target);
3223
+ });
3224
+
3225
+ var graphNodes = nodes.filter(function (node) {
3226
+ return linkedNodeIds.has(node.id);
3227
+ }).map(function (node) {
3228
+ return {
3229
+ id: node.id,
3230
+ label: node.label,
3231
+ kind: node.kind,
3232
+ detail: node.detail || "",
3233
+ count: Number(node.count) || 0
3234
+ };
3235
+ });
3236
+ var stages = sankeyStages(graphNodes);
3237
+ graphNodes.sort(function (a, b) {
3238
+ var rankDiff = sankeyKindRank(a.kind, stages) - sankeyKindRank(b.kind, stages);
3239
+ if (rankDiff !== 0) {
3240
+ return rankDiff;
3241
+ }
3242
+ return (b.count - a.count) || compareText(a.label, b.label);
3243
+ });
3244
+
3245
+ var graphLinks = liveLinks.map(function (link) {
3246
+ return {
3247
+ source: link.source,
3248
+ target: link.target,
3249
+ value: Math.max(0.001, Number(link.value) || 0)
3250
+ };
3251
+ }).sort(function (a, b) {
3252
+ return (b.value - a.value) || compareText(a.source + a.target, b.source + b.target);
3253
+ });
3254
+ if (!graphLinks.length) {
3255
+ return "";
3256
+ }
3257
+
3258
+ var compactEmbed = isCompactUsageFlowEmbed();
3259
+ var margin = compactEmbed
3260
+ ? { top: 44, right: 78, bottom: 16, left: 12 }
3261
+ : { top: 58, right: 250, bottom: 34, left: 30 };
3262
+ var graphWidth = Math.max(compactEmbed ? 180 : 320, width - margin.left - margin.right);
3263
+ var availableHeight = Math.max(1, height - margin.top - margin.bottom);
3264
+ var graphHeight = compactEmbed
3265
+ ? availableHeight
3266
+ : Math.max(280, availableHeight);
3267
+ var largestColumn = sankeyMaxColumnSize(graphNodes);
3268
+ var padding = compactEmbed
3269
+ ? Math.max(3, Math.min(12, (graphHeight / Math.max(5, largestColumn + 1)) * 0.32))
3270
+ : Math.max(4, Math.min(28, (graphHeight / Math.max(6, largestColumn + 1)) * 0.42));
3271
+ var graph = { nodes: graphNodes, links: graphLinks };
3272
+ window.d3.sankey()
3273
+ .nodeId(function (node) { return node.id; })
3274
+ .nodeAlign(function (node, columnCount) { return sankeyColumnAlign(node, columnCount, stages); })
3275
+ .nodeSort(function (a, b) {
3276
+ return (b.value - a.value) || compareText(a.label, b.label);
3277
+ })
3278
+ .linkSort(function (a, b) {
3279
+ return (b.value - a.value) || compareText(a.target.label, b.target.label);
3280
+ })
3281
+ .nodeWidth(compactEmbed ? 10 : 18)
3282
+ .nodePadding(padding)
3283
+ .extent([[margin.left, margin.top], [margin.left + graphWidth, margin.top + graphHeight]])
3284
+ .iterations(64)(graph);
3285
+
3286
+ var linkPath = window.d3.sankeyLinkHorizontal();
3287
+ var headings = sankeyHeadings(graph.nodes, margin, graphWidth, stages);
3288
+ var visibleLabels = visibleSankeyLabelIds(graph.nodes);
3289
+ var defs = '<defs>' + graph.links.map(function (link, index) {
3290
+ var id = "sankey-gradient-" + index;
3291
+ link.gradientId = id;
3292
+ return '<linearGradient id="' + id + '" gradientUnits="userSpaceOnUse" x1="' + num(link.source.x1) + '" y1="0" x2="' + num(link.target.x0) + '" y2="0">' +
3293
+ '<stop offset="0%" stop-color="#7f91a6" stop-opacity=".92"></stop>' +
3294
+ '<stop offset="100%" stop-color="#a3b1be" stop-opacity=".86"></stop>' +
3295
+ '</linearGradient>';
3296
+ }).join("") + '</defs>';
3297
+ var flowMarkup = graph.links.slice().sort(function (a, b) {
3298
+ return b.width - a.width;
3299
+ }).map(function (link) {
3300
+ var source = link.source;
3301
+ var target = link.target;
3302
+ var cls = "sankey-link from-" + attr(source.kind) + " to-" + attr(target.kind);
3303
+ return '<path class="' + cls + '" d="' + attr(linkPath(link)) + '" stroke="url(#' + attr(link.gradientId) + ')" stroke-width="' + num(Math.max(2.5, link.width)) + '" ' +
3304
+ 'role="button" tabindex="0" data-flow-link-source="' + attr(source.id) + '" data-flow-link-target="' + attr(target.id) + '" aria-label="' + attr(sankeyAccessibleTitle(source, target, link.value)) + '">' +
3305
+ '<title>' + escapeHtml(sankeyAccessibleTitle(source, target, link.value)) + '</title></path>';
3306
+ }).join("");
3307
+
3308
+ var nodeMarkup = graph.nodes.map(function (node) {
3309
+ var label = clipLabel(node.label || node.id, compactEmbed ? 16 : (node.kind === "auth" ? 42 : 34));
3310
+ var detail = clipLabel(node.detail || flowCountLabel(Math.round(node.value || node.count || 0)), compactEmbed ? 18 : 42);
3311
+ var labelOnRight = node.kind !== "target";
3312
+ var labelX = labelOnRight ? node.x1 + (compactEmbed ? 5 : 9) : node.x0 - (compactEmbed ? 5 : 9);
3313
+ var anchor = labelOnRight ? "start" : "end";
3314
+ var labelY = node.y0 + (node.y1 - node.y0) / 2;
3315
+ var labelClass = visibleLabels[node.id] ? "" : " is-secondary-label";
3316
+ return '<g class="sankey-node ' + attr(node.kind) + '" role="button" tabindex="0" data-flow-node="' + attr(node.id) + '" aria-label="' + attr((node.label || node.id) + ", " + flowKindLabel(node.kind)) + '">' +
3317
+ '<rect x="' + num(node.x0) + '" y="' + num(node.y0) + '" width="' + num(node.x1 - node.x0) + '" height="' + num(node.y1 - node.y0) + '" rx="2" ry="2" fill="' + attr(sankeyNodeColor(node)) + '"></rect>' +
3318
+ '<text class="sankey-label' + (compactEmbed ? ' is-compact-label' : '') + labelClass + '" x="' + num(labelX) + '" y="' + num(labelY - (compactEmbed ? 1 : 3)) + '" text-anchor="' + anchor + '">' + escapeHtml(label) + '</text>' +
3319
+ (compactEmbed ? '' : '<text class="sankey-value' + labelClass + '" x="' + num(labelX) + '" y="' + num(labelY + 14) + '" text-anchor="' + anchor + '">' + escapeHtml(detail) + '</text>') +
3320
+ '<title>' + escapeHtml((node.label || node.id) + " / " + (node.detail || "") + " / " + flowCountLabel(Math.round(node.value || node.count || 0))) + '</title>' +
3321
+ '</g>';
3322
+ }).join("");
3323
+
3324
+ return defs + headings + '<g data-d3-sankey-renderer="true">' + flowMarkup + nodeMarkup + '</g>';
3325
+ }
3326
+
3327
+ function sankeyHeadings(nodes, margin, graphWidth, stages) {
3328
+ var compactEmbed = isCompactUsageFlowEmbed();
3329
+ var byKind = {};
3330
+ stages.forEach(function (kind) {
3331
+ byKind[kind] = [];
3332
+ });
3333
+ nodes.forEach(function (node) {
3334
+ if (byKind[node.kind]) {
3335
+ byKind[node.kind].push(node);
3336
+ }
3337
+ });
3338
+ return stages.map(function (kind) {
3339
+ if (!byKind[kind].length) {
3340
+ return "";
3341
+ }
3342
+ var x = margin.left + graphWidth;
3343
+ byKind[kind].forEach(function (node) {
3344
+ x = Math.min(x, node.x0);
3345
+ });
3346
+ var label = flowKindLabel(kind);
3347
+ var cls = compactEmbed ? "sankey-heading is-compact-heading" : "sankey-heading";
3348
+ var y = compactEmbed ? margin.top - 22 : margin.top - 28;
3349
+ return '<text class="' + cls + '" x="' + num(Math.min(x, margin.left + graphWidth)) + '" y="' + num(y) + '">' + label + '</text>';
3350
+ }).join("");
3351
+ }
3352
+
3353
+ function sankeyColumnAlign(node, columnCount, stages) {
3354
+ var last = Math.max(0, columnCount - 1);
3355
+ var stageIndex = stages.indexOf(node.kind);
3356
+ if (stageIndex >= 0) {
3357
+ return Math.min(stageIndex, last);
3358
+ }
3359
+ return Math.min(last, Math.max(0, node.depth || 0));
3360
+ }
3361
+
3362
+ function sankeyKindRank(kind, stages) {
3363
+ var order = stages || ["agent", "auth", "target"];
3364
+ var index = order.indexOf(kind);
3365
+ if (index >= 0) {
3366
+ return index;
3367
+ }
3368
+ return order.length;
3369
+ }
3370
+
3371
+ function sankeyStages(nodes) {
3372
+ var knownOrder = ["agent", "auth", "target"];
3373
+ var seen = {};
3374
+ nodes.forEach(function (node) {
3375
+ seen[node.kind] = true;
3376
+ });
3377
+ var stages = knownOrder.filter(function (kind) {
3378
+ return seen[kind];
3379
+ });
3380
+ return stages.length ? stages : knownOrder;
3381
+ }
3382
+
3383
+ function visibleSankeyLabelIds(nodes) {
3384
+ var byKind = {};
3385
+ var visible = {};
3386
+ var limits = {
3387
+ agent: 6,
3388
+ auth: 12,
3389
+ target: 14
3390
+ };
3391
+ var minGap = {
3392
+ agent: 26,
3393
+ auth: 24,
3394
+ target: 22
3395
+ };
3396
+
3397
+ nodes.forEach(function (node) {
3398
+ if (!byKind[node.kind]) {
3399
+ byKind[node.kind] = [];
3400
+ }
3401
+ byKind[node.kind].push(node);
3402
+ });
3403
+
3404
+ Object.keys(byKind).forEach(function (kind) {
3405
+ var usedY = [];
3406
+ var shown = 0;
3407
+ byKind[kind].slice().sort(function (a, b) {
3408
+ return (b.value - a.value) || compareText(a.label, b.label);
3409
+ }).forEach(function (node) {
3410
+ var force = usageFlowLabelIsForced(node.id);
3411
+ if (!force && shown >= (limits[kind] || 8)) {
3412
+ return;
3413
+ }
3414
+
3415
+ var y = node.y0 + (node.y1 - node.y0) / 2;
3416
+ var tooClose = usedY.some(function (existingY) {
3417
+ return Math.abs(existingY - y) < (minGap[kind] || 22);
3418
+ });
3419
+ if (!force && tooClose) {
3420
+ return;
3421
+ }
3422
+
3423
+ visible[node.id] = true;
3424
+ usedY.push(y);
3425
+ shown++;
3426
+ });
3427
+ });
3428
+
3429
+ return visible;
3430
+ }
3431
+
3432
+ function usageFlowLabelIsForced(id) {
3433
+ if (!usageFlowFilter || !id) {
3434
+ return false;
3435
+ }
3436
+ if (usageFlowFilter.type === "node") {
3437
+ return usageFlowFilter.id === id;
3438
+ }
3439
+ if (usageFlowFilter.type === "link") {
3440
+ return usageFlowFilter.source === id || usageFlowFilter.target === id;
3441
+ }
3442
+ return false;
3443
+ }
3444
+
3445
+ function sankeyNodeColor(node) {
3446
+ if (node.kind === "agent") {
3447
+ return "#7567e8";
3448
+ }
3449
+ if (node.kind === "auth") {
3450
+ return "#0b9f91";
3451
+ }
3452
+ if (node.kind === "target") {
3453
+ return "#1d73d4";
3454
+ }
3455
+ return "#64748b";
3456
+ }
3457
+
3458
+ function sankeyMaxColumnSize(nodes) {
3459
+ var counts = {};
3460
+ for (var i = 0; i < nodes.length; i++) {
3461
+ var kind = nodes[i].kind || "other";
3462
+ counts[kind] = (counts[kind] || 0) + 1;
3463
+ }
3464
+
3465
+ var max = 1;
3466
+ Object.keys(counts).forEach(function (key) {
3467
+ max = Math.max(max, counts[key]);
3468
+ });
3469
+ return max;
3470
+ }
3471
+
3472
+ function flowCountLabel(value) {
3473
+ var count = Math.round(Number(value) || 0);
3474
+ return count + " request" + (count === 1 ? "" : "s");
3475
+ }
3476
+
3477
+ function flowKindLabel(kind) {
3478
+ if (kind === "agent") {
3479
+ return "Agent";
3480
+ }
3481
+ if (kind === "auth") {
3482
+ return "Authentication type";
3483
+ }
3484
+ if (kind === "target") {
3485
+ return "Target type";
3486
+ }
3487
+ return "Flow";
3488
+ }
3489
+
3490
+ function sankeyAccessibleTitle(source, target, value) {
3491
+ return source.label + " to " + target.label + ": " + flowCountLabel(value);
3492
+ }
3493
+
3494
+ function compareText(a, b) {
3495
+ return String(a || "").localeCompare(String(b || ""));
3496
+ }
3497
+
3498
+ function sankeyErrorSvg(width, height, error) {
3499
+ var message = error && error.message ? error.message : String(error || "");
3500
+ var label = /d3-sankey renderer is not available/i.test(message)
3501
+ ? "d3-sankey renderer unavailable."
3502
+ : "Usage flow layout failed: " + message;
3503
+ return '<text class="sankey-value" x="' + num(width / 2) + '" y="' + num(height / 2) + '" text-anchor="middle">' + escapeHtml(label) + '</text>';
3504
+ }
3505
+
3506
+ function num(value) {
3507
+ return Number(Number(value).toFixed(2)).toString();
3508
+ }
3509
+
3510
+ function renderApprovals(state) {
3511
+ var body = document.querySelector("#approvals tbody");
3512
+ var footer = document.querySelector("#approvals .table-footer");
3513
+ var wrap = body ? body.closest(".table-wrap") : null;
3514
+ if (!body) {
3515
+ return;
3516
+ }
3517
+
3518
+ var handles = handleMap(state.handles || []);
3519
+ var pending = state.pendingRequests || [];
3520
+ body.innerHTML = pending.map(function (request) {
3521
+ var handle = handles[request.handle] || {};
3522
+ var severity = handle.severity || "medium";
3523
+ var agent = requestAgentName(request);
3524
+ var command = request.action ? request.action.command : "";
3525
+ var commandView = commandMarkup(command);
3526
+ var target = requestTarget(request);
3527
+ var searchable = [agent, command, target, request.handle, severity, request.reason].join(" ");
3528
+ return '<tr data-searchable="' + attr(searchable) + '">' +
3529
+ '<td><span class="agent-cell"><span class="agent-badge">' + escapeHtml(agent.slice(0, 1) || "A") + '</span><span>' +
3530
+ escapeHtml(agent) + '<span class="cell-sub">local</span></span></span></td>' +
3531
+ '<td>' + commandView + '</td>' +
3532
+ '<td>' + escapeHtml(target) + '<span class="cell-sub">' + escapeHtml(request.action && request.action.workingDir || "local host") + '</span></td>' +
3533
+ '<td>' + handleMarkup(request.handle) + '</td>' +
3534
+ '<td>' + escapeHtml(timeAgo(request.createdAt)) + '<span class="cell-sub">' + escapeHtml(clockTime(request.createdAt)) + '</span></td>' +
3535
+ '<td><span class="risk ' + riskClass(severity) + '">' + escapeHtml(riskLabel(severity)) + '</span></td>' +
3536
+ '<td><span class="action-group"><button class="action-button approve" type="button" title="Approve" aria-label="Approve request" data-approve="' + attr(request.id) + '"><svg><use href="#i-check"></use></svg></button>' +
3537
+ '<button class="action-button deny" type="button" title="Deny request" aria-label="Deny request" data-deny="' + attr(request.id) + '"><svg><use href="#i-alert"></use></svg></button>' +
3538
+ '<button class="row-menu" type="button" title="Request menu" aria-label="Open request menu" data-menu="request" aria-haspopup="menu"><svg><use href="#i-more"></use></svg></button></span></td>' +
3539
+ '</tr>';
3540
+ }).join("");
3541
+
3542
+ if (footer) {
3543
+ footer.textContent = "Showing " + (pending.length ? "1-" + pending.length : "0") + " of " + pending.length;
3544
+ }
3545
+ if (wrap && pending.length === 0) {
3546
+ wrap.scrollLeft = 0;
3547
+ }
3548
+ }
3549
+
3550
+ function renderCredentials(state) {
3551
+ var body = document.querySelector(".panel-grid .panel:nth-child(2) tbody");
3552
+ var footer = document.querySelector(".panel-grid .panel:nth-child(2) .table-footer");
3553
+ if (!body) {
3554
+ return;
3555
+ }
3556
+
3557
+ var credentials = liveMode && state.handles ? state.handles : (state.credentials || []);
3558
+ body.innerHTML = credentials.map(function (item) {
3559
+ var liveHandle = Boolean(item.handle);
3560
+ var provider = item.provider || item.backend || item.type || "";
3561
+ var label = liveHandle ? item.name : item.label;
3562
+ var handle = liveHandle ? item.handle : item.prefix;
3563
+ var severity = item.severity || "low";
3564
+ var detail = liveHandle ? ((item.type || "credential") + " / " + (item.backend || "local")) : String(item.secrets || 0);
3565
+ var updatedAt = liveHandle ? item.updatedAt : item.lastUsed;
3566
+ var searchable = [provider, label, handle, severity, item.source || "", item.ruleId || ""].join(" ");
3567
+ return '<tr data-searchable="' + attr(searchable) + '" data-provider="' + attr(provider) + '" data-prefix="' + attr(handle) + '" data-handle="' + attr(handle) + '" data-label="' + attr(label || handle) + '">' +
3568
+ '<td><span class="provider-cell">' + providerIcon(provider) + escapeHtml(providerLabel(provider)) + '</span></td>' +
3569
+ '<td>' + handleMarkup(handle) + '</td>' +
3570
+ '<td>' + escapeHtml(detail) + '<span class="cell-sub">' + escapeHtml(label || "") + '</span></td>' +
3571
+ '<td><span class="risk ' + riskClass(severity) + '">' + escapeHtml(riskLabel(severity)) + '</span></td>' +
3572
+ '<td>' + escapeHtml(timeAgo(updatedAt)) + '<span class="cell-sub">' + escapeHtml(clockTime(updatedAt)) + '</span></td>' +
3573
+ '<td><button class="row-menu" type="button" data-menu="credential" aria-haspopup="menu" aria-label="Credential menu"><svg><use href="#i-more"></use></svg></button></td>' +
3574
+ '</tr>';
3575
+ }).join("");
3576
+
3577
+ if (footer) {
3578
+ footer.textContent = "Showing " + (credentials.length ? "1-" + credentials.length : "0") + " of " + credentials.length;
3579
+ }
3580
+ }
3581
+
3582
+ function renderApprovalGrants(state) {
3583
+ var body = document.querySelector("#approval-grants tbody");
3584
+ var footer = document.querySelector("#approval-grants .table-footer");
3585
+ var empty = document.querySelector('[data-empty="grants"]');
3586
+ if (!body) {
3587
+ return;
3588
+ }
3589
+
3590
+ var grants = state.approvalGrants || [];
3591
+ body.innerHTML = grants.map(function (grant) {
3592
+ var agent = grantAgentLabel(grant);
3593
+ var scope = grantScopeLabel(grant);
3594
+ var mode = approvalModeLabel(grant.mode);
3595
+ var expires = grant.expiresAt ? "Expires " + timeUntil(grant.expiresAt) : "No expiration";
3596
+ var searchable = [agent, scope, grant.handle, mode, expires].join(" ");
3597
+ return '<tr data-searchable="' + attr(searchable) + '">' +
3598
+ '<td><span class="agent-cell"><span class="agent-badge">' + escapeHtml(agent.slice(0, 1) || "A") + '</span><span>' +
3599
+ escapeHtml(agent) + '<span class="cell-sub">' + escapeHtml(timeAgo(grant.updatedAt)) + '</span></span></span></td>' +
3600
+ '<td>' + escapeHtml(scope) + '</td>' +
3601
+ '<td>' + handleMarkup(grant.handle) + '</td>' +
3602
+ '<td>' + escapeHtml(mode) + '</td>' +
3603
+ '<td>' + escapeHtml(expires) + '<span class="cell-sub">' + escapeHtml(grant.expiresAt ? clockTime(grant.expiresAt) : "Manual revoke") + '</span></td>' +
3604
+ '<td><button class="action-button deny" type="button" title="Revoke" aria-label="Revoke reusable approval" data-revoke-grant="' + attr(grant.id) + '"><svg><use href="#i-alert"></use></svg></button></td>' +
3605
+ '</tr>';
3606
+ }).join("");
3607
+
3608
+ if (empty) {
3609
+ empty.classList.toggle("show", grants.length === 0);
3610
+ }
3611
+ if (footer) {
3612
+ footer.textContent = "Showing " + (grants.length ? "1-" + grants.length : "0") + " of " + grants.length;
3613
+ }
3614
+ }
3615
+
3616
+ function renderAudit(state) {
3617
+ var body = document.querySelector(".audit tbody");
3618
+ var footer = document.querySelector(".audit .table-footer");
3619
+ if (!body) {
3620
+ return;
3621
+ }
3622
+
3623
+ var audit = (state.audit || []).slice(0, 100);
3624
+ body.innerHTML = audit.map(function (event) {
3625
+ var meta = auditMeta(event.type);
3626
+ var searchable = [meta.label, event.type, event.message, event.handle, event.requestId].join(" ");
3627
+ return '<tr data-searchable="' + attr(searchable) + '">' +
3628
+ '<td>' + escapeHtml(clockTime(event.ts)) + '</td>' +
3629
+ '<td>' + actorMarkup(meta.actor) + '</td>' +
3630
+ '<td><span class="event-kind"><svg' + (meta.color ? ' style="color:' + meta.color + '"' : "") + '><use href="#' + meta.icon + '"></use></svg>' +
3631
+ escapeHtml(meta.label) + '</span></td>' +
3632
+ '<td class="audit-details">' + auditDetails(event) + '</td>' +
3633
+ '<td>' + (event.handle ? '<code class="token">' + escapeHtml(tokenFor(event.handle)) + '</code>' : "") + '</td>' +
3634
+ '<td class="chevron-cell"><svg><use href="#i-chevron"></use></svg></td>' +
3635
+ '</tr>';
3636
+ }).join("");
3637
+
3638
+ if (footer) {
3639
+ footer.textContent = "Showing " + (audit.length ? "1-" + audit.length : "0") + " of " + (state.audit || []).length;
3640
+ }
3641
+ }
3642
+
3643
+ function updateSearch() {
3644
+ var query = search ? search.value.trim().toLowerCase() : "";
3645
+ var sections = [
3646
+ { rows: document.querySelectorAll("#usage-flow tbody tr"), empty: null },
3647
+ { rows: document.querySelectorAll("#approvals tbody tr"), empty: document.querySelector('[data-empty="approvals"]') },
3648
+ { rows: document.querySelectorAll(".panel-grid .panel:nth-child(2) tbody tr"), empty: document.querySelector('[data-empty="credentials"]') },
3649
+ { rows: document.querySelectorAll("#approval-grants tbody tr"), empty: document.querySelector('[data-empty="grants"]') },
3650
+ { rows: document.querySelectorAll(".audit tbody tr"), empty: document.querySelector('[data-empty="audit"]') }
3651
+ ];
3652
+
3653
+ for (var i = 0; i < sections.length; i++) {
3654
+ var visible = 0;
3655
+ for (var j = 0; j < sections[i].rows.length; j++) {
3656
+ var row = sections[i].rows[j];
3657
+ var haystack = (row.dataset.searchable || row.textContent).toLowerCase();
3658
+ var match = !query || haystack.indexOf(query) !== -1;
3659
+ row.classList.toggle("hidden-row", !match);
3660
+ if (match) {
3661
+ visible++;
3662
+ }
3663
+ }
3664
+
3665
+ if (sections[i].empty) {
3666
+ sections[i].empty.classList.toggle("show", visible === 0);
3667
+ }
3668
+ }
3669
+ }
3670
+
3671
+ function profileMenuItems() {
3672
+ var storePath = currentState && currentState.status ? currentState.status.storePath : "";
3673
+ return [
3674
+ { label: "Refresh console", sub: liveMode ? "Reload local state" : "Refresh mock view", action: function () { loadState("Console refreshed"); } },
3675
+ { label: "Copy store path", sub: storePath || "Live console only", disabled: !storePath, action: function () { copyText(storePath, "Store path copied"); } },
3676
+ { label: "Switch theme", sub: root.dataset.theme === "dark" ? "Use light mode" : "Use dark mode", action: function () { setTheme(root.dataset.theme === "dark" ? "light" : "dark", true); showToast("Theme saved: " + root.dataset.theme); } }
3677
+ ];
3678
+ }
3679
+
3680
+ function credentialMenuItems(button) {
3681
+ var row = button.closest("tr");
3682
+ var prefix = row && (row.dataset.prefix || cellText(row, 1));
3683
+ var provider = row && (row.dataset.provider || cellText(row, 0));
3684
+ var handle = row && row.dataset.handle;
3685
+ var label = row && row.dataset.label;
3686
+ return [
3687
+ { label: "Copy handle prefix", sub: prefix || "No prefix", disabled: !prefix, action: function () { copyText(prefix, "Handle prefix copied"); } },
3688
+ { label: "Show matching credentials", sub: provider || prefix || "Filter table", action: function () { filterSearch(provider || prefix || "", "Credential rows filtered"); scrollToPanel("#credentials-panel"); } },
3689
+ { label: "Find in audit", sub: prefix || provider || "Filter audit", action: function () { filterSearch(prefix || provider || "", "Audit filtered"); scrollToPanel("#audit"); } },
3690
+ { label: "Delete credential", sub: label || handle || "Live handles only", danger: true, disabled: !handle || handle.indexOf("s-gw:") !== 0, action: function () { deleteCredential(handle, label || handle); } }
3691
+ ];
3692
+ }
3693
+
3694
+ function requestMenuItems(button) {
3695
+ var row = button.closest("tr");
3696
+ var handle = row ? cellText(row, 3) : "";
3697
+ var command = row ? cellText(row, 1) : "";
3698
+ return [
3699
+ { label: "Copy handle", sub: handle || "No handle", disabled: !handle, action: function () { copyText(handle, "Request handle copied"); } },
3700
+ { label: "Filter audit", sub: handle || command || "Request activity", action: function () { filterSearch(handle || command || "approval requested", "Audit filtered"); scrollToPanel("#audit"); } },
3701
+ { label: "Focus queue", sub: "Pending approval list", action: function () { clearSearch(); scrollToPanel("#approvals", "Approval queue focused"); } }
3702
+ ];
3703
+ }
3704
+
3705
+ function auditFilterItems() {
3706
+ return [
3707
+ { label: "All events", sub: "Clear audit filter", action: function () { clearSearch(); scrollToPanel("#audit", "Showing all events"); } },
3708
+ { label: "Approval requested", sub: "Pending request events", action: function () { filterSearch("approval requested", "Audit filtered"); scrollToPanel("#audit"); } },
3709
+ { label: "Approved", sub: "User approval events", action: function () { filterSearch("approved", "Audit filtered"); scrollToPanel("#audit"); } },
3710
+ { label: "Secret accessed", sub: "Execution events", action: function () { filterSearch("secret accessed", "Audit filtered"); scrollToPanel("#audit"); } },
3711
+ { label: "Credential stored", sub: "Enrollment events", action: function () { filterSearch("credential stored", "Audit filtered"); scrollToPanel("#audit"); } }
3712
+ ];
3713
+ }
3714
+
3715
+ function credentialsPanelItems() {
3716
+ return [
3717
+ { label: "Show all credentials", sub: "Clear search and focus table", action: function () { clearSearch(); scrollToPanel("#credentials-panel", "Credential inventory focused"); } },
3718
+ { label: "GitHub handles", sub: "Filter provider", action: function () { filterSearch("github", "Credentials filtered"); scrollToPanel("#credentials-panel"); } },
3719
+ { label: "High risk handles", sub: "Filter severity", action: function () { filterSearch("high", "Credentials filtered"); scrollToPanel("#credentials-panel"); } }
3720
+ ];
3721
+ }
3722
+
3723
+ function requestsPanelItems() {
3724
+ return [
3725
+ { label: "Show pending requests", sub: "Focus approval queue", action: function () { clearSearch(); scrollToPanel("#approvals", "Approval queue focused"); } },
3726
+ { label: "High risk requests", sub: "Filter pending queue", action: function () { filterSearch("high", "Requests filtered"); scrollToPanel("#approvals"); } },
3727
+ { label: "Copy CLI command", sub: "s-gw requests --state pending", action: function () { copyText("s-gw requests --state pending", "CLI command copied"); } }
3728
+ ];
3729
+ }
3730
+
3731
+ function agentMenuItems() {
3732
+ var count = currentState && currentState.agents ? currentState.agents.length : 0;
3733
+ return [
3734
+ { label: "Refresh agent status", sub: count ? count + " profiles available" : "Load local profiles", action: function () { loadState("Agent status refreshed"); } },
3735
+ { label: "Copy list command", sub: "s-gw agent list", action: function () { copyText("s-gw agent list", "Agent command copied"); } },
3736
+ { label: "Copy Codex snippet command", sub: "s-gw agent mcp-snippet codex", action: function () { copyText("s-gw agent mcp-snippet codex", "Snippet command copied"); } }
3737
+ ];
3738
+ }
3739
+
3740
+ function settingsMenuItems() {
3741
+ var storePath = currentState && currentState.status ? currentState.status.storePath : "";
3742
+ var grants = currentState && currentState.approvalGrants ? currentState.approvalGrants : [];
3743
+ return [
3744
+ { label: "Refresh console", sub: "Reload local state", action: function () { loadState("Console refreshed"); } },
3745
+ { label: "Show reusable approvals", sub: grants.length + " active", action: function () { clearSearch(); scrollToPanel("#approval-grants", "Reusable approvals focused"); } },
3746
+ { label: "Revoke reusable approvals", sub: grants.length ? grants.length + " active approvals" : "None active", danger: true, disabled: !grants.length, action: function () { revokeAllApprovalGrants(); } },
3747
+ { label: "Copy console command", sub: "s-gw console", action: function () { copyText("s-gw console", "Console command copied"); } },
3748
+ { label: "Copy store path", sub: storePath || "Live console only", disabled: !storePath, action: function () { copyText(storePath, "Store path copied"); } }
3749
+ ];
3750
+ }
3751
+
3752
+ function cellText(row, index) {
3753
+ if (!row || !row.cells || !row.cells[index]) {
3754
+ return "";
3755
+ }
3756
+
3757
+ return row.cells[index].textContent.trim().replace(/\s+/g, " ");
3758
+ }
3759
+
3760
+ function filterSearch(value, message) {
3761
+ if (!search) {
3762
+ return;
3763
+ }
3764
+
3765
+ search.value = value || "";
3766
+ updateSearch();
3767
+ showToast(message || "Filtered");
3768
+ }
3769
+
3770
+ function clearSearch() {
3771
+ if (search) {
3772
+ search.value = "";
3773
+ }
3774
+ updateSearch();
3775
+ }
3776
+
3777
+ function scrollToPanel(selector, message) {
3778
+ var panel = document.querySelector(selector);
3779
+ if (panel) {
3780
+ panel.scrollIntoView({ behavior: "smooth", block: "start" });
3781
+ }
3782
+ if (message) {
3783
+ showToast(message);
3784
+ }
3785
+ }
3786
+
3787
+ function copyText(text, message) {
3788
+ if (!text) {
3789
+ showToast("Nothing to copy");
3790
+ return;
3791
+ }
3792
+
3793
+ if (!navigator.clipboard) {
3794
+ showToast("Clipboard is unavailable");
3795
+ return;
3796
+ }
3797
+
3798
+ navigator.clipboard.writeText(text).then(function () {
3799
+ showToast(message || "Copied");
3800
+ }).catch(function () {
3801
+ showToast("Copy failed");
3802
+ });
3803
+ }
3804
+
3805
+ setTheme(root.dataset.theme === "dark" ? "dark" : "light", false);
3806
+
3807
+ if (toggle) {
3808
+ toggle.addEventListener("click", function () {
3809
+ setTheme(root.dataset.theme === "dark" ? "light" : "dark", true);
3810
+ showToast("Theme saved: " + root.dataset.theme);
3811
+ });
3812
+ }
3813
+
3814
+ var retry = document.querySelector("[data-connection-retry]");
3815
+ if (retry) {
3816
+ retry.addEventListener("click", function () {
3817
+ loadState("Reconnecting to local daemon");
3818
+ });
3819
+ }
3820
+
3821
+ if (search) {
3822
+ search.addEventListener("input", updateSearch);
3823
+ window.addEventListener("keydown", function (event) {
3824
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
3825
+ event.preventDefault();
3826
+ search.focus();
3827
+ search.select();
3828
+ }
3829
+ });
3830
+ }
3831
+
3832
+ document.querySelectorAll("[data-nav]").forEach(function (button) {
3833
+ button.addEventListener("click", function () {
3834
+ document.querySelectorAll("[data-nav]").forEach(function (item) {
3835
+ item.classList.toggle("active", item === button);
3836
+ });
3837
+ var nav = button.dataset.nav;
3838
+ if (nav === "overview") {
3839
+ scrollToPanel(".content", "Overview focused");
3840
+ } else if (nav === "flow") {
3841
+ scrollToPanel("#usage-flow", "Usage flow focused");
3842
+ } else if (nav === "credentials") {
3843
+ scrollToPanel("#credentials-panel", "Credentials focused");
3844
+ } else if (nav === "requests") {
3845
+ scrollToPanel("#approvals", "Requests focused");
3846
+ } else if (nav === "audit") {
3847
+ scrollToPanel("#audit", "Audit focused");
3848
+ } else if (nav === "agents") {
3849
+ button.dataset.menu = "agents-nav";
3850
+ openMenuFor(button);
3851
+ } else if (nav === "settings") {
3852
+ button.dataset.menu = "settings-nav";
3853
+ openMenuFor(button);
3854
+ }
3855
+ });
3856
+ });
3857
+ var initialView = new URLSearchParams(window.location.search).get("view");
3858
+ if (window.location.hash === "#usage-flow" || initialView === "usage-flow") {
3859
+ var flowNav = document.querySelector('[data-nav="flow"]');
3860
+ if (flowNav) {
3861
+ flowNav.click();
3862
+ }
3863
+ }
3864
+
3865
+ document.addEventListener("pointerdown", function (event) {
3866
+ startUsageFlowDetailResize(event);
3867
+ });
3868
+
3869
+ document.addEventListener("keydown", function (event) {
3870
+ handleUsageFlowResizeKeydown(event);
3871
+ });
3872
+
3873
+ document.addEventListener("click", function (event) {
3874
+ var target = event.target;
3875
+ if (!target || !target.closest) {
3876
+ return;
3877
+ }
3878
+
3879
+ if (target.closest("[data-flow-resize-grip]")) {
3880
+ return;
3881
+ }
3882
+
3883
+ var flowNode = target.closest("[data-flow-node]");
3884
+ if (flowNode) {
3885
+ selectUsageFlowNode(flowNode.dataset.flowNode);
3886
+ return;
3887
+ }
3888
+
3889
+ var flowLink = target.closest("[data-flow-link-source]");
3890
+ if (flowLink) {
3891
+ selectUsageFlowLink(flowLink.dataset.flowLinkSource, flowLink.dataset.flowLinkTarget);
3892
+ return;
3893
+ }
3894
+
3895
+ dismissUsageFlowFilterForClick(target);
3896
+
3897
+ var menuItem = target.closest("[data-menu-item]");
3898
+ if (menuItem && menuBox && menuBox.contains(menuItem)) {
3899
+ var handler = activeMenuActions[menuItem.dataset.menuItem];
3900
+ hideMenu();
3901
+ if (handler) {
3902
+ handler();
3903
+ }
3904
+ return;
3905
+ }
3906
+
3907
+ var menuButton = target.closest("[data-menu]");
3908
+ if (menuButton) {
3909
+ event.preventDefault();
3910
+ openMenuFor(menuButton);
3911
+ return;
3912
+ }
3913
+
3914
+ var toastButton = target.closest("[data-toast]");
3915
+ if (toastButton) {
3916
+ showToast(toastButton.dataset.toast);
3917
+ return;
3918
+ }
3919
+
3920
+ var highRiskMetric = target.closest("[data-high-risk-jump]");
3921
+ if (highRiskMetric) {
3922
+ filterSearch("high", "High-risk credentials focused");
3923
+ scrollToPanel("#credentials-panel");
3924
+ return;
3925
+ }
3926
+
3927
+ var approve = target.closest("[data-approve]");
3928
+ var deny = target.closest("[data-deny]");
3929
+ if (approve) {
3930
+ approveRequest(approve.dataset.approve);
3931
+ return;
3932
+ }
3933
+
3934
+ if (deny) {
3935
+ denyRequest(deny.dataset.deny);
3936
+ return;
3937
+ }
3938
+
3939
+ var grant = target.closest("[data-revoke-grant]");
3940
+ if (grant) {
3941
+ revokeApprovalGrant(grant.dataset.revokeGrant);
3942
+ return;
3943
+ }
3944
+
3945
+ var allGrants = target.closest("[data-revoke-all-grants]");
3946
+ if (allGrants) {
3947
+ revokeAllApprovalGrants();
3948
+ return;
3949
+ }
3950
+
3951
+ if (!menuBox || !menuBox.contains(target)) {
3952
+ hideMenu();
3953
+ }
3954
+ });
3955
+
3956
+ window.addEventListener("keydown", function (event) {
3957
+ if (event.key === "Escape") {
3958
+ hideMenu();
3959
+ if (usageFlowFilter) {
3960
+ clearUsageFlowFilter();
3961
+ }
3962
+ return;
3963
+ }
3964
+
3965
+ if (event.key !== "Enter" && event.key !== " ") {
3966
+ return;
3967
+ }
3968
+
3969
+ var target = event.target;
3970
+ if (!target || !target.closest) {
3971
+ return;
3972
+ }
3973
+
3974
+ var flowNode = target.closest("[data-flow-node]");
3975
+ if (flowNode) {
3976
+ event.preventDefault();
3977
+ selectUsageFlowNode(flowNode.dataset.flowNode);
3978
+ return;
3979
+ }
3980
+
3981
+ var flowLink = target.closest("[data-flow-link-source]");
3982
+ if (flowLink) {
3983
+ event.preventDefault();
3984
+ selectUsageFlowLink(flowLink.dataset.flowLinkSource, flowLink.dataset.flowLinkTarget);
3985
+ }
3986
+ });
3987
+
3988
+ document.querySelectorAll("[data-jump]").forEach(function (button) {
3989
+ button.addEventListener("click", function () {
3990
+ var target = document.querySelector(button.dataset.jump);
3991
+ if (target) {
3992
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
3993
+ showToast("Approval queue focused");
3994
+ }
3995
+ });
3996
+ });
3997
+
3998
+ document.querySelectorAll("[data-download]").forEach(function (button) {
3999
+ button.addEventListener("click", function () {
4000
+ if (liveMode) {
4001
+ apiText("/api/audit.csv").then(function (csv) {
4002
+ downloadCsv(csv);
4003
+ showToast("Audit CSV downloaded");
4004
+ }).catch(function (error) {
4005
+ showToast(error.message || "Download failed");
4006
+ });
4007
+ return;
4008
+ }
4009
+
4010
+ var rows = [
4011
+ ["time", "actor", "event", "details"],
4012
+ ["12:43:02", "Codex", "Approval requested", "s-gw:private-key:rsa:prod-bastion"],
4013
+ ["12:41:05", "You", "Approved", "s-gw:private-key:ed25519:prod-bastion"]
4014
+ ];
4015
+ var csv = rows.map(function (row) {
4016
+ return row.map(function (cell) {
4017
+ return '"' + String(cell).replace(/"/g, '""') + '"';
4018
+ }).join(",");
4019
+ }).join("\n");
4020
+ downloadCsv(csv);
4021
+ showToast("Audit CSV downloaded");
4022
+ });
4023
+ });
4024
+
4025
+ function approveRequest(id) {
4026
+ decideRequest(id, "approve", "approve", "Request approved", "Approval failed");
4027
+ }
4028
+
4029
+ function denyRequest(id) {
4030
+ decideRequest(id, "deny", "deny", "Request denied", "Deny failed");
4031
+ }
4032
+
4033
+ function deleteCredential(handle, label) {
4034
+ if (!handle || !liveMode) {
4035
+ showToast("Launch with s-gw console to delete live credentials");
4036
+ return;
4037
+ }
4038
+
4039
+ var name = label || handle;
4040
+ if (!window.confirm("Delete " + name + " from s-gw? Unfinished requests for this credential will fail.")) {
4041
+ return;
4042
+ }
4043
+
4044
+ apiJson("/api/secrets/" + encodeURIComponent(handle), { method: "DELETE" })
4045
+ .then(function () {
4046
+ return loadState("Credential deleted");
4047
+ })
4048
+ .catch(function (error) {
4049
+ showToast(error.message || "Delete failed");
4050
+ });
4051
+ }
4052
+
4053
+ function revokeApprovalGrant(id) {
4054
+ if (!id || !liveMode) {
4055
+ showToast("Launch with s-gw console to revoke reusable approvals");
4056
+ return;
4057
+ }
4058
+
4059
+ apiJson("/api/approval/grants/" + encodeURIComponent(id), { method: "DELETE" })
4060
+ .then(function () {
4061
+ return loadState("Reusable approval revoked");
4062
+ })
4063
+ .catch(function (error) {
4064
+ showToast(error.message || "Revoke failed");
4065
+ });
4066
+ }
4067
+
4068
+ function revokeAllApprovalGrants() {
4069
+ var grants = currentState && currentState.approvalGrants ? currentState.approvalGrants : [];
4070
+ if (!liveMode) {
4071
+ showToast("Launch with s-gw console to revoke reusable approvals");
4072
+ return;
4073
+ }
4074
+ if (!grants.length) {
4075
+ showToast("No reusable approvals are active");
4076
+ return;
4077
+ }
4078
+ if (!window.confirm("Revoke all reusable approvals? Future matching requests will ask again.")) {
4079
+ return;
4080
+ }
4081
+
4082
+ apiJson("/api/approval/grants", { method: "DELETE" })
4083
+ .then(function () {
4084
+ return loadState("Reusable approvals revoked");
4085
+ })
4086
+ .catch(function (error) {
4087
+ showToast(error.message || "Revoke failed");
4088
+ });
4089
+ }
4090
+
4091
+ // One code path for both decisions so the in-flight guard and button
4092
+ // locking can't diverge between approve and deny.
4093
+ function decideRequest(id, endpoint, verb, okMessage, failMessage) {
4094
+ if (!id || !liveMode) {
4095
+ showToast("Launch with s-gw console to " + verb + " live requests");
4096
+ return;
4097
+ }
4098
+
4099
+ // Already approving/denying this row — ignore the extra click instead of
4100
+ // firing a second decision that races the first.
4101
+ if (pendingActions[id]) {
4102
+ return;
4103
+ }
4104
+
4105
+ pendingActions[id] = true;
4106
+ setRowBusy(id, true);
4107
+
4108
+ apiJson("/api/requests/" + encodeURIComponent(id) + "/" + endpoint, { method: "POST", body: "{}" })
4109
+ .then(function () {
4110
+ return loadState(okMessage);
4111
+ })
4112
+ .catch(function (error) {
4113
+ showToast(error.message || failMessage);
4114
+ })
4115
+ .then(function () {
4116
+ delete pendingActions[id];
4117
+ // loadState() re-renders the row from fresh state, so only re-enable
4118
+ // when the buttons survived (e.g. the POST failed and the row stayed).
4119
+ setRowBusy(id, false);
4120
+ });
4121
+ }
4122
+
4123
+ // Disable both decision buttons for a row while its action is in flight.
4124
+ function setRowBusy(id, busy) {
4125
+ var selector = '[data-approve="' + cssAttr(id) + '"], [data-deny="' + cssAttr(id) + '"]';
4126
+ document.querySelectorAll(selector).forEach(function (button) {
4127
+ button.disabled = busy;
4128
+ button.classList.toggle("busy", busy);
4129
+ });
4130
+ }
4131
+
4132
+ // Escape a value for use inside a CSS attribute selector.
4133
+ function cssAttr(value) {
4134
+ return String(value).replace(/["\\]/g, "\\$&");
4135
+ }
4136
+
4137
+ function downloadCsv(csv) {
4138
+ var blob = new Blob([csv], { type: "text/csv" });
4139
+ var url = URL.createObjectURL(blob);
4140
+ var link = document.createElement("a");
4141
+ link.href = url;
4142
+ link.download = "s-gw-audit.csv";
4143
+ link.click();
4144
+ URL.revokeObjectURL(url);
4145
+ }
4146
+
4147
+ function handleMap(handles) {
4148
+ var out = {};
4149
+ for (var i = 0; i < handles.length; i++) {
4150
+ out[handles[i].handle] = handles[i];
4151
+ }
4152
+
4153
+ return out;
4154
+ }
4155
+
4156
+ function requestTarget(request) {
4157
+ if (!request || !request.action) {
4158
+ return "-";
4159
+ }
4160
+
4161
+ var command = request.action.command || "";
4162
+ if (request.action.workingDir) {
4163
+ return request.action.workingDir;
4164
+ }
4165
+
4166
+ var args = request.action.args || [];
4167
+ if (args[0] === "-e") {
4168
+ return commandBase(command) + " inline script";
4169
+ }
4170
+
4171
+ for (var i = 0; i < args.length; i++) {
4172
+ if (args[i] && args[i].charAt(0) !== "-") {
4173
+ return args[i];
4174
+ }
4175
+ }
4176
+
4177
+ return args[0] || "local command";
4178
+ }
4179
+
4180
+ function commandMarkup(command) {
4181
+ var value = String(command || "");
4182
+ var base = commandBase(value);
4183
+ if (!value || value === base) {
4184
+ return escapeHtml(base || "-");
4185
+ }
4186
+
4187
+ return escapeHtml(base) + '<span class="cell-sub">' + escapeHtml(value) + '</span>';
4188
+ }
4189
+
4190
+ function commandBase(command) {
4191
+ var value = String(command || "");
4192
+ var normalized = value.replace(/\\/g, "/");
4193
+ var parts = normalized.split("/");
4194
+ return parts[parts.length - 1] || value;
4195
+ }
4196
+
4197
+ function shortPath(command) {
4198
+ return commandBase(command) || "local command";
4199
+ }
4200
+
4201
+ function agentName(reason) {
4202
+ var text = String(reason || "").toLowerCase();
4203
+ if (text.indexOf("codex") !== -1) {
4204
+ return "Codex";
4205
+ }
4206
+ if (text.indexOf("claude") !== -1) {
4207
+ return "Claude";
4208
+ }
4209
+ if (text.indexOf("cursor") !== -1) {
4210
+ return "Cursor";
4211
+ }
4212
+
4213
+ return "Local agent";
4214
+ }
4215
+
4216
+ function requestAgentName(request) {
4217
+ return request && request.agentName ? request.agentName : agentName(request && request.reason);
4218
+ }
4219
+
4220
+ function handleMarkup(handle) {
4221
+ var value = String(handle || "");
4222
+ var parts = value.split(":");
4223
+ if (parts.length < 3) {
4224
+ return escapeHtml(value);
4225
+ }
4226
+
4227
+ return escapeHtml(parts.slice(0, 2).join(":") + ":") +
4228
+ '<span class="cell-sub">' + escapeHtml(parts.slice(2).join(":")) + '</span>';
4229
+ }
4230
+
4231
+ function shortHandle(handle) {
4232
+ var value = String(handle || "");
4233
+ if (value.length <= 26) {
4234
+ return value;
4235
+ }
4236
+ return value.slice(0, 15) + "..." + value.slice(-7);
4237
+ }
4238
+
4239
+ function providerIcon(provider) {
4240
+ if (provider === "github") {
4241
+ return '<span class="provider-icon"><svg><use href="#i-github"></use></svg></span>';
4242
+ }
4243
+ if (provider === "aws") {
4244
+ return '<span class="provider-icon text-mark">aws</span>';
4245
+ }
4246
+ if (provider === "openai") {
4247
+ return '<span class="provider-icon text-mark">AI</span>';
4248
+ }
4249
+ if (provider === "ssh") {
4250
+ return '<span class="provider-icon"><svg><use href="#i-terminal"></use></svg></span>';
4251
+ }
4252
+ if (provider === "macos-keychain" || provider === "keychain") {
4253
+ return '<span class="provider-icon"><svg><use href="#i-key"></use></svg></span>';
4254
+ }
4255
+
4256
+ return '<span class="provider-icon"><svg><use href="#i-box"></use></svg></span>';
4257
+ }
4258
+
4259
+ function providerLabel(provider) {
4260
+ var value = String(provider || "generic");
4261
+ if (value === "onepassword") {
4262
+ return "1Password";
4263
+ }
4264
+ if (value === "keychain" || value === "macos-keychain") {
4265
+ return "macOS Keychain";
4266
+ }
4267
+ if (value === "api-token") {
4268
+ return "API token";
4269
+ }
4270
+ return value.charAt(0).toUpperCase() + value.slice(1);
4271
+ }
4272
+
4273
+ function auditMeta(type) {
4274
+ if (type === "request.approved") {
4275
+ return { label: "Approved", icon: "i-check", actor: "You", color: "var(--green)" };
4276
+ }
4277
+ if (type === "request.denied") {
4278
+ return { label: "Denied", icon: "i-alert", actor: "You", color: "var(--red)" };
4279
+ }
4280
+ if (type === "request.executed") {
4281
+ return { label: "Secret accessed", icon: "i-terminal", actor: "Agent" };
4282
+ }
4283
+ if (type === "request.created") {
4284
+ return { label: "Approval requested", icon: "i-alert", actor: "Agent" };
4285
+ }
4286
+ if (type === "secret.added" || type === "secret.matched") {
4287
+ return { label: "Credential stored", icon: "i-info", actor: "System" };
4288
+ }
4289
+ if (type === "request.failed") {
4290
+ return { label: "Execution failed", icon: "i-alert", actor: "Agent", color: "var(--red)" };
4291
+ }
4292
+
4293
+ return { label: type || "Event", icon: "i-info", actor: "System" };
4294
+ }
4295
+
4296
+ function actorMarkup(actor) {
4297
+ if (actor === "You") {
4298
+ return '<span class="actor"><span class="provider-icon"><svg><use href="#i-users"></use></svg></span><span>You (user)<span class="cell-sub">local console</span></span></span>';
4299
+ }
4300
+ if (actor === "Agent") {
4301
+ return '<span class="actor"><span class="agent-badge">A</span><span>Agent<span class="cell-sub">localhost</span></span></span>';
4302
+ }
4303
+
4304
+ return '<span class="actor"><span class="provider-icon"><svg><use href="#i-settings"></use></svg></span><span>s-gw (system)</span></span>';
4305
+ }
4306
+
4307
+ function auditDetails(event) {
4308
+ var parts = [event.message];
4309
+ if (event.handle) {
4310
+ parts.push("Handle: " + event.handle);
4311
+ }
4312
+ if (event.requestId) {
4313
+ parts.push("Request: " + event.requestId);
4314
+ }
4315
+
4316
+ return parts.map(function (part) {
4317
+ return "<span>" + escapeHtml(part) + "</span>";
4318
+ }).join("");
4319
+ }
4320
+
4321
+ function tokenFor(handle) {
4322
+ var value = String(handle);
4323
+ if (value.length <= 18) {
4324
+ return "<<SGW_SECRET:" + value + ">>";
4325
+ }
4326
+
4327
+ return "<<SGW_SECRET:" + value.slice(0, 8) + "..." + value.slice(-6) + ">>";
4328
+ }
4329
+
4330
+ function riskClass(severity) {
4331
+ return severity === "critical" ? "critical" : severity === "high" ? "high" :
4332
+ severity === "medium" ? "medium" : "low";
4333
+ }
4334
+
4335
+ function riskLabel(severity) {
4336
+ var value = severity || "low";
4337
+ return value.charAt(0).toUpperCase() + value.slice(1);
4338
+ }
4339
+
4340
+ function grantAgentLabel(grant) {
4341
+ if (grant && grant.agentScope === "any-agent") {
4342
+ return "All agents";
4343
+ }
4344
+
4345
+ return grant && grant.agentName ? grant.agentName : "This agent";
4346
+ }
4347
+
4348
+ function grantScopeLabel(grant) {
4349
+ return grant && grant.agentScope === "any-agent" ? "All agents" : "This agent";
4350
+ }
4351
+
4352
+ function approvalModeLabel(mode) {
4353
+ if (mode === "timed-session") {
4354
+ return "Same action for time";
4355
+ }
4356
+ if (mode === "login-session") {
4357
+ return "Same action for login";
4358
+ }
4359
+ if (mode === "always") {
4360
+ return "Same action always";
4361
+ }
4362
+ return "One time";
4363
+ }
4364
+
4365
+ function flowStateMarkup(states) {
4366
+ if (!states) {
4367
+ return "";
4368
+ }
4369
+
4370
+ var order = ["pending", "approved", "executing", "executed", "denied", "failed"];
4371
+ var parts = [];
4372
+ for (var i = 0; i < order.length; i++) {
4373
+ var state = order[i];
4374
+ var count = states[state] || 0;
4375
+ if (count > 0) {
4376
+ parts.push('<span class="state-chip">' + escapeHtml(state) + " " + escapeHtml(String(count)) + '</span>');
4377
+ }
4378
+ }
4379
+
4380
+ return parts.length ? '<span class="flow-state">' + parts.join("") + '</span>' : "";
4381
+ }
4382
+
4383
+ function clipLabel(value, max) {
4384
+ var text = String(value || "");
4385
+ if (text.length <= max) {
4386
+ return text;
4387
+ }
4388
+
4389
+ return text.slice(0, Math.max(1, max - 1)) + "...";
4390
+ }
4391
+
4392
+ function timeAgo(iso) {
4393
+ var date = iso ? new Date(iso) : null;
4394
+ if (!date || Number.isNaN(date.getTime())) {
4395
+ return "-";
4396
+ }
4397
+
4398
+ var seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));
4399
+ if (seconds < 60) {
4400
+ return seconds + "s ago";
4401
+ }
4402
+ var minutes = Math.floor(seconds / 60);
4403
+ if (minutes < 60) {
4404
+ return minutes + "m ago";
4405
+ }
4406
+ var hours = Math.floor(minutes / 60);
4407
+ if (hours < 24) {
4408
+ return hours + "h ago";
4409
+ }
4410
+
4411
+ return Math.floor(hours / 24) + "d ago";
4412
+ }
4413
+
4414
+ function timeUntil(iso) {
4415
+ var date = iso ? new Date(iso) : null;
4416
+ if (!date || Number.isNaN(date.getTime())) {
4417
+ return "unknown";
4418
+ }
4419
+
4420
+ var seconds = Math.floor((date.getTime() - Date.now()) / 1000);
4421
+ if (seconds <= 0) {
4422
+ return "expired";
4423
+ }
4424
+ if (seconds < 60) {
4425
+ return "in " + seconds + "s";
4426
+ }
4427
+ var minutes = Math.floor(seconds / 60);
4428
+ if (minutes < 60) {
4429
+ return "in " + minutes + "m";
4430
+ }
4431
+ var hours = Math.floor(minutes / 60);
4432
+ if (hours < 24) {
4433
+ return "in " + hours + "h";
4434
+ }
4435
+
4436
+ return "in " + Math.floor(hours / 24) + "d";
4437
+ }
4438
+
4439
+ function clockTime(iso) {
4440
+ var date = iso ? new Date(iso) : null;
4441
+ if (!date || Number.isNaN(date.getTime())) {
4442
+ return "-";
4443
+ }
4444
+
4445
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
4446
+ }
4447
+
4448
+ function escapeHtml(value) {
4449
+ return String(value === undefined || value === null ? "" : value).replace(/[&<>"']/g, function (char) {
4450
+ return {
4451
+ "&": "&amp;",
4452
+ "<": "&lt;",
4453
+ ">": "&gt;",
4454
+ '"': "&quot;",
4455
+ "'": "&#39;"
4456
+ }[char];
4457
+ });
4458
+ }
4459
+
4460
+ function attr(value) {
4461
+ return escapeHtml(value);
4462
+ }
4463
+
4464
+ observeUsageFlowDetailWidth();
4465
+ loadState();
4466
+
4467
+ // Quietly re-poll in live mode so readiness/connection banners self-heal
4468
+ // (e.g. after the user runs `s-gw setup` or restarts the daemon elsewhere).
4469
+ if (liveMode) {
4470
+ setInterval(function () {
4471
+ loadState(null, true);
4472
+ }, 15000);
4473
+ }
4474
+ }());
4475
+ </script>
4476
+ </body>
4477
+ </html>