@sleep2agi/agent-network-dashboard 0.4.6 → 0.5.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/0aauz~36q5n2a.css +2 -0
  173. package/.next/static/chunks/0f~0kgy2pycyc.js +1 -0
  174. package/.next/static/chunks/0l_~q07bhpkcx.js +7 -0
  175. package/.next/static/chunks/1175-dp18a1e_.js +1 -0
  176. package/.next/static/chunks/1552j0415oeal.js +4 -0
  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 +6409 -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-pulse-ease-test.mjs +120 -0
  216. package/scripts/topo-brand-logo-transition-test.mjs +94 -0
  217. package/scripts/topo-brand-watermark-test.mjs +97 -0
  218. package/scripts/topo-canvas-aria-test.mjs +108 -0
  219. package/scripts/topo-canvas-brand-fade-test.mjs +87 -0
  220. package/scripts/topo-chat-target-breath-test.mjs +84 -0
  221. package/scripts/topo-chat-target-ring-ease-test.mjs +82 -0
  222. package/scripts/topo-chat-target-ring-fade-test.mjs +124 -0
  223. package/scripts/topo-chip-empty-recede-test.mjs +116 -0
  224. package/scripts/topo-chip-focus-test.mjs +95 -0
  225. package/scripts/topo-chip-pin-mirror-test.mjs +113 -0
  226. package/scripts/topo-chip-row-gap-test.mjs +90 -0
  227. package/scripts/topo-chip-row-semantic-gap-test.mjs +97 -0
  228. package/scripts/topo-chip-row-tabular-test.mjs +111 -0
  229. package/scripts/topo-chip-unit-demotion-test.mjs +93 -0
  230. package/scripts/topo-chiprow-data-weight-test.mjs +94 -0
  231. package/scripts/topo-chrome-a11y-test.mjs +107 -0
  232. package/scripts/topo-chrome-active-weight-test.mjs +104 -0
  233. package/scripts/topo-chrome-focus-ring-unify-test.mjs +100 -0
  234. package/scripts/topo-chrome-gap-test.mjs +73 -0
  235. package/scripts/topo-chrome-icon-stroke-test.mjs +95 -0
  236. package/scripts/topo-chrome-inactive-color-test.mjs +90 -0
  237. package/scripts/topo-chrome-pop-extension-test.mjs +98 -0
  238. package/scripts/topo-chrome-pop-nodesize-test.mjs +88 -0
  239. package/scripts/topo-chrome-press-state-test.mjs +135 -0
  240. package/scripts/topo-chrome-strip-semantic-gap-test.mjs +99 -0
  241. package/scripts/topo-chrome-strip-theme-ease-test.mjs +99 -0
  242. package/scripts/topo-chrome-zoom-level-pop-test.mjs +122 -0
  243. package/scripts/topo-chrome-zoom-pop-test.mjs +130 -0
  244. package/scripts/topo-chrome-zoom-readout-weight-test.mjs +81 -0
  245. package/scripts/topo-click-ripple-ease-test.mjs +94 -0
  246. package/scripts/topo-cmdk-edge-clear-test.mjs +109 -0
  247. package/scripts/topo-cmdk-layout-view-test.mjs +108 -0
  248. package/scripts/topo-cmdk-pin-test.mjs +123 -0
  249. package/scripts/topo-cmdk-vendor-clear-test.mjs +133 -0
  250. package/scripts/topo-cmdk-vendor-pin-test.mjs +103 -0
  251. package/scripts/topo-corner-radius-unify-test.mjs +103 -0
  252. package/scripts/topo-dense-alias-fill-transition-test.mjs +133 -0
  253. package/scripts/topo-discrete-zoom-smooth-test.mjs +125 -0
  254. package/scripts/topo-edge-badge-click-pin-test.mjs +94 -0
  255. package/scripts/topo-edge-badge-crossfade-test.mjs +138 -0
  256. package/scripts/topo-edge-badge-hot-test.mjs +115 -0
  257. package/scripts/topo-edge-badge-hover-test.mjs +85 -0
  258. package/scripts/topo-edge-badge-lift-test.mjs +150 -0
  259. package/scripts/topo-edge-badge-ripple-test.mjs +111 -0
  260. package/scripts/topo-edge-badge-stroke-transition-test.mjs +118 -0
  261. package/scripts/topo-edge-badge-text-pin-test.mjs +121 -0
  262. package/scripts/topo-edge-badge-theme-ease-test.mjs +89 -0
  263. package/scripts/topo-edge-count-badge-test.mjs +86 -0
  264. package/scripts/topo-edge-endpoint-highlight-test.mjs +111 -0
  265. package/scripts/topo-edge-fadein-test.mjs +106 -0
  266. package/scripts/topo-edge-focus-test.mjs +113 -0
  267. package/scripts/topo-edge-hover-test.mjs +13 -5
  268. package/scripts/topo-edge-pill-hot-test.mjs +129 -0
  269. package/scripts/topo-edge-pill-test.mjs +105 -0
  270. package/scripts/topo-edge-ping-pulse-retired-test.mjs +119 -0
  271. package/scripts/topo-edge-stroke-ease-test.mjs +100 -0
  272. package/scripts/topo-edge-stroke-transition-test.mjs +132 -0
  273. package/scripts/topo-empty-hint-tracking-test.mjs +84 -0
  274. package/scripts/topo-empty-state-breath-test.mjs +150 -0
  275. package/scripts/topo-empty-state-crossfade-test.mjs +130 -0
  276. package/scripts/topo-empty-state-recenter-test.mjs +107 -0
  277. package/scripts/topo-empty-subhint-tracking-test.mjs +87 -0
  278. package/scripts/topo-endpoint-ring-fade-test.mjs +132 -0
  279. package/scripts/topo-endpoint-ring-test.mjs +93 -0
  280. package/scripts/topo-endpoint-ring-thicken-test.mjs +112 -0
  281. package/scripts/topo-esc-clears-pin-test.mjs +98 -0
  282. package/scripts/topo-filter-pill-aliases-test.mjs +121 -0
  283. package/scripts/topo-filter-pill-body-click-test.mjs +112 -0
  284. package/scripts/topo-filter-pill-count-tabular-test.mjs +135 -0
  285. package/scripts/topo-filter-pill-count-test.mjs +110 -0
  286. package/scripts/topo-filter-pill-mobile-test.mjs +90 -0
  287. package/scripts/topo-freshness-chip-transition-test.mjs +80 -0
  288. package/scripts/topo-freshness-chip-weight-test.mjs +90 -0
  289. package/scripts/topo-freshness-conditional-render-test.mjs +92 -0
  290. package/scripts/topo-fullscreen-active-test.mjs +115 -0
  291. package/scripts/topo-group-box-pinned-state-test.mjs +95 -0
  292. package/scripts/topo-group-count-test.mjs +6 -1
  293. package/scripts/topo-group-label-a11y-test.mjs +113 -0
  294. package/scripts/topo-group-label-count-inherit-test.mjs +99 -0
  295. package/scripts/topo-group-label-hover-test.mjs +119 -0
  296. package/scripts/topo-group-label-pin-spacing-test.mjs +136 -0
  297. package/scripts/topo-group-label-tint-test.mjs +99 -0
  298. package/scripts/topo-group-label-title-test.mjs +83 -0
  299. package/scripts/topo-group-pin-test.mjs +119 -0
  300. package/scripts/topo-group-pip-fadein-test.mjs +104 -0
  301. package/scripts/topo-group-pip-singletier-drop-test.mjs +131 -0
  302. package/scripts/topo-group-pip-tabular-test.mjs +95 -0
  303. package/scripts/topo-group-status-pips-test.mjs +103 -0
  304. package/scripts/topo-groupbox-fadein-test.mjs +95 -0
  305. package/scripts/topo-groupbox-hover-lift-test.mjs +127 -0
  306. package/scripts/topo-groupbox-hubcore-ease-test.mjs +106 -0
  307. package/scripts/topo-groupbox-live-test.mjs +100 -0
  308. package/scripts/topo-groupbox-march-busyness-test.mjs +155 -0
  309. package/scripts/topo-groupbox-test.mjs +4 -2
  310. package/scripts/topo-header-gap-test.mjs +94 -0
  311. package/scripts/topo-help-overlay-r109-test.mjs +67 -0
  312. package/scripts/topo-help-overlay-r118-test.mjs +69 -0
  313. package/scripts/topo-help-overlay-r123-test.mjs +69 -0
  314. package/scripts/topo-help-overlay-r134-test.mjs +74 -0
  315. package/scripts/topo-help-overlay-r137-test.mjs +81 -0
  316. package/scripts/topo-help-overlay-r141-test.mjs +86 -0
  317. package/scripts/topo-help-overlay-r153-test.mjs +85 -0
  318. package/scripts/topo-help-overlay-r59-test.mjs +67 -0
  319. package/scripts/topo-help-overlay-r65-test.mjs +64 -0
  320. package/scripts/topo-help-overlay-r70-test.mjs +63 -0
  321. package/scripts/topo-help-overlay-r78-test.mjs +59 -0
  322. package/scripts/topo-help-overlay-r81-test.mjs +58 -0
  323. package/scripts/topo-help-overlay-r87-test.mjs +64 -0
  324. package/scripts/topo-help-overlay-r91-test.mjs +65 -0
  325. package/scripts/topo-hot-tail-crossfade-test.mjs +117 -0
  326. package/scripts/topo-html-overlay-padding-unify-test.mjs +120 -0
  327. package/scripts/topo-hub-a11y-test.mjs +112 -0
  328. package/scripts/topo-hub-busyness-test.mjs +100 -0
  329. package/scripts/topo-hub-center-crossfade-test.mjs +124 -0
  330. package/scripts/topo-hub-click-fit-test.mjs +137 -0
  331. package/scripts/topo-hub-digit-scale-test.mjs +129 -0
  332. package/scripts/topo-hub-fadein-test.mjs +88 -0
  333. package/scripts/topo-hub-hover-lift-test.mjs +101 -0
  334. package/scripts/topo-hub-hover-ring-test.mjs +82 -0
  335. package/scripts/topo-hub-pip-theme-ease-test.mjs +116 -0
  336. package/scripts/topo-hub-spoke-ease-test.mjs +120 -0
  337. package/scripts/topo-hub-title-test.mjs +7 -2
  338. package/scripts/topo-hub-working-count-test.mjs +99 -0
  339. package/scripts/topo-ingroup-edge-test.mjs +103 -0
  340. package/scripts/topo-inner-radar-ring-retired-test.mjs +89 -0
  341. package/scripts/topo-kicker-color-test.mjs +93 -0
  342. package/scripts/topo-kicker-tracking-test.mjs +101 -0
  343. package/scripts/topo-kicker-weight-test.mjs +98 -0
  344. package/scripts/topo-label-card-theme-ease-test.mjs +85 -0
  345. package/scripts/topo-label-lift-test.mjs +5 -1
  346. package/scripts/topo-layout-hover-text-preview-test.mjs +101 -0
  347. package/scripts/topo-layout-switch-crossfade-test.mjs +126 -0
  348. package/scripts/topo-layout-toggle-border-unify-test.mjs +104 -0
  349. package/scripts/topo-layout-toggle-test.mjs +131 -0
  350. package/scripts/topo-layout-trailer-mr-test.mjs +82 -0
  351. package/scripts/topo-legend-compress-test.mjs +107 -0
  352. package/scripts/topo-legend-count-empty-test.mjs +126 -0
  353. package/scripts/topo-legend-count-tabular-parity-test.mjs +90 -0
  354. package/scripts/topo-legend-count-tabular-test.mjs +87 -0
  355. package/scripts/topo-legend-count-test.mjs +88 -0
  356. package/scripts/topo-legend-count-tier-fill-test.mjs +121 -0
  357. package/scripts/topo-legend-count-weight-test.mjs +86 -0
  358. package/scripts/topo-legend-header-symmetric-inset-test.mjs +112 -0
  359. package/scripts/topo-legend-hitbox-swatch-center-test.mjs +93 -0
  360. package/scripts/topo-legend-hover-test.mjs +113 -0
  361. package/scripts/topo-legend-offline-delimiter-test.mjs +84 -0
  362. package/scripts/topo-legend-offline-label-simplify-test.mjs +86 -0
  363. package/scripts/topo-legend-panel-count-weight-test.mjs +89 -0
  364. package/scripts/topo-legend-panel-header-test.mjs +83 -0
  365. package/scripts/topo-legend-pin-ring-fade-test.mjs +112 -0
  366. package/scripts/topo-legend-pin-test.mjs +128 -0
  367. package/scripts/topo-legend-row-hover-lift-test.mjs +109 -0
  368. package/scripts/topo-legend-row-labels-simplify-test.mjs +80 -0
  369. package/scripts/topo-legend-row-pin-spacing-test.mjs +124 -0
  370. package/scripts/topo-legend-row-tint-test.mjs +95 -0
  371. package/scripts/topo-legend-row-title-test.mjs +102 -0
  372. package/scripts/topo-legend-swatch-grow-test.mjs +116 -0
  373. package/scripts/topo-legend-swatch-radius-test.mjs +78 -0
  374. package/scripts/topo-minimap-a11y-test.mjs +101 -0
  375. package/scripts/topo-minimap-dot-transition-test.mjs +113 -0
  376. package/scripts/topo-minimap-radius-test.mjs +88 -0
  377. package/scripts/topo-minimap-rect-stroke-test.mjs +94 -0
  378. package/scripts/topo-minimap-viewport-smooth-test.mjs +124 -0
  379. package/scripts/topo-monogram-font-test.mjs +112 -0
  380. package/scripts/topo-monogram-stroke-test.mjs +101 -0
  381. package/scripts/topo-more-flows-letterspacing-test.mjs +111 -0
  382. package/scripts/topo-node-a11y-test.mjs +107 -0
  383. package/scripts/topo-node-alias-chat-pin-test.mjs +95 -0
  384. package/scripts/topo-node-hover-lift-test.mjs +85 -0
  385. package/scripts/topo-node-label-card-elevation-test.mjs +157 -0
  386. package/scripts/topo-node-label-card-stroke-test.mjs +135 -0
  387. package/scripts/topo-node-stroke-transition-test.mjs +103 -0
  388. package/scripts/topo-node-text-fill-transition-test.mjs +119 -0
  389. package/scripts/topo-node-title-peers-test.mjs +107 -0
  390. package/scripts/topo-node-title-test.mjs +101 -0
  391. package/scripts/topo-nodesize-active-hover-test.mjs +109 -0
  392. package/scripts/topo-nodesize-crossfade-test.mjs +141 -0
  393. package/scripts/topo-online-chip-nav-test.mjs +143 -0
  394. package/scripts/topo-orbit-particles-retired-test.mjs +91 -0
  395. package/scripts/topo-panel-count-unit-test.mjs +112 -0
  396. package/scripts/topo-panel-elevation-test.mjs +76 -0
  397. package/scripts/topo-panel-fadein-test.mjs +102 -0
  398. package/scripts/topo-panel-hot-tail-fade-test.mjs +132 -0
  399. package/scripts/topo-panel-hover-elevation-test.mjs +118 -0
  400. package/scripts/topo-panel-rx-test.mjs +90 -0
  401. package/scripts/topo-panel-theme-ease-test.mjs +85 -0
  402. package/scripts/topo-panel-title-fill-ease-test.mjs +91 -0
  403. package/scripts/topo-panel-title-tracking-test.mjs +78 -0
  404. package/scripts/topo-particle-stagger-test.mjs +82 -0
  405. package/scripts/topo-pin-intersection-color-ease-test.mjs +111 -0
  406. package/scripts/topo-pin-intersection-empty-test.mjs +129 -0
  407. package/scripts/topo-pin-intersection-tabular-test.mjs +96 -0
  408. package/scripts/topo-pin-intersection-test.mjs +121 -0
  409. package/scripts/topo-pin-intersection-warning-crossfade-test.mjs +107 -0
  410. package/scripts/topo-pin-mirror-transition-test.mjs +101 -0
  411. package/scripts/topo-pin-persistence-test.mjs +105 -0
  412. package/scripts/topo-prefix-opacity-test.mjs +107 -0
  413. package/scripts/topo-prepublish-guard-test.mjs +95 -0
  414. package/scripts/topo-pressure-hover-test.mjs +123 -0
  415. package/scripts/topo-pressure-pin-test.mjs +119 -0
  416. package/scripts/topo-pressure-seg-self-hover-test.mjs +126 -0
  417. package/scripts/topo-pressure-seg-tooltip-test.mjs +98 -0
  418. package/scripts/topo-pressure-seg-transition-test.mjs +107 -0
  419. package/scripts/topo-pulse-dot-crossfade-test.mjs +122 -0
  420. package/scripts/topo-pulse-dot-retired-test.mjs +86 -0
  421. package/scripts/topo-recent-panel-count-freshness-test.mjs +109 -0
  422. package/scripts/topo-recent-panel-count-test.mjs +72 -0
  423. package/scripts/topo-recent-panel-count-weight-test.mjs +91 -0
  424. package/scripts/topo-recent-panel-footer-breath-test.mjs +142 -0
  425. package/scripts/topo-recent-panel-hot-header-test.mjs +132 -0
  426. package/scripts/topo-recent-panel-hot-tabular-test.mjs +113 -0
  427. package/scripts/topo-recent-panel-more-crossfade-test.mjs +124 -0
  428. package/scripts/topo-recent-panel-more-cyan-hover-test.mjs +151 -0
  429. package/scripts/topo-recent-panel-more-nav-test.mjs +127 -0
  430. package/scripts/topo-recent-panel-more-test.mjs +106 -0
  431. package/scripts/topo-recent-panel-readability-bump-test.mjs +151 -0
  432. package/scripts/topo-recent-row-count-fill-test.mjs +129 -0
  433. package/scripts/topo-recent-row-count-weight-test.mjs +112 -0
  434. package/scripts/topo-recent-row-fadein-test.mjs +107 -0
  435. package/scripts/topo-recent-row-freshness-test.mjs +102 -0
  436. package/scripts/topo-recent-row-hot-test.mjs +110 -0
  437. package/scripts/topo-recent-row-hover-lift-test.mjs +128 -0
  438. package/scripts/topo-recent-row-hover-test.mjs +122 -0
  439. package/scripts/topo-recent-row-pin-spacing-test.mjs +137 -0
  440. package/scripts/topo-recent-row-pin-test.mjs +126 -0
  441. package/scripts/topo-recent-row-tint-test.mjs +91 -0
  442. package/scripts/topo-recent-row-title-test.mjs +112 -0
  443. package/scripts/topo-recent-row-ts-alpha-test.mjs +100 -0
  444. package/scripts/topo-recent-row-ts-tabular-test.mjs +108 -0
  445. package/scripts/topo-recent-row-ts-test.mjs +95 -0
  446. package/scripts/topo-recent-row-typography-test.mjs +119 -0
  447. package/scripts/topo-recent-signal-empty-hint-test.mjs +73 -0
  448. package/scripts/topo-reset-spin-test.mjs +120 -0
  449. package/scripts/topo-runtime-badge-grow-test.mjs +114 -0
  450. package/scripts/topo-spokes-retired-test.mjs +93 -0
  451. package/scripts/topo-starfield-density-test.mjs +89 -0
  452. package/scripts/topo-status-chip-hover-test.mjs +101 -0
  453. package/scripts/topo-status-chip-tooltip-test.mjs +95 -0
  454. package/scripts/topo-statusbar-chips-self-hover-test.mjs +177 -0
  455. package/scripts/topo-svg-focus-test.mjs +112 -0
  456. package/scripts/topo-sweep-busyness-test.mjs +94 -0
  457. package/scripts/topo-tabular-nums-sweep-test.mjs +93 -0
  458. package/scripts/topo-tier-guide-dashes-test.mjs +86 -0
  459. package/scripts/topo-tier-ring-fadein-test.mjs +107 -0
  460. package/scripts/topo-tier-ring-occupancy-test.mjs +87 -0
  461. package/scripts/topo-tier-ring-test.mjs +69 -0
  462. package/scripts/topo-tier-ring-tint-test.mjs +110 -0
  463. package/scripts/topo-tier-stagger-test.mjs +89 -0
  464. package/scripts/topo-title-block-tight-leading-test.mjs +95 -0
  465. package/scripts/topo-title-tracking-test.mjs +98 -0
  466. package/scripts/topo-titleblock-gap-test.mjs +86 -0
  467. package/scripts/topo-titleblock-margin-test.mjs +83 -0
  468. package/scripts/topo-toggle-inactive-cyan-hover-test.mjs +110 -0
  469. package/scripts/topo-top-rail-theme-ease-test.mjs +89 -0
  470. package/scripts/topo-vendor-chip-hover-test.mjs +111 -0
  471. package/scripts/topo-vendor-chip-self-hover-test.mjs +131 -0
  472. package/scripts/topo-vendor-chip-tabular-test.mjs +92 -0
  473. package/scripts/topo-vendor-chip-threshold-test.mjs +78 -0
  474. package/scripts/topo-vendor-chip-weight-test.mjs +87 -0
  475. package/scripts/topo-vendor-count-suffix-test.mjs +95 -0
  476. package/scripts/topo-vendor-letter-tooltip-test.mjs +92 -0
  477. package/scripts/topo-vendor-pill-test.mjs +125 -0
  478. package/scripts/topo-vendor-pin-test.mjs +125 -0
  479. package/scripts/topo-view-smooth-test.mjs +131 -0
  480. package/scripts/topo-watermark-tracking-test.mjs +99 -0
  481. package/scripts/topo-working-chip-pin-test.mjs +154 -0
  482. package/scripts/topo-working-halo-breath-retired-test.mjs +99 -0
  483. package/scripts/topo-wrapper-flow-arrow-theme-test.mjs +97 -0
  484. package/scripts/topo-wrapper-radius-test.mjs +73 -0
  485. package/scripts/topo-wrapper-shadow-theme-ease-test.mjs +89 -0
  486. package/.next/static/chunks/088ouqy7tekty.js +0 -2
  487. package/.next/static/chunks/0hyl7mdapsz38.js +0 -1
  488. package/.next/static/chunks/0mqwn-l7wtcdo.js +0 -7
  489. package/.next/static/chunks/127kw7~wsnyu7.css +0 -2
  490. package/.next/static/chunks/16yfxp9ak2gdm.js +0 -1
  491. /package/.next/static/{23MxP0RBVA4BaOJ_HMYmM → 424zX3i80AJ_wHF_otKuP}/_buildManifest.js +0 -0
  492. /package/.next/static/{23MxP0RBVA4BaOJ_HMYmM → 424zX3i80AJ_wHF_otKuP}/_clientMiddlewareManifest.js +0 -0
  493. /package/.next/static/{23MxP0RBVA4BaOJ_HMYmM → 424zX3i80AJ_wHF_otKuP}/_ssgManifest.js +0 -0
