@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
@@ -11,14 +11,15 @@ export function previewContent(raw: string | null | undefined): string {
11
11
  .trim() || '--';
12
12
  }
13
13
 
14
+ // Round 44 / Loop: timeAgo now delegates to the shared lib/time helper.
15
+ // The old in-line `replace + 'Z'` parse appended a Z to already-ISO
16
+ // inputs (producing "…ZZ", browser-fragile) — strict-superset semantics
17
+ // preserved for SQL-style inputs, fixed for ISO. Plus a 'just now' fallback
18
+ // when clock skew puts the timestamp in the future. Five callers benefit:
19
+ // /admin · /logs · /settings/networks · InboxPanel · /messages.
20
+ import { relativeAgo } from '../lib/time';
14
21
  export function timeAgo(dateStr: string): string {
15
- if (!dateStr) return '--';
16
- const diff = Date.now() - new Date(dateStr.replace(' ', 'T') + 'Z').getTime();
17
- const s = Math.floor(diff / 1000);
18
- if (s < 60) return `${s}s ago`;
19
- if (s < 3600) return `${Math.floor(s / 60)}m ago`;
20
- if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
21
- return `${Math.floor(s / 86400)}d ago`;
22
+ return relativeAgo(dateStr) ?? '--';
22
23
  }
23
24
 
