@sleep2agi/agent-network-dashboard 0.4.6 → 0.5.1-preview.0

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 (493) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +2 -0
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/diagnostics/route-bundle-stats.json +31 -31
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/routes-manifest.json +16 -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/server/[hostname]/agents/route/app-paths-manifest.json +3 -0
  34. package/.next/server/app/api/hub/server/[hostname]/agents/route/build-manifest.json +9 -0
  35. package/.next/server/app/api/hub/server/[hostname]/agents/route/server-reference-manifest.json +4 -0
  36. package/.next/server/app/api/hub/server/[hostname]/agents/route.js +7 -0
  37. package/.next/server/app/api/hub/server/[hostname]/agents/route.js.map +5 -0
  38. package/.next/server/app/api/hub/server/[hostname]/agents/route.js.nft.json +1 -0
  39. package/.next/server/app/api/hub/server/[hostname]/agents/route_client-reference-manifest.js +3 -0
  40. package/.next/server/app/api/hub/server/[hostname]/health/route/app-paths-manifest.json +3 -0
  41. package/.next/server/app/api/hub/server/[hostname]/health/route/build-manifest.json +9 -0
  42. package/.next/server/app/api/hub/server/[hostname]/health/route/server-reference-manifest.json +4 -0
  43. package/.next/server/app/api/hub/server/[hostname]/health/route.js +7 -0
  44. package/.next/server/app/api/hub/server/[hostname]/health/route.js.map +5 -0
  45. package/.next/server/app/api/hub/server/[hostname]/health/route.js.nft.json +1 -0
  46. package/.next/server/app/api/hub/server/[hostname]/health/route_client-reference-manifest.js +3 -0
  47. package/.next/server/app/index.html +2 -2
  48. package/.next/server/app/index.rsc +14 -14
  49. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  50. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  51. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  52. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  53. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app/login.html +2 -2
  56. package/.next/server/app/login.rsc +14 -14
  57. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  58. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  59. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  60. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  61. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  62. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  63. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  64. package/.next/server/app/logs.html +2 -2
  65. package/.next/server/app/logs.rsc +14 -14
  66. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  67. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  68. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  69. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  70. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  71. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  72. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  73. package/.next/server/app/messages.html +2 -2
  74. package/.next/server/app/messages.rsc +14 -14
  75. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  76. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  77. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  78. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  79. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  80. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  81. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  82. package/.next/server/app/node.html +2 -2
  83. package/.next/server/app/node.rsc +14 -14
  84. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  85. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  86. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  87. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  88. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  89. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  90. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  91. package/.next/server/app/nodes.html +2 -2
  92. package/.next/server/app/nodes.rsc +14 -14
  93. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  94. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  95. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  96. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  97. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  98. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  99. package/.next/server/app/page_client-reference-manifest.js +1 -1
  100. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  101. package/.next/server/app/server-logs.html +2 -2
  102. package/.next/server/app/server-logs.rsc +14 -14
  103. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  104. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  105. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  106. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  107. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  108. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  109. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  110. package/.next/server/app/settings/networks.html +2 -2
  111. package/.next/server/app/settings/networks.rsc +14 -14
  112. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  113. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  114. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  115. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  116. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  117. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  118. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  119. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  120. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  121. package/.next/server/app/settings/tokens.html +2 -2
  122. package/.next/server/app/settings/tokens.rsc +14 -14
  123. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  124. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  125. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  126. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  127. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  128. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  129. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  130. package/.next/server/app/settings.html +2 -2
  131. package/.next/server/app/settings.rsc +14 -14
  132. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  133. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  134. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  135. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  136. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  137. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  138. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  139. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  140. package/.next/server/app/tasks.html +2 -2
  141. package/.next/server/app/tasks.rsc +14 -14
  142. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  143. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  144. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  145. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  146. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  147. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  148. package/.next/server/app-paths-manifest.json +2 -0
  149. package/.next/server/chunks/0dk5_next-internal_server_app_api_hub_server_[hostname]_agents_route_actions_0vr92s4.js +3 -0
  150. package/.next/server/chunks/0dk5_next-internal_server_app_api_hub_server_[hostname]_agents_route_actions_0vr92s4.js.map +1 -0
  151. package/.next/server/chunks/0dk5_next-internal_server_app_api_hub_server_[hostname]_health_route_actions_0vr-bv5.js +3 -0
  152. package/.next/server/chunks/0dk5_next-internal_server_app_api_hub_server_[hostname]_health_route_actions_0vr-bv5.js.map +1 -0
  153. package/.next/server/chunks/[root-of-the-server]__05nyivw._.js +3 -0
  154. package/.next/server/chunks/[root-of-the-server]__05nyivw._.js.map +1 -0
  155. package/.next/server/chunks/[root-of-the-server]__0floe3h._.js +3 -0
  156. package/.next/server/chunks/[root-of-the-server]__0floe3h._.js.map +1 -0
  157. package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js +1 -1
  158. package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js.map +1 -1
  159. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +4 -2
  160. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  161. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  162. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  163. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  164. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  165. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  166. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  167. package/.next/server/middleware-build-manifest.js +3 -3
  168. package/.next/server/pages/404.html +2 -2
  169. package/.next/server/pages/500.html +1 -1
  170. package/.next/server/server-reference-manifest.js +1 -1
  171. package/.next/server/server-reference-manifest.json +1 -1
  172. package/.next/static/chunks/03v-0n3d~t1sm.js +4 -0
  173. package/.next/static/chunks/0aauz~36q5n2a.css +2 -0
  174. package/.next/static/chunks/0l_~q07bhpkcx.js +7 -0
  175. package/.next/static/chunks/{0hyl7mdapsz38.js → 0vt5wron8jig5.js} +1 -1
  176. package/.next/static/chunks/{16yfxp9ak2gdm.js → 180biiw78-hb_.js} +1 -1
  177. package/.next/trace +2 -2
  178. package/.next/trace-build +1 -1
  179. package/.next/types/routes.d.ts +3 -1
  180. package/.next/types/validator.ts +18 -0
  181. package/app/api/hub/server/[hostname]/agents/route.ts +62 -0
  182. package/app/api/hub/server/[hostname]/health/route.ts +58 -0
  183. package/app/api/hub/servers/route.ts +88 -1
  184. package/app/components/CommandPalette.tsx +107 -0
  185. package/app/components/HelpOverlay.tsx +109 -1
  186. package/app/components/ServersDrawer.tsx +181 -5
  187. package/app/components/TopoGraph.tsx +6420 -286
  188. package/app/globals.css +110 -5
  189. package/app/lib/vendorIdentity.ts +14 -3
  190. package/docs/rfc/v0.10.0-hero3-dashboard-upgrade.md +230 -0
  191. package/package.json +2 -2
  192. package/public/sleep2agi-logo.svg +11 -0
  193. package/public/vendors/claude.svg +11 -0
  194. package/public/vendors/minimax.svg +11 -0
  195. package/public/vendors/openai.svg +12 -0
  196. package/screenshots/r293-p0/after-full.png +0 -0
  197. package/screenshots/r293-p0/after-topo.png +0 -0
  198. package/screenshots/r293-p0/baseline-full.png +0 -0
  199. package/screenshots/r293-p0/baseline-topo.png +0 -0
  200. package/screenshots/r293-p0/logo-integrated-full.png +0 -0
  201. package/screenshots/r293-p0/logo-integrated-topo.png +0 -0
  202. package/screenshots/r293-p0/servers-drawer-v0.10.0-full.png +0 -0
  203. package/screenshots/r293-p0/servers-drawer-v0.10.0.png +0 -0
  204. package/screenshots/r293-p0/wave1-after-full.png +0 -0
  205. package/screenshots/r293-p0/wave1-after-topo.png +0 -0
  206. package/scripts/p0-servers-drawer-screenshot.mjs +99 -0
  207. package/scripts/p0-topo-screenshot.mjs +82 -0
  208. package/scripts/topo-active-filter-pill-test.mjs +101 -0
  209. package/scripts/topo-active-links-chip-hover-test.mjs +103 -0
  210. package/scripts/topo-active-links-chip-nav-test.mjs +117 -0
  211. package/scripts/topo-active-links-chip-self-hover-test.mjs +171 -0
  212. package/scripts/topo-active-links-empty-recede-test.mjs +125 -0
  213. package/scripts/topo-active-links-freshness-test.mjs +113 -0
  214. package/scripts/topo-active-links-tooltip-test.mjs +79 -0
  215. package/scripts/topo-active-links-unit-test.mjs +79 -0
  216. package/scripts/topo-active-pulse-ease-test.mjs +120 -0
  217. package/scripts/topo-brand-logo-transition-test.mjs +94 -0
  218. package/scripts/topo-brand-watermark-test.mjs +97 -0
  219. package/scripts/topo-canvas-aria-test.mjs +108 -0
  220. package/scripts/topo-canvas-brand-fade-test.mjs +87 -0
  221. package/scripts/topo-chat-target-breath-test.mjs +84 -0
  222. package/scripts/topo-chat-target-ring-ease-test.mjs +82 -0
  223. package/scripts/topo-chat-target-ring-fade-test.mjs +124 -0
  224. package/scripts/topo-chip-empty-recede-test.mjs +116 -0
  225. package/scripts/topo-chip-focus-test.mjs +95 -0
  226. package/scripts/topo-chip-pin-mirror-test.mjs +113 -0
  227. package/scripts/topo-chip-row-gap-test.mjs +90 -0
  228. package/scripts/topo-chip-row-semantic-gap-test.mjs +97 -0
  229. package/scripts/topo-chip-row-tabular-test.mjs +111 -0
  230. package/scripts/topo-chip-unit-demotion-test.mjs +93 -0
  231. package/scripts/topo-chiprow-data-weight-test.mjs +94 -0
  232. package/scripts/topo-chrome-a11y-test.mjs +107 -0
  233. package/scripts/topo-chrome-active-weight-test.mjs +104 -0
  234. package/scripts/topo-chrome-focus-ring-unify-test.mjs +100 -0
  235. package/scripts/topo-chrome-gap-test.mjs +73 -0
  236. package/scripts/topo-chrome-icon-stroke-test.mjs +95 -0
  237. package/scripts/topo-chrome-inactive-color-test.mjs +90 -0
  238. package/scripts/topo-chrome-pop-extension-test.mjs +98 -0
  239. package/scripts/topo-chrome-pop-nodesize-test.mjs +88 -0
  240. package/scripts/topo-chrome-press-state-test.mjs +135 -0
  241. package/scripts/topo-chrome-strip-semantic-gap-test.mjs +99 -0
  242. package/scripts/topo-chrome-strip-theme-ease-test.mjs +99 -0
  243. package/scripts/topo-chrome-zoom-level-pop-test.mjs +122 -0
  244. package/scripts/topo-chrome-zoom-pop-test.mjs +130 -0
  245. package/scripts/topo-chrome-zoom-readout-weight-test.mjs +81 -0
  246. package/scripts/topo-click-ripple-ease-test.mjs +94 -0
  247. package/scripts/topo-cmdk-edge-clear-test.mjs +109 -0
  248. package/scripts/topo-cmdk-layout-view-test.mjs +108 -0
  249. package/scripts/topo-cmdk-pin-test.mjs +123 -0
  250. package/scripts/topo-cmdk-vendor-clear-test.mjs +133 -0
  251. package/scripts/topo-cmdk-vendor-pin-test.mjs +103 -0
  252. package/scripts/topo-corner-radius-unify-test.mjs +103 -0
  253. package/scripts/topo-dense-alias-fill-transition-test.mjs +133 -0
  254. package/scripts/topo-discrete-zoom-smooth-test.mjs +125 -0
  255. package/scripts/topo-edge-badge-click-pin-test.mjs +94 -0
  256. package/scripts/topo-edge-badge-crossfade-test.mjs +138 -0
  257. package/scripts/topo-edge-badge-hot-test.mjs +115 -0
  258. package/scripts/topo-edge-badge-hover-test.mjs +85 -0
  259. package/scripts/topo-edge-badge-lift-test.mjs +150 -0
  260. package/scripts/topo-edge-badge-ripple-test.mjs +111 -0
  261. package/scripts/topo-edge-badge-stroke-transition-test.mjs +118 -0
  262. package/scripts/topo-edge-badge-text-pin-test.mjs +121 -0
  263. package/scripts/topo-edge-badge-theme-ease-test.mjs +89 -0
  264. package/scripts/topo-edge-count-badge-test.mjs +86 -0
  265. package/scripts/topo-edge-endpoint-highlight-test.mjs +111 -0
  266. package/scripts/topo-edge-fadein-test.mjs +106 -0
  267. package/scripts/topo-edge-focus-test.mjs +113 -0
  268. package/scripts/topo-edge-hover-test.mjs +13 -5
  269. package/scripts/topo-edge-pill-hot-test.mjs +129 -0
  270. package/scripts/topo-edge-pill-test.mjs +105 -0
  271. package/scripts/topo-edge-ping-pulse-retired-test.mjs +119 -0
  272. package/scripts/topo-edge-stroke-ease-test.mjs +100 -0
  273. package/scripts/topo-edge-stroke-transition-test.mjs +132 -0
  274. package/scripts/topo-empty-hint-tracking-test.mjs +84 -0
  275. package/scripts/topo-empty-state-breath-test.mjs +150 -0
  276. package/scripts/topo-empty-state-crossfade-test.mjs +130 -0
  277. package/scripts/topo-empty-state-recenter-test.mjs +107 -0
  278. package/scripts/topo-empty-sub-hint-italic-test.mjs +83 -0
  279. package/scripts/topo-empty-subhint-tracking-test.mjs +87 -0
  280. package/scripts/topo-endpoint-ring-fade-test.mjs +132 -0
  281. package/scripts/topo-endpoint-ring-test.mjs +93 -0
  282. package/scripts/topo-endpoint-ring-thicken-test.mjs +112 -0
  283. package/scripts/topo-esc-clears-pin-test.mjs +98 -0
  284. package/scripts/topo-filter-pill-aliases-test.mjs +121 -0
  285. package/scripts/topo-filter-pill-body-click-test.mjs +112 -0
  286. package/scripts/topo-filter-pill-count-tabular-test.mjs +135 -0
  287. package/scripts/topo-filter-pill-count-test.mjs +110 -0
  288. package/scripts/topo-filter-pill-mobile-test.mjs +90 -0
  289. package/scripts/topo-freshness-chip-transition-test.mjs +80 -0
  290. package/scripts/topo-freshness-chip-weight-test.mjs +90 -0
  291. package/scripts/topo-freshness-conditional-render-test.mjs +92 -0
  292. package/scripts/topo-fullscreen-active-test.mjs +115 -0
  293. package/scripts/topo-group-box-pinned-state-test.mjs +95 -0
  294. package/scripts/topo-group-count-test.mjs +6 -1
  295. package/scripts/topo-group-label-a11y-test.mjs +113 -0
  296. package/scripts/topo-group-label-count-inherit-test.mjs +99 -0
  297. package/scripts/topo-group-label-hover-test.mjs +119 -0
  298. package/scripts/topo-group-label-pin-spacing-test.mjs +136 -0
  299. package/scripts/topo-group-label-tint-test.mjs +99 -0
  300. package/scripts/topo-group-label-title-test.mjs +83 -0
  301. package/scripts/topo-group-pin-test.mjs +119 -0
  302. package/scripts/topo-group-pip-fadein-test.mjs +104 -0
  303. package/scripts/topo-group-pip-singletier-drop-test.mjs +131 -0
  304. package/scripts/topo-group-pip-tabular-test.mjs +95 -0
  305. package/scripts/topo-group-status-pips-test.mjs +103 -0
  306. package/scripts/topo-groupbox-fadein-test.mjs +95 -0
  307. package/scripts/topo-groupbox-hover-lift-test.mjs +127 -0
  308. package/scripts/topo-groupbox-hubcore-ease-test.mjs +106 -0
  309. package/scripts/topo-groupbox-live-test.mjs +100 -0
  310. package/scripts/topo-groupbox-march-busyness-test.mjs +155 -0
  311. package/scripts/topo-groupbox-test.mjs +4 -2
  312. package/scripts/topo-header-gap-test.mjs +94 -0
  313. package/scripts/topo-help-overlay-r109-test.mjs +67 -0
  314. package/scripts/topo-help-overlay-r118-test.mjs +69 -0
  315. package/scripts/topo-help-overlay-r123-test.mjs +69 -0
  316. package/scripts/topo-help-overlay-r134-test.mjs +74 -0
  317. package/scripts/topo-help-overlay-r137-test.mjs +81 -0
  318. package/scripts/topo-help-overlay-r141-test.mjs +86 -0
  319. package/scripts/topo-help-overlay-r153-test.mjs +85 -0
  320. package/scripts/topo-help-overlay-r59-test.mjs +67 -0
  321. package/scripts/topo-help-overlay-r65-test.mjs +64 -0
  322. package/scripts/topo-help-overlay-r70-test.mjs +63 -0
  323. package/scripts/topo-help-overlay-r78-test.mjs +59 -0
  324. package/scripts/topo-help-overlay-r81-test.mjs +58 -0
  325. package/scripts/topo-help-overlay-r87-test.mjs +64 -0
  326. package/scripts/topo-help-overlay-r91-test.mjs +65 -0
  327. package/scripts/topo-hot-tail-crossfade-test.mjs +117 -0
  328. package/scripts/topo-html-overlay-padding-unify-test.mjs +120 -0
  329. package/scripts/topo-hub-a11y-test.mjs +112 -0
  330. package/scripts/topo-hub-busyness-test.mjs +100 -0
  331. package/scripts/topo-hub-center-crossfade-test.mjs +124 -0
  332. package/scripts/topo-hub-click-fit-test.mjs +137 -0
  333. package/scripts/topo-hub-digit-scale-test.mjs +129 -0
  334. package/scripts/topo-hub-fadein-test.mjs +88 -0
  335. package/scripts/topo-hub-hover-lift-test.mjs +101 -0
  336. package/scripts/topo-hub-hover-ring-test.mjs +82 -0
  337. package/scripts/topo-hub-pip-theme-ease-test.mjs +116 -0
  338. package/scripts/topo-hub-spoke-ease-test.mjs +120 -0
  339. package/scripts/topo-hub-title-test.mjs +7 -2
  340. package/scripts/topo-hub-working-count-test.mjs +99 -0
  341. package/scripts/topo-ingroup-edge-test.mjs +103 -0
  342. package/scripts/topo-inner-radar-ring-retired-test.mjs +89 -0
  343. package/scripts/topo-kicker-color-test.mjs +93 -0
  344. package/scripts/topo-kicker-tracking-test.mjs +101 -0
  345. package/scripts/topo-kicker-weight-test.mjs +98 -0
  346. package/scripts/topo-label-card-theme-ease-test.mjs +85 -0
  347. package/scripts/topo-label-lift-test.mjs +5 -1
  348. package/scripts/topo-layout-hover-text-preview-test.mjs +101 -0
  349. package/scripts/topo-layout-switch-crossfade-test.mjs +126 -0
  350. package/scripts/topo-layout-toggle-border-unify-test.mjs +104 -0
  351. package/scripts/topo-layout-toggle-test.mjs +131 -0
  352. package/scripts/topo-layout-trailer-mr-test.mjs +82 -0
  353. package/scripts/topo-legend-compress-test.mjs +107 -0
  354. package/scripts/topo-legend-count-empty-test.mjs +126 -0
  355. package/scripts/topo-legend-count-tabular-parity-test.mjs +90 -0
  356. package/scripts/topo-legend-count-tabular-test.mjs +87 -0
  357. package/scripts/topo-legend-count-test.mjs +88 -0
  358. package/scripts/topo-legend-count-tier-fill-test.mjs +121 -0
  359. package/scripts/topo-legend-count-weight-test.mjs +86 -0
  360. package/scripts/topo-legend-header-symmetric-inset-test.mjs +112 -0
  361. package/scripts/topo-legend-hitbox-swatch-center-test.mjs +93 -0
  362. package/scripts/topo-legend-hover-test.mjs +113 -0
  363. package/scripts/topo-legend-offline-delimiter-test.mjs +84 -0
  364. package/scripts/topo-legend-offline-label-simplify-test.mjs +86 -0
  365. package/scripts/topo-legend-panel-count-weight-test.mjs +89 -0
  366. package/scripts/topo-legend-panel-header-test.mjs +83 -0
  367. package/scripts/topo-legend-pin-ring-fade-test.mjs +112 -0
  368. package/scripts/topo-legend-pin-test.mjs +128 -0
  369. package/scripts/topo-legend-row-hover-lift-test.mjs +109 -0
  370. package/scripts/topo-legend-row-labels-simplify-test.mjs +80 -0
  371. package/scripts/topo-legend-row-pin-spacing-test.mjs +124 -0
  372. package/scripts/topo-legend-row-tint-test.mjs +95 -0
  373. package/scripts/topo-legend-row-title-test.mjs +102 -0
  374. package/scripts/topo-legend-swatch-grow-test.mjs +116 -0
  375. package/scripts/topo-legend-swatch-radius-test.mjs +78 -0
  376. package/scripts/topo-minimap-a11y-test.mjs +101 -0
  377. package/scripts/topo-minimap-dot-transition-test.mjs +113 -0
  378. package/scripts/topo-minimap-radius-test.mjs +88 -0
  379. package/scripts/topo-minimap-rect-stroke-test.mjs +94 -0
  380. package/scripts/topo-minimap-viewport-smooth-test.mjs +124 -0
  381. package/scripts/topo-monogram-font-test.mjs +112 -0
  382. package/scripts/topo-monogram-stroke-test.mjs +101 -0
  383. package/scripts/topo-more-flows-letterspacing-test.mjs +111 -0
  384. package/scripts/topo-node-a11y-test.mjs +107 -0
  385. package/scripts/topo-node-alias-chat-pin-test.mjs +95 -0
  386. package/scripts/topo-node-hover-lift-test.mjs +85 -0
  387. package/scripts/topo-node-label-card-elevation-test.mjs +157 -0
  388. package/scripts/topo-node-label-card-stroke-test.mjs +135 -0
  389. package/scripts/topo-node-stroke-transition-test.mjs +103 -0
  390. package/scripts/topo-node-text-fill-transition-test.mjs +119 -0
  391. package/scripts/topo-node-title-peers-test.mjs +107 -0
  392. package/scripts/topo-node-title-test.mjs +101 -0
  393. package/scripts/topo-nodesize-active-hover-test.mjs +109 -0
  394. package/scripts/topo-nodesize-crossfade-test.mjs +141 -0
  395. package/scripts/topo-online-chip-nav-test.mjs +143 -0
  396. package/scripts/topo-orbit-particles-retired-test.mjs +91 -0
  397. package/scripts/topo-panel-count-unit-test.mjs +112 -0
  398. package/scripts/topo-panel-elevation-test.mjs +76 -0
  399. package/scripts/topo-panel-fadein-test.mjs +102 -0
  400. package/scripts/topo-panel-hot-tail-fade-test.mjs +132 -0
  401. package/scripts/topo-panel-hover-elevation-test.mjs +118 -0
  402. package/scripts/topo-panel-rx-test.mjs +90 -0
  403. package/scripts/topo-panel-theme-ease-test.mjs +85 -0
  404. package/scripts/topo-panel-title-fill-ease-test.mjs +91 -0
  405. package/scripts/topo-panel-title-tracking-test.mjs +78 -0
  406. package/scripts/topo-particle-stagger-test.mjs +82 -0
  407. package/scripts/topo-pin-intersection-color-ease-test.mjs +111 -0
  408. package/scripts/topo-pin-intersection-empty-test.mjs +129 -0
  409. package/scripts/topo-pin-intersection-tabular-test.mjs +96 -0
  410. package/scripts/topo-pin-intersection-test.mjs +121 -0
  411. package/scripts/topo-pin-intersection-warning-crossfade-test.mjs +107 -0
  412. package/scripts/topo-pin-mirror-transition-test.mjs +101 -0
  413. package/scripts/topo-pin-persistence-test.mjs +105 -0
  414. package/scripts/topo-prefix-opacity-test.mjs +107 -0
  415. package/scripts/topo-prepublish-guard-test.mjs +95 -0
  416. package/scripts/topo-pressure-hover-test.mjs +123 -0
  417. package/scripts/topo-pressure-pin-test.mjs +119 -0
  418. package/scripts/topo-pressure-seg-self-hover-test.mjs +126 -0
  419. package/scripts/topo-pressure-seg-tooltip-test.mjs +98 -0
  420. package/scripts/topo-pressure-seg-transition-test.mjs +107 -0
  421. package/scripts/topo-pulse-dot-crossfade-test.mjs +122 -0
  422. package/scripts/topo-pulse-dot-retired-test.mjs +86 -0
  423. package/scripts/topo-recent-panel-count-freshness-test.mjs +109 -0
  424. package/scripts/topo-recent-panel-count-test.mjs +72 -0
  425. package/scripts/topo-recent-panel-count-weight-test.mjs +91 -0
  426. package/scripts/topo-recent-panel-footer-breath-test.mjs +142 -0
  427. package/scripts/topo-recent-panel-hot-header-test.mjs +132 -0
  428. package/scripts/topo-recent-panel-hot-tabular-test.mjs +113 -0
  429. package/scripts/topo-recent-panel-more-crossfade-test.mjs +124 -0
  430. package/scripts/topo-recent-panel-more-cyan-hover-test.mjs +151 -0
  431. package/scripts/topo-recent-panel-more-nav-test.mjs +127 -0
  432. package/scripts/topo-recent-panel-more-test.mjs +106 -0
  433. package/scripts/topo-recent-panel-readability-bump-test.mjs +151 -0
  434. package/scripts/topo-recent-row-count-fill-test.mjs +129 -0
  435. package/scripts/topo-recent-row-count-weight-test.mjs +112 -0
  436. package/scripts/topo-recent-row-fadein-test.mjs +107 -0
  437. package/scripts/topo-recent-row-freshness-test.mjs +102 -0
  438. package/scripts/topo-recent-row-hot-test.mjs +110 -0
  439. package/scripts/topo-recent-row-hover-lift-test.mjs +128 -0
  440. package/scripts/topo-recent-row-hover-test.mjs +122 -0
  441. package/scripts/topo-recent-row-pin-spacing-test.mjs +137 -0
  442. package/scripts/topo-recent-row-pin-test.mjs +126 -0
  443. package/scripts/topo-recent-row-tint-test.mjs +91 -0
  444. package/scripts/topo-recent-row-title-test.mjs +112 -0
  445. package/scripts/topo-recent-row-ts-alpha-test.mjs +100 -0
  446. package/scripts/topo-recent-row-ts-tabular-test.mjs +108 -0
  447. package/scripts/topo-recent-row-ts-test.mjs +95 -0
  448. package/scripts/topo-recent-row-typography-test.mjs +119 -0
  449. package/scripts/topo-recent-signal-empty-hint-test.mjs +73 -0
  450. package/scripts/topo-reset-spin-test.mjs +120 -0
  451. package/scripts/topo-runtime-badge-grow-test.mjs +114 -0
  452. package/scripts/topo-spokes-retired-test.mjs +93 -0
  453. package/scripts/topo-starfield-density-test.mjs +89 -0
  454. package/scripts/topo-status-chip-hover-test.mjs +101 -0
  455. package/scripts/topo-status-chip-tooltip-test.mjs +95 -0
  456. package/scripts/topo-statusbar-chips-self-hover-test.mjs +177 -0
  457. package/scripts/topo-svg-focus-test.mjs +112 -0
  458. package/scripts/topo-sweep-busyness-test.mjs +94 -0
  459. package/scripts/topo-tabular-nums-sweep-test.mjs +93 -0
  460. package/scripts/topo-tier-guide-dashes-test.mjs +86 -0
  461. package/scripts/topo-tier-ring-fadein-test.mjs +107 -0
  462. package/scripts/topo-tier-ring-occupancy-test.mjs +87 -0
  463. package/scripts/topo-tier-ring-test.mjs +69 -0
  464. package/scripts/topo-tier-ring-tint-test.mjs +110 -0
  465. package/scripts/topo-tier-stagger-test.mjs +89 -0
  466. package/scripts/topo-title-block-tight-leading-test.mjs +95 -0
  467. package/scripts/topo-title-tracking-test.mjs +98 -0
  468. package/scripts/topo-titleblock-gap-test.mjs +86 -0
  469. package/scripts/topo-titleblock-margin-test.mjs +83 -0
  470. package/scripts/topo-toggle-inactive-cyan-hover-test.mjs +110 -0
  471. package/scripts/topo-top-rail-theme-ease-test.mjs +89 -0
  472. package/scripts/topo-vendor-chip-hover-test.mjs +111 -0
  473. package/scripts/topo-vendor-chip-self-hover-test.mjs +131 -0
  474. package/scripts/topo-vendor-chip-tabular-test.mjs +92 -0
  475. package/scripts/topo-vendor-chip-threshold-test.mjs +78 -0
  476. package/scripts/topo-vendor-chip-weight-test.mjs +87 -0
  477. package/scripts/topo-vendor-count-suffix-test.mjs +95 -0
  478. package/scripts/topo-vendor-letter-tooltip-test.mjs +92 -0
  479. package/scripts/topo-vendor-pill-test.mjs +125 -0
  480. package/scripts/topo-vendor-pin-test.mjs +125 -0
  481. package/scripts/topo-view-smooth-test.mjs +131 -0
  482. package/scripts/topo-watermark-tracking-test.mjs +99 -0
  483. package/scripts/topo-working-chip-pin-test.mjs +154 -0
  484. package/scripts/topo-working-halo-breath-retired-test.mjs +99 -0
  485. package/scripts/topo-wrapper-flow-arrow-theme-test.mjs +97 -0
  486. package/scripts/topo-wrapper-radius-test.mjs +73 -0
  487. package/scripts/topo-wrapper-shadow-theme-ease-test.mjs +89 -0
  488. package/.next/static/chunks/088ouqy7tekty.js +0 -2
  489. package/.next/static/chunks/0mqwn-l7wtcdo.js +0 -7
  490. package/.next/static/chunks/127kw7~wsnyu7.css +0 -2
  491. /package/.next/static/{23MxP0RBVA4BaOJ_HMYmM → ypixDJRds4g4TLoUdmrwN}/_buildManifest.js +0 -0
  492. /package/.next/static/{23MxP0RBVA4BaOJ_HMYmM → ypixDJRds4g4TLoUdmrwN}/_clientMiddlewareManifest.js +0 -0
  493. /package/.next/static/{23MxP0RBVA4BaOJ_HMYmM → ypixDJRds4g4TLoUdmrwN}/_ssgManifest.js +0 -0
