@sleep2agi/agent-network-dashboard 0.4.6-preview.8 → 0.4.6

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 (259) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/diagnostics/route-bundle-stats.json +69 -55
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/routes-manifest.json +6 -0
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/_not-found.html +2 -2
  17. package/.next/server/app/_not-found.rsc +12 -12
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  19. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  20. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  23. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  24. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  25. package/.next/server/app/admin.html +2 -2
  26. package/.next/server/app/admin.rsc +14 -14
  27. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  28. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  29. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  30. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  31. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  32. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  33. package/.next/server/app/api/hub/servers/route/app-paths-manifest.json +3 -0
  34. package/.next/server/app/api/hub/servers/route/build-manifest.json +9 -0
  35. package/.next/server/app/api/hub/servers/route/server-reference-manifest.json +4 -0
  36. package/.next/server/app/api/hub/servers/route.js +7 -0
  37. package/.next/server/app/api/hub/servers/route.js.map +5 -0
  38. package/.next/server/app/api/hub/servers/route.js.nft.json +1 -0
  39. package/.next/server/app/api/hub/servers/route_client-reference-manifest.js +3 -0
  40. package/.next/server/app/index.html +2 -2
  41. package/.next/server/app/index.rsc +14 -14
  42. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  43. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  44. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  45. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  46. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  47. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/login.html +2 -2
  49. package/.next/server/app/login.rsc +14 -14
  50. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  51. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  52. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  53. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  55. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  56. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/logs.html +2 -2
  58. package/.next/server/app/logs.rsc +14 -14
  59. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  60. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  61. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  62. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  64. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  65. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/messages.html +2 -2
  67. package/.next/server/app/messages.rsc +14 -14
  68. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  69. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  70. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  71. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  73. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  74. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/node.html +2 -2
  76. package/.next/server/app/node.rsc +14 -14
  77. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  78. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  79. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  80. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  82. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  83. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/nodes.html +2 -2
  85. package/.next/server/app/nodes.rsc +14 -14
  86. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  87. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  88. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  89. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  90. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  91. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  92. package/.next/server/app/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/server-logs.html +2 -2
  95. package/.next/server/app/server-logs.rsc +14 -14
  96. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  97. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  98. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  99. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  100. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  101. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  102. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  103. package/.next/server/app/settings/networks.html +2 -2
  104. package/.next/server/app/settings/networks.rsc +14 -14
  105. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  106. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  107. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  108. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  109. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  110. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  111. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  112. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  113. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  114. package/.next/server/app/settings/tokens.html +2 -2
  115. package/.next/server/app/settings/tokens.rsc +14 -14
  116. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  117. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  118. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  119. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  120. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  121. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  122. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  123. package/.next/server/app/settings.html +2 -2
  124. package/.next/server/app/settings.rsc +14 -14
  125. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  126. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  127. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  128. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  129. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  130. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  131. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  132. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  133. package/.next/server/app/tasks.html +2 -2
  134. package/.next/server/app/tasks.rsc +14 -14
  135. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  136. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  137. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  138. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  139. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  140. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  141. package/.next/server/app-paths-manifest.json +1 -0
  142. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_servers_route_actions_08-m-cs.js +3 -0
  143. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_servers_route_actions_08-m-cs.js.map +1 -0
  144. package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js +3 -0
  145. package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js.map +1 -0
  146. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js +1 -1
  147. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +1 -1
  148. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js +1 -1
  149. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js.map +1 -1
  150. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js +1 -1
  151. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js.map +1 -1
  152. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +2 -1
  153. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  154. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  155. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  156. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js +1 -1
  157. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js.map +1 -1
  158. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  159. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  160. package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js +1 -1
  161. package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js.map +1 -1
  162. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js +1 -1
  163. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js.map +1 -1
  164. package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js +1 -1
  165. package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js.map +1 -1
  166. package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js +1 -1
  167. package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js.map +1 -1
  168. package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js +1 -1
  169. package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js.map +1 -1
  170. package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js +1 -1
  171. package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js.map +1 -1
  172. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  173. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  174. package/.next/server/middleware-build-manifest.js +3 -3
  175. package/.next/server/pages/404.html +2 -2
  176. package/.next/server/pages/500.html +1 -1
  177. package/.next/server/server-reference-manifest.js +1 -1
  178. package/.next/server/server-reference-manifest.json +1 -1
  179. package/.next/static/chunks/0-mpa_947ipeq.js +1 -0
  180. package/.next/static/chunks/01lmbsd37fybu.js +1 -0
  181. package/.next/static/chunks/03o.h6kvmw4l_.js +1 -0
  182. package/.next/static/chunks/03~~oirxz7~vc.js +1 -0
  183. package/.next/static/chunks/05uk96gc~9mni.js +1 -0
  184. package/.next/static/chunks/088ouqy7tekty.js +2 -0
  185. package/.next/static/chunks/08kf-.bgs10ze.js +1 -0
  186. package/.next/static/chunks/0glmrdj9112tc.js +7 -0
  187. package/.next/static/chunks/0hyl7mdapsz38.js +1 -0
  188. package/.next/static/chunks/0mqwn-l7wtcdo.js +7 -0
  189. package/.next/static/chunks/0rxrez_cjn.eh.js +1 -0
  190. package/.next/static/chunks/0tvn2l1pc.h65.js +1 -0
  191. package/.next/static/chunks/0x8x.29hkju5a.js +1 -0
  192. package/.next/static/chunks/127kw7~wsnyu7.css +2 -0
  193. package/.next/static/chunks/16yfxp9ak2gdm.js +1 -0
  194. package/.next/static/chunks/174re5ar~kxne.js +1 -0
  195. package/.next/trace +2 -2
  196. package/.next/trace-build +1 -1
  197. package/.next/types/routes.d.ts +2 -1
  198. package/.next/types/validator.ts +9 -0
  199. package/app/api/hub/servers/route.ts +39 -0
  200. package/app/components/AppShell.tsx +2 -0
  201. package/app/components/ChatPopover.tsx +38 -2
  202. package/app/components/HelpOverlay.tsx +18 -0
  203. package/app/components/ServersDrawer.tsx +206 -0
  204. package/app/components/TopoGraph.tsx +871 -162
  205. package/app/components/utils.ts +8 -7
  206. package/app/lib/time.ts +36 -0
  207. package/package.json +1 -1
  208. package/scripts/help-overlay-topo-test.mjs +44 -0
  209. package/scripts/servers-drawer-live-test.mjs +96 -0
  210. package/scripts/topo-active-links-chip-test.mjs +73 -0
  211. package/scripts/topo-arrow-tier-test.mjs +61 -0
  212. package/scripts/topo-auto-fit-test.mjs +73 -0
  213. package/scripts/topo-chat-focus-ring-test.mjs +77 -0
  214. package/scripts/topo-click-ripple-test.mjs +66 -0
  215. package/scripts/topo-cwd-tooltip-test.mjs +64 -0
  216. package/scripts/topo-dblclick-reset-test.mjs +74 -0
  217. package/scripts/topo-edge-hover-test.mjs +85 -0
  218. package/scripts/topo-edge-tooltip-test.mjs +70 -0
  219. package/scripts/topo-fit-key-test.mjs +106 -0
  220. package/scripts/topo-freshness-chip-test.mjs +74 -0
  221. package/scripts/topo-group-count-test.mjs +63 -0
  222. package/scripts/topo-group-hover-test.mjs +82 -0
  223. package/scripts/topo-hub-title-test.mjs +88 -0
  224. package/scripts/topo-label-lift-test.mjs +115 -0
  225. package/scripts/topo-label-scale-test.mjs +78 -0
  226. package/scripts/topo-lastseen-tooltip-test.mjs +59 -0
  227. package/scripts/topo-layout-key-test.mjs +91 -0
  228. package/scripts/topo-minimap-test.mjs +68 -0
  229. package/scripts/topo-mobile-chips-test.mjs +103 -0
  230. package/scripts/topo-offline-dim-test.mjs +70 -0
  231. package/scripts/topo-overlap-test.mjs +164 -0
  232. package/scripts/topo-p0round-test.mjs +2 -2
  233. package/scripts/topo-pan-cursor-test.mjs +62 -0
  234. package/scripts/topo-popover-meta-test.mjs +78 -0
  235. package/scripts/topo-pressure-bar-test.mjs +69 -0
  236. package/scripts/topo-pulse-rate-test.mjs +71 -0
  237. package/scripts/topo-recent-signal-empty-test.mjs +63 -0
  238. package/scripts/topo-reduced-motion-test.mjs +77 -0
  239. package/scripts/topo-tz-ghost-test.mjs +69 -0
  240. package/scripts/topo-wheel-zoom-test.mjs +79 -0
  241. package/scripts/topo-zoom-keys-test.mjs +89 -0
  242. package/.next/static/chunks/0-7iowzu4.798.js +0 -1
  243. package/.next/static/chunks/02t5jr4shh.hy.js +0 -1
  244. package/.next/static/chunks/0_qfg5s5uzs7..js +0 -7
  245. package/.next/static/chunks/0_rmxom9~1lmk.css +0 -2
  246. package/.next/static/chunks/0cbezu61m-e~z.js +0 -1
  247. package/.next/static/chunks/0cj5qr_a8dzky.js +0 -7
  248. package/.next/static/chunks/0elalb9n_.~di.js +0 -1
  249. package/.next/static/chunks/0j_q__o8p6g83.js +0 -1
  250. package/.next/static/chunks/0jl6_w50zh4l5.js +0 -1
  251. package/.next/static/chunks/0mn0ruf0g4op5.js +0 -1
  252. package/.next/static/chunks/0r4q73.-1bkyl.js +0 -1
  253. package/.next/static/chunks/0r_c8eccg3z2o.js +0 -1
  254. package/.next/static/chunks/0y9subfnntv1s.js +0 -1
  255. package/.next/static/chunks/10oi7l-tycgl9.js +0 -1
  256. package/.next/static/chunks/11uk1izuv3~0t.js +0 -1
  257. /package/.next/static/{7xem03-473G3qw-fC160p → 23MxP0RBVA4BaOJ_HMYmM}/_buildManifest.js +0 -0
  258. /package/.next/static/{7xem03-473G3qw-fC160p → 23MxP0RBVA4BaOJ_HMYmM}/_clientMiddlewareManifest.js +0 -0
  259. /package/.next/static/{7xem03-473G3qw-fC160p → 23MxP0RBVA4BaOJ_HMYmM}/_ssgManifest.js +0 -0