@@ -0,0 +1,65 @@
1
+ /* Round 91 verification: HelpOverlay catches up on R88 vendor click-
2
+ * to-pin and R90 granular Cmd+K vendor-clear. Continues the
3
+ * R59/R62/R65/R70/R78/R81/R87 docs-catch-up cadence.
4
+ *
5
+ * Seven generations of help-overlay assertions stacked here — any
6
+ * future regression that drops an entry will be caught by one of
7
+ * R91 → R87 → R81 → R78 → R74 → R70 → R65 → R59.
8
+ */
9
+ import { chromium } from 'playwright';
10
+ import { readFileSync } from 'node:fs';
11
+
12
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
13
+ const browser = await chromium.launch({ headless: true });
14
+
15
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
16
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
17
+ await ctx.addInitScript(() => {
18
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
19
+ });
20
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
21
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
22
+ const page = await ctx.newPage();
23
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
24
+ await page.waitForTimeout(800);
25
+
26
+ await page.keyboard.press('?');
27
+ await page.waitForSelector('[role="dialog"][aria-label="Keyboard shortcuts"]', { timeout: 5000 });
28
+
29
+ const items = await page.evaluate(() => {
30
+ const dialog = document.querySelector('[role="dialog"][aria-label="Keyboard shortcuts"]');
31
+ const groups = [...(dialog?.querySelectorAll('div') || [])];
32
+ let active = null;
33
+ for (const g of groups) {
34
+ if (g.textContent && /^Topology canvas$/i.test(g.textContent.trim())) {
35
+ active = g.parentElement;
36
+ break;
37
+ }
38
+ }
39
+ return active ? [...active.querySelectorAll('li')].map(li => (li.textContent || '').trim()) : [];
40
+ });
41
+
42
+ await browser.close();
43
+
44
+ const has = (s) => items.some(t => t.includes(s));
45
+ const results = {
46
+ r91_vendorPin: has('Pin a vendor filter — click A/O/书/? in the chip row (R88)'),
47
+ r91_cmdkVendorClear: has('Clear vendor pin only via palette → "Clear topology vendor filter" (R90)'),
48
+ // R87 priors (hover preview pair)
49
+ r87_segHover: has('Preview a status filter — hover a pressure-bar segment (R83)'),
50
+ r87_groupLabelHover: has('Preview a team focus — hover a group label (R86)'),
51
+ // R81 priors (chip-row hover pair)
52
+ r81_workingChip: has('Highlight working nodes — chip-row sibling of legend (R79)'),
53
+ r81_vendorChip: has('Highlight one vendor — hover A/O/书/? in the chip row (R80)'),
54
+ // R78 / R74 / R70 / R65 / R59 priors — seven generations
55
+ r78_pillBodyClick: has('Clear filter — click anywhere on the pill body (R73)'),
56
+ r74_cmdkLayout: has('Toggle layout / Fit canvas via palette (R74)'),
57
+ r70_cmdkPin: has('(R69)'),
58
+ r65_groupClick: has('Pin a team focus (R63)'),
59
+ r62_pinChip: has('Pin a status filter (R60/R61)'),
60
+ r59_clickHub: has('Fit topology to canvas (R52)'),
61
+ };
62
+ const ok = Object.values(results).every(Boolean);
63
+ console.log(`${ok ? '✅' : '❌'} help overlay R91:`, JSON.stringify(results),
64
+ `\n items=`, items);
65
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,117 @@
1
+ /* Round 223 verification: recent-signal panel header hot-tail tspan
2
+ * ("· N hot") always-mounts; visibility crossfades via opacity gate
3
+ * instead of conditional mount/unmount on hotFlowCount > 0. R190's
4
+ * anet-fade-in entrance is preserved; R223 makes the EXIT also ease.
5
+ *
6
+ * Test scenarios:
7
+ * A. 0 hot flows (all counts < 10):
8
+ * · tspan IN DOM (always-mount proven)
9
+ * · data-recent-panel-hot-visible='false'
10
+ * · opacity attribute = 0
11
+ * · text content empty (no "· 0 hot" artifact)
12
+ * B. 1 hot flow (count=12):
13
+ * · visible='true', opacity=1
14
+ * · text content matches "· 1 hot"
15
+ * · anet-fade-in className retained for R190 compat
16
+ * Both: inline style.transition includes 'opacity 300ms'.
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-recent-panel-hot-count]', { timeout: 10000, state: 'attached' });
50
+ await page.waitForTimeout(400);
51
+ return page;
52
+ }
53
+
54
+ const probe = async (page) => page.evaluate(() => {
55
+ const tail = document.querySelector('[data-recent-panel-hot-count]');
56
+ if (!tail) return null;
57
+ return {
58
+ present: true,
59
+ visible: tail.getAttribute('data-recent-panel-hot-visible'),
60
+ count: tail.getAttribute('data-recent-panel-hot-count'),
61
+ opacity: parseFloat(tail.getAttribute('opacity') || ''),
62
+ text: tail.textContent,
63
+ className: tail.getAttribute('class') || '',
64
+ transition: tail.style.transition,
65
+ };
66
+ });
67
+
68
+ const now = Date.now();
69
+
70
+ // Scenario A: 0 hot flows — 1 cold flow with 3 messages (still cold, < 10)
71
+ const coldMsgs = [];
72
+ for (let i = 0; i < 3; i++) {
73
+ coldMsgs.push({
74
+ id: `cold${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
75
+ network_id: 'default', created_at: new Date(now - (1000 + i * 50)).toISOString(),
76
+ });
77
+ }
78
+ const pageA = await setup(coldMsgs);
79
+ const probeA = await probe(pageA);
80
+ await pageA.close();
81
+
82
+ // Scenario B: 1 hot flow — 12 messages from alpha to beta
83
+ const hotMsgs = [];
84
+ for (let i = 0; i < 12; i++) {
85
+ hotMsgs.push({
86
+ id: `hot${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
87
+ network_id: 'default', created_at: new Date(now - (1000 + i * 50)).toISOString(),
88
+ });
89
+ }
90
+ const pageB = await setup(hotMsgs);
91
+ const probeB = await probe(pageB);
92
+ await pageB.close();
93
+ await browser.close();
94
+
95
+ const results = {
96
+ // A: 0 hot flows — tspan in DOM but hidden
97
+ A_present_in_dom: probeA?.present === true,
98
+ A_visible_false: probeA?.visible === 'false',
99
+ A_opacity_0: probeA?.opacity === 0,
100
+ A_text_empty: probeA?.text === '' || probeA?.text === null,
101
+ A_count_attr_0: probeA?.count === '0',
102
+ A_has_transition: /opacity\s+(?:300ms|0\.3s)/.test(probeA?.transition || ''),
103
+
104
+ // B: 1 hot flow — visible
105
+ B_present_in_dom: probeB?.present === true,
106
+ B_visible_true: probeB?.visible === 'true',
107
+ B_opacity_1: probeB?.opacity === 1,
108
+ B_text_one_hot: /·\s*1\s*hot/.test(probeB?.text || ''),
109
+ B_count_attr_1: probeB?.count === '1',
110
+ B_fade_class_present: (probeB?.className || '').split(/\s+/).includes('anet-fade-in'),
111
+ B_has_transition: /opacity\s+(?:300ms|0\.3s)/.test(probeB?.transition || ''),
112
+ };
113
+ const ok = Object.values(results).every(Boolean);
114
+ console.log(`${ok ? '✅' : '❌'} hot-tail crossfade:`, JSON.stringify(results),
115
+ `\n probeA (0 hot):`, probeA,
116
+ `\n probeB (1 hot):`, probeB);
117
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,120 @@
1
+ /* Round 261 verification: HTML overlay padding aligned with SVG corner
2
+ * panel padding — chrome strip + minimap both at 16 CSS px from canvas
3
+ * edges, matching the SVG corner panels at (16, 16) panel-translate.
4
+ *
5
+ * Pre-R261:
6
+ * SVG panels: 16 SVG units from edges (≈ 15 CSS px @ ~0.94× scale)
7
+ * chrome: 12 CSS px (bottom-3 right-3) — ~3px optical asymmetry
8
+ * minimap: 12 CSS px (right-3) — same asymmetry
9
+ *
10
+ * Post-R261:
11
+ * chrome: bottom-4 right-4 → 16 CSS px from container edges
12
+ * minimap: right-4 → 16 CSS px from container right edge
13
+ *
14
+ * 16 CSS px ≈ 17 SVG units, slightly tighter than the SVG 16-unit
15
+ * inset due to render-scale rounding, but visually matches the SVG
16
+ * panel padding so HTML overlays and SVG panels speak one inset
17
+ * vocabulary.
18
+ *
19
+ * Test scope:
20
+ * 1. Chrome strip's right edge sits at ~16 CSS px from container
21
+ * right edge (allow ±2 px sub-pixel slop).
22
+ * 2. Chrome strip's bottom edge sits at ~16 CSS px from container
23
+ * bottom edge (±2 slop).
24
+ * 3. Minimap (mounted after zoom-in × 3): right edge at ~16 CSS px.
25
+ * 4. R255 chrome-strip semantic gap intact: fleet→view gap still ≥ 11.
26
+ */
27
+ import { chromium } from 'playwright';
28
+ import { readFileSync } from 'node:fs';
29
+
30
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
31
+ const browser = await chromium.launch({ headless: true });
32
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
33
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
34
+ await ctx.addInitScript(() => {
35
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
36
+ });
37
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
38
+ await ctx.route('**/api/hub/status*', async (route) => {
39
+ const r = await route.fetch();
40
+ const b = await r.json();
41
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
42
+ const mk = (alias) => ({
43
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
44
+ network_id: nid, project_dir: null,
45
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
46
+ });
47
+ await route.fulfill({ response: r, json: { ...b, sessions: [
48
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
49
+ ] } });
50
+ });
51
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
52
+ await ctx.route('**/api/hub/tasks*', (r) => r.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-topo-chrome]', { timeout: 10000 });
58
+ await page.waitForSelector('[data-topo-wrapper]', { timeout: 10000 });
59
+ await page.waitForTimeout(300);
60
+
61
+ const beforeMinimap = await page.evaluate(() => {
62
+ const wrapper = document.querySelector('[data-topo-wrapper]');
63
+ const chrome = document.querySelector('[data-topo-chrome]');
64
+ const fleet = document.querySelector('[data-topo-chrome-fleet-group-trailer]');
65
+ const view = document.querySelector('[data-topo-chrome-view-group-leader]');
66
+ const wrR = wrapper?.getBoundingClientRect() ?? null;
67
+ const chR = chrome?.getBoundingClientRect() ?? null;
68
+ return {
69
+ wrapperRight: wrR ? wrR.right : null,
70
+ wrapperBottom: wrR ? wrR.bottom : null,
71
+ chromeRight: chR ? chR.right : null,
72
+ chromeBottom: chR ? chR.bottom : null,
73
+ fleetRight: fleet ? fleet.getBoundingClientRect().right : null,
74
+ viewLeft: view ? view.getBoundingClientRect().left : null,
75
+ };
76
+ });
77
+
78
+ // Zoom in to force minimap mount
79
+ for (let i = 0; i < 3; i++) {
80
+ await page.locator('[data-topo-chrome-zoom-in]').click();
81
+ await page.waitForTimeout(120);
82
+ }
83
+ await page.waitForSelector('[data-topo-minimap]', { timeout: 5000 });
84
+ await page.waitForTimeout(300);
85
+
86
+ const minimapProbe = await page.evaluate(() => {
87
+ const wrapper = document.querySelector('[data-topo-wrapper]');
88
+ const mm = document.querySelector('[data-topo-minimap]');
89
+ const wrR = wrapper?.getBoundingClientRect() ?? null;
90
+ const mmR = mm?.getBoundingClientRect() ?? null;
91
+ return {
92
+ wrapperRight: wrR ? wrR.right : null,
93
+ minimapRight: mmR ? mmR.right : null,
94
+ };
95
+ });
96
+ await browser.close();
97
+
98
+ const chromeRightOffset = (beforeMinimap.wrapperRight != null && beforeMinimap.chromeRight != null)
99
+ ? (beforeMinimap.wrapperRight - beforeMinimap.chromeRight) : null;
100
+ const chromeBottomOffset = (beforeMinimap.wrapperBottom != null && beforeMinimap.chromeBottom != null)
101
+ ? (beforeMinimap.wrapperBottom - beforeMinimap.chromeBottom) : null;
102
+ const minimapRightOffset = (minimapProbe.wrapperRight != null && minimapProbe.minimapRight != null)
103
+ ? (minimapProbe.wrapperRight - minimapProbe.minimapRight) : null;
104
+ const fleetToViewGap = (beforeMinimap.fleetRight != null && beforeMinimap.viewLeft != null)
105
+ ? (beforeMinimap.viewLeft - beforeMinimap.fleetRight) : null;
106
+
107
+ const inWindow = (v, target, slop) => v != null && Math.abs(v - target) <= slop;
108
+
109
+ const results = {
110
+ chrome_right_at_16: inWindow(chromeRightOffset, 16, 2),
111
+ chrome_bottom_at_16: inWindow(chromeBottomOffset, 16, 2),
112
+ minimap_right_at_16: inWindow(minimapRightOffset, 16, 2),
113
+ r255_fleet_view_gap_intact: fleetToViewGap != null && fleetToViewGap >= 11,
114
+ };
115
+ const ok = Object.values(results).every(Boolean);
116
+ console.log(`${ok ? '✅' : '❌'} HTML overlay padding unify:`, JSON.stringify(results),
117
+ '\n chrome right offset:', chromeRightOffset, '/ bottom:', chromeBottomOffset,
118
+ '\n minimap right offset:', minimapRightOffset,
119
+ '\n R255 fleet→view gap:', fleetToViewGap);
120
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,112 @@
1
+ /* Round 159 verification: hub <g> gains keyboard a11y.
2
+ *
3
+ * R151-R157 a11y sweep covered nodes, group labels, edge badges,
4
+ * recent rows, legend rows, chrome buttons, minimap, chip-row.
5
+ * R158 named the canvas root. The hub <g> — the most visually
6
+ * prominent interactive element on the canvas (R39 enlarged,
7
+ * R43 tooltip, R52 click-to-fitView, R115 hover ring) — was the
8
+ * only major interactive R151-R157 skipped.
9
+ *
10
+ * R159 closes that gap with the same standard pattern:
11
+ * role="button"
12
+ * tabIndex={0}
13
+ * aria-label="Network hub · N online · K working · M links — Enter to fit view"
14
+ * className="anet-topo-svg-focus" (R156 cyan focus ring)
15
+ * onKeyDown(Enter/Space) → fitView() + ripple
16
+ *
17
+ * Test:
18
+ * 1. Mock 3 working + 2 idle sessions + zoom in (so fit-to-view
19
+ * has a visible effect — zoom level moves away from 100%)
20
+ * 2. Assert role / tabIndex / aria-label / className on hub
21
+ * 3. Focus hub + Enter → zoom resets to 100%
22
+ * 4. Confirm aria-label mentions counts + Enter
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: 1400, 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 {
34
+ localStorage.setItem('anet-theme', 'cyber');
35
+ sessionStorage.setItem('anet_v3_auth', '1');
36
+ // R159 hub is gated to layout === 'ring' (line 2946 — grid uses
37
+ // group boxes, no central hub). Force ring.
38
+ localStorage.setItem('anet-topo-layout', 'ring');
39
+ } catch {}
40
+ });
41
+
42
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
43
+ await ctx.route('**/api/hub/status*', async (route) => {
44
+ const r = await route.fetch();
45
+ const b = await r.json();
46
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
47
+ const mk = (alias, status) => ({
48
+ alias, status, model: 'claude-opus-4', runtime: 'cli-claude-code',
49
+ network_id: nid, project_dir: null,
50
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
51
+ });
52
+ await route.fulfill({ response: r, json: { ...b, sessions: [
53
+ mk('agents-a1', 'working'), mk('agents-a2', 'working'), mk('agents-a3', 'working'),
54
+ mk('infra-b1', 'idle'), mk('infra-b2', 'idle'),
55
+ ] } });
56
+ });
57
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
58
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
59
+
60
+ const page = await ctx.newPage();
61
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
62
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 5, { timeout: 30000 });
63
+ await page.waitForSelector('[data-topo-hub]', { timeout: 10000 });
64
+ await page.waitForTimeout(400);
65
+
66
+ // Probe ARIA before any interaction
67
+ const probe = await page.evaluate(() => {
68
+ const el = document.querySelector('[data-topo-hub]');
69
+ if (!el) return null;
70
+ return {
71
+ tagName: el.tagName.toLowerCase(),
72
+ role: el.getAttribute('role'),
73
+ tabIndex: el.getAttribute('tabindex'),
74
+ ariaLabel: el.getAttribute('aria-label'),
75
+ className: el.getAttribute('class') || '',
76
+ };
77
+ });
78
+
79
+ // Zoom in twice so fitView has a visible effect
80
+ await page.keyboard.press('+');
81
+ await page.keyboard.press('+');
82
+ await page.waitForTimeout(250);
83
+ const zoomBefore = await page.evaluate(() =>
84
+ document.querySelector('[data-topo-chrome-zoom-level]')?.textContent?.trim());
85
+
86
+ // Focus hub + Enter → fitView resets zoom to 100%
87
+ await page.locator('[data-topo-hub]').focus();
88
+ await page.keyboard.press('Enter');
89
+ await page.waitForTimeout(350);
90
+ const zoomAfter = await page.evaluate(() =>
91
+ document.querySelector('[data-topo-chrome-zoom-level]')?.textContent?.trim());
92
+
93
+ await browser.close();
94
+
95
+ const label = probe?.ariaLabel || '';
96
+ const results = {
97
+ hub_found: probe !== null,
98
+ is_g: probe?.tagName === 'g',
99
+ role_button: probe?.role === 'button',
100
+ tabIndex_0: probe?.tabIndex === '0',
101
+ label_mentions_online: /5 online/.test(label),
102
+ label_mentions_working: /3 working/.test(label),
103
+ label_mentions_enter: /Enter to fit view/.test(label),
104
+ has_focus_class: (probe?.className || '').includes('anet-topo-svg-focus'),
105
+ zoomedBefore: zoomBefore !== '100%',
106
+ enter_resetsZoom: zoomAfter === '100%',
107
+ };
108
+ const ok = Object.values(results).every(Boolean);
109
+ console.log(`${ok ? '✅' : '❌'} hub a11y:`, JSON.stringify(results),
110
+ `\n probe=`, probe,
111
+ `\n zoomBefore=${zoomBefore} zoomAfter=${zoomAfter}`);
112
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,100 @@
1
+ /* Round 84 verification: hub-halo breath amplitude + tempo reflect
2
+ * `workingCount`. Buckets: 0 / 1-2 / 3-5 / 6+. An idle fleet keeps
3
+ * the original 0.32→0.52 (light) / 0.08→0.16 (dark) cycle over 4s;
4
+ * a busy fleet pulses faster with a higher peak. Pure visual breath
5
+ * — geometry unchanged, overlap test untouched.
6
+ *
7
+ * Drive workingCount via mocked /api/hub/status sessions, then read
8
+ * the SMIL `<animate>` attributes off the hub halo. The bucket gets
9
+ * surfaced as `data-hub-busyness=0..3` for stable assertion without
10
+ * parsing values strings.
11
+ */
12
+ import { chromium } from 'playwright';
13
+ import { readFileSync } from 'node:fs';
14
+
15
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
16
+ const browser = await chromium.launch({ headless: true });
17
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
18
+
19
+ const probe = async (workers) => {
20
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1200 } });
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', 'ring');
26
+ sessionStorage.setItem('anet_v3_auth', '1');
27
+ } catch {}
28
+ });
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 sessions = [];
34
+ for (let i = 0; i < workers; i++) {
35
+ sessions.push({ alias: `wkr${i}`, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
36
+ network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh });
37
+ }
38
+ sessions.push({ alias: 'idl', status: 'idle', model: 'claude-opus-4', runtime: 'cli-claude-code',
39
+ network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh });
40
+ await route.fulfill({ response: r, json: { ...b, sessions } });
41
+ });
42
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
43
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
44
+ const page = await ctx.newPage();
45
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
46
+ await page.waitForFunction((n) => document.querySelectorAll('g[data-node]').length === n,
47
+ workers + 1, { timeout: 30000 });
48
+ await page.waitForTimeout(400);
49
+
50
+ const data = await page.evaluate(() => {
51
+ const halo = document.querySelector('[data-hub-busyness]');
52
+ const ani = halo?.querySelector('animate');
53
+ return {
54
+ busy: halo?.getAttribute('data-hub-busyness'),
55
+ values: ani?.getAttribute('values') || null,
56
+ dur: ani?.getAttribute('dur') || null,
57
+ };
58
+ });
59
+ await ctx.close();
60
+ return data;
61
+ };
62
+
63
+ // Helper: parse "trough;peak;trough" into [trough, peak].
64
+ const parseValues = (v) => v ? v.split(';').map(Number) : [];
65
+ const peakOf = (v) => Math.max(...parseValues(v));
66
+ const durOf = (s) => s ? parseFloat(s.replace('s', '')) : 0;
67
+
68
+ const b0 = await probe(0); // idle only
69
+ const b1 = await probe(1); // 1 worker → bucket 1
70
+ const b2 = await probe(4); // 4 workers → bucket 2
71
+ const b3 = await probe(7); // 7 workers → bucket 3
72
+ await browser.close();
73
+
74
+ const results = {
75
+ b0_bucket: b0.busy === '0',
76
+ b1_bucket: b1.busy === '1',
77
+ b2_bucket: b2.busy === '2',
78
+ b3_bucket: b3.busy === '3',
79
+ // Peak climbs with bucket — dark theme values.
80
+ peak_monotonic: peakOf(b0.values) < peakOf(b1.values) &&
81
+ peakOf(b1.values) < peakOf(b2.values) &&
82
+ peakOf(b2.values) < peakOf(b3.values),
83
+ // Duration shortens with bucket.
84
+ dur_monotonic: durOf(b0.dur) > durOf(b1.dur) &&
85
+ durOf(b1.dur) > durOf(b2.dur) &&
86
+ durOf(b2.dur) > durOf(b3.dur),
87
+ // Idle baseline preserved.
88
+ b0_idleDur_4s: durOf(b0.dur) === 4,
89
+ // Busy cap respected.
90
+ b3_capped_24s: durOf(b3.dur) === 2.4,
91
+ // All have the same trough (preserving the rest baseline).
92
+ trough_constant: parseValues(b0.values)[0] === parseValues(b3.values)[0],
93
+ };
94
+ const ok = Object.values(results).every(Boolean);
95
+ console.log(`${ok ? '✅' : '❌'} hub busyness:`, JSON.stringify(results),
96
+ `\n b0=`, b0,
97
+ `\n b1=`, b1,
98
+ `\n b2=`, b2,
99
+ `\n b3=`, b3);
100
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,124 @@
1
+ /* Round 213 verification: hub centre crossfades the workingCount
2
+ * digit and the decorative highlight when count crosses zero,
3
+ * instead of mount/unmount snap.
4
+ *
5
+ * Pre-R213 a fleet going workingCount 0 → 1 swapped highlight
6
+ * circle (R130 decorative) ↔ digit text in a single frame —
7
+ * visible flash at the canvas's focal point. R213 always-mounts
8
+ * both, opacity-gates them, and adds `transition: opacity 300ms
9
+ * ease-out` so they crossfade. Same R181/R182/R183 always-mount
10
+ * pattern applied at the hub-center scope.
11
+ *
12
+ * Test scenarios (cyber dark):
13
+ * A. 4 working sessions (workingCount=4):
14
+ * · digit visible (opacity 1), data-...-visible='true'
15
+ * · highlight hidden (opacity 0), data-...-visible='false'
16
+ * · BOTH elements present in DOM (always-mount)
17
+ * · Both have inline transition with 'opacity 300ms'
18
+ * B. 4 idle sessions (workingCount=0):
19
+ * · digit hidden (opacity 0), data-...-visible='false'
20
+ * · highlight visible (opacity 0.9), data-...-visible='true'
21
+ * · BOTH still in DOM
22
+ */
23
+ import { chromium } from 'playwright';
24
+ import { readFileSync } from 'node:fs';
25
+
26
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
27
+ const browser = await chromium.launch({ headless: true });
28
+
29
+ async function pageFor(allStatus) {
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
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
36
+ await ctx.route('**/api/hub/status*', async (route) => {
37
+ const r = await route.fetch();
38
+ const b = await r.json();
39
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
40
+ const mk = (alias) => ({
41
+ alias, status: allStatus, model: 'claude-opus-4', runtime: 'claude-code-cli',
42
+ network_id: nid, project_dir: null,
43
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
44
+ });
45
+ await route.fulfill({ response: r, json: { ...b, sessions: [
46
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
47
+ ] } });
48
+ });
49
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: [] } }));
50
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.fulfill({ json: { tasks: [] } }));
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.waitForTimeout(300);
55
+ return page;
56
+ }
57
+
58
+ const probe = async (page) => page.evaluate(() => {
59
+ const digit = document.querySelector('[data-topo-hub-working-count]');
60
+ const high = document.querySelector('[data-topo-hub-highlight]');
61
+ function px(el) {
62
+ if (!el) return null;
63
+ const cs = getComputedStyle(el);
64
+ return {
65
+ visible: el.getAttribute(el === document.querySelector('[data-topo-hub-working-count]')
66
+ ? 'data-topo-hub-working-count-visible'
67
+ : 'data-topo-hub-highlight-visible'),
68
+ opacity: parseFloat(cs.opacity),
69
+ transition: el.style.transition,
70
+ };
71
+ }
72
+ return {
73
+ digitPresent: !!digit,
74
+ highlightPresent: !!high,
75
+ digit: px(digit),
76
+ highlight: px(high),
77
+ digitText: digit?.textContent,
78
+ };
79
+ });
80
+
81
+ // Scenario A: all working → digit visible, highlight hidden
82
+ const pageA = await pageFor('working');
83
+ const probeA = await probe(pageA);
84
+ await pageA.close();
85
+
86
+ // Scenario B: all idle (workingCount=0) → highlight visible, digit hidden
87
+ const pageB = await pageFor('idle');
88
+ const probeB = await probe(pageB);
89
+ await pageB.close();
90
+ await browser.close();
91
+
92
+ const near = (got, want, tol = 0.05) => Math.abs(got - want) < tol;
93
+ const hasOpacityT = (s) => /opacity\s+(?:300ms|0\.3s)/.test(s || '');
94
+
95
+ const results = {
96
+ // A: working
97
+ A_digit_in_dom: probeA.digitPresent,
98
+ A_highlight_in_dom: probeA.highlightPresent,
99
+ A_digit_visible: probeA.digit?.visible === 'true',
100
+ A_digit_opacity_1: near(probeA.digit?.opacity ?? 0, 1),
101
+ A_highlight_hidden: probeA.highlight?.visible === 'false',
102
+ A_highlight_opacity_0: near(probeA.highlight?.opacity ?? 1, 0),
103
+ A_digit_text_4: probeA.digitText === '4',
104
+
105
+ // B: idle (workingCount=0)
106
+ B_digit_in_dom: probeB.digitPresent,
107
+ B_highlight_in_dom: probeB.highlightPresent,
108
+ B_digit_hidden: probeB.digit?.visible === 'false',
109
+ B_digit_opacity_0: near(probeB.digit?.opacity ?? 1, 0),
110
+ B_highlight_visible: probeB.highlight?.visible === 'true',
111
+ B_highlight_opacity_0p9: near(probeB.highlight?.opacity ?? 0, 0.9),
112
+ B_digit_text_0: probeB.digitText === '0',
113
+
114
+ // Both transitions present
115
+ A_digit_transition: hasOpacityT(probeA.digit?.transition),
116
+ A_highlight_transition: hasOpacityT(probeA.highlight?.transition),
117
+ B_digit_transition: hasOpacityT(probeB.digit?.transition),
118
+ B_highlight_transition: hasOpacityT(probeB.highlight?.transition),
119
+ };
120
+ const ok = Object.values(results).every(Boolean);
121
+ console.log(`${ok ? '✅' : '❌'} hub-center crossfade:`, JSON.stringify(results),
122
+ `\n probeA (working=4):`, probeA,
123
+ `\n probeB (working=0):`, probeB);
124
+ process.exit(ok ? 0 : 1);