@@ -0,0 +1,85 @@
1
+ /* Round 122 verification: hovering the edge midpoint count badge
2
+ * (R100/R121) sets hoveredEdgeKey — same as hovering the line
3
+ * itself (R48). Endpoint rings light, edge brightens. R121 only
4
+ * wired click; R122 closes the hover consistency gap.
5
+ *
6
+ * Fleet: alpha→beta with 4 msgs (badge renders), gamma idle.
7
+ * Hover the badge → alpha + beta get endpoint rings (R111),
8
+ * gamma stays bare. mouseleave → rings clear.
9
+ */
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
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
17
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
18
+ await ctx.addInitScript(() => {
19
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
20
+ });
21
+
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
+ const mk = (alias) => ({
28
+ alias, status: 'idle', model: 'claude-opus-4', runtime: 'cli-claude-code',
29
+ network_id: nid, project_dir: null,
30
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
31
+ });
32
+ await route.fulfill({ response: r, json: { ...b, sessions: [mk('alpha'), mk('beta'), mk('gamma')] } });
33
+ });
34
+
35
+ const now = Date.now();
36
+ const mkMsg = (id, from_alias, to_alias, ageMs) => ({
37
+ id, from_alias, to_alias, content: 'hi', network_id: 'default',
38
+ created_at: new Date(now - ageMs).toISOString(),
39
+ });
40
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [
41
+ mkMsg('m1', 'alpha', 'beta', 30 * 1000),
42
+ mkMsg('m2', 'alpha', 'beta', 40 * 1000),
43
+ mkMsg('m3', 'alpha', 'beta', 50 * 1000),
44
+ mkMsg('m4', 'alpha', 'beta', 60 * 1000),
45
+ ] } }));
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 === 3, { timeout: 30000 });
50
+ await page.waitForSelector('[data-edge-count-badge]', { timeout: 10000 });
51
+ await page.waitForTimeout(400);
52
+
53
+ const rings = () => page.evaluate(() => ({
54
+ alpha: !!document.querySelector('g[data-node="alpha"] [data-edge-endpoint-ring]'),
55
+ beta: !!document.querySelector('g[data-node="beta"] [data-edge-endpoint-ring]'),
56
+ gamma: !!document.querySelector('g[data-node="gamma"] [data-edge-endpoint-ring]'),
57
+ }));
58
+
59
+ const before = await rings();
60
+
61
+ // Hover the badge.
62
+ await page.locator('[data-edge-count-badge="alpha->beta"]').hover();
63
+ await page.waitForTimeout(300);
64
+ const onHover = await rings();
65
+
66
+ // Mouse far away.
67
+ await page.mouse.move(10, 10);
68
+ await page.waitForTimeout(300);
69
+ const afterLeave = await rings();
70
+
71
+ await browser.close();
72
+
73
+ const results = {
74
+ before_noRings: !before.alpha && !before.beta && !before.gamma,
75
+ hover_alphaRing: onHover.alpha === true,
76
+ hover_betaRing: onHover.beta === true,
77
+ hover_gammaBare: onHover.gamma === false,
78
+ leave_allClear: !afterLeave.alpha && !afterLeave.beta && !afterLeave.gamma,
79
+ };
80
+ const ok = Object.values(results).every(Boolean);
81
+ console.log(`${ok ? '✅' : '❌'} edge-badge hover:`, JSON.stringify(results),
82
+ `\n before=`, before,
83
+ `\n onHover=`, onHover,
84
+ `\n afterLeave=`, afterLeave);
85
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,150 @@
1
+ /* Round 164 verification: edge midpoint count badge gains
2
+ * hover-lift (6th surface in the hover-elevation idiom family).
3
+ *
4
+ * Pre-R164 the badge had R122 hover→edge-brighten propagation
5
+ * but the badge itself stayed static (r=9, strokeWidth=1 or 2).
6
+ * The hover-elevation family covered nodes/panels/group boxes/
7
+ * recent rows/legend rows but not the canvas edge badges.
8
+ *
9
+ * R164:
10
+ * r = (isHoveredEdge || isPinned) ? 10.5 : 9
11
+ * transition r 180ms ease-out
12
+ * data-edge-badge-lifted = 'true' | 'false'
13
+ *
14
+ * Pin still keeps its R121 stroke change (legendHeadline +
15
+ * width 2). Hover and pin share the radius lift.
16
+ *
17
+ * Test:
18
+ * 1. Mock 4 msgs on alpha→beta (R100 threshold = count >= 3)
19
+ * 2. Probe idle radius = 9
20
+ * 3. Hover the edge line → badge r = 10.5
21
+ * 4. Mouseleave → r = 9 again
22
+ * 5. Click badge → pin, r = 10.5 + stroke change
23
+ */
24
+ import { chromium } from 'playwright';
25
+ import { readFileSync } from 'node:fs';
26
+
27
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
28
+ const browser = await chromium.launch({ headless: true });
29
+
30
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
31
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
32
+ await ctx.addInitScript(() => {
33
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
34
+ });
35
+
36
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
37
+ await ctx.route('**/api/hub/status*', async (route) => {
38
+ const r = await route.fetch();
39
+ const b = await r.json();
40
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
41
+ const mk = (alias) => ({
42
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
43
+ network_id: nid, project_dir: null,
44
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
45
+ });
46
+ // 4 sessions so alpha and beta sit 90° apart on the ring
47
+ // (not 180° apart — which would put alpha→beta midpoint
48
+ // exactly on the hub at 500,330 and let the hub's r=18
49
+ // halo intercept the badge hover). gamma + delta are
50
+ // filler with no flows; the only edge badge that renders
51
+ // is alpha↔beta.
52
+ await route.fulfill({ response: r, json: { ...b, sessions: [
53
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
54
+ ] } });
55
+ });
56
+
57
+ const now = Date.now();
58
+ const msgs = [];
59
+ for (let i = 0; i < 4; i++) {
60
+ msgs.push({ id: `m${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
61
+ network_id: 'default', created_at: new Date(now - (10000 + i * 500)).toISOString() });
62
+ }
63
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: msgs } }));
64
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
65
+
66
+ const page = await ctx.newPage();
67
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
68
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
69
+ await page.waitForSelector('[data-edge-count-badge]', { timeout: 10000 });
70
+ await page.waitForTimeout(400);
71
+
72
+ // Probe idle state
73
+ const idle = await page.evaluate(() => {
74
+ const g = document.querySelector('[data-edge-count-badge]');
75
+ if (!g) return null;
76
+ const c = g.querySelector('circle');
77
+ return {
78
+ r: parseFloat(c.getAttribute('r')),
79
+ lifted: c.getAttribute('data-edge-badge-lifted'),
80
+ stroke: c.getAttribute('stroke'),
81
+ strokeWidth: parseFloat(c.getAttribute('stroke-width')),
82
+ };
83
+ });
84
+
85
+ // Hover the badge (it also propagates to hoveredEdgeKey)
86
+ await page.locator('[data-edge-count-badge]').hover();
87
+ await page.waitForTimeout(300);
88
+ const hovered = await page.evaluate(() => {
89
+ const g = document.querySelector('[data-edge-count-badge]');
90
+ if (!g) return null;
91
+ const c = g.querySelector('circle');
92
+ return {
93
+ r: parseFloat(c.getAttribute('r')),
94
+ lifted: c.getAttribute('data-edge-badge-lifted'),
95
+ };
96
+ });
97
+
98
+ // Move cursor away to a known empty area
99
+ await page.mouse.move(10, 10);
100
+ await page.waitForTimeout(300);
101
+ const idleAgain = await page.evaluate(() => {
102
+ const g = document.querySelector('[data-edge-count-badge]');
103
+ if (!g) return null;
104
+ const c = g.querySelector('circle');
105
+ return {
106
+ r: parseFloat(c.getAttribute('r')),
107
+ lifted: c.getAttribute('data-edge-badge-lifted'),
108
+ };
109
+ });
110
+
111
+ // Click badge → pin
112
+ await page.locator('[data-edge-count-badge]').click({ force: true });
113
+ await page.waitForTimeout(300);
114
+ // Move away to clear hover, then probe pinned state.
115
+ await page.mouse.move(10, 10);
116
+ await page.waitForTimeout(300);
117
+ const pinned = await page.evaluate(() => {
118
+ const g = document.querySelector('[data-edge-count-badge]');
119
+ if (!g) return null;
120
+ const c = g.querySelector('circle');
121
+ return {
122
+ r: parseFloat(c.getAttribute('r')),
123
+ lifted: c.getAttribute('data-edge-badge-lifted'),
124
+ strokeWidth: parseFloat(c.getAttribute('stroke-width')),
125
+ pinnedAttr: g.getAttribute('data-edge-count-badge-pinned'),
126
+ };
127
+ });
128
+
129
+ await browser.close();
130
+
131
+ const results = {
132
+ idle_r_9: idle && Math.abs(idle.r - 9) < 0.01,
133
+ idle_lifted_false: idle && idle.lifted === 'false',
134
+ idle_strokeWidth_1: idle && idle.strokeWidth === 1,
135
+ hover_r_10p5: hovered && Math.abs(hovered.r - 10.5) < 0.01,
136
+ hover_lifted_true: hovered && hovered.lifted === 'true',
137
+ idle_again_r_9: idleAgain && Math.abs(idleAgain.r - 9) < 0.01,
138
+ idle_again_lifted_false: idleAgain && idleAgain.lifted === 'false',
139
+ pinned_r_10p5: pinned && Math.abs(pinned.r - 10.5) < 0.01,
140
+ pinned_lifted_true: pinned && pinned.lifted === 'true',
141
+ pinned_strokeWidth_2: pinned && pinned.strokeWidth === 2,
142
+ pinned_attr_true: pinned && pinned.pinnedAttr === 'true',
143
+ };
144
+ const ok = Object.values(results).every(Boolean);
145
+ console.log(`${ok ? '✅' : '❌'} edge badge lift:`, JSON.stringify(results),
146
+ `\n idle =`, idle,
147
+ `\n hovered =`, hovered,
148
+ `\n idleAgain =`, idleAgain,
149
+ `\n pinned =`, pinned);
150
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,111 @@
1
+ /* Round 185 verification: edge badge click fires a one-shot
2
+ * ripple at the badge midpoint — closes the click-feel idiom
3
+ * across hub (R52) / node (R14) / edge badge (R185).
4
+ *
5
+ * Test:
6
+ * 1. Mock 4 sessions + 4-msg alpha→beta flow (R100 threshold)
7
+ * 2. Idle: no ripple <circle> visible
8
+ * 3. Click edge midpoint badge → ripple circle appears with
9
+ * stroke=pal.flowEdge (#67e8f9 cyber), positioned at
10
+ * badge coords (close to alpha-beta midpoint with R164
11
+ * perpendicular lift offset)
12
+ * 4. Wait 700ms → ripple cleared (R14 600ms + buffer)
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: 1500, 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 { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
24
+ });
25
+
26
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
27
+ await ctx.route('**/api/hub/status*', async (route) => {
28
+ const r = await route.fetch();
29
+ const b = await r.json();
30
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
31
+ const mk = (alias) => ({
32
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
33
+ network_id: nid, project_dir: null,
34
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
35
+ });
36
+ // 4 sessions so alpha/beta sit 90° apart (avoid hub overlap)
37
+ await route.fulfill({ response: r, json: { ...b, sessions: [
38
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
39
+ ] } });
40
+ });
41
+
42
+ const now = Date.now();
43
+ const msgs = [];
44
+ for (let i = 0; i < 4; i++) {
45
+ msgs.push({ id: `m${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
46
+ network_id: 'default', created_at: new Date(now - (10000 + i * 500)).toISOString() });
47
+ }
48
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: msgs } }));
49
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
50
+
51
+ const page = await ctx.newPage();
52
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
53
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
54
+ await page.waitForSelector('[data-edge-count-badge]', { timeout: 10000 });
55
+ await page.waitForTimeout(400);
56
+
57
+ // Idle: no click ripple anywhere. The ripple is identified by:
58
+ // <circle stroke="#67e8f9" stroke-width="2" opacity="0"> with an animate child
59
+ // Probe via "circles with stroke-width=2 and a child <animate>".
60
+ const probeRipple = () => page.evaluate(() => {
61
+ const circles = [...document.querySelectorAll('circle[stroke-width="2"]')];
62
+ const ripple = circles.find(c => {
63
+ const anim = c.querySelector('animate[attributeName="r"]');
64
+ return anim && anim.getAttribute('dur') === '0.5s';
65
+ });
66
+ if (!ripple) return null;
67
+ return {
68
+ cx: parseFloat(ripple.getAttribute('cx') || ''),
69
+ cy: parseFloat(ripple.getAttribute('cy') || ''),
70
+ stroke: ripple.getAttribute('stroke'),
71
+ };
72
+ });
73
+
74
+ const idle = await probeRipple();
75
+
76
+ // Capture badge position first
77
+ const badgeBox = await page.locator('[data-edge-count-badge]').first().boundingBox();
78
+
79
+ // Click the edge midpoint badge → ripple should appear
80
+ await page.locator('[data-edge-count-badge]').click();
81
+ await page.waitForTimeout(50);
82
+ const duringRipple = await probeRipple();
83
+
84
+ // Wait past 600ms ripple lifetime + buffer
85
+ await page.waitForTimeout(700);
86
+ const afterRipple = await probeRipple();
87
+
88
+ await browser.close();
89
+
90
+ const results = {
91
+ idle_no_ripple: idle === null,
92
+ ripple_appears: duringRipple !== null,
93
+ ripple_stroke_cyan: duringRipple?.stroke === '#67e8f9',
94
+ ripple_has_position: duringRipple !== null
95
+ && Number.isFinite(duringRipple.cx)
96
+ && Number.isFinite(duringRipple.cy),
97
+ // The badge has bbox in screen coords; the ripple is in SVG viewBox
98
+ // coords. We can't compare directly. Just verify it's a sane number
99
+ // (not 0/NaN) — that's enough to know it's placed at the badge.
100
+ ripple_cx_nonzero: duringRipple !== null && duringRipple.cx > 0,
101
+ ripple_cy_nonzero: duringRipple !== null && duringRipple.cy > 0,
102
+ ripple_cleared_after_600: afterRipple === null,
103
+ badge_was_clickable: badgeBox !== null,
104
+ };
105
+ const ok = Object.values(results).every(Boolean);
106
+ console.log(`${ok ? '✅' : '❌'} edge badge ripple:`, JSON.stringify(results),
107
+ `\n idle:`, idle,
108
+ `\n duringRipple:`, duringRipple,
109
+ `\n afterRipple:`, afterRipple,
110
+ `\n badgeBox:`, badgeBox);
111
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,118 @@
1
+ /* Round 188 verification: edge badge stroke + stroke-width
2
+ * transitions on hot-lane threshold crossing.
3
+ *
4
+ * Pre-R188 the badge had `transition: r 180ms ease-out` (from
5
+ * R164) but stroke/strokeWidth snapped when:
6
+ * - count crossed 10 (R126 cyan ↔ amber + 1 ↔ 2)
7
+ * - user clicked to pin (R121 cyan ↔ legendHeadline + 1 ↔ 2)
8
+ *
9
+ * R188 extends the transition list:
10
+ * r 180ms, stroke 300ms, stroke-width 300ms
11
+ *
12
+ * Test:
13
+ * 1. Mock 2 edges: alpha→beta (count=4, not hot),
14
+ * alpha→gamma (count=12, hot)
15
+ * 2. Probe both badges in idle state:
16
+ * - cold badge: stroke=#67e8f9 (flowEdge cyan), strokeWidth=1
17
+ * - hot badge: stroke=#fbbf24 (hotStroke cyber amber), strokeWidth=2
18
+ * 3. Both badges carry transition string including all three:
19
+ * r 180ms, stroke 300ms, stroke-width 300ms
20
+ */
21
+ import { chromium } from 'playwright';
22
+ import { readFileSync } from 'node:fs';
23
+
24
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
25
+ const browser = await chromium.launch({ headless: true });
26
+
27
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
28
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
29
+ await ctx.addInitScript(() => {
30
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } 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 mk = (alias) => ({
39
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
40
+ network_id: nid, project_dir: null,
41
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
42
+ });
43
+ // 4 sessions so node midpoints are well-separated from hub
44
+ await route.fulfill({ response: r, json: { ...b, sessions: [
45
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
46
+ ] } });
47
+ });
48
+
49
+ const now = Date.now();
50
+ const msgs = [];
51
+ // alpha→beta: 4 msgs (cold; count >= 3 to render badge, < 10 not hot)
52
+ for (let i = 0; i < 4; i++) {
53
+ msgs.push({ id: `cold${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
54
+ network_id: 'default', created_at: new Date(now - (10000 + i * 100)).toISOString() });
55
+ }
56
+ // alpha→gamma: 12 msgs (hot; count >= 10)
57
+ for (let i = 0; i < 12; i++) {
58
+ msgs.push({ id: `hot${i}`, from_alias: 'alpha', to_alias: 'gamma', content: 'hi',
59
+ network_id: 'default', created_at: new Date(now - (20000 + i * 100)).toISOString() });
60
+ }
61
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: msgs } }));
62
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
63
+
64
+ const page = await ctx.newPage();
65
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
66
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
67
+ await page.waitForSelector('[data-edge-count-badge]', { timeout: 10000 });
68
+ await page.waitForTimeout(500);
69
+
70
+ const probe = await page.evaluate(() => {
71
+ const badges = [...document.querySelectorAll('[data-edge-count-badge]')];
72
+ return badges.map(b => {
73
+ const circle = b.querySelector('circle');
74
+ return {
75
+ key: b.getAttribute('data-edge-count-badge'),
76
+ hot: b.getAttribute('data-edge-count-badge-hot'),
77
+ stroke: circle?.getAttribute('stroke'),
78
+ strokeWidth: parseFloat(circle?.getAttribute('stroke-width') || ''),
79
+ transition: circle?.style?.transition || getComputedStyle(circle).transition,
80
+ };
81
+ });
82
+ });
83
+
84
+ await browser.close();
85
+
86
+ const cold = probe.find(p => p.hot === 'false');
87
+ const hot = probe.find(p => p.hot === 'true');
88
+
89
+ const hasTransition = (s, prop) => {
90
+ const t = s?.transition || '';
91
+ return t.includes(`${prop} 300ms`) ||
92
+ new RegExp(`${prop}\\s+0\\.3s|${prop}\\s+300ms`).test(t);
93
+ };
94
+ const hasR180 = (s) => {
95
+ const t = s?.transition || '';
96
+ return t.includes('r 180ms') || /(^|[\s,])r\s+(180ms|0\.18s)/.test(t);
97
+ };
98
+
99
+ const results = {
100
+ two_badges_present: probe.length === 2,
101
+ cold_badge_found: cold !== undefined,
102
+ hot_badge_found: hot !== undefined,
103
+ cold_stroke_cyan: cold?.stroke === '#67e8f9',
104
+ cold_strokeWidth_1: cold?.strokeWidth === 1,
105
+ hot_stroke_amber: hot?.stroke === '#fbbf24',
106
+ hot_strokeWidth_2: hot?.strokeWidth === 2,
107
+ cold_has_r_transition: hasR180(cold),
108
+ cold_has_stroke_transition: hasTransition(cold, 'stroke'),
109
+ cold_has_strokewidth_transition: hasTransition(cold, 'stroke-width'),
110
+ hot_has_r_transition: hasR180(hot),
111
+ hot_has_stroke_transition: hasTransition(hot, 'stroke'),
112
+ hot_has_strokewidth_transition: hasTransition(hot, 'stroke-width'),
113
+ };
114
+ const ok = Object.values(results).every(Boolean);
115
+ console.log(`${ok ? '✅' : '❌'} edge badge stroke transition:`, JSON.stringify(results),
116
+ `\n cold:`, cold,
117
+ `\n hot:`, hot);
118
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,121 @@
1
+ /* Round 224 verification: edge midpoint count badge <text> gains
2
+ * tabular-nums (digit-width stable) and a letter-spacing pin
3
+ * signature — 4th surface in the pin-signature typography family
4
+ * after R218 group label / R219 legend row / R220 recent row.
5
+ *
6
+ * The badge stroke already eased on isPinned/isHot flips (R188).
7
+ * Pre-R224 the digit itself was dead-typographic — no transition,
8
+ * no width-stable digits (9 → 10 boundary jitters the centered
9
+ * glyph), no typographic signal on hot/pin crossings.
10
+ *
11
+ * Test scenarios:
12
+ * A. cold edge (count=4): pin='false', letter-spacing ≈ 0 ('normal' or '0px')
13
+ * B. hot edge (count=12): pin='true', letter-spacing ≈ 0.4px
14
+ * Both: fontVariantNumeric includes 'tabular-nums';
15
+ * transition includes 'letter-spacing 300ms';
16
+ * data-edge-badge-text + data-edge-badge-text-pin attrs.
17
+ */
18
+ import { chromium } from 'playwright';
19
+ import { readFileSync } from 'node:fs';
20
+
21
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
22
+ const browser = await chromium.launch({ headless: true });
23
+
24
+ async function setup(messages) {
25
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
26
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
27
+ await ctx.addInitScript(() => {
28
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
29
+ });
30
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
31
+ await ctx.route('**/api/hub/status*', async (route) => {
32
+ const r = await route.fetch();
33
+ const b = await r.json();
34
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
35
+ const mk = (alias) => ({
36
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
37
+ network_id: nid, project_dir: null,
38
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
39
+ });
40
+ await route.fulfill({ response: r, json: { ...b, sessions: [
41
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
42
+ ] } });
43
+ });
44
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages } }));
45
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.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.waitForSelector('[data-edge-badge-text]', { timeout: 10000, state: 'attached' });
50
+ await page.waitForTimeout(500);
51
+ return page;
52
+ }
53
+
54
+ const probe = async (page) => page.evaluate(() => {
55
+ const txt = document.querySelector('[data-edge-badge-text]');
56
+ if (!txt) return null;
57
+ const cs = getComputedStyle(txt);
58
+ // letter-spacing computed value: '0px' is normalised to 'normal' in
59
+ // some renderers; numeric 0.4px resolves to '0.4px'. Accept both
60
+ // forms for the cold case.
61
+ const ls = cs.letterSpacing;
62
+ const lsNum = ls === 'normal' ? 0 : parseFloat(ls);
63
+ return {
64
+ pin: txt.getAttribute('data-edge-badge-text-pin'),
65
+ inlineLS: txt.style.letterSpacing,
66
+ computedLS: ls,
67
+ lsNum: lsNum,
68
+ fvn: cs.fontVariantNumeric,
69
+ inlineTrans: txt.style.transition,
70
+ text: txt.textContent,
71
+ };
72
+ });
73
+
74
+ const now = Date.now();
75
+ // Scenario A: cold edge — 4 messages alpha→beta (count=4, not hot, not pinned)
76
+ const coldMsgs = [];
77
+ for (let i = 0; i < 4; i++) {
78
+ coldMsgs.push({
79
+ id: `cold${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
80
+ network_id: 'default', created_at: new Date(now - (1000 + i * 50)).toISOString(),
81
+ });
82
+ }
83
+ const pageA = await setup(coldMsgs);
84
+ const probeA = await probe(pageA);
85
+ await pageA.close();
86
+
87
+ // Scenario B: hot edge — 12 messages alpha→beta (count=12, isHot=true)
88
+ const hotMsgs = [];
89
+ for (let i = 0; i < 12; i++) {
90
+ hotMsgs.push({
91
+ id: `hot${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
92
+ network_id: 'default', created_at: new Date(now - (1000 + i * 50)).toISOString(),
93
+ });
94
+ }
95
+ const pageB = await setup(hotMsgs);
96
+ const probeB = await probe(pageB);
97
+ await pageB.close();
98
+ await browser.close();
99
+
100
+ const results = {
101
+ // A: cold — pin='false', letter-spacing baseline (0 / 'normal' / '0px')
102
+ A_text_present: probeA !== null,
103
+ A_pin_false: probeA?.pin === 'false',
104
+ A_ls_baseline: probeA?.lsNum === 0,
105
+ A_text_is_count_4: probeA?.text === '4',
106
+ A_tabular_nums: /tabular-nums/.test(probeA?.fvn || ''),
107
+ A_transition_has_ls: /letter-spacing\s+(?:300ms|0\.3s)/.test(probeA?.inlineTrans || ''),
108
+
109
+ // B: hot — pin='true', letter-spacing widened to 0.4px
110
+ B_text_present: probeB !== null,
111
+ B_pin_true: probeB?.pin === 'true',
112
+ B_ls_widened: Math.abs((probeB?.lsNum ?? 0) - 0.4) < 0.05,
113
+ B_text_is_count_12: probeB?.text === '12',
114
+ B_tabular_nums: /tabular-nums/.test(probeB?.fvn || ''),
115
+ B_transition_has_ls: /letter-spacing\s+(?:300ms|0\.3s)/.test(probeB?.inlineTrans || ''),
116
+ };
117
+ const ok = Object.values(results).every(Boolean);
118
+ console.log(`${ok ? '✅' : '❌'} edge-badge-text pin-signature:`, JSON.stringify(results),
119
+ `\n probeA (cold count=4):`, probeA,
120
+ `\n probeB (hot count=12):`, probeB);
121
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,89 @@
1
+ /* Round 251 verification: edge midpoint badge circle picks up fill +
2
+ * opacity in its transition list, closing the last per-edge theme-
3
+ * toggle snap.
4
+ *
5
+ * Pre-R251 transition was 'r 180ms ease-out, stroke 300ms ease-out,
6
+ * stroke-width 300ms ease-out'. Two theme-driven props snapped:
7
+ * · fill (pal.legendBox.fill: cyber #020617 ↔ light #ffffff)
8
+ * · opacity (cyber 0.82 ↔ light 0.95)
9
+ *
10
+ * R251 adds 'fill 200ms ease-out, opacity 200ms ease-out' so all
11
+ * five transition properties co-ease.
12
+ *
13
+ * Test: render with a single flow link (count ≥ 3 → badge visible),
14
+ * probe [data-edge-badge-lifted] (the badge circle carries this
15
+ * attribute). Verify transition list contains all 5 properties at
16
+ * their respective durations.
17
+ */
18
+ import { chromium } from 'playwright';
19
+ import { readFileSync } from 'node:fs';
20
+
21
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
22
+ const browser = await chromium.launch({ headless: true });
23
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
24
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
25
+ await ctx.addInitScript(() => {
26
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
27
+ });
28
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
29
+ await ctx.route('**/api/hub/status*', async (route) => {
30
+ const r = await route.fetch();
31
+ const b = await r.json();
32
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
33
+ const mk = (alias) => ({
34
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
35
+ 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
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
40
+ ] } });
41
+ });
42
+ // 5 messages alpha→beta → link.count=5 ≥ 3 → badge visible
43
+ const now = Date.now();
44
+ const msgs = [];
45
+ for (let i = 0; i < 5; i++) {
46
+ msgs.push({
47
+ id: `m${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
48
+ network_id: 'default', created_at: new Date(now - (1000 + i * 50)).toISOString(),
49
+ });
50
+ }
51
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: msgs } }));
52
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.fulfill({ json: { tasks: [] } }));
53
+
54
+ const page = await ctx.newPage();
55
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
56
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
57
+ await page.waitForSelector('[data-edge-badge-lifted]', { timeout: 10000, state: 'attached' });
58
+ await page.waitForTimeout(300);
59
+
60
+ const probe = await page.evaluate(() => {
61
+ const el = document.querySelector('[data-edge-badge-lifted]');
62
+ if (!el) return null;
63
+ return {
64
+ transition: el.style.transition,
65
+ fillAttr: el.getAttribute('fill'),
66
+ opacityAttr: el.getAttribute('opacity'),
67
+ };
68
+ });
69
+ await browser.close();
70
+
71
+ const hasProp = (s, prop, durations) =>
72
+ durations.some(d => new RegExp(`${prop}\\s+(?:${d}ms|0\\.${(d / 100).toString().replace(/^0\./, '')}s)`).test(s || ''));
73
+
74
+ // Helper: 180 → 0.18s, 200 → 0.2s, 300 → 0.3s
75
+ const has = (prop, ms) =>
76
+ new RegExp(`${prop}\\s+(?:${ms}ms|0\\.${(ms / 1000).toString().replace(/^0\./, '')}s)`).test(probe?.transition || '');
77
+
78
+ const results = {
79
+ badge_present: probe !== null,
80
+ has_r_180ms: has('r', 180),
81
+ has_stroke_300ms: has('stroke', 300),
82
+ has_stroke_width_300ms: has('stroke-width', 300),
83
+ has_fill_200ms: has('fill', 200),
84
+ has_opacity_200ms: has('opacity', 200),
85
+ };
86
+ const ok = Object.values(results).every(Boolean);
87
+ console.log(`${ok ? '✅' : '❌'} edge badge theme ease:`, JSON.stringify(results),
88
+ '\n probe:', probe);
89
+ process.exit(ok ? 0 : 1);