@@ -0,0 +1,39 @@
1
+ import { requireDashboardAuth, getV3UserToken } from '@/app/lib/dashboard-auth';
2
+
3
+ const HUB_URL = process.env.COMMHUB_URL || 'http://127.0.0.1:9200';
4
+
5
+ async function hubHeaders(): Promise<Record<string, string>> {
6
+ const userToken = await getV3UserToken();
7
+ const h: Record<string, string> = {};
8
+ if (userToken) h['Authorization'] = `Bearer ${userToken}`;
9
+ return h;
10
+ }
11
+
12
+ /**
13
+ * Proxy: GET /api/servers from CommHub. Aggregates host telemetry across
14
+ * sessions and returns one card per server (hostname, ip, cpu/mem load,
15
+ * agent_count, status). Per issue #119; lands in v0.8.1-preview.2 of the
16
+ * commhub-server (通信牛). The dashboard <ServersDrawer> mounts in
17
+ * AppShell and refreshes every 5s when expanded.
18
+ */
19
+ export async function GET() {
20
+ const authFailure = await requireDashboardAuth();
21
+ if (authFailure) return authFailure;
22
+
23
+ try {
24
+ const res = await fetch(`${HUB_URL}/api/servers`, {
25
+ headers: await hubHeaders(),
26
+ next: { revalidate: 0 },
27
+ });
28
+ if (!res.ok) {
29
+ // 404 means the upstream hub predates v0.8.1-preview.2 — surface a
30
+ // 'feature unavailable' shape so the drawer can render a friendly
31
+ // empty state instead of an angry error.
32
+ if (res.status === 404) return Response.json({ servers: [], unavailable: true });
33
+ return Response.json({ error: `hub ${res.status}` }, { status: res.status });
34
+ }
35
+ return Response.json(await res.json());
36
+ } catch (e: unknown) {
37
+ return Response.json({ error: 'hub unreachable', detail: e instanceof Error ? e.message : String(e) }, { status: 502 });
38
+ }
39
+ }
@@ -5,6 +5,7 @@ import { Sidebar } from './Sidebar';
5
5
  import { HealthBanner } from './HealthBanner';
