agent-embassy 1.9.5 → 3.0.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 (150) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/CONTRIBUTING.md +19 -34
  3. package/README.md +110 -221
  4. package/SECURITY.md +55 -89
  5. package/dist/src/errors.d.ts +10 -1
  6. package/dist/src/errors.js +3 -1
  7. package/dist/src/errors.js.map +1 -1
  8. package/dist/src/gateway/claude-helper-protocol.d.ts +9 -12
  9. package/dist/src/gateway/claude-helper-protocol.js +15 -13
  10. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  11. package/dist/src/gateway/claude-helper-supervisor.d.ts +2 -6
  12. package/dist/src/gateway/claude-helper-supervisor.js +10 -9
  13. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  14. package/dist/src/gateway/claude-helper.js +18 -10
  15. package/dist/src/gateway/claude-helper.js.map +1 -1
  16. package/dist/src/gateway/claude-peer.d.ts +0 -3
  17. package/dist/src/gateway/claude-peer.js +6 -18
  18. package/dist/src/gateway/claude-peer.js.map +1 -1
  19. package/dist/src/gateway/cli.d.ts +18 -13
  20. package/dist/src/gateway/cli.js +942 -298
  21. package/dist/src/gateway/cli.js.map +1 -1
  22. package/dist/src/gateway/codex-local-transport.d.ts +1 -1
  23. package/dist/src/gateway/codex-local-transport.js +2 -2
  24. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  25. package/dist/src/gateway/codex-socket-holder.d.ts +26 -0
  26. package/dist/src/gateway/codex-socket-holder.js +76 -0
  27. package/dist/src/gateway/codex-socket-holder.js.map +1 -0
  28. package/dist/src/gateway/codex-stateless-transport.js +1 -1
  29. package/dist/src/gateway/codex-stateless-transport.js.map +1 -1
  30. package/dist/src/gateway/config.d.ts +5 -13
  31. package/dist/src/gateway/config.js +7 -19
  32. package/dist/src/gateway/config.js.map +1 -1
  33. package/dist/src/gateway/control.d.ts +55 -108
  34. package/dist/src/gateway/control.js +95 -222
  35. package/dist/src/gateway/control.js.map +1 -1
  36. package/dist/src/gateway/federation-nodes.d.ts +29 -3
  37. package/dist/src/gateway/federation-nodes.js +200 -15
  38. package/dist/src/gateway/federation-nodes.js.map +1 -1
  39. package/dist/src/gateway/peer-client.d.ts +4 -3
  40. package/dist/src/gateway/peer-client.js +22 -13
  41. package/dist/src/gateway/peer-client.js.map +1 -1
  42. package/dist/src/gateway/peer-protocol.d.ts +8 -11
  43. package/dist/src/gateway/peer-protocol.js +5 -12
  44. package/dist/src/gateway/peer-protocol.js.map +1 -1
  45. package/dist/src/gateway/provenance-envelope.d.ts +0 -1
  46. package/dist/src/gateway/provenance-envelope.js +4 -19
  47. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  48. package/dist/src/gateway/providers.d.ts +10 -7
  49. package/dist/src/gateway/providers.js +21 -28
  50. package/dist/src/gateway/providers.js.map +1 -1
  51. package/dist/src/gateway/server.d.ts +4 -14
  52. package/dist/src/gateway/server.js +33 -59
  53. package/dist/src/gateway/server.js.map +1 -1
  54. package/dist/src/gateway/service-agent.d.ts +187 -0
  55. package/dist/src/gateway/service-agent.js +758 -0
  56. package/dist/src/gateway/service-agent.js.map +1 -0
  57. package/dist/src/gateway/service.d.ts +117 -32
  58. package/dist/src/gateway/service.js +525 -576
  59. package/dist/src/gateway/service.js.map +1 -1
  60. package/dist/src/gateway/status-view.d.ts +167 -0
  61. package/dist/src/gateway/status-view.js +488 -0
  62. package/dist/src/gateway/status-view.js.map +1 -0
  63. package/dist/src/gateway/store.d.ts +103 -21
  64. package/dist/src/gateway/store.js +473 -547
  65. package/dist/src/gateway/store.js.map +1 -1
  66. package/dist/src/gateway/types.d.ts +51 -102
  67. package/dist/src/gateway/types.js +15 -52
  68. package/dist/src/gateway/types.js.map +1 -1
  69. package/docs/CONFIGURATION.md +194 -59
  70. package/docs/DELIVERY.md +11 -11
  71. package/docs/GATEWAY-ARCHITECTURE.md +286 -394
  72. package/package.json +4 -12
  73. package/skills/embassy-peer/SKILL.md +72 -92
  74. package/skills/embassy-peer/agents/openai.yaml +1 -1
  75. package/README.zh-CN.md +0 -269
  76. package/assets/live-dashboard/app.css +0 -1619
  77. package/assets/vendor/react/LICENSE +0 -21
  78. package/assets/vendor/react/react-dom.production.min.js +0 -267
  79. package/assets/vendor/react/react.production.min.js +0 -31
  80. package/dist/src/gateway/acp-client.d.ts +0 -110
  81. package/dist/src/gateway/acp-client.js +0 -407
  82. package/dist/src/gateway/acp-client.js.map +0 -1
  83. package/dist/src/gateway/acp-provider.d.ts +0 -66
  84. package/dist/src/gateway/acp-provider.js +0 -275
  85. package/dist/src/gateway/acp-provider.js.map +0 -1
  86. package/dist/src/gateway/cli-copy.d.ts +0 -8
  87. package/dist/src/gateway/cli-copy.en.d.ts +0 -17
  88. package/dist/src/gateway/cli-copy.en.js +0 -58
  89. package/dist/src/gateway/cli-copy.en.js.map +0 -1
  90. package/dist/src/gateway/cli-copy.js +0 -22
  91. package/dist/src/gateway/cli-copy.js.map +0 -1
  92. package/dist/src/gateway/cli-copy.zh-CN.d.ts +0 -17
  93. package/dist/src/gateway/cli-copy.zh-CN.js +0 -58
  94. package/dist/src/gateway/cli-copy.zh-CN.js.map +0 -1
  95. package/dist/src/gateway/codex-doctor.d.ts +0 -36
  96. package/dist/src/gateway/codex-doctor.js +0 -127
  97. package/dist/src/gateway/codex-doctor.js.map +0 -1
  98. package/dist/src/gateway/dashboard-copy.d.ts +0 -7
  99. package/dist/src/gateway/dashboard-copy.en.d.ts +0 -504
  100. package/dist/src/gateway/dashboard-copy.en.js +0 -505
  101. package/dist/src/gateway/dashboard-copy.en.js.map +0 -1
  102. package/dist/src/gateway/dashboard-copy.js +0 -514
  103. package/dist/src/gateway/dashboard-copy.js.map +0 -1
  104. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +0 -504
  105. package/dist/src/gateway/dashboard-copy.zh-CN.js +0 -505
  106. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +0 -1
  107. package/dist/src/gateway/dashboard-model.d.ts +0 -343
  108. package/dist/src/gateway/dashboard-model.js +0 -1061
  109. package/dist/src/gateway/dashboard-model.js.map +0 -1
  110. package/dist/src/gateway/dashboard.d.ts +0 -20
  111. package/dist/src/gateway/dashboard.js +0 -874
  112. package/dist/src/gateway/dashboard.js.map +0 -1
  113. package/dist/src/gateway/deepseek-detect.d.ts +0 -14
  114. package/dist/src/gateway/deepseek-detect.js +0 -41
  115. package/dist/src/gateway/deepseek-detect.js.map +0 -1
  116. package/dist/src/gateway/live-dashboard-app/app.js +0 -2385
  117. package/dist/src/gateway/live-dashboard-assets.d.ts +0 -10
  118. package/dist/src/gateway/live-dashboard-assets.js +0 -74
  119. package/dist/src/gateway/live-dashboard-assets.js.map +0 -1
  120. package/dist/src/gateway/live-dashboard-command.d.ts +0 -58
  121. package/dist/src/gateway/live-dashboard-command.js +0 -334
  122. package/dist/src/gateway/live-dashboard-command.js.map +0 -1
  123. package/dist/src/gateway/live-dashboard-http.d.ts +0 -39
  124. package/dist/src/gateway/live-dashboard-http.js +0 -383
  125. package/dist/src/gateway/live-dashboard-http.js.map +0 -1
  126. package/dist/src/gateway/live-dashboard-protocol.d.ts +0 -34
  127. package/dist/src/gateway/live-dashboard-protocol.js +0 -114
  128. package/dist/src/gateway/live-dashboard-protocol.js.map +0 -1
  129. package/dist/src/gateway/live-dashboard-server.d.ts +0 -33
  130. package/dist/src/gateway/live-dashboard-server.js +0 -144
  131. package/dist/src/gateway/live-dashboard-server.js.map +0 -1
  132. package/dist/src/gateway/live-dashboard-stream.d.ts +0 -46
  133. package/dist/src/gateway/live-dashboard-stream.js +0 -234
  134. package/dist/src/gateway/live-dashboard-stream.js.map +0 -1
  135. package/dist/src/gateway/live-dashboard.d.ts +0 -28
  136. package/dist/src/gateway/live-dashboard.js +0 -154
  137. package/dist/src/gateway/live-dashboard.js.map +0 -1
  138. package/dist/src/gateway/locale.d.ts +0 -4
  139. package/dist/src/gateway/locale.js +0 -10
  140. package/dist/src/gateway/locale.js.map +0 -1
  141. package/dist/src/gateway/progress-watch-machine.d.ts +0 -45
  142. package/dist/src/gateway/progress-watch-machine.js +0 -70
  143. package/dist/src/gateway/progress-watch-machine.js.map +0 -1
  144. package/dist/src/gateway/state-v2-to-v3.d.ts +0 -24
  145. package/dist/src/gateway/state-v2-to-v3.js +0 -1034
  146. package/dist/src/gateway/state-v2-to-v3.js.map +0 -1
  147. package/docs/CONFIGURATION.zh-CN.md +0 -93
  148. package/docs/DASHBOARD.md +0 -98
  149. package/docs/DASHBOARD.zh-CN.md +0 -49
  150. package/docs/DELIVERY.zh-CN.md +0 -55
