@silicaclaw/cli 2026.3.20-3 → 2026.3.20-5

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 (31) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/INSTALL.md +2 -2
  3. package/README.md +2 -2
  4. package/VERSION +1 -1
  5. package/apps/local-console/dist/apps/local-console/src/server.d.ts +39 -0
  6. package/apps/local-console/dist/apps/local-console/src/server.js +229 -12
  7. package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +4 -0
  8. package/apps/local-console/dist/packages/network/src/relayPreview.js +37 -6
  9. package/apps/local-console/public/app/app.js +293 -2
  10. package/apps/local-console/public/app/network.js +144 -32
  11. package/apps/local-console/public/app/overview.js +43 -15
  12. package/apps/local-console/public/app/social.js +135 -53
  13. package/apps/local-console/public/app/styles.css +86 -0
  14. package/apps/local-console/public/app/template.js +7 -1
  15. package/apps/local-console/public/app/translations.js +44 -0
  16. package/apps/local-console/src/server.ts +262 -14
  17. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +4 -0
  18. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +37 -6
  19. package/node_modules/@silicaclaw/network/src/relayPreview.ts +41 -6
  20. package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
  21. package/openclaw-skills/silicaclaw-broadcast/manifest.json +1 -1
  22. package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
  23. package/openclaw-skills/silicaclaw-owner-push/manifest.json +1 -1
  24. package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +3 -0
  25. package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +67 -8
  26. package/package.json +1 -1
  27. package/packages/network/dist/packages/network/src/relayPreview.d.ts +4 -0
  28. package/packages/network/dist/packages/network/src/relayPreview.js +37 -6
  29. package/packages/network/src/relayPreview.ts +41 -6
  30. package/scripts/silicaclaw-cli.mjs +4 -1
  31. package/scripts/silicaclaw-gateway.mjs +108 -0
@@ -27,6 +27,7 @@ if (!root) {
27
27
  throw new Error("Missing root element: app-root");
28
28
  }
29
29
  root.innerHTML = appTemplate;
30
+ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
30
31
 
31
32
  const i18n = createI18n(TRANSLATIONS);
32
33
  const DEFAULT_LOCALE = i18n.DEFAULT_LOCALE;
@@ -70,6 +71,10 @@ root.innerHTML = appTemplate;
70
71
  document.getElementById('sidebarToggleBtn').setAttribute('aria-label', t('labels.collapseSidebar'));
71
72
  document.querySelector('.sidebar-version').title = t('common.version');
72
73
  setText('.sidebar-version__label', t('common.version'));