6
6
  import { CommandPalette } from './CommandPalette';
7
7
  import { HelpOverlay } from './HelpOverlay';
8
+ import { ServersDrawer } from './ServersDrawer';
8
9
 
9
10
  const NO_SHELL_PATHS = ['/login'];
10
11
 
@@ -24,6 +25,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
24
25
  </main>
25
26
  <CommandPalette />
26
27
  <HelpOverlay />
28
+ <ServersDrawer />
27
29
  </div>
28
30
  );
29
31
  }
@@ -1,8 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { AliasAvatar } from './AliasAvatar';
5
5
  import { TaskChatPanel } from './TaskChatPanel';
6
+ import { useSessions } from '../lib/hooks';
7
+ import { relativeAgo } from '../lib/time';
6
8
 
7
9
  interface ChatPopoverProps {
8
10
  /** Node alias to chat with. Changing it switches the conversation. */
@@ -37,6 +39,16 @@ const MOBILE_BP = 640;
37
39
  * history are already solved there; this component only adds the floating
38
40
  * shell + drag/resize behaviour.
39
41
  */
42
+ /** Round 37 / Loop: surface node metadata (cwd + last-seen) inside the
43
+ * ChatPopover header. The SVG <title> tooltip (Rounds 33-34) only shows
44
+ * on hover-over-node, which is lost once the popover is open and
45
+ * potentially dragged away. Lifting cwd and last-seen into the popover
46
+ * header keeps the context where the user actually needs it.
47
+ *
48
+ * Round 38: relativeAgo factored to app/lib/time.ts so this file shares
49
+ * the same TZ-safe parser as TopoGraph (was a duplicated mirror until
50
+ * this round). */
51
+
40
52
  export function ChatPopover({ alias, onClose }: ChatPopoverProps) {
41
53
  // Position + size are resolved on mount (SSR-safe defaults here).
42
54
  const [pos, setPos] = useState({ x: 0, y: 0 });
@@ -48,6 +60,13 @@ export function ChatPopover({ alias, onClose }: ChatPopoverProps) {
48
60
  active: false, startX: 0, startY: 0, baseW: 0, baseH: 0,
49
61
  });
50
62
 
63
+ // Round 37 / Loop: pull the target session out of the SWR cache that
64
+ // TopoGraph already populates — same URL, so no extra network fetch.
65
+ const { sessions } = useSessions();
66
+ const session = useMemo(() => sessions.find(s => s.alias === alias), [sessions, alias]);
67
+ const isOnline = !!session && session.status !== 'offline';
68
+ const lastSeenLine = !isOnline ? relativeAgo(session?.last_seen_at) : null;
69
+
51
70
  const clamp = useCallback((x: number, y: number, w: number, h: number) => {
52
71
  const maxX = Math.max(MARGIN, window.innerWidth - w - MARGIN);
53
72
  const maxY = Math.max(MARGIN, window.innerHeight - h - MARGIN);
@@ -146,7 +165,24 @@ export function ChatPopover({ alias, onClose }: ChatPopoverProps) {
146
165
  <AliasAvatar alias={alias} size={28} />
147
166
  <div className="min-w-0">
148
167
  <div className="text-sm font-semibold text-[var(--fg)] truncate">{alias}</div>
149
- <div className="text-[10px] text-[var(--fg-muted)]">Drag to move · Esc to close</div>
168
+ {/* Round 37: cwd / last-seen lines surface the same metadata
169
+ the SVG <title> tooltip carries (rounds 33-34), but inside
170
+ the popover where it stays accessible after dragging the
171
+ window away from the node. Fall back to the drag-hint
172
+ when no metadata is reported. */}
173
+ {session?.project_dir ? (
174
+ <div className="text-[10px] text-[var(--fg-muted)] truncate font-mono" title={session.project_dir} data-popover-cwd>
175
+ cwd: {session.project_dir}
176
+ </div>
177
+ ) : null}
178
+ {lastSeenLine ? (
179
+ <div className="text-[10px] text-[var(--fg-muted)] truncate" data-popover-lastseen>
180
+ last seen: {lastSeenLine}
181
+ </div>
182
+ ) : null}
183
+ {!session?.project_dir && !lastSeenLine ? (
184
+ <div className="text-[10px] text-[var(--fg-muted)]">Drag to move · Esc to close</div>
185
+ ) : null}
150
186
  </div>
151
187
  </div>
152
188
  <button
@@ -44,6 +44,24 @@ const SHORTCUTS: { group: string; items: Shortcut[] }[] = [
44
44
  { keys: ['type'], label: 'Filter — searches commands, agents, tasks' },
45
45
  ],
46
46
  },
47
+ // Round 25 / Loop: TopoGraph keys + mouse gestures from rounds 21-23
48
+ // were only surfaced in button titles. Consolidating here gives the
49
+ // Help overlay (the canonical discovery surface) a single place to
50
+ // learn the canvas controls.
51
+ {
52
+ group: 'Topology canvas',
53
+ items: [
54
+ { keys: ['+'], label: 'Zoom in (also =)' },
55
+ { keys: ['−'], label: 'Zoom out' },
56
+ { keys: ['0'], label: 'Reset zoom + pan' },
57
+ { keys: ['f'], label: 'Fit topology to canvas' },
58
+ { keys: ['l'], label: 'Toggle layout (ring ↔ grid)' },
59
+ { keys: ['⌘', 'wheel'], label: 'Zoom toward cursor (Ctrl on Windows/Linux)' },
60
+ { keys: ['drag'], label: 'Pan the canvas (cursor → grabbing)' },
61
+ { keys: ['dbl-click'], label: 'Reset zoom + pan' },
62
+ { keys: ['click'], label: 'Open chat popover on a node (Esc to close)' },
63
+ ],
64
+ },
47
65
  ];
48
66
 
49
67
  const GO_TARGETS: Record<string, string> = {
@@ -0,0 +1,206 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import useSWR from 'swr';
5
+
6
+ /** Issue #119 — server-side panel listing the hub's hosts + CPU/RAM bars.
7
+ *
8
+ * Steps 1+2 (SDK马 host telemetry + CommHub aggregation) shipped with
9
+ * commhub-server v0.8.1-preview.2. Step 3 (this file, Round 20 of the
10
+ * dashboard /loop) swaps the original mock seed for a SWR fetch of
11
+ * `/api/hub/servers` (which proxies to the hub's `/api/servers`).
12
+ *
13
+ * Collapsed by default to a thin vertical icon strip so it never competes
14
+ * with the topology graph for horizontal real-estate. State persists to
15
+ * localStorage just like the other dashboard drawers.
16
+ *
17
+ * Backward-compat: if the upstream hub predates v0.8.1-preview.2 the
18
+ * proxy returns `{ servers: [], unavailable: true }`. The drawer
19
+ * surfaces a friendly "not yet available" hint instead of an empty list,
20
+ * so old hubs don't read as "something broke".
21
+ */
22
+
23
+ interface ServerCard {
24
+ hostname: string;
25
+ ip?: string | null;
26
+ cpu_load_1min: number | null;
27
+ cpu_cores: number;
28
+ mem_used_gb: number | null;
29
+ mem_total_gb: number | null;
30
+ agent_count: number;
31
+ status: 'online' | 'offline';
32
+ note?: string;
33
+ }
34
+
35
+ interface ServersResponse {
36
+ servers: ServerCard[];
37
+ unavailable?: boolean;
38
+ }
39
+
40
+ const fetcher = async (url: string): Promise<ServersResponse> => {
41
+ const res = await fetch(url);
42
+ if (res.status === 401) {
43
+ if (typeof window !== 'undefined') window.location.assign('/login');
44
+ throw new Error('unauthorized');
45
+ }
46
+ if (!res.ok) throw new Error(`hub ${res.status}`);
47
+ return res.json();
48
+ };
49
+
50
+ // Bar tint by usage — green ≤60%, amber 60–85%, red >85%. Keeps the eye
51
+ // going to whichever box is actually about to tip over.
52
+ function barTint(pct: number) {
53
+ if (pct >= 85) return { fill: '#ef4444', track: 'rgb(239 68 68 / 0.16)' };
54
+ if (pct >= 60) return { fill: '#f59e0b', track: 'rgb(245 158 11 / 0.16)' };
55
+ return { fill: '#10b981', track: 'rgb(16 185 129 / 0.16)' };
56
+ }
57
+
58
+ function Bar({ pct, label }: { pct: number; label: string }) {
59
+ const t = barTint(pct);
60
+ const clamped = Math.max(0, Math.min(100, pct));
61
+ return (
62
+ <div className="space-y-0.5">
63
+ <div className="flex justify-between text-[10px] text-[var(--fg-muted)] font-mono">
64
+ <span>{label}</span>
65
+ <span className="tabular-nums">{Math.round(clamped)}%</span>
66
+ </div>
67
+ <div className="h-1.5 rounded-full overflow-hidden" style={{ background: t.track }}>
68
+ <div
69
+ className="h-full transition-[width] duration-300 ease-out"
70
+ style={{ width: `${clamped}%`, background: t.fill }}
71
+ />
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export function ServersDrawer() {
78
+ const [open, setOpen] = useState(false);
79
+ // Drawer state is per-user-machine — persist like the other dashboard
80
+ // sticky toggles (`anet-topo-layout`, `anet-topo-view`, etc.).
81
+ useEffect(() => {
82
+ try { if (localStorage.getItem('anet-servers-drawer') === '1') setOpen(true); } catch {}
83
+ }, []);
84
+ const toggle = () => setOpen(prev => {
85
+ const next = !prev;
86
+ try { localStorage.setItem('anet-servers-drawer', next ? '1' : '0'); } catch {}
87
+ return next;
88
+ });
89
+
90
+ // Round 20 / #119 step 3 final delivery — real SWR fetch. 5s refresh
91
+ // matches the other live drawers (HealthBanner, Sidebar) so an operator
92
+ // sees host telemetry update in roughly the same beat as session state.
93
+ // Only poll while expanded — collapsed icon strip doesn't need fresh data.
94
+ const { data, error } = useSWR<ServersResponse>(
95
+ open ? '/api/hub/servers' : null,
96
+ fetcher,
97
+ { refreshInterval: 5000, dedupingInterval: 3000 },
98
+ );
99
+ const servers = data?.servers ?? [];
100
+ const unavailable = data?.unavailable === true;
101
+ const onlineCount = servers.filter(s => s.status === 'online').length;
102
+ const loading = open && !data && !error;
103
+
104
+ return (
105
+ <aside
106
+ className="fixed right-0 top-20 z-40 hidden lg:flex flex-col rounded-l-xl border border-r-0 shadow-xl shadow-black/30 anet-fade-in"
107
+ style={{
108
+ background: 'var(--bg-secondary)',
109
+ borderColor: 'var(--border)',
110
+ width: open ? 296 : 40,
111
+ maxHeight: 'calc(100vh - 6rem)',
112
+ transition: 'width 200ms ease-out',
113
+ }}
114
+ aria-label="Servers panel"
115
+ >
116
+ <button
117
+ onClick={toggle}
118
+ aria-expanded={open}
119
+ aria-label={open ? 'Collapse servers panel' : 'Expand servers panel'}
120
+ className="flex items-center gap-2 px-2.5 py-2 border-b text-[11px] font-semibold tracking-wide select-none"
121
+ style={{ color: 'var(--fg)', borderColor: 'var(--border)' }}
122
+ >
123
+ <svg className="w-4 h-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
124
+ <rect x="3" y="4" width="18" height="6" rx="1.5" />
125
+ <rect x="3" y="14" width="18" height="6" rx="1.5" />
126
+ <circle cx="7" cy="7" r="0.7" fill="currentColor" />
127
+ <circle cx="7" cy="17" r="0.7" fill="currentColor" />
128
+ </svg>
129
+ {open && (
130
+ <>
131
+ <span className="flex-1 text-left">Servers</span>
132
+ <span className="text-[10px] text-[var(--fg-muted)] font-mono">{onlineCount}/{servers.length}</span>
133
+ </>
134
+ )}
135
+ </button>
136
+
137
+ {open && (
138
+ <div className="flex-1 min-h-0 overflow-y-auto px-2 py-2 space-y-2" data-servers-body>
139
+ {loading && (
140
+ <div className="text-[10px] text-[var(--fg-muted)] font-mono text-center py-4">
141
+ loading servers…
142
+ </div>
143
+ )}
144
+ {error && !data && (
145
+ <div className="rounded-md border px-2.5 py-2 text-[10px] font-mono"
146
+ style={{ background: 'rgb(239 68 68 / 0.06)', borderColor: 'rgb(239 68 68 / 0.25)', color: '#ef4444' }}>
147
+ hub unreachable · retrying every 5s
148
+ </div>
149
+ )}
150
+ {unavailable && (
151
+ <div className="text-[10px] text-[var(--fg-muted)] font-mono text-center py-3 leading-relaxed">
152
+ host telemetry not available<br/>
153
+ <span className="text-[var(--fg-dim)]">upgrade commhub-server ≥ 0.8.1-preview.2</span>
154
+ </div>
155
+ )}
156
+ {!loading && !error && !unavailable && servers.length === 0 && (
157
+ <div className="text-[10px] text-[var(--fg-muted)] font-mono text-center py-4">
158
+ no servers reporting yet
159
+ </div>
160
+ )}
161
+ {servers.map(s => {
162
+ const offline = s.status === 'offline';
163
+ const cpuPct = s.cpu_load_1min != null && s.cpu_cores > 0 ? (s.cpu_load_1min / s.cpu_cores) * 100 : null;
164
+ const memPct = s.mem_used_gb != null && s.mem_total_gb != null && s.mem_total_gb > 0 ? (s.mem_used_gb / s.mem_total_gb) * 100 : null;
165
+ return (
166
+ <div
167
+ key={s.hostname}
168
+ className="rounded-lg border px-2.5 py-2 space-y-1.5"
169
+ style={{
170
+ background: 'var(--bg)',
171
+ borderColor: 'var(--border)',
172
+ opacity: offline ? 0.55 : 1,
173
+ }}
174
+ title={s.ip ? `${s.hostname} · ${s.ip}` : s.hostname}
175
+ >
176
+ <div className="flex items-baseline justify-between gap-2">
177
+ <div className="min-w-0 flex items-baseline gap-1.5">
178
+ <span className="font-mono text-[12px] font-semibold truncate" style={{ color: 'var(--fg)' }}>{s.hostname}</span>
179
+ {s.note && <span className="text-[9px] text-[var(--fg-dim)]">({s.note})</span>}
180
+ </div>
181
+ <span className="text-[10px] text-[var(--fg-muted)] font-mono tabular-nums shrink-0">
182
+ {s.agent_count}&nbsp;agent{s.agent_count === 1 ? '' : 's'}
183
+ </span>
184
+ </div>
185
+ {!offline && cpuPct != null && (
186
+ <Bar pct={cpuPct} label={`CPU ${s.cpu_load_1min!.toFixed(2)}/${s.cpu_cores}`} />
187
+ )}
188
+ {!offline && memPct != null && (
189
+ <Bar pct={memPct} label={`RAM ${s.mem_used_gb!.toFixed(1)}/${s.mem_total_gb!.toFixed(1)}G`} />
190
+ )}
191
+ {offline && (
192
+ <div className="text-[10px] text-[var(--fg-dim)] font-mono italic">CPU n/a · RAM n/a · offline</div>
193
+ )}
194
+ </div>
195
+ );
196
+ })}
197
+ {servers.length > 0 && (
198
+ <div className="pt-1 text-[9px] text-[var(--fg-dim)] font-mono text-center">
199
+ live · refreshing every 5s
200
+ </div>
201
+ )}
202
+ </div>
203
+ )}
204
+ </aside>
205
+ );
206
+ }