24
25
  export function statusColor(status: string, hasSse: boolean): string {
@@ -0,0 +1,36 @@
1
+ /** Shared timestamp helpers for hub-sourced data.
2
+ *
3
+ * Background — the CommHub serialises timestamps SQL-style without a
4
+ * zone ("2026-05-15 06:00:28"), but some test data uses full ISO
5
+ * ("…T06:00:28Z"). `Date.parse` on bare SQL is **local-time** in every
6
+ * browser the dashboard supports, so a UTC+8 operator silently sees
7
+ * every timestamp 8 h older than reality (Round 35 dug this out via
8
+ * the `isGhost` filter mis-fire).
9
+ *
10
+ * Round 38 / Loop consolidates parsing — TopoGraph (R35), ChatPopover
11
+ * (R37) and utils.ts::timeAgo all mirrored the same SQL→UTC rewrite;
12
+ * one source of truth here. */
13
+
14
+ /** Parse a hub timestamp to ms-since-epoch, treating SQL-style strings
15
+ * as UTC. ISO strings (with `T`) bypass the rewrite and are parsed as-is.
16
+ * Returns null for empty / unparseable input. */
17
+ export function parseHubTime(dateStr: string | null | undefined): number | null {
18
+ if (!dateStr) return null;
19
+ const iso = dateStr.includes('T') ? dateStr : dateStr.replace(' ', 'T') + 'Z';
20
+ const t = Date.parse(iso);
21
+ return Number.isFinite(t) ? t : null;
22
+ }
23
+
24
+ /** Format `dateStr` as a short relative-time string ("6m ago" / "2h ago").
25
+ * Future timestamps (clock skew) collapse to "just now". Returns null when
26
+ * the input doesn't parse — callers can decide on a fallback. */
27
+ export function relativeAgo(dateStr: string | null | undefined): string | null {
28
+ const t = parseHubTime(dateStr);
29
+ if (t === null) return null;
30
+ const s = Math.floor((Date.now() - t) / 1000);
31
+ if (s < 0) return 'just now';
32
+ if (s < 60) return `${s}s ago`;
33
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
34
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
35
+ return `${Math.floor(s / 86400)}d ago`;
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.4.6-preview.8",
3
+ "version": "0.4.6",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,44 @@
1
+ /* Round 25 verification: pressing `?` opens the help overlay, and the
2
+ * new "Topology canvas" section is present with the documented keys. */
3
+ import { chromium } from 'playwright';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
7
+ const browser = await chromium.launch({ headless: true });
8
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
9
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
10
+ await ctx.addInitScript(() => {
11
+ try { sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
12
+ });
13
+ await ctx.route('**/api/hub/status*', (route) => route.fulfill({ json: { sessions: [] } }));
14
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
15
+ const page = await ctx.newPage();
16
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
17
+ await page.waitForTimeout(400);
18
+
19
+ await page.keyboard.press('?');
20
+ await page.waitForTimeout(200);
21
+
22
+ // The overlay contains group headings; find the Topology canvas group + its rows.
23
+ const data = await page.evaluate(() => {
24
+ // The group heading is a <div>; its parent wraps heading + the <ul> of items.
25
+ const heading = [...document.querySelectorAll('[role="dialog"] div')].find(el => el.textContent === 'Topology canvas');
26
+ if (!heading) return null;
27
+ return {
28
+ headingPresent: true,
29
+ sectionText: (heading.parentElement?.textContent || '').replace(/\s+/g, ' ').trim(),
30
+ };
31
+ });
32
+
33
+ await browser.close();
34
+ const results = {
35
+ overlayOpened: !!data,
36
+ topologySectionPresent: !!data?.headingPresent,
37
+ hasZoomKeys: !!data && /Zoom in/.test(data.sectionText) && /Zoom out/.test(data.sectionText) && /Reset zoom/.test(data.sectionText),
38
+ hasMouseGestures: !!data && /wheel/.test(data.sectionText) && /Pan the canvas/.test(data.sectionText),
39
+ hasClickHint: !!data && /chat popover/.test(data.sectionText) && /dbl-click/.test(data.sectionText),
40
+ };
41
+ const ok = Object.values(results).every(Boolean);
42
+ console.log(`${ok ? '✅' : '❌'} help overlay topology section:`, JSON.stringify(results));
43
+ if (!ok && data) console.log('section text:', data.sectionText.slice(0, 300));
44
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,96 @@
1
+ /* Round 20 / #119 step 3: ServersDrawer now consumes /api/hub/servers via
2
+ * useSWR. Verifies:
3
+ * 1. drawer renders live cards from the proxied endpoint (3 hosts)
4
+ * 2. polling fires (refreshInterval 5s) — second snapshot reflects an
5
+ * updated payload after >5.2s
6
+ * 3. when the hub predates 0.8.1-preview.2 the proxy returns
7
+ * `{ servers: [], unavailable: true }` and the drawer shows the
8
+ * friendly upgrade hint instead of an angry empty state
9
+ * 4. hub-down (502) shows the retry chip */
10
+ import { chromium } from 'playwright';
11
+ import { readFileSync } from 'node:fs';
12
+
13
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
14
+ const browser = await chromium.launch({ headless: true });
15
+
16
+ async function load({ payload, status = 200 }) {
17
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 900 } });
18
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
19
+ await ctx.addInitScript(() => {
20
+ try {
21
+ localStorage.setItem('anet-theme', 'cyber');
22
+ localStorage.setItem('anet-servers-drawer', '1'); // start expanded so SWR fires
23
+ sessionStorage.setItem('anet_v3_auth', '1');
24
+ } catch {}
25
+ });
26
+ let calls = 0;
27
+ await ctx.route('**/api/hub/servers', async (route) => {
28
+ calls++;
29
+ const body = typeof payload === 'function' ? payload(calls) : payload;
30
+ await route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) });
31
+ });
32
+ // keep the rest of the dashboard quiet
33
+ await ctx.route('**/api/hub/status*', (route) => route.fulfill({ json: { sessions: [] } }));
34
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
35
+ const page = await ctx.newPage();
36
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
37
+ await page.waitForSelector('aside[aria-label="Servers panel"]', { timeout: 30000 });
38
+ return { ctx, page, getCalls: () => calls };
39
+ }
40
+
41
+ const results = {};
42
+
43
+ // 1+2: live cards + polling
44
+ {
45
+ const seed = (n) => ({
46
+ servers: [
47
+ { hostname: 'srv-01', ip: '10.0.0.1', cpu_load_1min: 1.2, cpu_cores: 4, mem_used_gb: 3, mem_total_gb: 16, agent_count: n, status: 'online' },
48
+ { hostname: 'srv-02', cpu_load_1min: 0.4, cpu_cores: 8, mem_used_gb: 6, mem_total_gb: 32, agent_count: 2, status: 'online' },
49
+ { hostname: 'srv-03', cpu_load_1min: null, cpu_cores: 0, mem_used_gb: null, mem_total_gb: null, agent_count: 1, status: 'offline' },
50
+ ],
51
+ });
52
+ const { ctx, page, getCalls } = await load({ payload: seed });
53
+ await page.waitForFunction(() => {
54
+ const cards = document.querySelectorAll('aside[aria-label="Servers panel"] [data-servers-body] > div[title]');
55
+ return cards.length >= 3;
56
+ }, { timeout: 10000 });
57
+ const hostnames = await page.$$eval(
58
+ 'aside[aria-label="Servers panel"] [data-servers-body] span.font-semibold',
59
+ els => els.map(e => e.textContent),
60
+ );
61
+ results.rendersLiveCards = hostnames.includes('srv-01') && hostnames.includes('srv-02') && hostnames.includes('srv-03');
62
+
63
+ // wait past one poll cycle (5s + slack) and confirm the proxy was hit again
64
+ await page.waitForTimeout(5400);
65
+ results.polls = getCalls() >= 2;
66
+ await ctx.close();
67
+ }
68
+
69
+ // 3: hub predates 0.8.1-preview.2 → unavailable hint
70
+ {
71
+ const { ctx, page } = await load({ payload: { servers: [], unavailable: true } });
72
+ await page.waitForFunction(() => {
73
+ const el = document.querySelector('aside[aria-label="Servers panel"] [data-servers-body]');
74
+ return el && el.textContent.includes('host telemetry not available');
75
+ }, { timeout: 10000 }).catch(() => {});
76
+ const body = await page.$eval('aside[aria-label="Servers panel"] [data-servers-body]', el => el.textContent);
77
+ results.showsUpgradeHint = /host telemetry not available/.test(body) && /commhub-server/.test(body);
78
+ await ctx.close();
79
+ }
80
+
81
+ // 4: hub-down 502 → retry chip
82
+ {
83
+ const { ctx, page } = await load({ payload: { error: 'hub unreachable' }, status: 502 });
84
+ await page.waitForFunction(() => {
85
+ const el = document.querySelector('aside[aria-label="Servers panel"] [data-servers-body]');
86
+ return el && /hub unreachable/.test(el.textContent || '');
87
+ }, { timeout: 10000 }).catch(() => {});
88
+ const body = await page.$eval('aside[aria-label="Servers panel"] [data-servers-body]', el => el.textContent);
89
+ results.showsRetryChip = /hub unreachable/.test(body) && /every 5s/.test(body);
90
+ await ctx.close();
91
+ }
92
+
93
+ await browser.close();
94
+ const ok = Object.values(results).every(Boolean);
95
+ console.log(`${ok ? '✅' : '❌'} servers drawer live:`, JSON.stringify(results));
96
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,73 @@
1
+ /* Round 42 verification: "X active links" chip in the header includes
2
+ * "· last <relative>" when flow links exist, drops the `· last …` tail
3
+ * when no flow links are present, and renders singular/plural correctly. */
4
+ import { chromium } from 'playwright';
5
+ import { readFileSync } from 'node:fs';
6
+
7
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
8
+ const browser = await chromium.launch({ headless: true });
9
+
10
+ async function probe({ msgs }) {
11
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
12
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
13
+ await ctx.addInitScript(() => {
14
+ try {
15
+ localStorage.setItem('anet-theme', 'cyber');
16
+ localStorage.setItem('anet-topo-layout', 'grid');
17
+ sessionStorage.setItem('anet_v3_auth', '1');
18
+ } catch {}
19
+ });
20
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
21
+ await ctx.route('**/api/hub/status*', async (route) => {
22
+ const r = await route.fetch();
23
+ const b = await r.json();
24
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
25
+ const sessions = ['alpha', 'beta', 'gamma'].map(a => ({
26
+ alias: a, status: 'idle', network_id: nid, project_dir: null,
27
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
28
+ }));
29
+ await route.fulfill({ response: r, json: { ...b, sessions } });
30
+ });
31
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: msgs } }));
32
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
33
+ const page = await ctx.newPage();
34
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
35
+ await page.waitForSelector('svg[viewBox="0 0 1000 680"]', { timeout: 30000 });
36
+ await page.waitForTimeout(500);
37
+ const text = await page.evaluate(() => {
38
+ // The active-links chip is the LAST chip in the topo header row (after
39
+ // pressure-bar + vendor + working/online). Match by `active link`.
40
+ const spans = [...document.querySelectorAll('span')];
41
+ return spans.find(s => /active link/.test(s.textContent || ''))?.textContent?.replace(/\s+/g, ' ').trim() || null;
42
+ });
43
+ await ctx.close();
44
+ return text;
45
+ }
46
+
47
+ // 1. No messages → 0 active links, no "· last X" tail
48
+ const empty = await probe({ msgs: [] });
49
+ // 2. One message just now → 1 active link · last Ns ago
50
+ const recent = await probe({ msgs: [
51
+ { from_alias: 'alpha', to_alias: 'beta', content: 'hi', created_at: new Date(Date.now() - 30 * 1000).toISOString() },
52
+ ] });
53
+ // 3. Five messages, latest 4 min ago → 5 active links · last 4m ago (singular link uses "link", plural "links")
54
+ const fourMinAgo = new Date(Date.now() - 4 * 60 * 1000).toISOString();
55
+ const eightMinAgo = new Date(Date.now() - 8 * 60 * 1000).toISOString();
56
+ const five = await probe({ msgs: [
57
+ { from_alias: 'alpha', to_alias: 'beta', content: '1', created_at: eightMinAgo },
58
+ { from_alias: 'alpha', to_alias: 'gamma', content: '2', created_at: eightMinAgo },
59
+ { from_alias: 'alpha', to_alias: 'gamma', content: '3', created_at: eightMinAgo },
60
+ { from_alias: 'beta', to_alias: 'gamma', content: '4', created_at: eightMinAgo },
61
+ { from_alias: 'beta', to_alias: 'gamma', content: '5', created_at: fourMinAgo }, // latest
62
+ ] });
63
+
64
+ await browser.close();
65
+ const results = {
66
+ emptyChipHasNoTail: empty === '0 active links',
67
+ recentSingularChip: recent !== null && /^1 active link · last \d+s ago$/.test(recent),
68
+ pluralChipShowsLatest: five !== null && /active links · last [34]m ago/.test(five),
69
+ };
70
+ const ok = Object.values(results).every(Boolean);
71
+ console.log(`${ok ? '✅' : '❌'} active-links chip:`, JSON.stringify(results),
72
+ `\n empty=${JSON.stringify(empty)}\n recent=${JSON.stringify(recent)}\n five=${JSON.stringify(five)}`);
73
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,61 @@
1
+ /* Round 16 verification: three flow-link arrow markers exist with the
2
+ * right sizes, all using userSpaceOnUse so they're decoupled from stroke. */
3
+ import { chromium } from 'playwright';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
7
+ const browser = await chromium.launch({ headless: true });
8
+
9
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
10
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
11
+ await ctx.addInitScript(() => {
12
+ try {
13
+ localStorage.setItem('anet-theme', 'cyber');
14
+ localStorage.removeItem('anet-brand');
15
+ localStorage.removeItem('anet-topo-view');
16
+ localStorage.setItem('anet-topo-layout', 'grid');
17
+ sessionStorage.setItem('anet_v3_auth', '1');
18
+ } catch {}
19
+ });
20
+ await ctx.route('**/api/hub/status*', async (route) => {
21
+ const r = await route.fetch();
22
+ const b = await r.json();
23
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
24
+ const sessions = ['a', 'b', 'c'].map(a => ({
25
+ alias: a, status: 'idle', network_id: nid, project_dir: null,
26
+ created_at: '2026-05-15T00:00:00Z', updated_at: '2026-05-15T00:00:00Z',
27
+ last_seen_at: new Date().toISOString(),
28
+ }));
29
+ await route.fulfill({ response: r, json: { ...b, sessions } });
30
+ });
31
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
32
+ const page = await ctx.newPage();
33
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
34
+ await page.waitForFunction(() => {
35
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
36
+ return !!svg && svg.querySelectorAll('g[data-node]').length === 3;
37
+ }, { timeout: 30000 }).catch(() => {});
38
+ await page.waitForTimeout(400);
39
+
40
+ const m = await page.evaluate(() => {
41
+ const reads = (id) => {
42
+ const el = document.querySelector(`marker#${id}`);
43
+ if (!el) return null;
44
+ return {
45
+ width: +el.getAttribute('markerWidth'),
46
+ units: el.getAttribute('markerUnits'),
47
+ };
48
+ };
49
+ return { s: reads('topo-arrow-s'), m: reads('topo-arrow'), l: reads('topo-arrow-l') };
50
+ });
51
+
52
+ await browser.close();
53
+ const results = {
54
+ smallExists: m.s?.width === 12 && m.s?.units === 'userSpaceOnUse',
55
+ mediumExists: m.m?.width === 16 && m.m?.units === 'userSpaceOnUse',
56
+ largeExists: m.l?.width === 22 && m.l?.units === 'userSpaceOnUse',
57
+ legendCompatible: m.m !== null, // legend uses url(#topo-arrow); medium tier must exist by that id
58
+ };
59
+ const ok = Object.values(results).every(Boolean);
60
+ console.log(`${ok ? '✅' : '❌'} arrow tiers:`, JSON.stringify(results), 'sizes:', m);
61
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,73 @@
1
+ /* Round 28 verification: first-paint auto-fit zoom when grid content
2
+ * would overflow viewBox; respects persisted view; doesn't fire on
3
+ * small fleets (no overflow). */
4
+ import { chromium } from 'playwright';
5
+ import { readFileSync } from 'node:fs';
6
+
7
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
8
+ const browser = await chromium.launch({ headless: true });
9
+
10
+ async function probe({ fleetSize, persisted }) {
11
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 1600 } });
12
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
13
+ await ctx.addInitScript(([pers]) => {
14
+ try {
15
+ localStorage.setItem('anet-theme', 'cyber');
16
+ localStorage.setItem('anet-topo-layout', 'grid');
17
+ if (pers) localStorage.setItem('anet-topo-view', JSON.stringify(pers));
18
+ else localStorage.removeItem('anet-topo-view');
19
+ sessionStorage.setItem('anet_v3_auth', '1');
20
+ } catch {}
21
+ }, [persisted]);
22
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
23
+ await ctx.route('**/api/hub/status*', async (route) => {
24
+ const r = await route.fetch();
25
+ const b = await r.json();
26
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
27
+ // Use prefix groups with 5 members each — 7 bands stack vertically
28
+ // and overflow the 680-px viewBox at cellH ≥ 81 (Round 27 floor).
29
+ const aliases = Array.from({ length: fleetSize }, (_, i) => {
30
+ const groups = ['A站', 'B站', 'C站', 'D站', 'E站', 'F站', 'G站', 'H站', 'I站', 'J站'];
31
+ const group = groups[Math.floor(i / 5) % groups.length];
32
+ return `${group}n${i + 1}`;
33
+ });
34
+ const sessions = aliases.map(a => ({
35
+ alias: a, status: 'idle', network_id: nid, project_dir: null,
36
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
37
+ }));
38
+ await route.fulfill({ response: r, json: { ...b, sessions } });
39
+ });
40
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
41
+ const page = await ctx.newPage();
42
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
43
+ await page.waitForFunction((n) => {
44
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
45
+ return !!svg && svg.querySelectorAll('g[data-node]').length === n;
46
+ }, fleetSize, { timeout: 30000 }).catch(() => {});
47
+ await page.waitForTimeout(600);
48
+ const zoom = await page.evaluate(() => {
49
+ const span = document.querySelector('button[aria-label="Zoom in"]').parentElement.querySelector('span[title*="zoom level"]');
50
+ return +span.textContent.replace('%', '');
51
+ });
52
+ await ctx.close();
53
+ return zoom;
54
+ }
55
+
56
+ // 1. Big fleet (35 nodes / 7 bands → overflows ~680-px viewBox at cellH≥81)
57
+ // with no persisted view → auto-fit triggers (zoom < 100%)
58
+ const bigFresh = await probe({ fleetSize: 35 });
59
+ // 2. Big fleet, user has persisted view (zoom=1) → respect it, no auto-fit
60
+ const bigPersisted = await probe({ fleetSize: 35, persisted: { zoom: 1, x: 0, y: 0 } });
61
+ // 3. Small fleet, no persisted view → no overflow, zoom stays 100%
62
+ const smallFresh = await probe({ fleetSize: 6 });
63
+
64
+ await browser.close();
65
+ const results = {
66
+ bigFleetAutoFits: bigFresh < 100 && bigFresh >= 50,
67
+ persistedRespected: bigPersisted === 100,
68
+ smallFleetStays100: smallFresh === 100,
69
+ };
70
+ const ok = Object.values(results).every(Boolean);
71
+ console.log(`${ok ? '✅' : '❌'} auto-fit:`, JSON.stringify(results),
72
+ `bigFresh=${bigFresh}% bigPersisted=${bigPersisted}% smallFresh=${smallFresh}%`);
73
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,77 @@
1
+ /* Round 11 verification: clicking a node opens the ChatPopover AND draws a
2
+ * persistent chat-focus ring on that node (strokeWidth=2.5, status colour).
3
+ * Closing the popover removes the ring. The ring's strokeWidth stays clear
4
+ * of the overlap-test selectors (1.5 / 3). */
5
+ import { chromium } from 'playwright';
6
+ import { readFileSync } from 'node:fs';
7
+
8
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
9
+ const browser = await chromium.launch({ headless: true });
10
+
11
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
12
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
13
+ await ctx.addInitScript(() => {
14
+ try {
15
+ localStorage.setItem('anet-theme', 'cyber');
16
+ localStorage.removeItem('anet-brand');
17
+ localStorage.removeItem('anet-topo-view');
18
+ localStorage.setItem('anet-topo-layout', 'grid');
19
+ localStorage.setItem('anet-topo-nodescale', '1'); // L so radius=26
20
+ sessionStorage.setItem('anet_v3_auth', '1');
21
+ } catch {}
22
+ });
23
+ await ctx.route('**/api/hub/status*', async (route) => {
24
+ const r = await route.fetch();
25
+ const b = await r.json();
26
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
27
+ const sessions = ['A站马', 'B站马', 'C站马'].map(a => ({
28
+ alias: a, status: 'idle', network_id: nid,
29
+ project_dir: null,
30
+ created_at: '2026-05-15T00:00:00Z', updated_at: '2026-05-15T00:00:00Z',
31
+ last_seen_at: new Date().toISOString(),
32
+ }));
33
+ await route.fulfill({ response: r, json: { ...b, sessions } });
34
+ });
35
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
36
+ const page = await ctx.newPage();
37
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
38
+ await page.waitForFunction(() => {
39
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
40
+ return !!svg && svg.querySelectorAll('g[data-node]').length >= 3;
41
+ }, { timeout: 30000 }).catch(() => {});
42
+ await page.waitForTimeout(400);
43
+
44
+ // Pre-click: NO chat-focus ring on any node (strokeWidth=2.5, no group-hover class)
45
+ const ringsBefore = await page.$$eval('svg[viewBox="0 0 1000 680"] g[data-node] circle[stroke-width="2.5"]', els => els.length);
46
+
47
+ // Click A站马 → opens chat popover
48
+ await page.locator('g[data-node="A站马"]').click();
49
+ await page.waitForTimeout(300);
50
+
51
+ const ringsAfter = await page.$$eval('svg[viewBox="0 0 1000 680"] g[data-node] circle[stroke-width="2.5"]', els => els.length);
52
+ const ringOnTarget = await page.$eval(
53
+ 'g[data-node="A站马"] circle[stroke-width="2.5"]',
54
+ el => ({ r: +el.getAttribute('r'), opacity: +el.getAttribute('opacity') })
55
+ ).catch(() => null);
56
+
57
+ // Confirm popover is open
58
+ const popover = await page.locator('[role="dialog"], [data-chat-popover]').count();
59
+
60
+ // Ring should be ONLY on the clicked node, not on the others
61
+ const ringOnOther = await page.$$eval('g[data-node="B站马"] circle[stroke-width="2.5"], g[data-node="C站马"] circle[stroke-width="2.5"]', els => els.length);
62
+
63
+ const results = {
64
+ noRingBefore: ringsBefore === 0,
65
+ oneRingAfter: ringsAfter === 1,
66
+ ringOnTargetNode: !!ringOnTarget && ringOnTarget.r === 40, // radius=26 + 14
67
+ ringNotOnOthers: ringOnOther === 0,
68
+ };
69
+
70
+ // Re-run the overlap selector to confirm the chat-focus ring isn't mis-counted as a status ring
71
+ const statusRings = await page.$$eval('svg[viewBox="0 0 1000 680"] g[data-node] circle[stroke-width="3"], svg[viewBox="0 0 1000 680"] g[data-node] circle[stroke-width="1.5"]', els => els.length);
72
+ results.statusRingCountUnchanged = statusRings === 3; // exactly one per online node
73
+
74
+ await browser.close();
75
+ const ok = Object.values(results).every(Boolean);
76
+ console.log(`${ok ? '✅' : '❌'} chat-focus ring:`, JSON.stringify(results));
77
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,66 @@
1
+ /* Round 14 verification: clicking a node spawns a one-shot ripple circle
2
+ * (strokeWidth=2, status-coloured, ~500ms) that's gone by t=800ms. */
3
+ import { chromium } from 'playwright';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
7
+ const browser = await chromium.launch({ headless: true });
8
+
9
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
10
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
11
+ await ctx.addInitScript(() => {
12
+ try {
13
+ localStorage.setItem('anet-theme', 'cyber');
14
+ localStorage.removeItem('anet-brand');
15
+ localStorage.removeItem('anet-topo-view');
16
+ localStorage.setItem('anet-topo-layout', 'grid');
17
+ localStorage.setItem('anet-topo-nodescale', '1');
18
+ sessionStorage.setItem('anet_v3_auth', '1');
19
+ } catch {}
20
+ });
21
+ await ctx.route('**/api/hub/status*', async (route) => {
22
+ const r = await route.fetch();
23
+ const b = await r.json();
24
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
25
+ const sessions = ['ripA', 'ripB'].map(a => ({
26
+ alias: a, status: 'idle', network_id: nid, project_dir: null,
27
+ created_at: '2026-05-15T00:00:00Z', updated_at: '2026-05-15T00:00:00Z',
28
+ last_seen_at: new Date().toISOString(),
29
+ }));
30
+ await route.fulfill({ response: r, json: { ...b, sessions } });
31
+ });
32
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
33
+ const page = await ctx.newPage();
34
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
35
+ await page.waitForFunction(() => {
36
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
37
+ return !!svg && svg.querySelectorAll('g[data-node]').length >= 2;
38
+ }, { timeout: 30000 }).catch(() => {});
39
+ await page.waitForTimeout(400);
40
+
41
+ // Ripple circle = direct child of the zoom/pan <g>, strokeWidth=2, with a
42
+ // <animate> child whose attributeName="r" (distinguishes it from the
43
+ // hover ring, which has strokeWidth=2 but no <animate> children).
44
+ const ripplePresent = async () => page.evaluate(() => {
45
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
46
+ for (const c of svg.querySelectorAll('circle[stroke-width="2"]')) {
47
+ const anims = c.querySelectorAll('animate');
48
+ if (anims.length && [...anims].some(a => a.getAttribute('attributeName') === 'r')) return true;
49
+ }
50
+ return false;
51
+ });
52
+
53
+ const before = await ripplePresent();
54
+ await page.locator('g[data-node="ripA"]').click();
55
+ // Sample at ~150ms (mid-animation) — ripple should be present.
56
+ await page.waitForTimeout(150);
57
+ const during = await ripplePresent();
58
+ // After 800ms total — well past the 600ms clear timer.
59
+ await page.waitForTimeout(700);
60
+ const after = await ripplePresent();
61
+
62
+ await browser.close();
63
+ const results = { noRippleBefore: before === false, rippleDuring: during === true, rippleClearedAfter: after === false };
64
+ const ok = Object.values(results).every(Boolean);
65
+ console.log(`${ok ? '✅' : '❌'} click ripple:`, JSON.stringify(results));
66
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,64 @@
1
+ /* Round 33 verification: hover tooltip carries `cwd: <project_dir>` on a
2
+ * second line when reported; falls back to the identity-only line
3
+ * otherwise; no `cwd:` line for nodes without project_dir. */
4
+ import { chromium } from 'playwright';
5
+ import { readFileSync } from 'node:fs';
6
+
7
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
8
+ const browser = await chromium.launch({ headless: true });
9
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
10
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
11
+ await ctx.addInitScript(() => {
12
+ try {
13
+ localStorage.setItem('anet-theme', 'cyber');
14
+ localStorage.setItem('anet-topo-layout', 'grid');
15
+ sessionStorage.setItem('anet_v3_auth', '1');
16
+ } catch {}
17
+ });
18
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
19
+ await ctx.route('**/api/hub/status*', async (route) => {
20
+ const r = await route.fetch();
21
+ const b = await r.json();
22
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
23
+ const sessions = [
24
+ {
25
+ alias: 'full', status: 'idle', network_id: nid,
26
+ project_dir: '/home/vansin/agent-orchestra',
27
+ model: 'claude-opus-4-7', runtime: 'cli-claude-code',
28
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
29
+ },
30
+ {
31
+ alias: 'cwdOnly', status: 'idle', network_id: nid,
32
+ project_dir: '/home/vansin/some-project',
33
+ model: null, runtime: null,
34
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
35
+ },
36
+ {
37
+ alias: 'bare', status: 'idle', network_id: nid,
38
+ project_dir: null, model: null, runtime: null,
39
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
40
+ },
41
+ ];
42
+ await route.fulfill({ response: r, json: { ...b, sessions } });
43
+ });
44
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
45
+ const page = await ctx.newPage();
46
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
47
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 3, { timeout: 30000 });
48
+ await page.waitForTimeout(400);
49
+
50
+ const titleOf = alias => page.$eval(`g[data-node="${alias}"] title`, el => el.textContent);
51
+ const full = await titleOf('full');
52
+ const cwdOnly = await titleOf('cwdOnly');
53
+ const bare = await titleOf('bare');
54
+
55
+ await browser.close();
56
+ const results = {
57
+ fullHasIdentityAndCwd: /claude-opus-4-7/.test(full) && /cwd: \/home\/vansin\/agent-orchestra/.test(full) && full.includes('\n'),
58
+ cwdOnlyShowsAliasAndCwd: cwdOnly.startsWith('cwdOnly') && /cwd: \/home\/vansin\/some-project/.test(cwdOnly),
59
+ bareShowsAliasOnly: bare === 'bare',
60
+ };
61
+ const ok = Object.values(results).every(Boolean);
62
+ console.log(`${ok ? '✅' : '❌'} cwd tooltip:`, JSON.stringify(results),
63
+ `\n full=${JSON.stringify(full)}\n cwdOnly=${JSON.stringify(cwdOnly)}\n bare=${JSON.stringify(bare)}`);
64
+ process.exit(ok ? 0 : 1);