74
+ document.getElementById('brandUpdateHint').textContent = t('labels.versionChecking');
75
+ document.getElementById('brandRelayHint').textContent = t('labels.relayQueuesHealthy');
76
+ document.getElementById('brandCheckUpdateBtn').textContent = t('actions.checkUpdate');
77
+ document.getElementById('brandUpdateBtn').textContent = t('actions.updateNow');
73
78
  document.getElementById('integrationStatusBar').textContent = t('social.barStatus', {
74
79
  connected: '-',
75
80
  mode: '-',
@@ -319,6 +324,216 @@ root.innerHTML = appTemplate;
319
324
  toast,
320
325
  writeUiCache,
321
326
  } = shell;
327
+ let appUpdatePollTimer = null;
328
+ let appUpdateCheckInFlight = false;
329
+ let relayQueueCheckInFlight = false;
330
+ let lastRelayQueueCheckAt = 0;
331
+
332
+ function setAppUpdateUi({
333
+ hint,
334
+ buttonVisible = false,
335
+ buttonDisabled = false,
336
+ buttonText = t('actions.updateNow'),
337
+ checkVisible = false,
338
+ checkDisabled = false,
339
+ }) {
340
+ const hintEl = document.getElementById('brandUpdateHint');
341
+ const buttonEl = document.getElementById('brandUpdateBtn');
342
+ const checkEl = document.getElementById('brandCheckUpdateBtn');
343
+ if (hintEl) {
344
+ hintEl.textContent = hint;
345
+ hintEl.classList.toggle('hidden', !hint);
346
+ }
347
+ if (checkEl) {
348
+ checkEl.textContent = t('actions.checkUpdate');
349
+ checkEl.classList.toggle('hidden', !checkVisible);
350
+ checkEl.disabled = checkDisabled;
351
+ }
352
+ if (buttonEl) {
353
+ buttonEl.textContent = buttonText;
354
+ buttonEl.classList.toggle('hidden', !buttonVisible);
355
+ buttonEl.disabled = buttonDisabled;
356
+ }
357
+ }
358
+
359
+ function platformUpdateHint(platform) {
360
+ if (platform === 'darwin') return t('labels.versionPlatformMac');
361
+ if (platform === 'linux') return t('labels.versionPlatformLinux');
362
+ return t('labels.versionPlatformOther');
363
+ }
364
+
365
+ function setRelayQueueUi({ hint = '', tone = 'ok', visible = false }) {
366
+ const hintEl = document.getElementById('brandRelayHint');
367
+ if (!hintEl) return;
368
+ hintEl.textContent = hint;
369
+ hintEl.classList.toggle('hidden', !visible || !hint);
370
+ hintEl.classList.remove('warn', 'danger');
371
+ if (tone === 'warn' || tone === 'danger') {
372
+ hintEl.classList.add(tone);
373
+ }
374
+ }
375
+
376
+ async function refreshRelayQueueStatus({ force = false } = {}) {
377
+ const now = Date.now();
378
+ if (relayQueueCheckInFlight) return null;
379
+ if (!force && now - lastRelayQueueCheckAt < 15_000) return null;
380
+ relayQueueCheckInFlight = true;
381
+ try {
382
+ const result = await api('/api/peers');
383
+ const peers = result.data || {};
384
+ const peerItems = Array.isArray(peers.items) ? peers.items : [];
385
+ const relayQueueMax = peerItems.reduce((max, peer) => Math.max(max, Number(peer?.meta?.relay_queue_size || 0)), 0);
386
+ const signalQueueMax = peerItems.reduce((max, peer) => Math.max(max, Number(peer?.meta?.signal_queue_size || 0)), 0);
387
+ const queueMax = Math.max(relayQueueMax, signalQueueMax);
388
+ if (queueMax >= 100) {
389
+ setRelayQueueUi({ hint: t('labels.relayQueuesHigh'), tone: 'danger', visible: true });
390
+ } else if (queueMax >= 20) {
391
+ setRelayQueueUi({ hint: t('labels.relayQueuesWatch'), tone: 'warn', visible: true });
392
+ } else {
393
+ setRelayQueueUi({ hint: t('labels.relayQueuesHealthy'), tone: 'ok', visible: true });
394
+ }
395
+ lastRelayQueueCheckAt = now;
396
+ return { relayQueueMax, signalQueueMax };
397
+ } catch (_error) {
398
+ return null;
399
+ } finally {
400
+ relayQueueCheckInFlight = false;
401
+ }
402
+ }
403
+
404
+ async function refreshAppUpdateStatus({ silent = false } = {}) {
405
+ if (appUpdateCheckInFlight) return null;
406
+ appUpdateCheckInFlight = true;
407
+ try {
408
+ const result = await api('/api/app/update-status');
409
+ const status = result.data || {};
410
+ const currentVersion = String(status.current_version || '').trim();
411
+ const latestVersion = String(status.latest_version || '').trim();
412
+ const platformHint = platformUpdateHint(String(status.platform || ''));
413
+ if (currentVersion) {
414
+ document.getElementById('brandVersion').textContent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`;
415
+ }
416
+ if (status.update_available && status.latest_version) {
417
+ setAppUpdateUi({
418
+ hint: `${t('labels.versionUpdateReady', { version: `v${status.latest_version}` })} · ${platformHint}`,
419
+ buttonVisible: true,
420
+ buttonDisabled: false,
421
+ buttonText: t('actions.updateNowVersion', { version: latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}` }),
422
+ checkVisible: true,
423
+ checkDisabled: false,
424
+ });
425
+ } else if (status.check_error) {
426
+ setAppUpdateUi({
427
+ hint: t('labels.versionCheckFailed'),
428
+ buttonVisible: false,
429
+ buttonDisabled: false,
430
+ buttonText: t('actions.updateNow'),
431
+ checkVisible: true,
432
+ checkDisabled: false,
433
+ });
434
+ if (!silent) {
435
+ setFeedback('networkFeedback', t('feedback.appUpdateCheckFailed'), 'warn');
436
+ }
437
+ } else {
438
+ setAppUpdateUi({
439
+ hint: `${t('labels.versionCurrent')} · ${platformHint}`,
440
+ buttonVisible: false,
441
+ buttonDisabled: false,
442
+ buttonText: t('actions.updateNow'),
443
+ checkVisible: true,
444
+ checkDisabled: false,
445
+ });
446
+ }
447
+ return status;
448
+ } catch (_error) {
449
+ setAppUpdateUi({
450
+ hint: t('labels.versionCheckFailed'),
451
+ buttonVisible: false,
452
+ buttonDisabled: false,
453
+ buttonText: t('actions.updateNow'),
454
+ checkVisible: true,
455
+ checkDisabled: false,
456
+ });
457
+ if (!silent) {
458
+ setFeedback('networkFeedback', t('feedback.appUpdateCheckFailed'), 'warn');
459
+ }
460
+ return null;
461
+ } finally {
462
+ appUpdateCheckInFlight = false;
463
+ }
464
+ }
465
+
466
+ function startAppUpdatePolling(targetVersion) {
467
+ if (appUpdatePollTimer) {
468
+ window.clearInterval(appUpdatePollTimer);
469
+ }
470
+ let attempts = 0;
471
+ appUpdatePollTimer = window.setInterval(async () => {
472
+ attempts += 1;
473
+ const status = await refreshAppUpdateStatus({ silent: true });
474
+ if (status && !status.update_available && String(status.current_version || '') === String(targetVersion || '')) {
475
+ window.clearInterval(appUpdatePollTimer);
476
+ appUpdatePollTimer = null;
477
+ if (targetVersion) {
478
+ window.sessionStorage.setItem(APP_UPDATE_SESSION_KEY, String(targetVersion));
479
+ }
480
+ window.location.reload();
481
+ return;
482
+ }
483
+ if (attempts >= 24) {
484
+ window.clearInterval(appUpdatePollTimer);
485
+ appUpdatePollTimer = null;
486
+ setAppUpdateUi({
487
+ hint: t('labels.versionCurrent'),
488
+ buttonVisible: false,
489
+ buttonDisabled: false,
490
+ buttonText: t('actions.updateNow'),
491
+ checkVisible: true,
492
+ checkDisabled: false,
493
+ });
494
+ }
495
+ }, 5000);
496
+ }
497
+
498
+ async function triggerAppUpdate() {
499
+ const buttonEl = document.getElementById('brandUpdateBtn');
500
+ setAppUpdateUi({
501
+ hint: t('labels.versionUpdating'),
502
+ buttonVisible: true,
503
+ buttonDisabled: true,
504
+ buttonText: t('labels.versionUpdating'),
505
+ checkVisible: true,
506
+ checkDisabled: true,
507
+ });
508
+ try {
509
+ const result = await api('/api/app/update', { method: 'POST' });
510
+ const data = result.data || {};
511
+ if (!data.started) {
512
+ setAppUpdateUi({
513
+ hint: t('labels.versionCurrent'),
514
+ buttonVisible: false,
515
+ buttonDisabled: false,
516
+ buttonText: t('actions.updateNow'),
517
+ checkVisible: true,
518
+ checkDisabled: false,
519
+ });
520
+ toast(t('feedback.appUpdateLatest'));
521
+ return;
522
+ }
523
+ toast(t('feedback.appUpdateStarted'));
524
+ startAppUpdatePolling(String(data.target_version || ''));
525
+ } catch (error) {
526
+ setAppUpdateUi({
527
+ hint: t('labels.versionCheckFailed'),
528
+ buttonVisible: true,
529
+ buttonDisabled: false,
530
+ buttonText: t('actions.updateNow'),
531
+ checkVisible: true,
532
+ checkDisabled: false,
533
+ });
534
+ setFeedback('networkFeedback', error instanceof Error ? error.message : t('feedback.appUpdateFailed'), 'error');
535
+ }
536
+ }
322
537
  setLocale(currentLocale);
