@silicaclaw/cli 2026.3.19-6 → 2026.3.19-7

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.
@@ -0,0 +1,422 @@
1
+ export function bindAppEvents({
2
+ api,
3
+ applyTheme,
4
+ exportSocialTemplate,
5
+ flashButton,
6
+ messageSendReasonText,
7
+ parseCsv,
8
+ profileController,
9
+ pulseOverviewBroadcastStep,
10
+ refreshAll,
11
+ refreshLogs,
12
+ refreshMessages,
13
+ refreshNetwork,
14
+ refreshOverview,
15
+ refreshSkills,
16
+ refreshSocial,
17
+ renderLogs,
18
+ renderSocialMessages,
19
+ renderSocialModeHint,
20
+ setFeedback,
21
+ setOnlyShowOnline,
22
+ setProfileNextStepVisible,
23
+ setSocialMessageFilter,
24
+ setSocialModeDirty,
25
+ setSocialModePending,
26
+ setSocialModePendingState,
27
+ shouldWarnBeforeUnload,
28
+ socialController,
29
+ switchTab,
30
+ t,
31
+ toast,
32
+ getSocialTemplate,
33
+ setAgentsPage,
34
+ getSocialMessagesCache,
35
+ toPrettyJson,
36
+ }) {
37
+ document.querySelectorAll(".tab").forEach((btn) => {
38
+ btn.addEventListener("click", () => switchTab(btn.dataset.tab));
39
+ });
40
+
41
+ document.getElementById("sidebarToggleBtn").addEventListener("click", () => {
42
+ const shell = document.getElementById("appShell");
43
+ const next = !shell.classList.contains("nav-collapsed");
44
+ shell.classList.toggle("nav-collapsed", next);
45
+ const btn = document.getElementById("sidebarToggleBtn");
46
+ const icon = btn.querySelector(".nav-collapse-toggle__icon");
47
+ btn.classList.toggle("active", next);
48
+ btn.title = next ? t("labels.expandSidebar") : t("labels.collapseSidebar");
49
+ btn.setAttribute("aria-label", btn.title);
50
+ if (icon) {
51
+ icon.innerHTML = next
52
+ ? `
53
+ <svg viewBox="0 0 24 24">
54
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
55
+ <path d="M9 3v18"></path>
56
+ <path d="M14 10l3 2-3 2"></path>
57
+ </svg>
58
+ `
59
+ : `
60
+ <svg viewBox="0 0 24 24">
61
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
62
+ <path d="M9 3v18"></path>
63
+ <path d="M16 10l-3 2 3 2"></path>
64
+ </svg>
65
+ `;
66
+ }
67
+ });
68
+
69
+ document.getElementById("focusModeBtn").addEventListener("click", () => {
70
+ const shell = document.getElementById("appShell");
71
+ const next = !shell.classList.contains("focus-mode");
72
+ shell.classList.toggle("focus-mode", next);
73
+ const btn = document.getElementById("focusModeBtn");
74
+ btn.classList.toggle("active", next);
75
+ btn.title = next ? t("labels.exitFocusMode") : t("labels.toggleFocusMode");
76
+ btn.setAttribute("aria-label", btn.title);
77
+ });
78
+
79
+ document.querySelectorAll("[data-theme-choice]").forEach((btn) => {
80
+ btn.addEventListener("click", () => {
81
+ applyTheme(btn.dataset.themeChoice || "dark");
82
+ });
83
+ });
84
+
85
+ document.getElementById("overviewStepProfileBtn").addEventListener("click", () => switchTab("profile"));
86
+ document.getElementById("overviewStepPublicBtn").addEventListener("click", () => switchTab("profile"));
87
+ document.getElementById("overviewStepBroadcastBtn").addEventListener("click", () => {
88
+ document.getElementById("broadcastNowBtn").click();
89
+ });
90
+ document.getElementById("homeOpenAgentBtn").addEventListener("click", () => switchTab("agent"));
91
+ document.getElementById("homeOpenSocialBtn").addEventListener("click", () => switchTab("social"));
92
+ document.getElementById("homeOpenNetworkBtn").addEventListener("click", () => switchTab("network"));
93
+ document.getElementById("homeBroadcastNowBtn").addEventListener("click", () => {
94
+ document.getElementById("broadcastNowBtn").click();
95
+ });
96
+ document.getElementById("profileNextStepBtn").addEventListener("click", () => {
97
+ setProfileNextStepVisible(false);
98
+ switchTab("overview");
99
+ });
100
+ document.getElementById("profileNextStepDismissBtn").addEventListener("click", () => {
101
+ setProfileNextStepVisible(false);
102
+ });
103
+
104
+ document.getElementById("socialMessageRefreshBtn").addEventListener("click", async () => {
105
+ try {
106
+ await refreshMessages();
107
+ setFeedback("socialMessageFeedback", t("feedback.messageInboxRefreshed"));
108
+ toast(t("feedback.messageInboxRefreshed"));
109
+ } catch (e) {
110
+ setFeedback("socialMessageFeedback", e instanceof Error ? e.message : t("feedback.messageRefreshFailed"), "error");
111
+ }
112
+ });
113
+
114
+ document.getElementById("socialMessageFilterSelect").addEventListener("change", (event) => {
115
+ setSocialMessageFilter(String(event.target?.value || "all"));
116
+ renderSocialMessages();
117
+ });
118
+
119
+ document.getElementById("socialMessageSendBtn").addEventListener("click", async () => {
120
+ const input = document.getElementById("socialMessageInput");
121
+ const topic = String(document.getElementById("socialMessageTopicSelect").value || "global");
122
+ const body = String(input.value || "").trim();
123
+ if (!body) {
124
+ setFeedback("socialMessageFeedback", t("feedback.messageEmpty"), "warn");
125
+ return;
126
+ }
127
+
128
+ const sendBtn = document.getElementById("socialMessageSendBtn");
129
+ sendBtn.disabled = true;
130
+ setFeedback("socialMessageFeedback", t("feedback.messageSending"));
131
+ try {
132
+ const result = await api("/api/messages/broadcast", {
133
+ method: "POST",
134
+ body: JSON.stringify({ body, topic }),
135
+ });
136
+ if (!result.data?.sent) {
137
+ setFeedback("socialMessageFeedback", messageSendReasonText(t, String(result.data?.reason || "failed")), "warn");
138
+ return;
139
+ }
140
+ const messageId = String(result.data?.message?.message_id || "");
141
+ input.value = "";
142
+ await refreshMessages();
143
+ const visiblePeers = await api("/api/search?q=");
144
+ const remoteVisibleCount = Array.isArray(visiblePeers.data)
145
+ ? visiblePeers.data.filter((item) => !item.is_self && item.online).length
146
+ : 0;
147
+ const published = getSocialMessagesCache().find((item) => item.message_id === messageId);
148
+ const confirmed = Boolean(published);
149
+ const remoteObservedCount = Number(published?.remote_observation_count || 0);
150
+ const remoteHint = t("feedback.messageRemoteVisibility", { count: String(remoteVisibleCount) });
151
+ setFeedback(
152
+ "socialMessageFeedback",
153
+ `${confirmed ? t("feedback.messageInboxConfirmed") : t("feedback.messageInboxPending")} ${
154
+ remoteObservedCount > 0
155
+ ? t("feedback.messageRemoteObserved", { count: String(remoteObservedCount) })
156
+ : remoteHint
157
+ }`,
158
+ confirmed ? "info" : "warn",
159
+ );
160
+ toast(confirmed ? t("feedback.messageInboxConfirmed") : t("feedback.messagePublishedLocal"));
161
+ await refreshNetwork();
162
+ } catch (e) {
163
+ setFeedback("socialMessageFeedback", e instanceof Error ? e.message : t("feedback.messageBroadcastFailed"), "error");
164
+ } finally {
165
+ sendBtn.disabled = false;
166
+ }
167
+ });
168
+
169
+ profileController.bindProfileForm({ refreshAll });
170
+
171
+ window.addEventListener("beforeunload", (event) => {
172
+ if (!shouldWarnBeforeUnload()) return;
173
+ event.preventDefault();
174
+ event.returnValue = "";
175
+ });
176
+
177
+ async function runAction(path, text, level = "info") {
178
+ setFeedback("networkFeedback", text);
179
+ try {
180
+ const response = await api(path, { method: "POST" });
181
+ setFeedback("networkFeedback", response.meta?.message || t("common.done"), level);
182
+ toast(response.meta?.message || t("common.done"));
183
+ await refreshAll();
184
+ if (path === "/api/broadcast/now") {
185
+ pulseOverviewBroadcastStep();
186
+ }
187
+ } catch (e) {
188
+ setFeedback("networkFeedback", e instanceof Error ? e.message : t("feedback.failed"), "error");
189
+ }
190
+ }
191
+
192
+ document.getElementById("startBroadcastBtn").addEventListener("click", () => runAction("/api/broadcast/start", t("actions.startBroadcast")));
193
+ document.getElementById("stopBroadcastBtn").addEventListener("click", () => runAction("/api/broadcast/stop", t("actions.stopBroadcast"), "warn"));
194
+ document.getElementById("broadcastNowBtn").addEventListener("click", () => runAction("/api/broadcast/now", t("actions.broadcastNow")));
195
+
196
+ document.getElementById("quickGlobalPreviewBtn").addEventListener("click", async () => {
197
+ const currentSignaling = window.prompt(t("feedback.promptSignalingUrl"), "http://localhost:4510");
198
+ if (!currentSignaling) return;
199
+ const room = window.prompt(t("feedback.promptRoom"), "silicaclaw-global-preview") || "silicaclaw-global-preview";
200
+ setFeedback("networkFeedback", t("feedback.crossPreviewEnabling"));
201
+ try {
202
+ const result = await api("/api/network/quick-connect-global-preview", {
203
+ method: "POST",
204
+ body: JSON.stringify({
205
+ signaling_url: currentSignaling.trim(),
206
+ room: room.trim(),
207
+ }),
208
+ });
209
+ setFeedback("networkFeedback", result.meta?.message || t("feedback.crossPreviewEnabled"));
210
+ toast(t("feedback.crossPreviewEnabled"));
211
+ await refreshAll();
212
+ } catch (e) {
213
+ setFeedback("networkFeedback", e instanceof Error ? e.message : t("feedback.enableCrossPreviewFailed"), "error");
214
+ }
215
+ });
216
+
217
+ document.getElementById("onlyOnlineToggle").addEventListener("change", async (event) => {
218
+ setOnlyShowOnline(Boolean(event.target?.checked));
219
+ setAgentsPage(1);
220
+ await refreshOverview();
221
+ });
222
+
223
+ document.getElementById("clearDiscoveryCacheBtn").addEventListener("click", async () => {
224
+ try {
225
+ await api("/api/cache/clear", { method: "POST" });
226
+ setFeedback("networkFeedback", t("feedback.discoveryCacheCleared"));
227
+ toast(t("feedback.discoveryCacheCleared"));
228
+ setAgentsPage(1);
229
+ await refreshAll();
230
+ } catch (e) {
231
+ setFeedback("networkFeedback", e instanceof Error ? e.message : t("feedback.failed"), "error");
232
+ }
233
+ });
234
+
235
+ document.getElementById("refreshLogsBtn").addEventListener("click", async () => {
236
+ await refreshLogs();
237
+ toast(t("feedback.logsRefreshed"));
238
+ });
239
+
240
+ document.getElementById("socialExportBtn").addEventListener("click", async () => {
241
+ setFeedback("socialFeedback", t("feedback.exportingTemplate"));
242
+ try {
243
+ await exportSocialTemplate();
244
+ setFeedback("socialFeedback", t("feedback.templateExported"));
245
+ toast(t("feedback.templateExported"));
246
+ } catch (e) {
247
+ setFeedback("socialFeedback", e instanceof Error ? e.message : t("feedback.exportFailed"), "error");
248
+ }
249
+ });
250
+
251
+ document.getElementById("openclawSkillInstallBtn").addEventListener("click", async () => {
252
+ const btn = document.getElementById("openclawSkillInstallBtn");
253
+ btn.disabled = true;
254
+ setFeedback("openclawSkillFeedback", t("feedback.openclawSkillInstalling"));
255
+ try {
256
+ await api("/api/openclaw/bridge/skill-install", { method: "POST" });
257
+ setFeedback("openclawSkillFeedback", t("feedback.openclawSkillInstalled"));
258
+ toast(t("feedback.openclawSkillInstalled"));
259
+ await refreshSocial();
260
+ } catch (e) {
261
+ setFeedback("openclawSkillFeedback", e instanceof Error ? e.message : t("feedback.openclawSkillInstallFailed"), "error");
262
+ } finally {
263
+ await refreshSocial().catch(() => {});
264
+ }
265
+ });
266
+
267
+ document.getElementById("skillsInstallBtn").addEventListener("click", async () => {
268
+ const btn = document.getElementById("skillsInstallBtn");
269
+ btn.disabled = true;
270
+ setFeedback("skillsFeedback", t("feedback.openclawSkillInstalling"));
271
+ try {
272
+ await api("/api/openclaw/bridge/skill-install", { method: "POST" });
273
+ setFeedback("skillsFeedback", t("feedback.openclawSkillInstalled"));
274
+ toast(t("feedback.openclawSkillInstalled"));
275
+ await refreshSkills();
276
+ await refreshSocial();
277
+ } catch (e) {
278
+ setFeedback("skillsFeedback", e instanceof Error ? e.message : t("feedback.openclawSkillInstallFailed"), "error");
279
+ } finally {
280
+ await refreshSkills().catch(() => {});
281
+ }
282
+ });
283
+
284
+ document.getElementById("saveGovernanceBtn").addEventListener("click", async () => {
285
+ setFeedback("socialGovernanceFeedback", t("common.saving"));
286
+ try {
287
+ await api("/api/social/message-governance", {
288
+ method: "PUT",
289
+ body: JSON.stringify({
290
+ send_limit_max: Number(document.getElementById("governanceSendLimitInput").value || 5),
291
+ send_window_ms: Number(document.getElementById("governanceSendWindowInput").value || 60) * 1000,
292
+ receive_limit_max: Number(document.getElementById("governanceReceiveLimitInput").value || 8),
293
+ receive_window_ms: Number(document.getElementById("governanceReceiveWindowInput").value || 60) * 1000,
294
+ duplicate_window_ms: Number(document.getElementById("governanceDuplicateWindowInput").value || 180) * 1000,
295
+ blocked_agent_ids: parseCsv(document.getElementById("governanceBlockedAgentsInput").value || ""),
296
+ blocked_terms: parseCsv(document.getElementById("governanceBlockedTermsInput").value || ""),
297
+ }),
298
+ });
299
+ setFeedback("socialGovernanceFeedback", t("hints.governanceSaved"));
300
+ toast(t("hints.governanceSaved"));
301
+ await refreshSocial();
302
+ await refreshMessages();
303
+ } catch (e) {
304
+ setFeedback("socialGovernanceFeedback", e instanceof Error ? e.message : t("feedback.failed"), "error");
305
+ }
306
+ });
307
+
308
+ document.getElementById("socialModeSelect").addEventListener("change", (event) => {
309
+ const mode = String(event.target?.value || "").trim();
310
+ if (!mode) return;
311
+ setSocialModeDirty(true);
312
+ setSocialModePending(mode);
313
+ renderSocialModeHint(mode, mode, false, true);
314
+ setSocialModePendingState(true);
315
+ });
316
+
317
+ document.getElementById("socialModeApplyBtn").addEventListener("click", async () => {
318
+ const mode = document.getElementById("socialModeSelect").value;
319
+ setFeedback("socialFeedback", t("feedback.runtimeModeApplying", { mode }));
320
+ try {
321
+ const res = await api("/api/social/runtime-mode", {
322
+ method: "POST",
323
+ body: JSON.stringify({ mode }),
324
+ });
325
+ setSocialModeDirty(false);
326
+ setSocialModePending("");
327
+ document.getElementById("socialModeSelect").value = mode;
328
+ setSocialModePendingState(false);
329
+ setFeedback("socialFeedback", `${res.meta?.message || t("feedback.runtimeUpdated")} ${t("hints.socialModeHint", {
330
+ selected: mode,
331
+ effective: mode,
332
+ restart: res.data?.network_requires_restart ? t("hints.restartRequiredHint") : t("hints.restartNotRequiredHint"),
333
+ })}`);
334
+ toast(t("feedback.runtimeMode", { mode }));
335
+ await refreshAll();
336
+ } catch (e) {
337
+ setFeedback("socialFeedback", e instanceof Error ? e.message : t("feedback.failed"), "error");
338
+ }
339
+ });
340
+
341
+ document.getElementById("socialCopyBtn").addEventListener("click", async () => {
342
+ setFeedback("socialFeedback", t("feedback.copyingTemplate"));
343
+ const btn = document.getElementById("socialCopyBtn");
344
+ try {
345
+ if (!getSocialTemplate()) {
346
+ await exportSocialTemplate();
347
+ }
348
+ await navigator.clipboard.writeText(getSocialTemplate());
349
+ setFeedback("socialFeedback", t("feedback.templateCopied"));
350
+ toast(t("common.copied"));
351
+ flashButton(btn, t("common.copied"));
352
+ } catch (e) {
353
+ setFeedback("socialFeedback", e instanceof Error ? e.message : t("feedback.copyFailed"), "error");
354
+ }
355
+ });
356
+
357
+ document.getElementById("socialDownloadBtn").addEventListener("click", async () => {
358
+ setFeedback("socialFeedback", t("feedback.preparingDownload"));
359
+ try {
360
+ const payload = await exportSocialTemplate();
361
+ const filename = String(payload.filename || "social.md");
362
+ const blob = new Blob([getSocialTemplate()], { type: "text/markdown;charset=utf-8" });
363
+ const url = URL.createObjectURL(blob);
364
+ const anchor = document.createElement("a");
365
+ anchor.href = url;
366
+ anchor.download = filename;
367
+ document.body.appendChild(anchor);
368
+ anchor.click();
369
+ anchor.remove();
370
+ URL.revokeObjectURL(url);
371
+ setFeedback("socialFeedback", t("feedback.downloaded", { filename }));
372
+ toast(t("feedback.downloaded", { filename }));
373
+ } catch (e) {
374
+ setFeedback("socialFeedback", e instanceof Error ? e.message : t("feedback.downloadFailed"), "error");
375
+ }
376
+ });
377
+
378
+ document.getElementById("copyPublicProfilePreviewBtn").addEventListener("click", async () => {
379
+ const btn = document.getElementById("copyPublicProfilePreviewBtn");
380
+ try {
381
+ const summary = (await api("/api/public-profile/preview")).data || null;
382
+ await navigator.clipboard.writeText(toPrettyJson(summary));
383
+ toast(t("actions.copyPublicProfilePreview"));
384
+ flashButton(btn, t("common.copied"));
385
+ } catch (e) {
386
+ setFeedback("profileFeedback", e instanceof Error ? e.message : t("feedback.copyPreviewFailed"), "error");
387
+ }
388
+ });
389
+
390
+ document.getElementById("logLevelFilter").addEventListener("change", (event) => {
391
+ socialController.setLogLevelFilter(String(event.target?.value || "all"));
392
+ renderLogs();
393
+ });
394
+
395
+ (() => {
396
+ const logo = document.getElementById("brandLogo");
397
+ const fallback = document.getElementById("brandFallback");
398
+ if (!logo || !fallback) return;
399
+ logo.addEventListener("error", () => {
400
+ logo.style.display = "none";
401
+ fallback.classList.remove("hidden");
402
+ });
403
+ logo.addEventListener("load", () => {
404
+ logo.style.display = "block";
405
+ fallback.classList.add("hidden");
406
+ });
407
+ })();
408
+
409
+ if (window.matchMedia) {
410
+ const media = window.matchMedia("(prefers-color-scheme: light)");
411
+ const handleThemeMedia = () => {
412
+ if ((localStorage.getItem("silicaclaw_theme_mode") || "dark") === "system") {
413
+ applyTheme("system");
414
+ }
415
+ };
416
+ if (typeof media.addEventListener === "function") {
417
+ media.addEventListener("change", handleThemeMedia);
418
+ } else if (typeof media.addListener === "function") {
419
+ media.addListener(handleThemeMedia);
420
+ }
421
+ }
422
+ }
@@ -0,0 +1,43 @@
1
+ export function createI18n(translations) {
2
+ const LOCALE_STORAGE_KEY = "silicaclaw.i18n.locale";
3
+ const DEFAULT_LOCALE = "en";
4
+ const SUPPORTED_LOCALES = ["en", "zh-CN"];
5
+
6
+ function isSupportedLocale(value) {
7
+ return SUPPORTED_LOCALES.includes(value);
8
+ }
9
+
10
+ function resolveNavigatorLocale(language) {
11
+ return String(language || "").toLowerCase().startsWith("zh") ? "zh-CN" : DEFAULT_LOCALE;
12
+ }
13
+
14
+ function resolveInitialLocale() {
15
+ const saved = localStorage.getItem(LOCALE_STORAGE_KEY);
16
+ if (isSupportedLocale(saved)) return saved;
17
+ return resolveNavigatorLocale(globalThis.navigator?.language || "");
18
+ }
19
+
20
+ let currentLocale = resolveInitialLocale();
21
+
22
+ function t(key, params = {}) {
23
+ const parts = key.split(".");
24
+ const resolve = (bundle) => parts.reduce((acc, part) => (acc && typeof acc === "object" ? acc[part] : undefined), bundle);
25
+ let value = resolve(translations[currentLocale]);
26
+ if (typeof value !== "string") value = resolve(translations[DEFAULT_LOCALE]);
27
+ if (typeof value !== "string") return key;
28
+ return value.replace(/\{(\w+)\}/g, (_, name) => params[name] ?? `{${name}}`);
29
+ }
30
+
31
+ function setLocale(locale) {
32
+ currentLocale = isSupportedLocale(locale) ? locale : DEFAULT_LOCALE;
33
+ document.documentElement.lang = currentLocale;
34
+ return currentLocale;
35
+ }
36
+
37
+ return {
38
+ DEFAULT_LOCALE,
39
+ getCurrentLocale: () => currentLocale,
40
+ setLocale,
41
+ t,
42
+ };
43
+ }
@@ -0,0 +1,212 @@
1
+ export function createNetworkController({
2
+ ago,
3
+ api,
4
+ describeCurrentMode,
5
+ peerStatusText,
6
+ shortId,
7
+ t,
8
+ toPrettyJson,
9
+ writeUiCache,
10
+ }) {
11
+ async function refreshNetwork() {
12
+ const [cfg, sts, rtp] = await Promise.all([api("/api/network/config"), api("/api/network/stats"), api("/api/runtime/paths")]);
13
+ const c = cfg.data;
14
+ const s = sts.data;
15
+ const runtimePaths = rtp.data || {};
16
+ const msg = s.message_counters || {};
17
+ const p = s.peer_counters || {};
18
+ const a = s.adapter_stats || {};
19
+ const transportStats = s.adapter_transport_stats || {};
20
+ const d = s.adapter_discovery_stats || {};
21
+ const dx = s.adapter_diagnostics_summary || {};
22
+ const ac = s.adapter_config || c.adapter_config || {};
23
+ document.getElementById("heroAdapter").textContent = c.adapter || "-";
24
+ document.getElementById("heroRelay").textContent = dx.signaling_url || "-";
25
+ document.getElementById("heroRoom").textContent = dx.room || "-";
26
+
27
+ document.getElementById("pillAdapter").textContent = `${t("labels.adapter")}: ${c.adapter || "-"}`;
28
+ writeUiCache("silicaclaw_ui_network", {
29
+ heroAdapterText: c.adapter || "-",
30
+ heroRelayText: dx.signaling_url || "-",
31
+ heroRoomText: dx.room || "-",
32
+ pillAdapterText: `${t("labels.adapter")}: ${c.adapter || "-"}`,
33
+ });
34
+ document.getElementById("networkCards").innerHTML = [
35
+ [t("labels.adapter"), c.adapter],
36
+ [t("labels.namespace"), c.namespace || "-"],
37
+ [t("labels.port"), c.port ?? "-"],
38
+ [t("network.started"), ac.started === true ? t("common.yes") : ac.started === false ? t("common.no") : "-"],
39
+ [t("network.transportState"), ac.transport?.state || "-"],
40
+ [t("network.signalingUrl"), dx.signaling_url || "-"],
41
+ [t("network.signalingEndpoints"), (dx.signaling_endpoints || []).length || 0],
42
+ [t("network.webrtcRoom"), dx.room || "-"],
43
+ [t("network.bootstrapSources"), (dx.bootstrap_sources || []).length || 0],
44
+ [t("network.seedPeers"), dx.seed_peers_count ?? 0],
45
+ [t("network.discoveryEvents"), dx.discovery_events_total ?? 0],
46
+ [t("network.recv"), msg.received_total ?? 0],
47
+ [t("network.sent"), msg.broadcast_total ?? 0],
48
+ [t("network.peers"), p.total ?? 0],
49
+ [t("network.onlinePeers"), p.online ?? 0],
50
+ [t("network.activeWebrtcPeers"), dx.active_webrtc_peers ?? "-"],
51
+ [t("network.reconnectAttempts"), dx.reconnect_attempts_total ?? "-"],
52
+ [t("network.lastInbound"), ago(msg.last_message_at)],
53
+ [t("network.lastOutbound"), ago(msg.last_broadcast_at)],
54
+ ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join("");
55
+ document.getElementById("networkSummaryList").innerHTML = [
56
+ [t("labels.mode"), describeCurrentMode(t, c.mode || "lan")],
57
+ [t("network.relayHealth"), dx.last_error ? t("network.degraded") : t("network.connected")],
58
+ [t("network.currentRelay"), dx.signaling_url || "-"],
59
+ [t("network.currentRoom"), dx.room || "-"],
60
+ [t("network.lastJoin"), ago(dx.last_join_at)],
61
+ [t("network.lastPoll"), ago(dx.last_poll_at)],
62
+ [t("network.lastPublish"), ago(dx.last_publish_at)],
63
+ [t("network.lastError"), dx.last_error || t("network.none")],
64
+ ].map(([k, v]) => `<div class="summary-item"><div class="label">${k}</div><div class="mono">${v}</div></div>`).join("");
65
+
66
+ const comp = c.components || {};
67
+ const lim = c.limits || {};
68
+ document.getElementById("networkComponents").textContent = [
69
+ `demo_mode: ${c.demo_mode || "-"}`,
70
+ `transport: ${comp.transport || "-"}`,
71
+ `discovery: ${comp.discovery || "-"}`,
72
+ `envelope_codec: ${comp.envelope_codec || "-"}`,
73
+ `topic_codec: ${comp.topic_codec || "-"}`,
74
+ `max_message_bytes: ${lim.max_message_bytes ?? "-"}`,
75
+ `dedupe_window_ms: ${lim.dedupe_window_ms ?? "-"}`,
76
+ `dedupe_max_entries: ${lim.dedupe_max_entries ?? "-"}`,
77
+ `max_future_drift_ms: ${lim.max_future_drift_ms ?? "-"}`,
78
+ `max_past_drift_ms: ${lim.max_past_drift_ms ?? "-"}`,
79
+ `dropped_duplicate: ${a.dropped_duplicate ?? "-"}`,
80
+ `dropped_self: ${a.dropped_self ?? "-"}`,
81
+ `dropped_malformed: ${a.dropped_malformed ?? "-"}`,
82
+ `dropped_decode_failed: ${a.dropped_decode_failed ?? "-"}`,
83
+ `dropped_timestamp_future_drift: ${a.dropped_timestamp_future_drift ?? "-"}`,
84
+ `dropped_timestamp_past_drift: ${a.dropped_timestamp_past_drift ?? "-"}`,
85
+ `dropped_namespace_mismatch: ${a.dropped_namespace_mismatch ?? "-"}`,
86
+ `received_validated: ${a.received_validated ?? "-"}`,
87
+ `send_errors: ${a.send_errors ?? "-"}`,
88
+ `transport_send_errors: ${transportStats.send_errors ?? "-"}`,
89
+ `discovery_heartbeat_send_errors: ${d.heartbeat_send_errors ?? "-"}`,
90
+ `signaling_messages_sent_total: ${dx.signaling_messages_sent_total ?? "-"}`,
91
+ `signaling_messages_received_total: ${dx.signaling_messages_received_total ?? "-"}`,
92
+ `last_join_at: ${dx.last_join_at ? new Date(dx.last_join_at).toISOString() : "-"}`,
93
+ `last_poll_at: ${dx.last_poll_at ? new Date(dx.last_poll_at).toISOString() : "-"}`,
94
+ `last_publish_at: ${dx.last_publish_at ? new Date(dx.last_publish_at).toISOString() : "-"}`,
95
+ `last_peer_refresh_at: ${dx.last_peer_refresh_at ? new Date(dx.last_peer_refresh_at).toISOString() : "-"}`,
96
+ `last_error_at: ${dx.last_error_at ? new Date(dx.last_error_at).toISOString() : "-"}`,
97
+ `last_error: ${dx.last_error || "-"}`,
98
+ `signaling_endpoints: ${Array.isArray(dx.signaling_endpoints) ? dx.signaling_endpoints.join(", ") : "-"}`,
99
+ `bootstrap_sources: ${Array.isArray(dx.bootstrap_sources) ? dx.bootstrap_sources.join(", ") : "-"}`,
100
+ `seed_peers_count: ${dx.seed_peers_count ?? "-"}`,
101
+ `discovery_events_total: ${dx.discovery_events_total ?? "-"}`,
102
+ `last_discovery_event_at: ${dx.last_discovery_event_at ? new Date(dx.last_discovery_event_at).toISOString() : "-"}`,
103
+ ].join("\n");
104
+
105
+ document.getElementById("networkConfigSnapshot").textContent = toPrettyJson({
106
+ config: c,
107
+ adapter_config: ac,
108
+ runtime_paths: runtimePaths,
109
+ });
110
+ document.getElementById("networkStatsSnapshot").textContent = toPrettyJson({ stats: s });
111
+ }
112
+
113
+ async function refreshPeers() {
114
+ const [peerRes, statsRes] = await Promise.all([api("/api/peers"), api("/api/network/stats")]);
115
+ const peers = peerRes.data || {};
116
+ const ds = statsRes.data?.adapter_discovery_stats || {};
117
+ const summary = peers.diagnostics_summary || {};
118
+
119
+ document.getElementById("peerCards").innerHTML = [
120
+ [t("network.total"), peers.total || 0],
121
+ [t("overview.online"), peers.online || 0],
122
+ [t("network.stale"), peers.stale || 0],
123
+ [t("labels.namespace"), peers.namespace || "-"],
124
+ [t("network.signalingUrl"), summary.signaling_url || "-"],
125
+ [t("network.signalingEndpoints"), (summary.signaling_endpoints || []).length || 0],
126
+ [t("labels.room"), summary.room || "-"],
127
+ [t("network.lastJoin"), ago(summary.last_join_at)],
128
+ [t("network.lastPoll"), ago(summary.last_poll_at)],
129
+ [t("network.lastPublish"), ago(summary.last_publish_at)],
130
+ [t("network.bootstrapSources"), (summary.bootstrap_sources || []).length || 0],
131
+ [t("network.seedPeers"), summary.seed_peers_count ?? 0],
132
+ [t("network.discoveryEvents"), summary.discovery_events_total ?? 0],
133
+ [t("network.activeWebrtcPeers"), summary.active_webrtc_peers ?? "-"],
134
+ [t("network.observeCalls"), ds.observe_calls || 0],
135
+ [t("network.heartbeats"), ds.heartbeat_sent || 0],
136
+ [t("network.peersAdded"), ds.peers_added || 0],
137
+ [t("network.peersRemoved"), ds.peers_removed || 0],
138
+ ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join("");
139
+
140
+ if (!peers.items || !peers.items.length) {
141
+ document.getElementById("peerTableWrap").innerHTML = `<div class="empty-state">${t("network.noPeersDiscovered")}</div>`;
142
+ document.getElementById("peerStatsWrap").textContent = toPrettyJson({
143
+ discovery_stats: ds,
144
+ diagnostics_summary: summary,
145
+ });
146
+ return;
147
+ }
148
+
149
+ document.getElementById("peerTableWrap").innerHTML = `
150
+ <table class="table">
151
+ <thead><tr><th>${t("network.peer")}</th><th>${t("network.status")}</th><th>${t("network.lastSeen")}</th><th>${t("network.staleSince")}</th><th>${t("network.messages")}</th><th>${t("network.firstSeen")}</th><th>${t("network.meta")}</th></tr></thead>
152
+ <tbody>
153
+ ${peers.items.map((peer) => `
154
+ <tr>
155
+ <td class="mono">${shortId(peer.peer_id)}</td>
156
+ <td class="${peer.status === "online" ? "online" : peer.status === "offline" ? "offline" : "stale"}">${peerStatusText(peer.status)}</td>
157
+ <td>${ago(peer.last_seen_at)}</td>
158
+ <td>${peer.stale_since_at ? ago(peer.stale_since_at) : "-"}</td>
159
+ <td>${peer.messages_seen || 0}</td>
160
+ <td>${new Date(peer.first_seen_at).toLocaleTimeString()}</td>
161
+ <td class="mono">${peer.meta ? JSON.stringify(peer.meta) : "-"}</td>
162
+ </tr>
163
+ `).join("")}
164
+ </tbody>
165
+ </table>
166
+ `;
167
+ document.getElementById("peerStatsWrap").textContent = toPrettyJson({
168
+ discovery_stats: ds,
169
+ diagnostics_summary: summary,
170
+ adapter_stats: statsRes.data?.adapter_stats || {},
171
+ });
172
+ }
173
+
174
+ async function refreshDiscovery() {
175
+ const eventsRes = await api("/api/discovery/events");
176
+ const payload = eventsRes.data || {};
177
+ const items = Array.isArray(payload.items) ? payload.items : [];
178
+
179
+ document.getElementById("discoveryCards").innerHTML = [
180
+ [t("labels.adapter"), payload.adapter || "-"],
181
+ [t("labels.namespace"), payload.namespace || "-"],
182
+ [t("network.eventsTotal"), payload.total ?? 0],
183
+ [t("network.lastEvent"), ago(payload.last_event_at)],
184
+ [t("network.signalingEndpoints"), (payload.signaling_endpoints || []).length || 0],
185
+ [t("network.seedPeers"), payload.seed_peers_count ?? 0],
186
+ ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join("");
187
+
188
+ if (!items.length) {
189
+ document.getElementById("discoveryEventList").innerHTML = `<div class="empty-state">${t("network.noDiscoveryEvents")}</div>`;
190
+ } else {
191
+ document.getElementById("discoveryEventList").innerHTML = items
192
+ .slice()
193
+ .reverse()
194
+ .map((event) => `
195
+ <div class="log-item">
196
+ <div class="mono" style="color:#c5d8ff;">[${event.type}] ${event.peer_id || "-"} ${event.endpoint || ""}</div>
197
+ <div class="label">${event.detail || "-"}</div>
198
+ <div class="mono" style="color:#90a2c3;">${new Date(event.at).toLocaleString()}</div>
199
+ </div>
200
+ `)
201
+ .join("");
202
+ }
203
+
204
+ document.getElementById("discoverySnapshot").textContent = toPrettyJson(payload);
205
+ }
206
+
207
+ return {
208
+ refreshDiscovery,
209
+ refreshNetwork,
210
+ refreshPeers,
211
+ };
212
+ }