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

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 (286) 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/04eilowxyfm3z.js +1 -0
  184. package/.next/static/chunks/05uk96gc~9mni.js +1 -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/0rxrez_cjn.eh.js +1 -0
  188. package/.next/static/chunks/0s5km_6ow6jzv.js +7 -0
  189. package/.next/static/chunks/0tvn2l1pc.h65.js +1 -0
  190. package/.next/static/chunks/0w2-uz~4phw2f.css +2 -0
  191. package/.next/static/chunks/0x8x.29hkju5a.js +1 -0
  192. package/.next/static/chunks/0zyef0ov9aijd.js +1 -0
  193. package/.next/static/chunks/13_ce6isvlsqf.js +2 -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/CommandPalette.tsx +33 -0
  203. package/app/components/HelpOverlay.tsx +40 -0
  204. package/app/components/ServersDrawer.tsx +206 -0
  205. package/app/components/TopoGraph.tsx +1454 -166
  206. package/app/components/utils.ts +8 -7
  207. package/app/lib/time.ts +36 -0
  208. package/package.json +1 -1
  209. package/scripts/help-overlay-topo-test.mjs +44 -0
  210. package/scripts/servers-drawer-live-test.mjs +96 -0
  211. package/scripts/topo-active-filter-pill-test.mjs +101 -0
  212. package/scripts/topo-active-links-chip-test.mjs +73 -0
  213. package/scripts/topo-arrow-tier-test.mjs +61 -0
  214. package/scripts/topo-auto-fit-test.mjs +73 -0
  215. package/scripts/topo-chat-focus-ring-test.mjs +77 -0
  216. package/scripts/topo-click-ripple-test.mjs +66 -0
  217. package/scripts/topo-cmdk-pin-test.mjs +123 -0
  218. package/scripts/topo-cwd-tooltip-test.mjs +64 -0
  219. package/scripts/topo-dblclick-reset-test.mjs +74 -0
  220. package/scripts/topo-edge-endpoint-highlight-test.mjs +111 -0
  221. package/scripts/topo-edge-focus-test.mjs +113 -0
  222. package/scripts/topo-edge-hover-test.mjs +93 -0
  223. package/scripts/topo-edge-tooltip-test.mjs +70 -0
  224. package/scripts/topo-esc-clears-pin-test.mjs +98 -0
  225. package/scripts/topo-filter-pill-body-click-test.mjs +112 -0
  226. package/scripts/topo-filter-pill-count-test.mjs +110 -0
  227. package/scripts/topo-filter-pill-mobile-test.mjs +90 -0
  228. package/scripts/topo-fit-key-test.mjs +106 -0
  229. package/scripts/topo-freshness-chip-test.mjs +74 -0
  230. package/scripts/topo-group-box-pinned-state-test.mjs +95 -0
  231. package/scripts/topo-group-count-test.mjs +68 -0
  232. package/scripts/topo-group-hover-test.mjs +82 -0
  233. package/scripts/topo-group-pin-test.mjs +119 -0
  234. package/scripts/topo-group-status-pips-test.mjs +103 -0
  235. package/scripts/topo-groupbox-test.mjs +4 -2
  236. package/scripts/topo-help-overlay-r59-test.mjs +67 -0
  237. package/scripts/topo-help-overlay-r65-test.mjs +64 -0
  238. package/scripts/topo-help-overlay-r70-test.mjs +63 -0
  239. package/scripts/topo-hub-click-fit-test.mjs +137 -0
  240. package/scripts/topo-hub-title-test.mjs +93 -0
  241. package/scripts/topo-ingroup-edge-test.mjs +103 -0
  242. package/scripts/topo-label-lift-test.mjs +119 -0
  243. package/scripts/topo-label-scale-test.mjs +78 -0
  244. package/scripts/topo-lastseen-tooltip-test.mjs +59 -0
  245. package/scripts/topo-layout-key-test.mjs +91 -0
  246. package/scripts/topo-legend-hover-test.mjs +113 -0
  247. package/scripts/topo-legend-pin-test.mjs +128 -0
  248. package/scripts/topo-minimap-test.mjs +68 -0
  249. package/scripts/topo-mobile-chips-test.mjs +103 -0
  250. package/scripts/topo-node-hover-lift-test.mjs +85 -0
  251. package/scripts/topo-offline-dim-test.mjs +70 -0
  252. package/scripts/topo-overlap-test.mjs +164 -0
  253. package/scripts/topo-p0round-test.mjs +2 -2
  254. package/scripts/topo-pan-cursor-test.mjs +62 -0
  255. package/scripts/topo-panel-elevation-test.mjs +76 -0
  256. package/scripts/topo-pin-persistence-test.mjs +105 -0
  257. package/scripts/topo-popover-meta-test.mjs +78 -0
  258. package/scripts/topo-pressure-bar-test.mjs +69 -0
  259. package/scripts/topo-pressure-pin-test.mjs +112 -0
  260. package/scripts/topo-pulse-rate-test.mjs +71 -0
  261. package/scripts/topo-recent-row-hover-test.mjs +122 -0
  262. package/scripts/topo-recent-signal-empty-test.mjs +63 -0
  263. package/scripts/topo-reduced-motion-test.mjs +77 -0
  264. package/scripts/topo-tier-ring-test.mjs +69 -0
  265. package/scripts/topo-tier-stagger-test.mjs +89 -0
  266. package/scripts/topo-tz-ghost-test.mjs +69 -0
  267. package/scripts/topo-wheel-zoom-test.mjs +79 -0
  268. package/scripts/topo-zoom-keys-test.mjs +89 -0
  269. package/.next/static/chunks/0-7iowzu4.798.js +0 -1
  270. package/.next/static/chunks/02t5jr4shh.hy.js +0 -1
  271. package/.next/static/chunks/0_qfg5s5uzs7..js +0 -7
  272. package/.next/static/chunks/0_rmxom9~1lmk.css +0 -2
  273. package/.next/static/chunks/0cbezu61m-e~z.js +0 -1
  274. package/.next/static/chunks/0cj5qr_a8dzky.js +0 -7
  275. package/.next/static/chunks/0elalb9n_.~di.js +0 -1
  276. package/.next/static/chunks/0j_q__o8p6g83.js +0 -1
  277. package/.next/static/chunks/0jl6_w50zh4l5.js +0 -1
  278. package/.next/static/chunks/0mn0ruf0g4op5.js +0 -1
  279. package/.next/static/chunks/0r4q73.-1bkyl.js +0 -1
  280. package/.next/static/chunks/0r_c8eccg3z2o.js +0 -1
  281. package/.next/static/chunks/0y9subfnntv1s.js +0 -1
  282. package/.next/static/chunks/10oi7l-tycgl9.js +0 -1
  283. package/.next/static/chunks/11uk1izuv3~0t.js +0 -1
  284. /package/.next/static/{7xem03-473G3qw-fC160p → b35iGYnW-KxCMXl7VYDhg}/_buildManifest.js +0 -0
  285. /package/.next/static/{7xem03-473G3qw-fC160p → b35iGYnW-KxCMXl7VYDhg}/_clientMiddlewareManifest.js +0 -0
  286. /package/.next/static/{7xem03-473G3qw-fC160p → b35iGYnW-KxCMXl7VYDhg}/_ssgManifest.js +0 -0