323
538
  applyStaticTranslations();
324
539
 
@@ -427,6 +642,23 @@ root.innerHTML = appTemplate;
427
642
  document.getElementById('publicDiscoveryHint')?.classList.toggle('hidden', tab !== 'overview');
428
643
  if (tab === 'profile' && !profileController.isDirty() && !profileController.isSaving()) {
429
644
  refreshProfile().catch(() => {});
645
+ } else if (tab === 'overview') {
646
+ refreshOverview().catch(() => {});
647
+ refreshMessages().catch(() => {});
648
+ } else if (tab === 'agent') {
649
+ refreshOverview().catch(() => {});
650
+ } else if (tab === 'chat') {
651
+ refreshMessages().catch(() => {});
652
+ } else if (tab === 'skills') {
653
+ refreshSkills().catch(() => {});
654
+ } else if (tab === 'network') {
655
+ refreshNetwork().catch(() => {});
656
+ refreshPeers().catch(() => {});
657
+ refreshDiscovery().catch(() => {});
658
+ refreshLogs().catch(() => {});
659
+ } else if (tab === 'social') {
660
+ refreshSocial().catch(() => {});
661
+ refreshMessages().catch(() => {});
430
662
  }
431
663
  }
432
664
 
@@ -453,9 +685,46 @@ root.innerHTML = appTemplate;
453
685
  const renderLogs = socialController.renderLogs;