@@ -1,874 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { lstat, open, realpath, rename, unlink, } from "node:fs/promises";
3
- import path from "node:path";
4
- import { formatDashboardCopy, getDashboardCopy, } from "./dashboard-copy.js";
5
- import { buildDashboardViewModel, DASHBOARD_MODEL_LIMITS, dashboardTone, deliveryMeaningKey, guidanceCopyKey, nextActionCopyKey, } from "./dashboard-model.js";
6
- import { parseDirection } from "./types.js";
7
- export const DASHBOARD_FILE_NAME = "gateway-dashboard.html";
8
- export const DASHBOARD_ZH_CN_FILE_NAME = "gateway-dashboard.zh-CN.html";
9
- export const DASHBOARD_MESSAGE_LIMIT = DASHBOARD_MODEL_LIMITS.messages;
10
- export const DASHBOARD_CONNECTOR_LIMIT = DASHBOARD_MODEL_LIMITS.connectors;
11
- export const DASHBOARD_AVAILABLE_PEER_LIMIT = DASHBOARD_MODEL_LIMITS.availablePeers;
12
- export const DASHBOARD_ROUTE_LIMIT = DASHBOARD_MODEL_LIMITS.routes;
13
- export const DASHBOARD_ALERT_LIMIT = DASHBOARD_MODEL_LIMITS.alerts;
14
- export const DASHBOARD_MAX_HTML_BYTES = 256 * 1024;
15
- const HTML_ESCAPE_PATTERN = /[&<>"']/g;
16
- const HTML_ESCAPES = {
17
- "&": "&amp;",
18
- "<": "&lt;",
19
- ">": "&gt;",
20
- '"': "&quot;",
21
- "'": "&#39;",
22
- };
23
- export function escapeDashboardHtml(value) {
24
- return value.replace(HTML_ESCAPE_PATTERN, (character) => HTML_ESCAPES[character] ?? "");
25
- }
26
- function t(context, key, values = {}) {
27
- return escapeDashboardHtml(formatDashboardCopy(context.copy, key, values));
28
- }
29
- function normalizeDashboardLocale(value) {
30
- if (value === undefined || value === "en")
31
- return "en";
32
- if (value === "zh-CN")
33
- return "zh-CN";
34
- throw new Error("DASHBOARD_LOCALE_UNSUPPORTED");
35
- }
36
- function formatInteger(value) {
37
- return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
38
- }
39
- function renderCount(context, value, lowerBound) {
40
- const count = formatInteger(value);
41
- return lowerBound
42
- ? t(context, "count.atLeast", { count })
43
- : count;
44
- }
45
- function formatDuration(milliseconds) {
46
- if (milliseconds === undefined)
47
- return "—";
48
- if (milliseconds < 1_000)
49
- return `${milliseconds} ms`;
50
- if (milliseconds < 60_000) {
51
- const seconds = milliseconds / 1_000;
52
- return `${seconds < 10 ? seconds.toFixed(1) : Math.round(seconds)} s`;
53
- }
54
- const minutes = Math.floor(milliseconds / 60_000);
55
- const seconds = Math.floor((milliseconds % 60_000) / 1_000);
56
- if (minutes < 60)
57
- return `${minutes}m ${seconds}s`;
58
- const hours = Math.floor(minutes / 60);
59
- return `${hours}h ${minutes % 60}m`;
60
- }
61
- function formatQueueAge(milliseconds, locale) {
62
- if (milliseconds === undefined)
63
- return undefined;
64
- const seconds = Math.floor(milliseconds / 1_000);
65
- if (seconds < 60)
66
- return locale === "zh-CN" ? `${seconds} 秒` : `${seconds}s`;
67
- const minutes = Math.floor(seconds / 60);
68
- if (minutes < 60) {
69
- return locale === "zh-CN"
70
- ? `${minutes} 分 ${seconds % 60} 秒`
71
- : `${minutes}m ${seconds % 60}s`;
72
- }
73
- const hours = Math.floor(minutes / 60);
74
- if (hours < 48) {
75
- return locale === "zh-CN"
76
- ? `${hours} 小时 ${minutes % 60} 分`
77
- : `${hours}h ${minutes % 60}m`;
78
- }
79
- return locale === "zh-CN"
80
- ? `${Math.floor(hours / 24)} 天 ${hours % 24} 小时`
81
- : `${Math.floor(hours / 24)}d ${hours % 24}h`;
82
- }
83
- function formatBytes(bytes) {
84
- if (bytes < 1_024)
85
- return `${bytes} B`;
86
- if (bytes < 1_048_576) {
87
- const kibibytes = bytes / 1_024;
88
- return `${kibibytes < 10 ? kibibytes.toFixed(1) : Math.round(kibibytes)} KiB`;
89
- }
90
- const mebibytes = bytes / 1_048_576;
91
- return `${mebibytes < 10 ? mebibytes.toFixed(1) : Math.round(mebibytes)} MiB`;
92
- }
93
- function formatShortAge(milliseconds, locale) {
94
- const seconds = Math.max(0, Math.floor(milliseconds / 1_000));
95
- if (seconds < 60)
96
- return locale === "zh-CN" ? `${seconds} 秒` : `${seconds}s`;
97
- const minutes = Math.floor(seconds / 60);
98
- if (minutes < 60)
99
- return locale === "zh-CN" ? `${minutes} 分` : `${minutes}m`;
100
- const hours = Math.floor(minutes / 60);
101
- if (hours < 48)
102
- return locale === "zh-CN" ? `${hours} 小时` : `${hours}h`;
103
- const days = Math.floor(hours / 24);
104
- return locale === "zh-CN" ? `${days} 天` : `${days}d`;
105
- }
106
- function renderTimestampAtSnapshot(context, timestamp) {
107
- if (timestamp === undefined) {
108
- return `<span class="quiet">${t(context, "time.unavailable")}</span>`;
109
- }
110
- if (context.model.generatedAt === undefined) {
111
- return `<time datetime="${timestamp}" title="${timestamp}">${escapeDashboardHtml(timestamp.slice(0, 19).replace("T", " "))} UTC</time>`;
112
- }
113
- const difference = Date.parse(context.model.generatedAt) - Date.parse(timestamp);
114
- let label;
115
- if (Math.abs(difference) < 1_000) {
116
- label = t(context, "time.atSnapshot");
117
- }
118
- else if (difference > 0) {
119
- label = t(context, "time.beforeSnapshot", {
120
- age: formatShortAge(difference, context.locale),
121
- });
122
- }
123
- else {
124
- label = t(context, "time.afterSnapshot", {
125
- age: formatShortAge(-difference, context.locale),
126
- });
127
- }
128
- return `<time datetime="${timestamp}" title="${timestamp}">${label}</time>`;
129
- }
130
- function toneForOverall(overall) {
131
- return dashboardTone("overall", overall);
132
- }
133
- function toneForParty(status) {
134
- return dashboardTone("party", status);
135
- }
136
- function toneForHealth(health) {
137
- return dashboardTone("health", health);
138
- }
139
- function toneForRoute(state) {
140
- return dashboardTone("route", state);
141
- }
142
- function toneForPeer(state) {
143
- return dashboardTone("peer", state);
144
- }
145
- function toneForDelivery(state, safeErrorCode) {
146
- return dashboardTone("delivery", state, safeErrorCode);
147
- }
148
- function statusPill(label, tone) {
149
- return `<span class="status status--${tone}"><span class="status__dot" aria-hidden="true"></span>${label}</span>`;
150
- }
151
- function overallLabel(context) {
152
- return t(context, context.model.overall === "ready"
153
- ? "overall.ready"
154
- : context.model.overall === "attention"
155
- ? "overall.attention"
156
- : "overall.setup");
157
- }
158
- function partyStatusLabel(context, status) {
159
- const key = status === "ready"
160
- ? "status.ready"
161
- : status === "busy"
162
- ? "status.busy"
163
- : status === "waiting"
164
- ? "status.waiting"
165
- : status === "attention"
166
- ? "status.attention"
167
- : "status.missing";
168
- return t(context, key);
169
- }
170
- function nextActionLabel(context, action) {
171
- return t(context, nextActionCopyKey(action));
172
- }
173
- function renderParty(context, party) {
174
- const isClaude = party.kind === "claude";
175
- const title = party.kind === "claude"
176
- ? t(context, "exchange.claude.title")
177
- : party.kind === "codex"
178
- ? t(context, "exchange.codex.title")
179
- : providerLabel(context, party.kind);
180
- const note = party.kind === "claude"
181
- ? t(context, "exchange.claude.note")
182
- : party.kind === "codex"
183
- ? t(context, "exchange.codex.note")
184
- : t(context, "exchange.provider.note");
185
- const ready = renderCount(context, party.ready, party.countIsLowerBound);
186
- const total = renderCount(context, party.total, party.countIsLowerBound);
187
- const selectable = renderCount(context, party.selectable ?? 0, party.countIsLowerBound);
188
- return `<article class="exchange-party exchange-party--${party.kind}" data-semantic-party="${party.kind}">
189
- <div class="party-heading">
190
- <div><h3>${title}</h3><p>${note}</p></div>
191
- ${statusPill(partyStatusLabel(context, party.status), toneForParty(party.status))}
192
- </div>
193
- <p class="party-count">${t(context, isClaude ? "exchange.count.claude" : "app.overview.count.provider", { ready, selectable, total })}</p>
194
- ${party.primaryAlias === undefined ? "" : `<p class="party-alias">${escapeDashboardHtml(party.primaryAlias)}</p>`}
195
- <p class="next-action"><strong>${t(context, "next.label")}:</strong> ${nextActionLabel(context, party.nextAction)}</p>
196
- </article>`;
197
- }
198
- function renderPairGraph(context) {
199
- const graph = context.model.graph;
200
- const locallyOmittedPairs = Math.max(0, graph.consentEdgeCount - context.model.consentEdges.length);
201
- const upstreamOmittedPairs = Math.max(0, context.model.omissions.consentEdges - locallyOmittedPairs);
202
- const knownPairCount = graph.consentEdgeCount + upstreamOmittedPairs;
203
- const summary = t(context, "app.routes.pairSummary", {
204
- ready: renderCount(context, graph.readyConsentEdgeCount, graph.consentEdgeCountIsLowerBound),
205
- total: renderCount(context, knownPairCount, graph.consentEdgeCountIsLowerBound),
206
- });
207
- const unpaired = context.model.exchange.parties.map((party) => t(context, "app.routes.unpairedProvider", {
208
- provider: providerLabel(context, party.kind),
209
- count: String(graph.unpairedReadyByProvider[party.kind]),
210
- })).join(" · ");
211
- const hasPairEvidence = graph.consentEdgeCount > 0 || graph.consentEdgeCountIsLowerBound;
212
- const rows = context.model.consentEdges.length === 0 && !hasPairEvidence
213
- ? `<p class="quiet">${t(context, "app.routes.noPairInline")}</p>`
214
- : `<ul class="pair-list">${context.model.consentEdges.map((edge) => {
215
- const [left, right] = edge.endpoints;
216
- const label = t(context, "app.routes.consentEdge", {
217
- left: t(context, "app.routes.consentEndpoint", {
218
- provider: providerLabel(context, left.provider),
219
- alias: escapeDashboardHtml(left.alias),
220
- }),
221
- right: t(context, "app.routes.consentEndpoint", {
222
- provider: providerLabel(context, right.provider),
223
- alias: escapeDashboardHtml(right.alias),
224
- }),
225
- });
226
- const stateLabel = edge.state === "ready"
227
- ? t(context, "status.ready")
228
- : t(context, edge.state === "degraded"
229
- ? "app.routes.pairState.degraded"
230
- : "app.routes.pairState.unavailable");
231
- const tone = edge.state === "ready"
232
- ? "good"
233
- : edge.state === "degraded"
234
- ? "warning"
235
- : "danger";
236
- const reason = edge.state === "ready"
237
- ? ""
238
- : `<small>${t(context, edge.state === "degraded" ? "app.routes.pairDegradedReason" : "app.routes.pairUnavailableReason")}</small>`;
239
- return `<li><span>${label}${reason}</span>${statusPill(stateLabel, tone)}</li>`;
240
- }).join("")}</ul>`;
241
- const omitted = context.model.omissions.consentEdges === 0
242
- ? ""
243
- : `<p class="quiet">${t(context, "app.omitted.pairs", {
244
- count: String(context.model.omissions.consentEdges),
245
- })}</p>`;
246
- return `<section class="pair-graph" aria-labelledby="pair-graph-title">
247
- <div class="pair-graph__heading"><h3 id="pair-graph-title">${t(context, "app.routes.pairs")}</h3><strong>${summary}</strong></div>
248
- <p>${unpaired}</p>
249
- ${rows}
250
- ${omitted}
251
- </section>`;
252
- }
253
- function renderExchange(context) {
254
- const age = formatQueueAge(context.model.exchange.oldestQueueAgeMs, context.locale);
255
- const pouch = context.model.exchange.queuedMessages === 0
256
- ? t(context, "exchange.pouch.empty")
257
- : t(context, "exchange.pouch.queued", {
258
- count: renderCount(context, context.model.exchange.queuedMessages, context.model.exchange.queueCountIsLowerBound),
259
- });
260
- const paired = context.model.inboundMode === "paired";
261
- const hasPair = context.model.graph.consentEdgeCount > 0 ||
262
- context.model.graph.consentEdgeCountIsLowerBound;
263
- const policyBody = paired
264
- ? t(context, hasPair ? "inbound.paired.body" : "inbound.noPair.body")
265
- : t(context, "inbound.open.body");
266
- const directionParties = context.model.exchange.parties.length === 2
267
- ? [...context.model.exchange.parties].reverse()
268
- : context.model.exchange.parties;
269
- return `<section class="section exchange" aria-labelledby="exchange-title">
270
- <div class="section-heading">
271
- <div><p class="eyebrow">${t(context, "exchange.eyebrow")}</p><h2 id="exchange-title">${t(context, "exchange.title")}</h2></div>
272
- <p>${t(context, "exchange.note")}</p>
273
- </div>
274
- <div class="inbound-policy" data-inbound-mode="${context.model.inboundMode}">
275
- ${statusPill(t(context, paired ? "inbound.paired.badge" : "inbound.open.badge"), paired ? "quiet" : "warning")}
276
- <p>${policyBody}</p>
277
- </div>
278
- <div class="exchange-board${context.model.exchange.parties.length > 2 ? " exchange-board--many" : ""}">
279
- ${renderParty(context, context.model.exchange.parties[0])}
280
- <div class="pouch" aria-label="${t(context, "exchange.pouch.title")}">
281
- <span class="pouch__line" aria-hidden="true"></span>
282
- <div class="seal-mark" aria-hidden="true"><span></span></div>
283
- <p class="pouch__title">${t(context, "exchange.pouch.title")}</p>
284
- <strong>${pouch}</strong>
285
- ${age === undefined ? "" : `<small>${t(context, "exchange.pouch.oldest", { age })}</small>`}
286
- </div>
287
- ${context.model.exchange.parties.slice(1).map((party) => renderParty(context, party)).join("")}
288
- </div>
289
- <div class="direction-key" aria-label="${t(context, "exchange.title")}">${directionParties.flatMap((source) => context.model.exchange.parties.filter((target) => target.kind !== source.kind).map((target) => `<span>${providerLabel(context, source.kind)} → ${providerLabel(context, target.kind)}</span>`)).join("")}</div>
290
- ${renderPairGraph(context)}
291
- </section>`;
292
- }
293
- function severityLabel(context, severity) {
294
- return t(context, severity === "error"
295
- ? "severity.error"
296
- : severity === "warning"
297
- ? "severity.warning"
298
- : "severity.info");
299
- }
300
- function alertTone(severity) {
301
- return dashboardTone("severity", severity);
302
- }
303
- function guidanceKeys(guidance) {
304
- const segment = guidanceCopyKey(guidance);
305
- return [
306
- `guidance.${segment}.title`,
307
- `guidance.${segment}.body`,
308
- `guidance.${segment}.action`,
309
- ];
310
- }
311
- function renderAttention(context) {
312
- const hasOmissions = context.model.omissions.upstreamAlerts > 0 ||
313
- context.model.omissions.attentionItems > 0;
314
- if (context.model.attention.length === 0 && !hasOmissions)
315
- return "";
316
- return `<section class="section attention" aria-labelledby="attention-title">
317
- <div class="section-heading"><div><p class="eyebrow">${t(context, "attention.eyebrow")}</p><h2 id="attention-title">${t(context, "attention.title")}</h2></div><p>${t(context, hasOmissions ? "attention.countVisible" : "attention.count", { count: formatInteger(context.model.attention.length) })}</p></div>
318
- ${context.model.attention.length === 0 ? `<p class="attention-projection-note">${t(context, "attention.projectionOnly")}</p>` : `<ol class="attention-list">${context.model.attention
319
- .map((item) => {
320
- const [titleKey, bodyKey, actionKey] = guidanceKeys(item.guidance);
321
- const scope = [
322
- item.provider === undefined
323
- ? undefined
324
- : providerLabel(context, item.provider),
325
- item.alias === undefined ? undefined : escapeDashboardHtml(item.alias),
326
- item.host === undefined ? undefined : escapeDashboardHtml(item.host),
327
- ].filter((value) => value !== undefined);
328
- return `<li class="attention-item attention-item--${alertTone(item.severity)}">
329
- <div class="attention-item__meta">${statusPill(severityLabel(context, item.severity), alertTone(item.severity))}${item.code === undefined ? "" : `<code>${item.code}</code>`}${renderTimestampAtSnapshot(context, item.timestamp)}</div>
330
- <h3>${t(context, titleKey)}</h3><p>${t(context, bodyKey)}</p>
331
- ${item.queueDepth === undefined ? "" : `<p><strong>${t(context, "transit.queued")}:</strong> ${formatInteger(item.queueDepth)}</p><p>${t(context, "guidance.queueStalled.busy")}</p>`}
332
- ${scope.length === 0 ? "" : `<p class="scope"><strong>${t(context, "attention.scope")}:</strong> ${scope.join(" · ")}</p>`}
333
- <p class="next-action"><strong>${t(context, "next.label")}:</strong> ${t(context, actionKey, { alias: item.alias ?? "<alias>" })}</p>
334
- </li>`;
335
- })
336
- .join("")}</ol>`}
337
- </section>`;
338
- }
339
- function renderTransit(context) {
340
- const age = formatQueueAge(context.model.transit.oldestQueueAgeMs, context.locale);
341
- return `<section class="section transit" aria-labelledby="transit-title">
342
- <div class="section-heading"><div><p class="eyebrow">${t(context, "transit.eyebrow")}</p><h2 id="transit-title">${t(context, "transit.title")}</h2></div>${context.model.transit.queuedMessages === 0 ? `<p>${t(context, "transit.empty")}</p>` : ""}</div>
343
- <dl class="ledger-strip">
344
- <div><dt>${t(context, "transit.queued")}</dt><dd>${renderCount(context, context.model.transit.queuedMessages, context.model.transit.queueCountIsLowerBound)}</dd></div>
345
- <div><dt>${t(context, "transit.active")}</dt><dd>${renderCount(context, context.model.transit.activeDeliveries, context.model.transit.activeCountIsLowerBound)}</dd></div>
346
- <div><dt>${t(context, "transit.oldest")}</dt><dd>${age === undefined ? t(context, context.model.transit.queuedMessages === 0 ? "time.unavailable" : "transit.unavailable") : `<time datetime="${context.model.transit.oldestQueuedAt}">${escapeDashboardHtml(age)}</time>`}</dd></div>
347
- </dl>
348
- </section>`;
349
- }
350
- function deliveryLabelKey(state) {
351
- switch (state) {
352
- case "queued": return "delivery.queued";
353
- case "duplicate": return "delivery.duplicate";
354
- case "dispatching": return "delivery.dispatching";
355
- case "transport_written": return "delivery.transportWritten";
356
- case "held": return "delivery.held";
357
- case "delivered": return "delivery.delivered";
358
- case "unconfirmed": return "delivery.unconfirmed";
359
- case "failed": return "delivery.failed";
360
- case "ambiguous": return "delivery.ambiguous";
361
- case "expired": return "delivery.expired";
362
- case "cancelled": return "delivery.cancelled";
363
- case "abandoned": return "delivery.abandoned";
364
- case "rejected": return "delivery.rejected";
365
- }
366
- }
367
- function renderMessageHistory(context, message) {
368
- if (message.events.length <= 1) {
369
- return `<span class="quiet">${t(context, "activity.history.one")}</span>`;
370
- }
371
- return `<details class="history"><summary>${t(context, "activity.history.many", { count: formatInteger(message.events.length) })}</summary><ol>${message.events
372
- .map((event) => `<li data-dashboard-row="message-event">${renderTimestampAtSnapshot(context, event.timestamp)} ${statusPill(t(context, deliveryLabelKey(event.state)), toneForDelivery(event.state, event.safeErrorCode))}${event.safeErrorCode === undefined ? "" : `<code>${event.safeErrorCode}</code>`}</li>`)
373
- .join("")}</ol></details>`;
374
- }
375
- function renderActivity(context) {
376
- const rows = context.model.activity.length === 0
377
- ? `<tr class="empty-row"><td colspan="7">${t(context, "activity.empty")}</td></tr>`
378
- : context.model.activity
379
- .map((message) => `<tr data-dashboard-row="message-summary" data-delivery-state="${message.state}">
380
- <td data-label="${t(context, "activity.column.updated")}">${renderTimestampAtSnapshot(context, message.timestamp)}</td>
381
- <td data-label="${t(context, "activity.column.route")}" class="route-cell"><strong>${directionLabel(context, message.direction)}${message.steer === true ? ' <span class="pill quiet">STEER</span>' : ""}</strong><span>${escapeDashboardHtml(message.sourceAlias)} → ${escapeDashboardHtml(message.targetAlias)}</span></td>
382
- <td data-label="${t(context, "activity.column.id")}"><code>…${message.messageIdSuffix ?? "—"}</code>${message.conversationIdSuffix === undefined ? "" : `<span class="cell-note"><code>conv …${escapeDashboardHtml(message.conversationIdSuffix)}</code></span>`}</td>
383
- <td data-label="${t(context, "activity.column.result")}">${statusPill(t(context, deliveryLabelKey(message.state)), toneForDelivery(message.state, message.safeErrorCode))}<span class="cell-note">${t(context, deliveryMeaningKey(message.state, message.direction, message.safeErrorCode))}</span>${message.safeErrorCode === undefined ? "" : `<code class="cell-code">${message.safeErrorCode}</code>`}</td>
384
- <td data-label="${t(context, "activity.column.elapsed")}" class="numeric">${formatDuration(message.latencyMs)}</td>
385
- <td data-label="${t(context, "activity.column.size")}" class="numeric">${formatBytes(message.bytes)}</td>
386
- <td data-label="${t(context, "activity.column.history")}">${renderMessageHistory(context, message)}</td>
387
- </tr>`)
388
- .join("");
389
- const operationCopyKeys = {
390
- discovery_refreshed: "app.activity.operation.discoveryRefreshed",
391
- claude_selected: "app.activity.operation.claudeSelected",
392
- claude_unselected: "app.activity.operation.claudeUnselected",
393
- codex_registered: "app.activity.operation.codexRegistered",
394
- codex_succeeded: "app.activity.operation.codexSucceeded",
395
- codex_unregistered: "app.activity.operation.codexUnregistered",
396
- routes_paired: "app.activity.operation.routesPaired",
397
- routes_unpaired: "app.activity.operation.routesUnpaired",
398
- watch_ended: "app.activity.operation.watchEnded",
399
- };
400
- const operations = context.model.brokerActivity.length === 0
401
- ? ""
402
- : `<details class="history"><summary>${t(context, "app.activity.kinds.operation")} (${formatInteger(context.model.brokerActivity.length)})</summary><ol>${context.model.brokerActivity
403
- .slice()
404
- .reverse()
405
- .map((event) => `<li data-dashboard-row="${event.operatorAction ? "operator-action" : "automatic-event"}" data-activity-authority="${event.operatorAction ? "operator" : "automatic"}">${renderTimestampAtSnapshot(context, event.timestamp)} ${statusPill(t(context, event.operatorAction ? "app.activity.operation.operator" : "app.activity.operation.automatic"), event.operatorAction ? "info" : "quiet")} ${escapeDashboardHtml(t(context, operationCopyKeys[event.action]))} ${statusPill(t(context, event.outcome === "accepted" ? "app.activity.operation.accepted" : "app.activity.operation.rejected"), event.outcome === "accepted" ? "good" : "warning")}${event.aliases.length === 0 ? "" : ` <code>${event.aliases.map(escapeDashboardHtml).join(" · ")}</code>`}${event.safeErrorCode === undefined ? "" : ` <code>${event.safeErrorCode}</code>`}</li>`)
406
- .join("")}</ol></details>`;
407
- return `<section class="section" aria-labelledby="activity-title">
408
- <div class="section-heading"><div><p class="eyebrow">${t(context, "activity.eyebrow")}</p><h2 id="activity-title">${t(context, "activity.title")}</h2></div><p>${t(context, "activity.note")}</p></div>
409
- <div class="table-wrap" tabindex="0" role="region" aria-labelledby="activity-title"><table class="responsive-table"><caption>${t(context, "activity.note")}</caption><thead><tr><th>${t(context, "activity.column.updated")}</th><th>${t(context, "activity.column.route")}</th><th>${t(context, "activity.column.id")}</th><th>${t(context, "activity.column.result")}</th><th class="numeric">${t(context, "activity.column.elapsed")}</th><th class="numeric">${t(context, "activity.column.size")}</th><th>${t(context, "activity.column.history")}</th></tr></thead><tbody>${rows}</tbody></table></div>${operations}
410
- </section>`;
411
- }
412
- function renderProgressWatches(context) {
413
- const rows = context.model.watches.length === 0
414
- ? `<tr class="empty-row"><td colspan="5">${t(context, "watches.empty")}</td></tr>`
415
- : context.model.watches
416
- .map((watch) => `<tr data-dashboard-row="progress-watch">
417
- <th scope="row" data-label="${t(context, "watches.column.conversation")}"><code>…${escapeDashboardHtml(watch.conversationIdSuffix)}</code></th>
418
- <td data-label="${t(context, "watches.column.parties")}" class="route-cell"><strong>${escapeDashboardHtml(watch.ownerAlias)} → ${escapeDashboardHtml(watch.workerAlias)}</strong></td>
419
- <td data-label="${t(context, "watches.column.quietFor")}" class="numeric">${formatDuration(watch.idleForMs)}</td>
420
- <td data-label="${t(context, "watches.column.nextAction")}" class="numeric">${formatDuration(watch.dueInMs)}</td>
421
- <td data-label="${t(context, "watches.column.nudges")}" class="numeric">${formatInteger(watch.nudgeCount)}</td>
422
- </tr>`)
423
- .join("");
424
- return `<section class="section" aria-labelledby="watches-title">
425
- <div class="section-heading"><div><p class="eyebrow">${t(context, "watches.eyebrow")}</p><h2 id="watches-title">${t(context, "watches.title")}</h2></div><p>${t(context, "watches.note")}</p></div>
426
- <div class="table-wrap" tabindex="0" role="region" aria-labelledby="watches-title"><table class="responsive-table"><caption>${t(context, "watches.note")}</caption><thead><tr><th>${t(context, "watches.column.conversation")}</th><th>${t(context, "watches.column.parties")}</th><th class="numeric">${t(context, "watches.column.quietFor")}</th><th class="numeric">${t(context, "watches.column.nextAction")}</th><th class="numeric">${t(context, "watches.column.nudges")}</th></tr></thead><tbody>${rows}</tbody></table></div>
427
- </section>`;
428
- }
429
- function providerLabel(context, provider) {
430
- return t(context, `provider.${provider}`);
431
- }
432
- function directionLabel(context, direction) {
433
- const parsed = parseDirection(direction);
434
- return parsed === undefined
435
- ? direction
436
- : `${providerLabel(context, parsed.sourceProvider)} → ${providerLabel(context, parsed.targetProvider)}`;
437
- }
438
- function healthLabel(context, health) {
439
- return t(context, `health.${health}`);
440
- }
441
- function routeStateLabel(context, state) {
442
- const suffix = state === "awaiting_approval" ? "awaitingApproval" : state;
443
- return t(context, `route.${suffix}`);
444
- }
445
- function peerStateLabel(context, state) {
446
- const suffix = state === "awaiting_approval" ? "awaitingApproval" : state;
447
- return t(context, `peer.${suffix}`);
448
- }
449
- function peerSelectionGuidance(context, guidance) {
450
- const key = guidance === "alias_collision"
451
- ? "peer.reason.aliasCollision"
452
- : guidance === "session_collision"
453
- ? "peer.reason.sessionCollision"
454
- : guidance === "discovery_incomplete"
455
- ? "peer.reason.discoveryIncomplete"
456
- : guidance === "offline"
457
- ? "peer.reason.offline"
458
- : "peer.reason.offline";
459
- return t(context, key);
460
- }
461
- function renderSessions(context) {
462
- const peerRows = context.model.peers.length === 0
463
- ? `<tr class="empty-row"><td colspan="7">${t(context, "sessions.peers.empty")}</td></tr>`
464
- : context.model.peers.map((peer) => `<tr><th scope="row" data-label="${t(context, "column.alias")}">${escapeDashboardHtml(peer.alias)}</th><td data-label="${t(context, "column.host")}">${escapeDashboardHtml(peer.host)}</td><td data-label="${t(context, "column.state")}">${statusPill(peerStateLabel(context, peer.state), toneForPeer(peer.state))}</td><td data-label="${t(context, "column.validation")}">${statusPill(t(context, peer.validated ? "status.validated" : "status.validationRejected"), peer.validated ? "good" : "warning")}</td><td data-label="${t(context, "column.selection")}">${peer.selectable ? statusPill(t(context, peer.selected ? "status.selected" : "status.available"), peer.selected ? "good" : "quiet") : `${statusPill(t(context, "status.notSelectable"), "warning")}${peer.selectionGuidance === undefined ? "" : `<span class="cell-note">${peerSelectionGuidance(context, peer.selectionGuidance)}</span>`}`}</td><td data-label="${t(context, "column.observed")}">${renderTimestampAtSnapshot(context, peer.lastSeenAt)}</td><td data-label="${t(context, "column.issue")}">${peer.safeErrorCode === undefined ? "—" : `<code>${peer.safeErrorCode}</code>`}</td></tr>`).join("");
465
- const routeRows = context.model.routes.length === 0
466
- ? `<tr class="empty-row"><td colspan="7">${t(context, "sessions.routes.empty")}</td></tr>`
467
- : context.model.routes.map((route) => `<tr><th scope="row" data-label="${t(context, "column.alias")}">${escapeDashboardHtml(route.alias)}</th><td data-label="${t(context, "column.provider")}">${providerLabel(context, route.provider)}</td><td data-label="${t(context, "column.host")}">${escapeDashboardHtml(route.host)}</td><td data-label="${t(context, "column.state")}">${statusPill(routeStateLabel(context, route.enabled ? route.state : "disabled"), toneForRoute(route.enabled ? route.state : "disabled"))}</td><td data-label="${t(context, "column.queue")}">${formatInteger(route.queueDepth)}${route.queueAgeMs === undefined ? "" : `<span class="cell-note">${escapeDashboardHtml(formatQueueAge(route.queueAgeMs, context.locale) ?? "")}</span>`}</td><td data-label="${t(context, "column.observed")}">${renderTimestampAtSnapshot(context, route.lastSeenAt)}</td><td data-label="${t(context, "column.issue")}">${route.safeErrorCode === undefined ? "—" : `<code>${route.safeErrorCode}</code>`}</td></tr>`).join("");
468
- return `<section class="section" aria-labelledby="sessions-title">
469
- <div class="section-heading"><div><p class="eyebrow">${t(context, "sessions.eyebrow")}</p><h2 id="sessions-title">${t(context, "sessions.title")}</h2></div><p>${t(context, "sessions.note")}</p></div>
470
- <div class="register-grid">
471
- <section aria-labelledby="peers-title"><h3 id="peers-title">${t(context, "sessions.peers.title")}</h3><div class="table-wrap" tabindex="0" role="region" aria-labelledby="peers-title"><table class="responsive-table"><caption>${t(context, "sessions.peers.caption")}</caption><thead><tr><th>${t(context, "column.alias")}</th><th>${t(context, "column.host")}</th><th>${t(context, "column.state")}</th><th>${t(context, "column.validation")}</th><th>${t(context, "column.selection")}</th><th>${t(context, "column.observed")}</th><th>${t(context, "column.issue")}</th></tr></thead><tbody>${peerRows}</tbody></table></div></section>
472
- <section aria-labelledby="routes-title"><h3 id="routes-title">${t(context, "sessions.routes.title")}</h3><div class="table-wrap" tabindex="0" role="region" aria-labelledby="routes-title"><table class="responsive-table"><caption>${t(context, "sessions.routes.caption")}</caption><thead><tr><th>${t(context, "column.alias")}</th><th>${t(context, "column.provider")}</th><th>${t(context, "column.host")}</th><th>${t(context, "column.state")}</th><th>${t(context, "column.queue")}</th><th>${t(context, "column.observed")}</th><th>${t(context, "column.issue")}</th></tr></thead><tbody>${routeRows}</tbody></table></div></section>
473
- </div>
474
- </section>`;
475
- }
476
- function renderDiagnostics(context) {
477
- const connectorRows = context.model.connectors.length === 0
478
- ? `<tr class="empty-row"><td colspan="7">${t(context, "diagnostics.connectors.empty")}</td></tr>`
479
- : context.model.connectors.map((connector) => {
480
- const observation = connector.registry;
481
- let registry = "—";
482
- if (observation !== undefined) {
483
- const stateKey = observation.parseableRecordSeenSinceBoot
484
- ? "diagnostics.registry.state.parseableRecordObserved"
485
- : observation.entriesScanned === 0
486
- ? "diagnostics.registry.state.emptySinceBoot"
487
- : "diagnostics.registry.state.noParseableRecordSinceBoot";
488
- const rejected = observation.rejected.length === 0
489
- ? t(context, "diagnostics.registry.rejectedNone")
490
- : observation.rejected
491
- .map((row) => `<code>${escapeDashboardHtml(row.safeErrorCode)}</code> × ${formatInteger(row.count)}`)
492
- .join(" · ");
493
- const omitted = observation.rejectedCodesOmitted === 0
494
- ? ""
495
- : `<span class="cell-note">${t(context, "diagnostics.registry.rejectedCodesOmitted", { count: formatInteger(observation.rejectedCodesOmitted) })}</span>`;
496
- registry = `${statusPill(t(context, stateKey), observation.parseableRecordSeenSinceBoot ? "good" : "warning")}<span class="cell-note">${t(context, "diagnostics.registry.entriesScanned")}: ${formatInteger(observation.entriesScanned)} · ${t(context, "diagnostics.registry.parseableRecords")}: ${formatInteger(observation.parseableRecords)}</span><span class="cell-note">${t(context, "diagnostics.registry.rejected")}: ${rejected}</span>${omitted}`;
497
- }
498
- const doctor = connector.codexDoctor?.conditions.map((condition) => `<span class="cell-note">${t(context, `diagnostics.codexDoctor.${condition}`)}</span>`).join("") ?? "";
499
- const issue = `${connector.safeErrorCode === undefined ? "" : `<code>${connector.safeErrorCode}</code>`}${doctor}` || "—";
500
- return `<tr><th scope="row" data-label="${t(context, "column.provider")}">${providerLabel(context, connector.provider)}</th><td data-label="${t(context, "column.host")}">${escapeDashboardHtml(connector.host)}</td><td data-label="${t(context, "diagnostics.health")}">${statusPill(healthLabel(context, connector.health), toneForHealth(connector.health))}</td><td data-label="${t(context, "diagnostics.protocol")}"><code>${escapeDashboardHtml([connector.protocol, connector.protocolVersion].filter(Boolean).join(" ") || "—")}</code></td><td data-label="${t(context, "diagnostics.registry.title")}">${registry}</td><td data-label="${t(context, "column.observed")}">${renderTimestampAtSnapshot(context, connector.lastSeenAt)}</td><td data-label="${t(context, "column.issue")}">${issue}</td></tr>`;
501
- }).join("");
502
- const omissions = context.model.omissions;
503
- const omissionEntries = [
504
- [omissions.connectors, "diagnostics.omissions.connectors"],
505
- [omissions.availablePeers, "diagnostics.omissions.peers"],
506
- [omissions.routes, "diagnostics.omissions.routes"],
507
- [omissions.consentEdges, "diagnostics.omissions.consentEdges"],
508
- [omissions.progressWatches, "diagnostics.omissions.progressWatches"],
509
- [
510
- omissions.upstreamProgressWatchEvents,
511
- "diagnostics.omissions.upstreamProgressWatchEvents",
512
- ],
513
- [
514
- omissions.progressWatchEvents,
515
- "diagnostics.omissions.progressWatchEvents",
516
- ],
517
- [
518
- omissions.upstreamMessageEvents,
519
- "diagnostics.omissions.upstreamMessageEvents",
520
- ],
521
- [omissions.messageGroups, "diagnostics.omissions.messageGroups"],
522
- [omissions.messageEvents, "diagnostics.omissions.messageEvents"],
523
- [omissions.upstreamAlerts, "diagnostics.omissions.upstreamAlerts"],
524
- [omissions.attentionItems, "diagnostics.omissions.attentionItems"],
525
- [
526
- omissions.upstreamActivityEvents,
527
- "diagnostics.omissions.upstreamActivityEvents",
528
- ],
529
- [omissions.activityEvents, "diagnostics.omissions.activityEvents"],
530
- ];
531
- const visibleOmissions = omissionEntries.filter(([count]) => count > 0);
532
- const omissionText = visibleOmissions.length === 0
533
- ? t(context, "diagnostics.omissions.none")
534
- : visibleOmissions
535
- .map(([count, key]) => t(context, key, { count: formatInteger(count) }))
536
- .join(" · ");
537
- const accounting = context.model.accounting;
538
- const deadline = context.model.deadlinePressure;
539
- const deadlineBucketKeys = {
540
- under_1m: "app.diag.deadline.bucket.under1m",
541
- "1m_to_5m": "app.diag.deadline.bucket.1to5m",
542
- "5m_to_15m": "app.diag.deadline.bucket.5to15m",
543
- "15m_to_60m": "app.diag.deadline.bucket.15to60m",
544
- over_60m: "app.diag.deadline.bucket.over60m",
545
- };
546
- const deadlineSection = deadline === undefined
547
- ? ""
548
- : `<section aria-labelledby="deadline-pressure-title"><h3 id="deadline-pressure-title">${t(context, "app.diag.deadline.title")}</h3><p>${t(context, "app.diag.deadline.retained", { terminal: formatInteger(deadline.terminalEvents), expired: formatInteger(deadline.expiredEvents), deadline: formatDuration(deadline.configuredDeadlineMs) })}</p><dl class="accounting">${deadline.buckets.map((bucket) => `<div><dt>${t(context, deadlineBucketKeys[bucket.bucket])}</dt><dd>${formatInteger(bucket.settled)} · ${formatInteger(bucket.expired)} ${t(context, "delivery.expired")}</dd></div>`).join("")}</dl></section>`;
549
- return `<details class="section diagnostics" aria-labelledby="diagnostics-title" aria-describedby="diagnostics-note">
550
- <summary><h2 id="diagnostics-title"><span class="disclosure-icon" aria-hidden="true"><span class="disclosure-icon__closed">+</span><span class="disclosure-icon__open">−</span></span>${t(context, "diagnostics.title")}</h2></summary>
551
- <div class="diagnostics__body">
552
- <p id="diagnostics-note" class="diagnostics__note">${t(context, "diagnostics.note")}</p>
553
- <section aria-labelledby="connectors-title"><h3 id="connectors-title">${t(context, "diagnostics.connectors")}</h3><div class="table-wrap" tabindex="0" role="region" aria-labelledby="connectors-title"><table class="responsive-table"><caption>${t(context, "diagnostics.connectors.caption")}</caption><thead><tr><th>${t(context, "column.provider")}</th><th>${t(context, "column.host")}</th><th>${t(context, "diagnostics.health")}</th><th>${t(context, "diagnostics.protocol")}</th><th>${t(context, "diagnostics.registry.title")}</th><th>${t(context, "column.observed")}</th><th>${t(context, "column.issue")}</th></tr></thead><tbody>${connectorRows}</tbody></table></div></section>
554
- ${deadlineSection}
555
- <section aria-labelledby="accounting-title"><h3 id="accounting-title">${t(context, "diagnostics.accounting")}</h3><dl class="accounting"><div><dt>${t(context, "diagnostics.accepted")}</dt><dd>${formatInteger(accounting.accepted)}</dd></div><div><dt>${t(context, "diagnostics.duplicates")}</dt><dd>${formatInteger(accounting.duplicates)}</dd></div><div><dt>${t(context, "diagnostics.delivered")}</dt><dd>${formatInteger(accounting.delivered)}</dd></div><div><dt>${t(context, "diagnostics.unconfirmed")}</dt><dd>${formatInteger(accounting.unconfirmed)}</dd></div><div><dt>${t(context, "diagnostics.ambiguous")}</dt><dd>${formatInteger(accounting.ambiguous)}</dd></div><div><dt>${t(context, "diagnostics.failed")}</dt><dd>${formatInteger(accounting.failed)}</dd></div><div><dt>${t(context, "diagnostics.expired")}</dt><dd>${formatInteger(accounting.expired)}</dd></div><div><dt>${t(context, "diagnostics.cancelled")}</dt><dd>${formatInteger(accounting.cancelled)}</dd></div><div><dt>${t(context, "diagnostics.abandoned")}</dt><dd>${formatInteger(accounting.abandoned)}</dd></div><div><dt>${t(context, "diagnostics.rejected")}</dt><dd>${formatInteger(accounting.rejected)}</dd></div><div><dt>${t(context, "diagnostics.bytesAccepted")}</dt><dd>${formatBytes(accounting.bytesAccepted)}</dd></div><div><dt>${t(context, "diagnostics.queuedBytes")}</dt><dd>${formatBytes(accounting.queuedBytes)}</dd></div></dl></section>
556
- <p class="omissions"><strong>${t(context, "diagnostics.omissions")}:</strong> ${omissionText}</p>
557
- </div>
558
- </details>`;
559
- }
560
- function languageNavigation(context) {
561
- const en = context.locale === "en"
562
- ? `<span lang="en" aria-current="page">${t(context, "language.en")}</span>`
563
- : `<a lang="en" hreflang="en" href="./${DASHBOARD_FILE_NAME}">${t(context, "language.en")}</a>`;
564
- const zh = context.locale === "zh-CN"
565
- ? `<span lang="zh-CN" aria-current="page">${t(context, "language.zhCn")}</span>`
566
- : `<a lang="zh-CN" hreflang="zh-CN" href="./${DASHBOARD_ZH_CN_FILE_NAME}">${t(context, "language.zhCn")}</a>`;
567
- return `<nav class="language" aria-label="${t(context, "language.label")}">${en}<span aria-hidden="true">/</span>${zh}</nav>`;
568
- }
569
- const DASHBOARD_STYLES = `
570
- :root {
571
- color-scheme: light;
572
- --paper: #f5f0e6;
573
- --paper-deep: #ebe3d4;
574
- --sheet: #fffdf7;
575
- --ink: #28251f;
576
- --muted: #5f594f;
577
- --hairline: #cbc2b2;
578
- --hairline-strong: #9e9587;
579
- --seal: #b63a2c;
580
- --seal-soft: #f7e5df;
581
- --good: #256348;
582
- --good-soft: #e4f0e8;
583
- --info: #315f78;
584
- --info-soft: #e5eef1;
585
- --warning: #8a5a18;
586
- --warning-soft: #f5ebd7;
587
- --danger: #a13232;
588
- --danger-soft: #f6e3df;
589
- --quiet: #5c574f;
590
- --quiet-soft: #ece7de;
591
- }
592
- * { box-sizing: border-box; }
593
- html { background: var(--paper); scroll-behavior: smooth; }
594
- body { margin: 0; color: var(--ink); background: linear-gradient(90deg, transparent 0 49.92%, rgba(78,67,52,.035) 49.92% 50.08%, transparent 50.08%), var(--paper); font: 15px/1.55 ui-serif, Georgia, "Times New Roman", serif; }
595
- a { color: inherit; text-underline-offset: .2em; }
596
- a:focus-visible, summary:focus-visible, .table-wrap:focus-visible { outline: 3px solid var(--seal); outline-offset: 3px; }
597
- .skip-link { position: fixed; z-index: 10; left: 1rem; top: -6rem; padding: .65rem .9rem; color: var(--sheet); background: var(--ink); }
598
- .skip-link:focus { top: 1rem; }
599
- main { width: min(100% - 2rem, 1240px); margin: 0 auto; padding: 2rem 0 3rem; }
600
- .masthead { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2rem; align-items: start; padding: 1.35rem 0 2.4rem; border-top: 1px solid var(--ink); border-bottom: 1px solid var(--ink); }
601
- .brand-row { display: flex; gap: 1.15rem; align-items: center; }
602
- .brand-mark, .seal-mark { position: relative; display: grid; place-items: center; flex: 0 0 auto; border: 1px solid var(--ink); border-radius: 50%; }
603
- .brand-mark { width: 4.4rem; height: 4.4rem; }
604
- .brand-mark::before, .brand-mark::after, .seal-mark::before, .seal-mark::after { content: ""; position: absolute; top: 23%; bottom: 23%; width: 23%; border-radius: 50%; }
605
- .brand-mark::before, .seal-mark::before { left: 20%; border-right: 1px solid var(--ink); }
606
- .brand-mark::after, .seal-mark::after { right: 20%; border-left: 1px solid var(--ink); }
607
- .brand-mark span, .seal-mark span { position: relative; z-index: 1; width: 24%; aspect-ratio: 1; border-radius: 50%; background: var(--seal); }
608
- .eyebrow { margin: 0 0 .28rem; color: var(--seal); font: 700 .72rem/1.2 ui-sans-serif, system-ui, sans-serif; letter-spacing: .14em; text-transform: uppercase; }
609
- h1, h2, h3, p { margin-top: 0; }
610
- h1 { margin-bottom: .25rem; font-size: clamp(2.5rem, 7vw, 5.7rem); font-weight: 500; letter-spacing: -.055em; line-height: .9; }
611
- h2 { margin-bottom: 0; font-size: clamp(1.55rem, 3vw, 2.25rem); font-weight: 500; letter-spacing: -.025em; line-height: 1.05; }
612
- h3 { margin-bottom: .28rem; font-size: 1.03rem; }
613
- .subtitle { max-width: 44rem; margin: .65rem 0 0; color: var(--muted); font-size: 1.03rem; }
614
- .masthead__meta { display: grid; justify-items: end; gap: .8rem; max-width: 23rem; text-align: right; }
615
- .masthead__meta p { margin: 0; color: var(--muted); font-size: .82rem; }
616
- .language { display: inline-flex; gap: .5rem; align-items: center; font: 700 .76rem/1 ui-sans-serif, system-ui, sans-serif; }
617
- .language [aria-current="page"] { color: var(--seal); }
618
- .status { display: inline-flex; align-items: center; gap: .42rem; padding: .23rem .52rem; border: 1px solid currentColor; border-radius: 999px; font: 720 .72rem/1.2 ui-sans-serif, system-ui, sans-serif; white-space: nowrap; }
619
- .status__dot { width: .42rem; height: .42rem; border-radius: 50%; background: currentColor; }
620
- .status--good { color: var(--good); background: var(--good-soft); }
621
- .status--info { color: var(--info); background: var(--info-soft); }
622
- .status--warning { color: var(--warning); background: var(--warning-soft); }
623
- .status--danger { color: var(--danger); background: var(--danger-soft); }
624
- .status--quiet { color: var(--quiet); background: var(--quiet-soft); }
625
- .section { padding: 2.4rem 0; border-bottom: 1px solid var(--hairline-strong); }
626
- .section-heading { display: grid; grid-template-columns: minmax(0, 1fr) minmax(14rem, .72fr); gap: 2rem; align-items: end; margin-bottom: 1.25rem; }
627
- .section-heading > p { margin: 0; color: var(--muted); font-size: .88rem; }
628
- .exchange-board { display: grid; grid-template-columns: minmax(0, 1fr) minmax(11rem, .38fr) minmax(0, 1fr); align-items: stretch; border: 1px solid var(--ink); background: var(--sheet); }
629
- .exchange-board--many { grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); }
630
- .exchange-board--many .exchange-party { order: 1; border: 0; border-right: 1px solid var(--hairline); }
631
- .exchange-board--many .pouch { order: 2; grid-column: 1 / -1; border-top: 1px solid var(--hairline); }
632
- .inbound-policy { display: flex; align-items: center; gap: .75rem; margin: 0 0 1rem; padding: .65rem .8rem; border: 1px solid var(--hairline); background: var(--paper-deep); }
633
- .inbound-policy p { margin: 0; }
634
- .inbound-policy[data-inbound-mode="open"] { border-color: var(--warning); background: var(--warning-soft); }
635
- .exchange-party { min-width: 0; padding: 1.3rem; }
636
- .exchange-party--claude { border-right: 1px solid var(--hairline); }
637
- .exchange-party--codex { border-left: 1px solid var(--hairline); }
638
- .party-heading { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; }
639
- .party-heading p, .party-count { color: var(--muted); font-size: .82rem; }
640
- .party-count { margin: 1.8rem 0 .3rem; font-family: ui-sans-serif, system-ui, sans-serif; }
641
- .party-alias { margin: 0; font: 650 .85rem/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; }
642
- .next-action { margin: 1.15rem 0 0; padding-top: .85rem; border-top: 1px solid var(--hairline); color: var(--muted); font-size: .81rem; }
643
- .next-action strong { color: var(--ink); }
644
- .pouch { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 15rem; padding: 1rem; text-align: center; overflow: hidden; }
645
- .pouch__line { position: absolute; left: -10%; right: -10%; top: 50%; height: 1px; background: var(--hairline-strong); }
646
- .seal-mark { width: 3.7rem; height: 3.7rem; margin-bottom: .75rem; background: var(--paper); }
647
- .pouch p, .pouch strong, .pouch small { position: relative; z-index: 1; background: var(--sheet); }
648
- .pouch__title { margin: 0; padding: 0 .35rem; color: var(--muted); font-size: .74rem; text-transform: uppercase; letter-spacing: .1em; }
649
- .pouch strong { padding: .1rem .35rem; font: 720 .88rem/1.4 ui-sans-serif, system-ui, sans-serif; }
650
- .pouch small { padding: 0 .35rem; color: var(--muted); }
651
- .direction-key { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: .25rem 1rem; padding-top: .55rem; color: var(--muted); font-size: .74rem; }
652
- .pair-graph { margin-top: 1.2rem; padding: 1rem; border: 1px solid var(--hairline); background: var(--sheet); }
653
- .pair-graph__heading { display: flex; flex-wrap: wrap; justify-content: space-between; gap: .7rem 1rem; align-items: baseline; }
654
- .pair-graph__heading h3, .pair-graph p { margin-bottom: .55rem; }
655
- .pair-graph__heading strong, .pair-graph p { color: var(--muted); font: .78rem/1.45 ui-sans-serif, system-ui, sans-serif; }
656
- .pair-list { display: grid; gap: .45rem; margin: .8rem 0 0; padding: 0; list-style: none; }
657
- .pair-list li { display: flex; justify-content: space-between; gap: .8rem; align-items: center; padding-top: .45rem; border-top: 1px solid var(--hairline); overflow-wrap: anywhere; }
658
- .pair-list small { display: block; margin-top: .2rem; color: var(--muted); font-size: .8em; }
659
- .attention { border-bottom-color: var(--danger); }
660
- .attention-list { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--hairline); }
661
- .attention-projection-note { margin: 0; padding: 1rem; background: var(--warning-soft); }
662
- .attention-item { padding: 1.1rem 0; border-bottom: 1px solid var(--hairline); }
663
- .attention-item--danger { color: var(--danger); }
664
- .attention-item--warning { color: var(--warning); }
665
- .attention-item--info { color: var(--info); }
666
- .attention-item h3, .attention-item p { color: var(--ink); }
667
- .attention-item__meta { display: flex; flex-wrap: wrap; gap: .55rem; align-items: center; margin-bottom: .7rem; color: var(--muted); font-size: .75rem; }
668
- .scope { margin-bottom: 0; color: var(--muted) !important; font-size: .78rem; }
669
- .ledger-strip, .accounting { display: grid; grid-template-columns: repeat(3, 1fr); margin: 0; border-top: 1px solid var(--ink); border-bottom: 1px solid var(--ink); }
670
- .ledger-strip > div, .accounting > div { padding: .9rem 1rem; border-right: 1px solid var(--hairline); }
671
- .ledger-strip > div:last-child, .accounting > div:nth-child(3n) { border-right: 0; }
672
- dt { color: var(--muted); font: 700 .7rem/1.2 ui-sans-serif, system-ui, sans-serif; letter-spacing: .07em; text-transform: uppercase; }
673
- dd { margin: .25rem 0 0; font-size: 1.45rem; font-variant-numeric: tabular-nums; }
674
- .table-wrap { max-width: 100%; overflow: auto; border: 1px solid var(--hairline); background: var(--sheet); }
675
- table { width: 100%; border-collapse: collapse; font: .79rem/1.4 ui-sans-serif, system-ui, sans-serif; font-variant-numeric: tabular-nums; }
676
- caption { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
677
- th, td { padding: .68rem .75rem; border-bottom: 1px solid var(--hairline); text-align: left; vertical-align: top; white-space: nowrap; }
678
- thead th { color: var(--muted); background: var(--paper-deep); font-size: .66rem; letter-spacing: .06em; text-transform: uppercase; }
679
- tbody tr:last-child > * { border-bottom: 0; }
680
- tbody th { font-weight: 720; }
681
- code { font: .76rem/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; }
682
- .route-cell { white-space: normal; min-width: 16rem; }
683
- .route-cell span, .cell-note, .cell-code { display: block; margin-top: .2rem; color: var(--muted); font-size: .72rem; white-space: normal; }
684
- .numeric { text-align: right; }
685
- .history summary, .diagnostics summary { cursor: pointer; }
686
- .history ol { min-width: 22rem; margin: .55rem 0 0; padding-left: 1.1rem; }
687
- .history li { margin: .3rem 0; }
688
- .history .status { margin: 0 .4rem; }
689
- .empty-row td { padding: 1.2rem; color: var(--muted); text-align: center; }
690
- .register-grid { display: grid; gap: 1.4rem; }
691
- .register-grid > section { min-width: 0; }
692
- .register-grid h3, .diagnostics__body h3 { margin: 0 0 .55rem; }
693
- .diagnostics { padding-bottom: 0; }
694
- .diagnostics > summary { list-style: none; padding-bottom: 2rem; }
695
- .diagnostics > summary::-webkit-details-marker { display: none; }
696
- .diagnostics > summary h2 { display: flex; align-items: center; gap: .4rem; margin: 0; font-size: 1.05rem; }
697
- .disclosure-icon { display: inline-grid; width: 1.5rem; color: var(--seal); }
698
- .disclosure-icon > span { grid-area: 1 / 1; }
699
- .disclosure-icon__open { visibility: hidden; }
700
- .diagnostics[open] .disclosure-icon__closed { visibility: hidden; }
701
- .diagnostics[open] .disclosure-icon__open { visibility: visible; }
702
- .diagnostics__body { display: grid; gap: 1.5rem; padding-bottom: 2.2rem; }
703
- .diagnostics__note { margin: 0; color: var(--muted); font-size: .8rem; }
704
- .accounting { grid-template-columns: repeat(6, 1fr); }
705
- .accounting > div { border-bottom: 0; }
706
- .accounting dd { font-size: 1.1rem; }
707
- .omissions { margin: 0; color: var(--muted); font-size: .8rem; }
708
- .quiet { color: var(--muted); }
709
- footer { padding-top: 1rem; color: var(--muted); font-size: .74rem; text-align: center; }
710
- @media (max-width: 860px) {
711
- .masthead, .section-heading { grid-template-columns: 1fr; gap: 1rem; }
712
- .masthead__meta { justify-items: start; max-width: none; text-align: left; }
713
- .exchange-board { grid-template-columns: 1fr; }
714
- .exchange-party--claude { border-right: 0; border-bottom: 1px solid var(--hairline); }
715
- .exchange-party--codex { border-left: 0; border-top: 1px solid var(--hairline); }
716
- .pouch { min-height: 10rem; }
717
- .direction-key { display: none; }
718
- .accounting { grid-template-columns: repeat(3, 1fr); }
719
- .accounting > div:nth-child(3) { border-right: 0; }
720
- }
721
- @media (max-width: 560px) {
722
- main { width: min(100% - 1rem, 1240px); padding-top: .5rem; }
723
- .brand-row { align-items: flex-start; }
724
- .brand-mark { width: 3.3rem; height: 3.3rem; }
725
- .party-heading { flex-direction: column; }
726
- .ledger-strip { grid-template-columns: 1fr; }
727
- .ledger-strip > div { border-right: 0; border-bottom: 1px solid var(--hairline); }
728
- .ledger-strip > div:last-child { border-bottom: 0; }
729
- .responsive-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
730
- .responsive-table, .responsive-table tbody, .responsive-table tr { display: block; width: 100%; }
731
- .responsive-table tbody tr:not(.empty-row) { padding: .5rem .7rem; border-bottom: 1px solid var(--hairline); }
732
- .responsive-table tbody th, .responsive-table tbody td { display: grid; grid-template-columns: minmax(6.7rem, .42fr) minmax(0, 1fr); gap: .55rem; padding: .3rem 0; border: 0; white-space: normal; text-align: left; }
733
- .responsive-table tbody th::before, .responsive-table tbody td::before { content: attr(data-label); color: var(--muted); font-size: .65rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
734
- .responsive-table .empty-row td { display: block; }
735
- .route-cell { min-width: 0; }
736
- .history ol { min-width: 0; }
737
- .accounting { grid-template-columns: repeat(2, 1fr); }
738
- .accounting > div, .accounting > div:nth-child(3n) { border-right: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline); }
739
- .accounting > div:nth-child(2n) { border-right: 0; }
740
- }
741
- @media (prefers-color-scheme: dark) {
742
- :root { color-scheme: dark; --paper: #201e1a; --paper-deep: #2b2823; --sheet: #25221d; --ink: #eee7da; --muted: #b5ac9e; --hairline: #514a40; --hairline-strong: #776f62; --seal: #e06b55; --seal-soft: #452822; --good: #8bc5a7; --good-soft: #20362b; --info: #96bdcf; --info-soft: #24343b; --warning: #e2b26b; --warning-soft: #3d3020; --danger: #ef958c; --danger-soft: #412725; --quiet: #b5aca0; --quiet-soft: #34302a; }
743
- }
744
- @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
745
- @media (forced-colors: active) {
746
- .exchange-board, .table-wrap, .ledger-strip, .accounting, .status, .brand-mark, .seal-mark, .attention-item { border-color: CanvasText; }
747
- .status__dot, .brand-mark span, .seal-mark span { background: CanvasText; }
748
- a:focus-visible, summary:focus-visible, .table-wrap:focus-visible { outline-color: Highlight; }
749
- }
750
- @media print { body { background: white; } }
751
- `;
752
- export function renderGatewayDashboard(snapshot, options = {}) {
753
- const locale = normalizeDashboardLocale(options.locale);
754
- const context = {
755
- locale,
756
- copy: getDashboardCopy(locale),
757
- model: buildDashboardViewModel(snapshot),
758
- };
759
- const languageFile = locale === "en" ? DASHBOARD_FILE_NAME : DASHBOARD_ZH_CN_FILE_NAME;
760
- const generatedAt = context.model.generatedAt === undefined
761
- ? t(context, "time.unavailable")
762
- : `<time datetime="${context.model.generatedAt}">${escapeDashboardHtml(context.model.generatedAt.slice(0, 19).replace("T", " "))} UTC</time>`;
763
- const html = `<!doctype html>
764
- <html lang="${locale}">
765
- <head>
766
- <meta charset="utf-8">
767
- <meta name="viewport" content="width=device-width, initial-scale=1">
768
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'none'; font-src 'none'; media-src 'none'; connect-src 'none'; object-src 'none'; frame-src 'none'; worker-src 'none'; manifest-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'">
769
- <meta name="referrer" content="no-referrer">
770
- <meta name="color-scheme" content="light dark">
771
- <title>${t(context, "meta.title")}</title>
772
- <style>${DASHBOARD_STYLES}</style>
773
- </head>
774
- <body data-dashboard-locale="${locale}" data-dashboard-file="${languageFile}">
775
- <a class="skip-link" href="#main">${t(context, "skip")}</a>
776
- <main id="main">
777
- <header class="masthead">
778
- <div class="brand-row"><div class="brand-mark" aria-hidden="true"><span></span></div><div><p class="eyebrow">${t(context, "brand.eyebrow")}</p><h1>${t(context, "brand.title")}</h1><p class="subtitle">${t(context, "brand.subtitle")}</p></div></div>
779
- <div class="masthead__meta">${languageNavigation(context)}${statusPill(overallLabel(context), toneForOverall(context.model.overall))}<p><strong>${t(context, "snapshot.asOf", { time: "" })}</strong>${generatedAt}<br>${t(context, "snapshot.static")}</p></div>
780
- </header>
781
- ${renderExchange(context)}
782
- ${renderAttention(context)}
783
- ${renderTransit(context)}
784
- ${renderProgressWatches(context)}
785
- ${renderActivity(context)}
786
- ${renderSessions(context)}
787
- ${renderDiagnostics(context)}
788
- <footer>${t(context, "footer")}</footer>
789
- </main>
790
- </body>
791
- </html>
792
- `;
793
- if (Buffer.byteLength(html, "utf8") > DASHBOARD_MAX_HTML_BYTES) {
794
- throw new Error("DASHBOARD_SNAPSHOT_TOO_LARGE");
795
- }
796
- return html;
797
- }
798
- export const renderDashboardHtml = renderGatewayDashboard;
799
- async function validatePrivateStateDirectory(stateDirectory) {
800
- const resolved = path.resolve(stateDirectory);
801
- const info = await lstat(resolved);
802
- if (info.isSymbolicLink() || !info.isDirectory()) {
803
- throw new Error("DASHBOARD_STATE_DIRECTORY_UNSAFE");
804
- }
805
- if ((info.mode & 0o777) !== 0o700) {
806
- throw new Error("DASHBOARD_STATE_DIRECTORY_UNSAFE");
807
- }
808
- if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
809
- throw new Error("DASHBOARD_STATE_DIRECTORY_UNSAFE");
810
- }
811
- if ((await realpath(resolved)) !== resolved) {
812
- throw new Error("DASHBOARD_STATE_DIRECTORY_UNSAFE");
813
- }
814
- return resolved;
815
- }
816
- async function validateExistingDashboard(outputPath) {
817
- try {
818
- const info = await lstat(outputPath);
819
- if (info.isSymbolicLink() || !info.isFile()) {
820
- throw new Error("DASHBOARD_TARGET_UNSAFE");
821
- }
822
- if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
823
- throw new Error("DASHBOARD_TARGET_UNSAFE");
824
- }
825
- }
826
- catch (error) {
827
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
828
- return;
829
- }
830
- throw error;
831
- }
832
- }
833
- export async function publishGatewayDashboard(stateDirectory, snapshot, _options = {}) {
834
- const directory = await validatePrivateStateDirectory(stateDirectory);
835
- const artifacts = [
836
- {
837
- outputPath: path.join(directory, DASHBOARD_FILE_NAME),
838
- temporaryPath: path.join(directory, `.${DASHBOARD_FILE_NAME}.${process.pid}.${randomUUID()}.tmp`),
839
- html: renderGatewayDashboard(snapshot, { locale: "en" }),
840
- },
841
- {
842
- outputPath: path.join(directory, DASHBOARD_ZH_CN_FILE_NAME),
843
- temporaryPath: path.join(directory, `.${DASHBOARD_ZH_CN_FILE_NAME}.${process.pid}.${randomUUID()}.tmp`),
844
- html: renderGatewayDashboard(snapshot, { locale: "zh-CN" }),
845
- },
846
- ];
847
- for (const artifact of artifacts) {
848
- await validateExistingDashboard(artifact.outputPath);
849
- }
850
- try {
851
- for (const artifact of artifacts) {
852
- artifact.handle = await open(artifact.temporaryPath, "wx", 0o600);
853
- await artifact.handle.writeFile(artifact.html, "utf8");
854
- await artifact.handle.chmod(0o600);
855
- await artifact.handle.sync();
856
- await artifact.handle.close();
857
- artifact.handle = undefined;
858
- }
859
- for (const artifact of artifacts) {
860
- await rename(artifact.temporaryPath, artifact.outputPath);
861
- }
862
- return artifacts[0].outputPath;
863
- }
864
- finally {
865
- for (const artifact of artifacts) {
866
- if (artifact.handle !== undefined) {
867
- await artifact.handle.close().catch(() => undefined);
868
- }
869
- await unlink(artifact.temporaryPath).catch(() => undefined);
870
- }
871
- }
872
- }
873
- export const writeDashboardSnapshot = publishGatewayDashboard;
874
- //# sourceMappingURL=dashboard.js.map