@@ -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,123 @@
1
+ /* Round 69 verification: Cmd+K palette can pin topology filters from
2
+ * anywhere. The palette dispatches a CustomEvent('anet:topo-pin') that
3
+ * TopoGraph listens for and translates into pinnedStatus / pinnedGroup
4
+ * state. sessionStorage is written in lockstep so a reload preserves it
5
+ * (R66 carries it through).
6
+ *
7
+ * - Dispatch {kind:'status', value:'working'} → pill appears + idle/
8
+ * offline nodes dim. sessionStorage holds 'working'.
9
+ * - Dispatch {kind:'clear'} → pill disappears + opacities restore.
10
+ * sessionStorage entries cleared.
11
+ * - Dispatch {kind:'group', value:'alpha'} → group pill + opacities
12
+ * composed; storage holds 'alpha'.
13
+ */
14
+ import { chromium } from 'playwright';
15
+ import { readFileSync } from 'node:fs';
16
+
17
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
18
+ const browser = await chromium.launch({ headless: true });
19
+
20
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
21
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
22
+ await ctx.addInitScript(() => {
23
+ try {
24
+ localStorage.setItem('anet-theme', 'cyber');
25
+ localStorage.setItem('anet-topo-layout', 'grid');
26
+ sessionStorage.setItem('anet_v3_auth', '1');
27
+ sessionStorage.removeItem('anet-topo-pinned-status');
28
+ sessionStorage.removeItem('anet-topo-pinned-group');
29
+ } catch {}
30
+ });
31
+
32
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
33
+ await ctx.route('**/api/hub/status*', async (route) => {
34
+ const r = await route.fetch();
35
+ const b = await r.json();
36
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
37
+ const sessions = [
38
+ { alias: 'alpha1', status: 'working', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
39
+ { alias: 'alpha2', status: 'idle', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
40
+ { alias: 'alpha3', status: 'idle', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
41
+ { alias: 'beta', status: 'idle', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
42
+ ];
43
+ await route.fulfill({ response: r, json: { ...b, sessions } });
44
+ });
45
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
46
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
47
+ const page = await ctx.newPage();
48
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
49
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
50
+ await page.waitForTimeout(500);
51
+
52
+ const snapshot = () => page.evaluate(() => {
53
+ const pills = [...document.querySelectorAll('[data-active-filter]')].map(el => ({
54
+ kind: el.getAttribute('data-active-filter'),
55
+ text: (el.innerText || el.textContent || '').trim(),
56
+ }));
57
+ const opacities = {};
58
+ for (const a of ['alpha1', 'alpha2', 'beta']) {
59
+ const g = document.querySelector(`g[data-node="${a}"]`);
60
+ opacities[a] = g ? +(g.style.opacity || '1') : null;
61
+ }
62
+ return {
63
+ pills,
64
+ opacities,
65
+ storageStatus: sessionStorage.getItem('anet-topo-pinned-status'),
66
+ storageGroup: sessionStorage.getItem('anet-topo-pinned-group'),
67
+ };
68
+ });
69
+
70
+ const before = await snapshot();
71
+
72
+ // Dispatch palette pin: status=working
73
+ await page.evaluate(() => {
74
+ // The Cmd+K commands write to storage AND dispatch the event in
75
+ // lockstep. We mirror both here so we exercise the same code path
76
+ // a palette `perform()` would take.
77
+ sessionStorage.setItem('anet-topo-pinned-status', 'working');
78
+ window.dispatchEvent(new CustomEvent('anet:topo-pin', { detail: { kind: 'status', value: 'working' } }));
79
+ });
80
+ await page.waitForTimeout(250);
81
+ const afterStatus = await snapshot();
82
+
83
+ // Dispatch palette pin: group=alpha (additionally)
84
+ await page.evaluate(() => {
85
+ sessionStorage.setItem('anet-topo-pinned-group', 'alpha');
86
+ window.dispatchEvent(new CustomEvent('anet:topo-pin', { detail: { kind: 'group', value: 'alpha' } }));
87
+ });
88
+ await page.waitForTimeout(250);
89
+ const afterGroup = await snapshot();
90
+
91
+ // Dispatch palette clear
92
+ await page.evaluate(() => {
93
+ sessionStorage.removeItem('anet-topo-pinned-status');
94
+ sessionStorage.removeItem('anet-topo-pinned-group');
95
+ window.dispatchEvent(new CustomEvent('anet:topo-pin', { detail: { kind: 'clear' } }));
96
+ });
97
+ await page.waitForTimeout(250);
98
+ const afterClear = await snapshot();
99
+
100
+ await browser.close();
101
+
102
+ const bright = (v) => v != null && v >= 0.55;
103
+ const dim = (v) => v != null && v < 0.4;
104
+ const results = {
105
+ before_noPills: before.pills.length === 0,
106
+ before_baselineBright: bright(before.opacities.alpha1) && bright(before.opacities.alpha2),
107
+ status_pillRendered: afterStatus.pills.some(p => p.kind === 'status' && /working/.test(p.text)),
108
+ status_dimsIdle: dim(afterStatus.opacities.alpha2) && dim(afterStatus.opacities.beta),
109
+ status_keepsWorking: bright(afterStatus.opacities.alpha1),
110
+ status_storageWritten: afterStatus.storageStatus === 'working',
111
+ groupAdded_bothPills: afterGroup.pills.length === 2,
112
+ groupAdded_storageBoth: afterGroup.storageStatus === 'working' && afterGroup.storageGroup === 'alpha',
113
+ clear_noPills: afterClear.pills.length === 0,
114
+ clear_restoredOpacities: bright(afterClear.opacities.alpha1) && bright(afterClear.opacities.alpha2) && bright(afterClear.opacities.beta),
115
+ clear_storageCleared: afterClear.storageStatus === null && afterClear.storageGroup === null,
116
+ };
117
+ const ok = Object.values(results).every(Boolean);
118
+ console.log(`${ok ? '✅' : '❌'} cmdk-pin:`, JSON.stringify(results),
119
+ `\n before=`, before,
120
+ `\n afterStatus=`, afterStatus,
121
+ `\n afterGroup=`, afterGroup,
122
+ `\n afterClear=`, afterClear);
123
+ 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);
@@ -0,0 +1,74 @@
1
+ /* Round 41 verification: double-clicking empty canvas resets view to
2
+ * {zoom:1, x:0, y:0}. Double-clicking a node does NOT reset (the
3
+ * node's own onClick wins, and resetView should not fire). */
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: 1600 } });
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
+ localStorage.setItem('anet-topo-view', JSON.stringify({ zoom: 1, x: 0, y: 0 }));
16
+ sessionStorage.setItem('anet_v3_auth', '1');
17
+ } catch {}
18
+ });
19
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
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'].map(a => ({
25
+ alias: a, status: 'idle', network_id: nid, project_dir: null,
26
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
27
+ }));
28
+ await route.fulfill({ response: r, json: { ...b, sessions } });
29
+ });
30
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
31
+ const page = await ctx.newPage();
32
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
33
+ await page.waitForSelector('svg[viewBox="0 0 1000 680"]', { timeout: 30000 });
34
+ await page.waitForTimeout(500);
35
+
36
+ const readZoom = () => page.evaluate(() => {
37
+ const span = document.querySelector('button[aria-label="Zoom in"]').parentElement.querySelector('span[title*="zoom level"]');
38
+ return +span.textContent.replace('%', '');
39
+ });
40
+
41
+ // Zoom in a couple times via keyboard, then dbl-click empty canvas → expect reset to 100%.
42
+ await page.keyboard.press('=');
43
+ await page.keyboard.press('=');
44
+ await page.waitForTimeout(120);
45
+ const zoomedIn = await readZoom();
46
+
47
+ // Find empty-canvas coords: top-left area of the SVG, well outside node positions.
48
+ const box = await page.$eval('svg[viewBox="0 0 1000 680"]', el => el.getBoundingClientRect());
49
+ // y=0.35 is below the top-overlay panels (which sit at y<100 inside the SVG)
50
+ const emptyX = box.x + box.width * 0.20;
51
+ const emptyY = box.y + box.height * 0.40;
52
+ await page.mouse.dblclick(emptyX, emptyY);
53
+ await page.waitForTimeout(200);
54
+ const afterEmptyDbl = await readZoom();
55
+
56
+ // Now zoom in again and dbl-click a node — view should NOT reset.
57
+ await page.keyboard.press('=');
58
+ await page.keyboard.press('=');
59
+ await page.waitForTimeout(120);
60
+ const zoomedIn2 = await readZoom();
61
+ await page.locator('g[data-node="a"]').dblclick().catch(() => {});
62
+ await page.waitForTimeout(200);
63
+ const afterNodeDbl = await readZoom();
64
+
65
+ await browser.close();
66
+ const results = {
67
+ zoomedInBeforeDbl: zoomedIn > 100,
68
+ emptyCanvasResets: afterEmptyDbl === 100,
69
+ nodeDoesNotReset: zoomedIn2 > 100 && afterNodeDbl === zoomedIn2,
70
+ };
71
+ const ok = Object.values(results).every(Boolean);
72
+ console.log(`${ok ? '✅' : '❌'} dblclick reset:`, JSON.stringify(results),
73
+ `\n zoomedIn=${zoomedIn} afterEmptyDbl=${afterEmptyDbl} zoomedIn2=${zoomedIn2} afterNodeDbl=${afterNodeDbl}`);
74
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,111 @@
1
+ /* Round 49 verification: hovering a flow edge highlights its two endpoint
2
+ * nodes and dims the rest — the reverse direction of R40 (node→edges) so
3
+ * "who is this edge between" reads at a glance without the tooltip.
4
+ *
5
+ * Sessions: alpha, beta, gamma, delta. Messages: alpha→beta + gamma→delta.
6
+ * - Before hover: all 4 nodes at base opacity (~1.0).
7
+ * - Hover alpha→beta hitbox: alpha + beta opacity ≥ 0.6, gamma + delta
8
+ * drop below 0.4 (composed via `hoveredEdgeEndpoints`).
9
+ * - Move off the edge: opacities return to base.
10
+ */
11
+ import { chromium } from 'playwright';
12
+ import { readFileSync } from 'node:fs';
13
+
14
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
15
+ const browser = await chromium.launch({ headless: true });
16
+
17
+ // The dashboard chrome (sidebar + header + stats row) pushes the topology
18
+ // SVG well below the fold at width 1280. Height 1500 fits the whole canvas
19
+ // so SVG-local coordinates map to in-viewport screen coordinates.
20
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 1500 } });
21
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
22
+ await ctx.addInitScript(() => {
23
+ try {
24
+ localStorage.setItem('anet-theme', 'cyber');
25
+ // Ring layout places nodes around the SVG center and edges curve through
26
+ // it — well inside the 1280x900 viewport. Grid layout pushes the lower-
27
+ // row edges below the fold and breaks locator.hover positioning.
28
+ localStorage.setItem('anet-topo-layout', 'ring');
29
+ sessionStorage.setItem('anet_v3_auth', '1');
30
+ } catch {}
31
+ });
32
+
33
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
34
+ await ctx.route('**/api/hub/status*', async (route) => {
35
+ const r = await route.fetch();
36
+ const b = await r.json();
37
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
38
+ const sessions = ['alpha', 'beta', 'gamma', 'delta'].map(a => ({
39
+ alias: a, status: 'idle', network_id: nid, project_dir: null,
40
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
41
+ }));
42
+ await route.fulfill({ response: r, json: { ...b, sessions } });
43
+ });
44
+ const now = new Date().toISOString();
45
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [
46
+ { from_alias: 'alpha', to_alias: 'beta', content: 'm', created_at: now },
47
+ { from_alias: 'gamma', to_alias: 'delta', content: 'm', created_at: now },
48
+ ] } }));
49
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
50
+ const page = await ctx.newPage();
51
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
52
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
53
+ await page.waitForTimeout(600);
54
+
55
+ const opacities = () => page.evaluate(() => {
56
+ const out = {};
57
+ for (const a of ['alpha', 'beta', 'gamma', 'delta']) {
58
+ const g = document.querySelector(`g[data-node="${a}"]`);
59
+ out[a] = g ? +(g.style.opacity || '1') : null;
60
+ }
61
+ return out;
62
+ });
63
+
64
+ const before = await opacities();
65
+
66
+ // Hover the alpha→beta edge hitbox. The hitbox is a curved <path> with
67
+ // `pointer-events: stroke`, so the cursor only triggers events when it
68
+ // sits on the stroke itself — the bbox centroid is inside an empty region.
69
+ // Sample a real point at half the path's length via getPointAtLength and
70
+ // convert SVG-local coordinates to screen via getScreenCTM, then move the
71
+ // physical mouse there.
72
+ const target = await page.evaluate(() => {
73
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
74
+ for (const g of svg.querySelectorAll(':scope > g > g')) {
75
+ const t = g.querySelector('path[data-edge-hitbox] title');
76
+ if (t && /^alpha → beta/.test(t.textContent || '')) {
77
+ const hb = g.querySelector('path[data-edge-hitbox]');
78
+ const pt = hb.getPointAtLength(hb.getTotalLength() / 2);
79
+ const ctm = hb.getScreenCTM();
80
+ return { x: pt.x * ctm.a + pt.y * ctm.c + ctm.e, y: pt.x * ctm.b + pt.y * ctm.d + ctm.f };
81
+ }
82
+ }
83
+ return null;
84
+ });
85
+ if (!target) { console.log('❌ no alpha→beta hitbox found'); process.exit(1); }
86
+ console.log(' hover target:', target);
87
+ // Move twice — first to an empty spot, then to the curve point — so the
88
+ // browser registers a pointermove from "elsewhere" to the hitbox stroke.
89
+ await page.mouse.move(10, 10);
90
+ await page.mouse.move(target.x, target.y);
91
+ await page.waitForTimeout(350);
92
+ const during = await opacities();
93
+
94
+ // Move far away — over an empty corner — so no node/edge is hovered.
95
+ await page.mouse.move(20, 20);
96
+ await page.waitForTimeout(250);
97
+ const after = await opacities();
98
+
99
+ await browser.close();
100
+ const baseOk = (o) => o >= 0.55; // online nodes at base ~1.0; allow 0.6 floor
101
+ const dimOk = (o) => o !== null && o < 0.4;
102
+ const results = {
103
+ beforeBaseAll: baseOk(before.alpha) && baseOk(before.beta) && baseOk(before.gamma) && baseOk(before.delta),
104
+ hoverKeepsEndpoints: baseOk(during.alpha) && baseOk(during.beta),
105
+ hoverDimsNonEndpoints: dimOk(during.gamma) && dimOk(during.delta),
106
+ releaseRestoresAll: baseOk(after.alpha) && baseOk(after.beta) && baseOk(after.gamma) && baseOk(after.delta),
107
+ };
108
+ const ok = Object.values(results).every(Boolean);
109
+ console.log(`${ok ? '✅' : '❌'} edge-endpoint highlight:`, JSON.stringify(results),
110
+ `\n before=`, before, `\n during=`, during, `\n after=`, after);
111
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,113 @@
1
+ /* Round 50 verification: hovering a flow edge boosts that edge's own
2
+ * opacity 2× and thickens its stroke 1.4×, while every other edge dims
3
+ * to 0.35×. Complements R49 (which highlights the endpoint nodes) —
4
+ * together they make "this is the edge you're pointing at" unmistakable.
5
+ *
6
+ * Sessions: alpha, beta, gamma, delta. Messages: alpha→beta + gamma→delta.
7
+ * Hover the alpha→beta hitbox and read:
8
+ * - alpha→beta visible-path opacity boosts (~2× base)
9
+ * - alpha→beta visible-path strokeWidth = 1.4 × base (rounded by browser)
10
+ * - gamma→delta visible-path opacity dims (~0.35× base)
11
+ * - gamma→delta visible-path strokeWidth unchanged
12
+ */
13
+ import { chromium } from 'playwright';
14
+ import { readFileSync } from 'node:fs';
15
+
16
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
17
+ const browser = await chromium.launch({ headless: true });
18
+
19
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 1500 } });
20
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
21
+ await ctx.addInitScript(() => {
22
+ try {
23
+ localStorage.setItem('anet-theme', 'cyber');
24
+ localStorage.setItem('anet-topo-layout', 'ring');
25
+ sessionStorage.setItem('anet_v3_auth', '1');
26
+ } catch {}
27
+ });
28
+
29
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
30
+ await ctx.route('**/api/hub/status*', async (route) => {
31
+ const r = await route.fetch();
32
+ const b = await r.json();
33
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
34
+ const sessions = ['alpha', 'beta', 'gamma', 'delta'].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
+ const now = new Date().toISOString();
41
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [
42
+ { from_alias: 'alpha', to_alias: 'beta', content: 'm', created_at: now },
43
+ { from_alias: 'gamma', to_alias: 'delta', content: 'm', created_at: now },
44
+ ] } }));
45
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
46
+ const page = await ctx.newPage();
47
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
48
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
49
+ await page.waitForTimeout(600);
50
+
51
+ // Read each flow edge's visible base path (the one with marker-end).
52
+ const readEdges = () => page.evaluate(() => {
53
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
54
+ const rows = [];
55
+ for (const g of svg.querySelectorAll(':scope > g > g')) {
56
+ const t = g.querySelector('path[data-edge-hitbox] title');
57
+ if (!t) continue;
58
+ const route = (t.textContent || '').split('\n')[0];
59
+ const base = [...g.querySelectorAll(':scope > path')].find(
60
+ p => !p.hasAttribute('data-edge-hitbox') && p.hasAttribute('marker-end')
61
+ );
62
+ if (!base) continue;
63
+ rows.push({
64
+ route,
65
+ opacity: +base.getAttribute('opacity'),
66
+ strokeWidth: +base.getAttribute('stroke-width'),
67
+ });
68
+ }
69
+ return rows;
70
+ });
71
+
72
+ const before = await readEdges();
73
+
74
+ // Move cursor to a point ON the alpha→beta hitbox curve.
75
+ const target = await page.evaluate(() => {
76
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
77
+ for (const g of svg.querySelectorAll(':scope > g > g')) {
78
+ const t = g.querySelector('path[data-edge-hitbox] title');
79
+ if (t && /^alpha → beta/.test(t.textContent || '')) {
80
+ const hb = g.querySelector('path[data-edge-hitbox]');
81
+ const pt = hb.getPointAtLength(hb.getTotalLength() / 2);
82
+ const ctm = hb.getScreenCTM();
83
+ return { x: pt.x * ctm.a + pt.y * ctm.c + ctm.e, y: pt.x * ctm.b + pt.y * ctm.d + ctm.f };
84
+ }
85
+ }
86
+ return null;
87
+ });
88
+ if (!target) { console.log('❌ no alpha→beta hitbox found'); process.exit(1); }
89
+ await page.mouse.move(10, 10);
90
+ await page.mouse.move(target.x, target.y);
91
+ await page.waitForTimeout(350);
92
+ const during = await readEdges();
93
+
94
+ await browser.close();
95
+
96
+ const find = (rows, r) => rows.find(e => e.route === r);
97
+ const b_ab = find(before, 'alpha → beta');
98
+ const b_gd = find(before, 'gamma → delta');
99
+ const a_ab = find(during, 'alpha → beta');
100
+ const a_gd = find(during, 'gamma → delta');
101
+
102
+ const results = {
103
+ baselineMatches: b_ab && b_gd && Math.abs(b_ab.opacity - b_gd.opacity) < 0.001 && b_ab.strokeWidth === b_gd.strokeWidth,
104
+ hoveredEdgeBrightens: a_ab && a_ab.opacity > b_ab.opacity * 1.5,
105
+ hoveredEdgeThickens: a_ab && a_ab.strokeWidth > b_ab.strokeWidth * 1.2,
106
+ otherEdgeDims: a_gd && a_gd.opacity < b_gd.opacity * 0.6,
107
+ otherEdgeStaysThin: a_gd && a_gd.strokeWidth === b_gd.strokeWidth,
108
+ };
109
+ const ok = Object.values(results).every(Boolean);
110
+ console.log(`${ok ? '✅' : '❌'} edge focus:`, JSON.stringify(results),
111
+ `\n before α→β=${JSON.stringify(b_ab)} γ→δ=${JSON.stringify(b_gd)}`,
112
+ `\n during α→β=${JSON.stringify(a_ab)} γ→δ=${JSON.stringify(a_gd)}`);
113
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,93 @@
1
+ /* Round 40 verification: hovering a node brightens edges touching it
2
+ * and dims the rest. Three edges in the fleet — hover one node and
3
+ * compare base-path opacity of touching vs non-touching edges. */
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
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
11
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
12
+ await ctx.addInitScript(() => {
13
+ try {
14
+ localStorage.setItem('anet-theme', 'cyber');
15
+ localStorage.setItem('anet-topo-layout', 'grid');
16
+ sessionStorage.setItem('anet_v3_auth', '1');
17
+ } catch {}
18
+ });
19
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
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 = ['alpha', 'beta', 'gamma', 'delta'].map(a => ({
25
+ alias: a, status: 'idle', network_id: nid, project_dir: null,
26
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
27
+ }));
28
+ await route.fulfill({ response: r, json: { ...b, sessions } });
29
+ });
30
+ // Edges:
31
+ // alpha → beta (touches alpha + beta)
32
+ // alpha → gamma (touches alpha + gamma)
33
+ // delta → gamma (touches delta + gamma — does NOT touch alpha)
34
+ const now = new Date().toISOString();
35
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [
36
+ { from_alias: 'alpha', to_alias: 'beta', content: 'm', created_at: now },
37
+ { from_alias: 'alpha', to_alias: 'gamma', content: 'm', created_at: now },
38
+ { from_alias: 'delta', to_alias: 'gamma', content: 'm', created_at: now },
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(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
44
+ await page.waitForTimeout(600);
45
+
46
+ const edgeOpacities = () => page.evaluate(() => {
47
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
48
+ // R48 reordered the children of each flow-link <g>:
49
+ // path[data-edge-hitbox] <-- transparent hover surface, carries <title>
50
+ // path[marker-end] <-- the visible base edge with opacity
51
+ // path[id^=flow-path-] <-- the dashed runway
52
+ // Read the route from the hitbox's <title>, but the opacity from the
53
+ // visible base path. Picking by marker-end keeps the selector resilient
54
+ // to future siblings.
55
+ return [...svg.querySelectorAll(':scope > g > g')]
56
+ .map(g => {
57
+ const hitbox = g.querySelector('path[data-edge-hitbox]');
58
+ const title = hitbox?.querySelector('title');
59
+ const base = [...g.querySelectorAll(':scope > path')].find(
60
+ p => !p.hasAttribute('data-edge-hitbox') && p.hasAttribute('marker-end')
61
+ );
62
+ if (!title || !base) return null;
63
+ return { route: title.textContent.split('\n')[0], opacity: +base.getAttribute('opacity') };
64
+ })
65
+ .filter(Boolean);
66
+ });
67
+
68
+ const before = await edgeOpacities();
69
+ // Hover alpha → its two outgoing edges should brighten, delta→gamma should dim.
70
+ await page.locator('g[data-node="alpha"]').hover();
71
+ await page.waitForTimeout(350);
72
+ const after = await edgeOpacities();
73
+
74
+ await browser.close();
75
+ const get = (rows, r) => rows.find(e => e.route === r)?.opacity ?? null;
76
+ const b_ab = get(before, 'alpha → beta');
77
+ const b_ag = get(before, 'alpha → gamma');
78
+ const b_dg = get(before, 'delta → gamma');
79
+ const a_ab = get(after, 'alpha → beta');
80
+ const a_ag = get(after, 'alpha → gamma');
81
+ const a_dg = get(after, 'delta → gamma');
82
+
83
+ const results = {
84
+ baselineMatches: b_ab !== null && b_ag !== null && b_dg !== null && Math.abs(b_ab - b_dg) < 0.001,
85
+ hoverBrightensAlphaBeta: a_ab !== null && a_ab > b_ab + 0.05,
86
+ hoverBrightensAlphaGamma: a_ag !== null && a_ag > b_ag + 0.05,
87
+ hoverDimsDeltaGamma: a_dg !== null && a_dg < b_dg - 0.05,
88
+ };
89
+ const ok = Object.values(results).every(Boolean);
90
+ console.log(`${ok ? '✅' : '❌'} edge hover:`, JSON.stringify(results),
91
+ `\n before α→β=${b_ab} α→γ=${b_ag} δ→γ=${b_dg}`,
92
+ `\n after α→β=${a_ab} α→γ=${a_ag} δ→γ=${a_dg}`);
93
+ process.exit(ok ? 0 : 1);