454
686
  const refreshLogs = socialController.refreshLogs;
455
687
  const refreshSkills = socialController.refreshSkills;
688
+ let autoRefreshInFlight = false;
689
+
690
+ async function refreshActiveView() {
691
+ const tasks = [refreshPublicProfilePreview(), refreshRelayQueueStatus()];
692
+ if (activeTab === 'overview') {
693
+ tasks.push(refreshOverview(), refreshMessages(), refreshSocial());
694
+ } else if (activeTab === 'agent') {
695
+ tasks.push(refreshOverview());
696
+ } else if (activeTab === 'chat') {
697
+ tasks.push(refreshMessages());
698
+ } else if (activeTab === 'skills') {
699
+ tasks.push(refreshSkills());
700
+ } else if (activeTab === 'network') {
701
+ tasks.push(refreshNetwork(), refreshPeers(), refreshDiscovery(), refreshLogs());
702
+ } else if (activeTab === 'social') {
703
+ tasks.push(refreshSocial(), refreshMessages());
704
+ } else if (activeTab === 'profile' && !profileController.isDirty() && !profileController.isSaving()) {
705
+ tasks.push(refreshProfile());
706
+ }
707
+ const results = await Promise.allSettled(tasks);
708
+ const firstError = results.find((result) => result.status === 'rejected');
709
+ if (firstError && firstError.status === 'rejected') {
710
+ setFeedback('networkFeedback', firstError.reason instanceof Error ? firstError.reason.message : t('common.unknownError'), 'error');
711
+ }
712
+ }
713
+
714
+ async function refreshAuto() {
715
+ if (document.hidden || autoRefreshInFlight) {
716
+ return;
717
+ }
718
+ autoRefreshInFlight = true;
719
+ try {
720
+ await refreshActiveView();
721
+ } finally {
722
+ autoRefreshInFlight = false;
723
+ }
724
+ }
456
725
 
457
726
  async function refreshAll() {
458
- const tasks = [refreshOverview(), refreshNetwork(), refreshSocial(), refreshSkills(), refreshPublicProfilePreview(), refreshMessages()];
727
+ const tasks = [refreshOverview(), refreshNetwork(), refreshSocial(), refreshSkills(), refreshPublicProfilePreview(), refreshMessages(), refreshRelayQueueStatus({ force: true })];
459
728
  if (activeTab === 'network') {
460
729
  tasks.push(refreshPeers(), refreshDiscovery(), refreshLogs());
461
730
  }
@@ -511,6 +780,28 @@ root.innerHTML = appTemplate;
511
780
 
512
781
  applyTheme(localStorage.getItem('silicaclaw_theme_mode') || 'dark');
513
782
  hydrateCachedShell();
783
+ document.getElementById('brandUpdateBtn').addEventListener('click', () => {
784
+ triggerAppUpdate().catch(() => {});
785
+ });
786
+ document.getElementById('brandCheckUpdateBtn').addEventListener('click', () => {
787
+ refreshAppUpdateStatus().catch(() => {});
788
+ });
514
789
  refreshAll();
790
+ refreshAppUpdateStatus({ silent: true }).catch(() => {});
791
+ const updatedVersion = window.sessionStorage.getItem(APP_UPDATE_SESSION_KEY);
792
+ if (updatedVersion) {
793
+ window.sessionStorage.removeItem(APP_UPDATE_SESSION_KEY);
794
+ toast(t('feedback.appUpdatedTo', { version: updatedVersion.startsWith('v') ? updatedVersion : `v${updatedVersion}` }));
795
+ }
515
796
  exportSocialTemplate().catch(() => {});
516
- setInterval(refreshAll, 4000);
797
+ document.addEventListener('visibilitychange', () => {
798
+ if (!document.hidden) {
799
+ refreshAuto().catch(() => {});
800
+ refreshAppUpdateStatus({ silent: true }).catch(() => {});
801
+ refreshRelayQueueStatus({ force: true }).catch(() => {});
802
+ }
803
+ });
804
+ setInterval(refreshAuto, 4000);
805
+ setInterval(() => {
806
+ refreshAppUpdateStatus({ silent: true }).catch(() => {});
807
+ }, 15 * 60 * 1000);
@@ -13,6 +13,21 @@ export function createNetworkController({
13
13
  room: "",
14
14
  };
15
15
  let quickConnectDefaults = { ...fallbackQuickConnectDefaults };
16
+ let lastNetworkRenderKey = "";
17
+ let lastPeersRenderKey = "";
18
+ let lastDiscoveryRenderKey = "";
19
+
20
+ function queueState(value) {
21
+ const n = Number(value || 0);
22
+ if (n >= 100) return { tone: "danger", label: t("labels.queueHigh") };
23
+ if (n >= 20) return { tone: "warn", label: t("labels.queueWatch") };
24
+ return { tone: "ok", label: t("labels.queueHealthy") };
25
+ }
26
+
27
+ function queueBadge(value) {
28
+ const state = queueState(value);
29
+ return `<span class="pill ${state.tone}">${Number(value || 0)} · ${state.label}</span>`;
30
+ }
16
31
 
17
32
  async function refreshNetwork() {
18
33
  const [cfg, sts, rtp] = await Promise.all([api("/api/network/config"), api("/api/network/stats"), api("/api/runtime/paths")]);
@@ -25,6 +40,10 @@ export function createNetworkController({
25
40
  const transportStats = s.adapter_transport_stats || {};
26
41
  const d = s.adapter_discovery_stats || {};
27
42
  const dx = s.adapter_diagnostics_summary || {};
43
+ const runtimeDiag = s.runtime_diagnostics || {};
44
+ const runtimeMemory = runtimeDiag.memory_mib || {};
45
+ const runtimeDirectory = runtimeDiag.directory || {};
46
+ const runtimeSocial = runtimeDiag.social || {};
28
47
  const ac = s.adapter_config || c.adapter_config || {};
29
48
  quickConnectDefaults = {
30
49
  signalingUrl: String(
@@ -59,7 +78,7 @@ export function createNetworkController({
59
78
  heroRoomText: dx.room || "-",
60
79
  pillAdapterText: `${t("labels.adapter")}: ${c.adapter || "-"}`,
61
80
  });
62
- document.getElementById("networkCards").innerHTML = [
81
+ const networkCardsHtml = [
63
82
  [t("labels.adapter"), c.adapter],
64
83
  [t("labels.namespace"), c.namespace || "-"],
65
84
  [t("labels.port"), c.port ?? "-"],
@@ -75,12 +94,18 @@ export function createNetworkController({
75
94
  [t("network.sent"), msg.broadcast_total ?? 0],
76
95
  [t("network.peers"), p.total ?? 0],
77
96
  [t("network.onlinePeers"), p.online ?? 0],
97
+ ["RSS", runtimeMemory.rss ?? "-"],
98
+ ["Heap", runtimeMemory.heap_used ?? "-"],
99
+ ["Profiles", runtimeDirectory.profile_count ?? "-"],
100
+ ["Index keys", runtimeDirectory.index_key_count ?? "-"],
101
+ ["Messages", runtimeSocial.message_count ?? "-"],
102
+ ["Observations", runtimeSocial.observation_count ?? "-"],
78
103
  [t("network.activeWebrtcPeers"), dx.active_webrtc_peers ?? "-"],
79
104
  [t("network.reconnectAttempts"), dx.reconnect_attempts_total ?? "-"],
80
105
  [t("network.lastInbound"), ago(msg.last_message_at)],
81
106
  [t("network.lastOutbound"), ago(msg.last_broadcast_at)],
82
107
  ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join("");
83
- document.getElementById("networkSummaryList").innerHTML = [
108
+ const networkSummaryHtml = [
84
109
  [t("labels.mode"), describeCurrentMode(t, c.mode || "lan")],
85
110
  [t("network.relayHealth"), relayHealth],
86
111
  [t("network.currentRelay"), dx.signaling_url || "-"],
@@ -93,7 +118,7 @@ export function createNetworkController({
93
118
 
94
119
  const comp = c.components || {};
95
120
  const lim = c.limits || {};
96
- document.getElementById("networkComponents").textContent = [
121
+ const networkComponentsText = [
97
122
  `demo_mode: ${c.demo_mode || "-"}`,
98
123
  `transport: ${comp.transport || "-"}`,
99
124
  `discovery: ${comp.discovery || "-"}`,
@@ -130,12 +155,39 @@ export function createNetworkController({
130
155
  `last_discovery_event_at: ${dx.last_discovery_event_at ? new Date(dx.last_discovery_event_at).toISOString() : "-"}`,
131
156
  ].join("\n");
132
157
 
133
- document.getElementById("networkConfigSnapshot").textContent = toPrettyJson({
158
+ const networkConfigSnapshotText = toPrettyJson({
134
159
  config: c,
135
160
  adapter_config: ac,
136
161
  runtime_paths: runtimePaths,
137
162
  });
138
- document.getElementById("networkStatsSnapshot").textContent = toPrettyJson({ stats: s });
163
+ const networkStatsSnapshotText = toPrettyJson({ stats: s });
164
+ const renderKey = JSON.stringify({
165
+ adapter: c.adapter || "",
166
+ mode: c.mode || "",
167
+ namespace: c.namespace || "",
168
+ port: c.port ?? null,
169
+ signaling_url: dx.signaling_url || "",
170
+ room: dx.room || "",
171
+ relay_health: relayHealth,
172
+ last_poll_at: dx.last_poll_at || 0,
173
+ last_publish_at: dx.last_publish_at || 0,
174
+ last_error: dx.last_error || "",
175
+ msg_received_total: msg.received_total ?? 0,
176
+ msg_broadcast_total: msg.broadcast_total ?? 0,
177
+ peers_total: p.total ?? 0,
178
+ peers_online: p.online ?? 0,
179
+ reconnect_attempts_total: dx.reconnect_attempts_total ?? 0,
180
+ active_webrtc_peers: dx.active_webrtc_peers ?? 0,
181
+ });
182
+ if (renderKey === lastNetworkRenderKey) {
183
+ return;
184
+ }
185
+ document.getElementById("networkCards").innerHTML = networkCardsHtml;
186
+ document.getElementById("networkSummaryList").innerHTML = networkSummaryHtml;
187
+ document.getElementById("networkComponents").textContent = networkComponentsText;
188
+ document.getElementById("networkConfigSnapshot").textContent = networkConfigSnapshotText;
189
+ document.getElementById("networkStatsSnapshot").textContent = networkStatsSnapshotText;
190
+ lastNetworkRenderKey = renderKey;
139
191
  }
140
192
 
141
193
  async function refreshPeers() {
@@ -143,8 +195,12 @@ export function createNetworkController({
143
195
  const peers = peerRes.data || {};
144
196
  const ds = statsRes.data?.adapter_discovery_stats || {};
145
197
  const summary = peers.diagnostics_summary || {};
146
-
147
- document.getElementById("peerCards").innerHTML = [
198
+ const peerItems = Array.isArray(peers.items) ? peers.items : [];
199
+ const relayQueueTotal = peerItems.reduce((sum, peer) => sum + Number(peer?.meta?.relay_queue_size || 0), 0);
200
+ const relayQueueMax = peerItems.reduce((max, peer) => Math.max(max, Number(peer?.meta?.relay_queue_size || 0)), 0);
201
+ const signalQueueTotal = peerItems.reduce((sum, peer) => sum + Number(peer?.meta?.signal_queue_size || 0), 0);
202
+ const signalQueueMax = peerItems.reduce((max, peer) => Math.max(max, Number(peer?.meta?.signal_queue_size || 0)), 0);
203
+ const peerCardsHtml = [
148
204
  [t("network.total"), peers.total || 0],
149
205
  [t("overview.online"), peers.online || 0],
150
206
  [t("network.stale"), peers.stale || 0],
@@ -159,26 +215,28 @@ export function createNetworkController({
159
215
  [t("network.seedPeers"), summary.seed_peers_count ?? 0],
160
216
  [t("network.discoveryEvents"), summary.discovery_events_total ?? 0],
161
217
  [t("network.activeWebrtcPeers"), summary.active_webrtc_peers ?? "-"],
218
+ ["Relay queue", queueBadge(relayQueueTotal)],
219
+ ["Max relay queue", queueBadge(relayQueueMax)],
220
+ ["Signal queue", queueBadge(signalQueueTotal)],
221
+ ["Max signal queue", queueBadge(signalQueueMax)],
162
222
  [t("network.observeCalls"), ds.observe_calls || 0],
163
223
  [t("network.heartbeats"), ds.heartbeat_sent || 0],
164
224
  [t("network.peersAdded"), ds.peers_added || 0],
165
225
  [t("network.peersRemoved"), ds.peers_removed || 0],
166
226
  ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join("");
227
+ const peerStatsText = toPrettyJson({
228
+ discovery_stats: ds,
229
+ diagnostics_summary: summary,
230
+ adapter_stats: statsRes.data?.adapter_stats || {},
231
+ });
167
232
 
168
- if (!peers.items || !peers.items.length) {
169
- document.getElementById("peerTableWrap").innerHTML = `<div class="empty-state">${t("network.noPeersDiscovered")}</div>`;
170
- document.getElementById("peerStatsWrap").textContent = toPrettyJson({
171
- discovery_stats: ds,
172
- diagnostics_summary: summary,
173
- });
174
- return;
175
- }
176
-
177
- document.getElementById("peerTableWrap").innerHTML = `
233
+ const peerTableHtml = !peers.items || !peers.items.length
234
+ ? `<div class="empty-state">${t("network.noPeersDiscovered")}</div>`
235
+ : `
178
236
  <table class="table">
179
- <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>
237
+ <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>Relay Q</th><th>Signal Q</th><th>${t("network.meta")}</th></tr></thead>
180
238
  <tbody>
181
- ${peers.items.map((peer) => `
239
+ ${peerItems.map((peer) => `
182
240
  <tr>
183
241
  <td class="mono">${shortId(peer.peer_id)}</td>
184
242
  <td class="${peer.status === "online" ? "online" : peer.status === "offline" ? "offline" : "stale"}">${peerStatusText(peer.status)}</td>
@@ -186,25 +244,67 @@ export function createNetworkController({
186
244
  <td>${peer.stale_since_at ? ago(peer.stale_since_at) : "-"}</td>
187
245
  <td>${peer.messages_seen || 0}</td>
188
246
  <td>${new Date(peer.first_seen_at).toLocaleTimeString()}</td>
247
+ <td>${queueBadge(Number(peer?.meta?.relay_queue_size || 0))}</td>
248
+ <td>${queueBadge(Number(peer?.meta?.signal_queue_size || 0))}</td>
189
249
  <td class="mono">${peer.meta ? JSON.stringify(peer.meta) : "-"}</td>
190
250
  </tr>
191
251
  `).join("")}
192
252
  </tbody>
193
253
  </table>
194
254
  `;
195
- document.getElementById("peerStatsWrap").textContent = toPrettyJson({
196
- discovery_stats: ds,
197
- diagnostics_summary: summary,
198
- adapter_stats: statsRes.data?.adapter_stats || {},
255
+ const renderKey = JSON.stringify({
256
+ total: peers.total || 0,
257
+ online: peers.online || 0,
258
+ stale: peers.stale || 0,
259
+ namespace: peers.namespace || "",
260
+ summary: {
261
+ signaling_url: summary.signaling_url || "",
262
+ room: summary.room || "",
263
+ last_join_at: summary.last_join_at || 0,
264
+ last_poll_at: summary.last_poll_at || 0,
265
+ last_publish_at: summary.last_publish_at || 0,
266
+ last_error: summary.last_error || "",
267
+ },
268
+ ds: {
269
+ observe_calls: ds.observe_calls || 0,
270
+ heartbeat_sent: ds.heartbeat_sent || 0,
271
+ peers_added: ds.peers_added || 0,
272
+ peers_removed: ds.peers_removed || 0,
273
+ },
274
+ queues: {
275
+ relay_total: relayQueueTotal,
276
+ relay_max: relayQueueMax,
277
+ signal_total: signalQueueTotal,
278
+ signal_max: signalQueueMax,
279
+ },
280
+ items: peerItems
281
+ ? peerItems.map((peer) => [
282
+ peer.peer_id,
283
+ peer.status || "",
284
+ peer.last_seen_at || 0,
285
+ peer.stale_since_at || 0,
286
+ peer.messages_seen || 0,
287
+ peer.first_seen_at || 0,
288
+ Number(peer?.meta?.relay_queue_size || 0),
289
+ Number(peer?.meta?.signal_queue_size || 0),
290
+ peer.meta ? JSON.stringify(peer.meta) : "",
291
+ ])
292
+ : [],
199
293
  });
294
+ if (renderKey === lastPeersRenderKey) {
295
+ return;
296
+ }
297
+ document.getElementById("peerCards").innerHTML = peerCardsHtml;
298
+ document.getElementById("peerTableWrap").innerHTML = peerTableHtml;
299
+ document.getElementById("peerStatsWrap").textContent = peerStatsText;
300
+ lastPeersRenderKey = renderKey;
200
301
  }
201
302
 
202
303
  async function refreshDiscovery() {
203
304
  const eventsRes = await api("/api/discovery/events");
204
305
  const payload = eventsRes.data || {};
205
306
  const items = Array.isArray(payload.items) ? payload.items : [];
206
-
207
- document.getElementById("discoveryCards").innerHTML = [
307
+ const discoveryCardsHtml = [
208
308
  [t("labels.adapter"), payload.adapter || "-"],
209
309
  [t("labels.namespace"), payload.namespace || "-"],
210
310
  [t("network.eventsTotal"), payload.total ?? 0],
@@ -212,11 +312,9 @@ export function createNetworkController({
212
312
  [t("network.signalingEndpoints"), (payload.signaling_endpoints || []).length || 0],
213
313
  [t("network.seedPeers"), payload.seed_peers_count ?? 0],
214
314
  ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join("");
215
-
216
- if (!items.length) {
217
- document.getElementById("discoveryEventList").innerHTML = `<div class="empty-state">${t("network.noDiscoveryEvents")}</div>`;
218
- } else {
219
- document.getElementById("discoveryEventList").innerHTML = items
315
+ const discoveryEventListHtml = !items.length
316
+ ? `<div class="empty-state">${t("network.noDiscoveryEvents")}</div>`
317
+ : items
220
318
  .slice()
221
319
  .reverse()
222
320
  .map((event) => `
@@ -227,9 +325,23 @@ export function createNetworkController({
227
325
  </div>
228
326
  `)
229
327
  .join("");
328
+ const discoverySnapshotText = toPrettyJson(payload);
329
+ const renderKey = JSON.stringify({
330
+ adapter: payload.adapter || "",
331
+ namespace: payload.namespace || "",
332
+ total: payload.total ?? 0,
333
+ last_event_at: payload.last_event_at || 0,
334
+ signaling_endpoints: payload.signaling_endpoints || [],
335
+ seed_peers_count: payload.seed_peers_count ?? 0,
336
+ items: items.map((event) => [event.type || "", event.peer_id || "", event.endpoint || "", event.detail || "", event.at || 0]),
337
+ });
338
+ if (renderKey === lastDiscoveryRenderKey) {
339
+ return;
230
340
  }
231
-
232
- document.getElementById("discoverySnapshot").textContent = toPrettyJson(payload);
341
+ document.getElementById("discoveryCards").innerHTML = discoveryCardsHtml;
342
+ document.getElementById("discoveryEventList").innerHTML = discoveryEventListHtml;
343
+ document.getElementById("discoverySnapshot").textContent = discoverySnapshotText;
344
+ lastDiscoveryRenderKey = renderKey;
233
345
  }
234
346
 
235
347
  return {