@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,131 @@
1
+ /* Round 319 verification: prefix-group label drops the redundant
2
+ * tier pip when its count equals box.count (single-tier groups).
3
+ *
4
+ * Vincent telegram 5304 (2026-05-16) flagged real-data screenshot
5
+ * with `ai-insight · 6 6i`, `blueleap · 3 3i`, `P站 · 4 4i` as
6
+ * 比较难看 — the same number was visually doubled (total chip "·N"
7
+ * + tier pip "Ni") whenever a group was single-tier.
8
+ *
9
+ * R319: tier pip is now conditionally rendered with both
10
+ * `box.statuses.{tier} > 0` AND `box.statuses.{tier} !== box.count`
11
+ * so single-tier groups collapse to `· N` (no duplicated digit).
12
+ * Multi-tier groups keep all pips — the breakdown is genuinely
13
+ * additional info there.
14
+ *
15
+ * Fixture:
16
+ * - alpha-1..alpha-3: all working (single-tier, count=3, working=3)
17
+ * - beta-1, beta-2: working; beta-3, beta-4: idle (multi-tier)
18
+ *
19
+ * Contract:
20
+ * - alpha group: no [data-group-pip="working"] tspan (dropped because
21
+ * working === count)
22
+ * - alpha group: still has [data-group-label-count] with value=3
23
+ * (R229 total chip untouched)
24
+ * - beta group: has [data-group-pip="working"] AND [data-group-pip="idle"]
25
+ * (multi-tier — both pips render because neither equals count)
26
+ * - beta group: [data-group-label-count-value]=4 still rendered
27
+ * - R317 inactive Layout toggle gray-400 + R318 active font-medium
28
+ * + R294 pulse absent all preserved.
29
+ */
30
+ import { chromium } from 'playwright';
31
+ import { readFileSync } from 'node:fs';
32
+
33
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
34
+ const browser = await chromium.launch({ headless: true });
35
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
36
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
37
+ await ctx.addInitScript(() => {
38
+ try {
39
+ localStorage.setItem('anet-theme', 'cyber');
40
+ sessionStorage.setItem('anet_v3_auth', '1');
41
+ localStorage.setItem('anet-topo-layout', 'grid');
42
+ } catch {}
43
+ });
44
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
45
+ await ctx.route('**/api/hub/status*', async (route) => {
46
+ const r = await route.fetch();
47
+ const b = await r.json();
48
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
49
+ const mk = (alias, status) => ({
50
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
51
+ network_id: nid, project_dir: null,
52
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
53
+ });
54
+ await route.fulfill({ response: r, json: { ...b, sessions: [
55
+ // alpha — single-tier (all working, count=3)
56
+ mk('alpha-1', 'working'),
57
+ mk('alpha-2', 'working'),
58
+ mk('alpha-3', 'working'),
59
+ // beta — multi-tier (2 working + 2 idle, count=4)
60
+ mk('beta-1', 'working'),
61
+ mk('beta-2', 'working'),
62
+ mk('beta-3', 'idle'),
63
+ mk('beta-4', 'idle'),
64
+ ] } });
65
+ });
66
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
67
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
68
+
69
+ const page = await ctx.newPage();
70
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
71
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 7, { timeout: 30000 });
72
+ await page.waitForSelector('[data-group-label-count]', { timeout: 10000, state: 'attached' });
73
+ await page.waitForTimeout(500);
74
+
75
+ const probe = await page.evaluate(() => {
76
+ // Find each group's <text data-group-label> wrapper and its pip
77
+ // children + count tspan.
78
+ const labels = Array.from(document.querySelectorAll('[data-group-label]'));
79
+ const byGroup = {};
80
+ for (const label of labels) {
81
+ const key = label.getAttribute('data-group-label');
82
+ const countTspan = label.querySelector('[data-group-label-count]');
83
+ const pips = Array.from(label.querySelectorAll('[data-group-pip]'));
84
+ byGroup[key] = {
85
+ hasCountTspan: !!countTspan,
86
+ countValue: countTspan?.getAttribute('data-group-label-count-value') ?? null,
87
+ pipTiers: pips.map(p => p.getAttribute('data-group-pip')),
88
+ pipTexts: pips.map(p => p.textContent),
89
+ };
90
+ }
91
+ return {
92
+ groups: byGroup,
93
+ r317InactiveLayoutCls:
94
+ document.querySelector('[data-topo-chrome-layout="grid"]')?.className ?? '',
95
+ r317ActiveLayoutCls:
96
+ document.querySelector('[data-topo-chrome-layout="ring"]')?.className ?? '',
97
+ pulseCount: document.querySelectorAll('[data-pulse-wrapper]').length,
98
+ };
99
+ });
100
+ await browser.close();
101
+
102
+ // R106 cluster algorithm preserves the trailing separator in the
103
+ // group key (e.g. alpha-1/2/3 → key 'alpha-'); match by prefix.
104
+ const alpha = probe.groups['alpha-'] || probe.groups['alpha'] || null;
105
+ const beta = probe.groups['beta-'] || probe.groups['beta'] || null;
106
+
107
+ const results = {
108
+ alpha_group_found: alpha !== null,
109
+ alpha_has_count_tspan: alpha?.hasCountTspan === true,
110
+ alpha_count_value_3: alpha?.countValue === '3',
111
+ alpha_no_working_pip: !alpha?.pipTiers.includes('working'),
112
+ alpha_no_pips_at_all: alpha?.pipTiers.length === 0,
113
+ beta_group_found: beta !== null,
114
+ beta_has_count_tspan: beta?.hasCountTspan === true,
115
+ beta_count_value_4: beta?.countValue === '4',
116
+ beta_has_working_pip: beta?.pipTiers.includes('working'),
117
+ beta_has_idle_pip: beta?.pipTiers.includes('idle'),
118
+ beta_no_offline_pip: !beta?.pipTiers.includes('offline'),
119
+ // R317 regression — Layout toggle inactive Grid is the layout-active
120
+ // here (we forced grid via initScript), so check that Ring is the
121
+ // INACTIVE one with text-gray-400. After R318, the ACTIVE button
122
+ // (grid) has font-medium.
123
+ r317_ring_inactive_gray_400: probe.r317ActiveLayoutCls.includes('text-gray-400'),
124
+ r318_grid_active_font_medium: probe.r317InactiveLayoutCls.includes('font-medium'),
125
+ r294_pulse_absent: probe.pulseCount === 0,
126
+ };
127
+ const ok = Object.values(results).every(Boolean);
128
+ console.log(`${ok ? '✅' : '❌'} group-pip single-tier drop:`, JSON.stringify(results),
129
+ '\n alpha:', alpha,
130
+ '\n beta:', beta);
131
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,95 @@
1
+ /* Round 230 verification: R58 status mix pip strip tspans
2
+ * ([data-group-pip='working|idle|offline']) get fontVariantNumeric:
3
+ * 'tabular-nums' so the digit doesn't jitter the dx-offset pip
4
+ * chain at the 9→10 boundary.
5
+ *
6
+ * Scenario: 1 group with mixed tiers — alpha-1/2 working, alpha-3
7
+ * idle, alpha-4 offline. R106 cluster forms one alpha- group with
8
+ * box.statuses.working=2, idle=1, offline=1 → all three pips
9
+ * present.
10
+ *
11
+ * Verifies each pip:
12
+ * getComputedStyle.fontVariantNumeric contains 'tabular-nums'
13
+ * text matches the expected '{N}{tier-letter}' shape
14
+ * fill is tier-specific (color check — sanity)
15
+ */
16
+ import { chromium } from 'playwright';
17
+ import { readFileSync } from 'node:fs';
18
+
19
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
20
+ const browser = await chromium.launch({ headless: true });
21
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
22
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
23
+ await ctx.addInitScript(() => {
24
+ try {
25
+ localStorage.setItem('anet-theme', 'cyber');
26
+ sessionStorage.setItem('anet_v3_auth', '1');
27
+ localStorage.setItem('anet-topo-layout', 'grid');
28
+ } 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, status) => ({
36
+ alias, status,
37
+ model: 'claude-opus-4', runtime: 'claude-code-cli',
38
+ network_id: nid, project_dir: null,
39
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
40
+ });
41
+ await route.fulfill({ response: r, json: { ...b, sessions: [
42
+ mk('alpha-1', 'working'),
43
+ mk('alpha-2', 'working'),
44
+ mk('alpha-3', 'idle'),
45
+ mk('alpha-4', 'offline'),
46
+ ] } });
47
+ });
48
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: [] } }));
49
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.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-group-pip="working"]', { timeout: 10000, state: 'attached' });
55
+ await page.waitForTimeout(400);
56
+
57
+ const out = await page.evaluate(() => {
58
+ const grab = (tier) => {
59
+ const el = document.querySelector(`[data-group-pip="${tier}"]`);
60
+ if (!el) return null;
61
+ return {
62
+ tier,
63
+ text: el.textContent,
64
+ fontVarNumeric: getComputedStyle(el).fontVariantNumeric,
65
+ fill: getComputedStyle(el).fill,
66
+ };
67
+ };
68
+ return {
69
+ working: grab('working'),
70
+ idle: grab('idle'),
71
+ offline: grab('offline'),
72
+ };
73
+ });
74
+ await browser.close();
75
+
76
+ const hasTab = (s) => /tabular-nums/.test(s || '');
77
+
78
+ const results = {
79
+ working_present: out.working !== null,
80
+ working_text: out.working?.text === '2w',
81
+ working_tabular: hasTab(out.working?.fontVarNumeric),
82
+ idle_present: out.idle !== null,
83
+ idle_text: out.idle?.text === '1i',
84
+ idle_tabular: hasTab(out.idle?.fontVarNumeric),
85
+ offline_present: out.offline !== null,
86
+ offline_text: out.offline?.text === '1o',
87
+ offline_tabular: hasTab(out.offline?.fontVarNumeric),
88
+ // Sanity: tier-specific fills still distinct from each other
89
+ fills_distinct: out.working && out.idle && out.offline &&
90
+ new Set([out.working.fill, out.idle.fill, out.offline.fill]).size === 3,
91
+ };
92
+ const ok = Object.values(results).every(Boolean);
93
+ console.log(`${ok ? '✅' : '❌'} group-pip tabular:`, JSON.stringify(results),
94
+ '\n pips:', out);
95
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,103 @@
1
+ /* Round 58 verification: group label includes a status-mix pip strip
2
+ * showing how many members are working / idle / offline.
3
+ *
4
+ * Sessions (mixed-status alpha team + singleton beta):
5
+ * alpha1 — working
6
+ * alpha2 — working
7
+ * alpha3 — idle
8
+ * alpha4 — offline
9
+ * beta — idle (singleton, no group box)
10
+ *
11
+ * Grid layout draws a group box for alpha (4 members). Label should
12
+ * end in something like "alpha · 4 · 2w 1i 1o". Status pips render
13
+ * only when their count > 0, so a uniform group reads more simply.
14
+ */
15
+ import { chromium } from 'playwright';
16
+ import { readFileSync } from 'node:fs';
17
+
18
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
19
+ const browser = await chromium.launch({ headless: true });
20
+
21
+ async function probe(sessions) {
22
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 1500 } });
23
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
24
+ await ctx.addInitScript(() => {
25
+ try {
26
+ localStorage.setItem('anet-theme', 'cyber');
27
+ localStorage.setItem('anet-topo-layout', 'grid');
28
+ sessionStorage.setItem('anet_v3_auth', '1');
29
+ } catch {}
30
+ });
31
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
32
+ const stale = new Date(Date.now() - 30 * 60 * 1000).toISOString();
33
+ await ctx.route('**/api/hub/status*', async (route) => {
34
+ const r = await route.fetch();
35
+ const b = await r.json();
36
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
37
+ const seeded = sessions.map(s => ({
38
+ alias: s.alias, status: s.status,
39
+ network_id: nid, project_dir: null,
40
+ created_at: s.status === 'offline' ? stale : fresh,
41
+ updated_at: s.status === 'offline' ? stale : fresh,
42
+ last_seen_at: s.status === 'offline' ? stale : fresh,
43
+ }));
44
+ await route.fulfill({ response: r, json: { ...b, sessions: seeded } });
45
+ });
46
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
47
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
48
+ const page = await ctx.newPage();
49
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
50
+ await page.waitForFunction(c => document.querySelectorAll('g[data-node]').length === c, sessions.length, { timeout: 30000 });
51
+ await page.waitForTimeout(500);
52
+ const labels = await page.evaluate(() => {
53
+ const out = {};
54
+ for (const t of document.querySelectorAll('text[data-group-label]')) {
55
+ out[t.getAttribute('data-group-label')] = (t.textContent || '').trim();
56
+ }
57
+ return out;
58
+ });
59
+ await ctx.close();
60
+ return labels;
61
+ }
62
+
63
+ // Grouping is consecutive-run on the sorted [...online, ...offline] array,
64
+ // so an offline member of the "alpha" prefix family ends up in a separate
65
+ // band (it falls into the offline tier rather than joining the online
66
+ // alpha group). All members that share a multi-member band are by
67
+ // construction online, so the "offline pip" case is unreachable via the
68
+ // group label and the test covers only the working/idle dimensions here.
69
+ const mixed = await probe([
70
+ { alias: 'alpha1', status: 'working' },
71
+ { alias: 'alpha2', status: 'working' },
72
+ { alias: 'alpha3', status: 'idle' },
73
+ { alias: 'beta', status: 'idle' },
74
+ ]);
75
+
76
+ const allWorking = await probe([
77
+ { alias: 'gamma1', status: 'working' },
78
+ { alias: 'gamma2', status: 'working' },
79
+ { alias: 'gamma3', status: 'working' },
80
+ { alias: 'beta', status: 'idle' },
81
+ ]);
82
+
83
+ await browser.close();
84
+
85
+ const alphaLabel = mixed.alpha || '';
86
+ const gammaLabel = allWorking.gamma || '';
87
+
88
+ const results = {
89
+ alpha_keyPresent: /^alpha/.test(alphaLabel),
90
+ alpha_countCorrect: /·\s*3/.test(alphaLabel),
91
+ alpha_workingPip: /2w/.test(alphaLabel),
92
+ alpha_idlePip: /1i/.test(alphaLabel),
93
+ alpha_noOfflinePip: !/\d+o\b/.test(alphaLabel),
94
+ // Uniform-status group: only the working pip renders.
95
+ gamma_workingPip: /3w/.test(gammaLabel),
96
+ gamma_noIdlePip: !/\d+i\b/.test(gammaLabel),
97
+ gamma_noOfflinePip: !/\d+o\b/.test(gammaLabel),
98
+ };
99
+ const ok = Object.values(results).every(Boolean);
100
+ console.log(`${ok ? '✅' : '❌'} group status pips:`, JSON.stringify(results),
101
+ `\n alpha="${alphaLabel}"`,
102
+ `\n gamma="${gammaLabel}"`);
103
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,95 @@
1
+ /* Round 173 verification: group boxes (grid layout) fade-in
2
+ * alongside the R9/R172 first-paint wave.
3
+ *
4
+ * Pre-R173 the structural box frames appeared instantly while
5
+ * nodes inside eased in via R9 (0-540ms) and edges via R172
6
+ * (280-980ms). The reveal felt like 'frame slams down, nodes
7
+ * drift in'.
8
+ *
9
+ * R173 wraps each group box <g> in:
10
+ * className="transition-opacity anet-fade-in"
11
+ * style={{ animationDelay: 'boxIdx × 60ms (cap 8)' }}
12
+ * data-group-fade-delay={boxIdx × 60}
13
+ *
14
+ * Test:
15
+ * 1. Force grid layout
16
+ * 2. Mock 4 prefix-clustered sessions → 2 groups
17
+ * 3. Probe each [data-group] wrapper
18
+ * 4. Assert anet-fade-in className present
19
+ * 5. Assert animation-delay 0ms, 60ms
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 {
31
+ localStorage.setItem('anet-theme', 'cyber');
32
+ sessionStorage.setItem('anet_v3_auth', '1');
33
+ localStorage.setItem('anet-topo-layout', 'grid');
34
+ } catch {}
35
+ });
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: 'cli-claude-code',
44
+ network_id: nid, project_dir: null,
45
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
46
+ });
47
+ // 2 distinct prefix groups, 2 sessions each → 2 group boxes
48
+ await route.fulfill({ response: r, json: { ...b, sessions: [
49
+ mk('agents-a1'), mk('agents-a2'),
50
+ mk('infra-b1'), mk('infra-b2'),
51
+ ] } });
52
+ });
53
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
54
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
55
+
56
+ const page = await ctx.newPage();
57
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
58
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
59
+ await page.waitForSelector('[data-group]', { timeout: 10000 });
60
+ await page.waitForTimeout(400);
61
+
62
+ const probe = await page.evaluate(() => {
63
+ const boxes = [...document.querySelectorAll('g[data-group]')];
64
+ return boxes.map(b => ({
65
+ key: b.getAttribute('data-group'),
66
+ hasFadeClass: (b.getAttribute('class') || '').includes('anet-fade-in'),
67
+ fadeDelayAttr: parseFloat(b.getAttribute('data-group-fade-delay') || ''),
68
+ animationDelay: b.style.animationDelay || getComputedStyle(b).animationDelay,
69
+ }));
70
+ });
71
+
72
+ await browser.close();
73
+
74
+ const parseDelay = (s) => {
75
+ if (!s) return null;
76
+ if (s.endsWith('ms')) return parseFloat(s);
77
+ if (s.endsWith('s')) return parseFloat(s) * 1000;
78
+ return parseFloat(s);
79
+ };
80
+ const delays = probe.map(p => parseDelay(p.animationDelay));
81
+
82
+ const results = {
83
+ two_boxes_found: probe.length === 2,
84
+ all_have_fade_class: probe.every(p => p.hasFadeClass),
85
+ box0_delay_attr_0: probe[0]?.fadeDelayAttr === 0,
86
+ box1_delay_attr_60: probe[1]?.fadeDelayAttr === 60,
87
+ box0_anim_delay_0ms: delays[0] === 0,
88
+ box1_anim_delay_60ms: Math.abs(delays[1] - 60) < 0.5,
89
+ delays_strictly_increase: delays[0] < delays[1],
90
+ };
91
+ const ok = Object.values(results).every(Boolean);
92
+ console.log(`${ok ? '✅' : '❌'} groupbox fade-in:`, JSON.stringify(results),
93
+ `\n boxes =`, probe,
94
+ `\n delays (ms) =`, delays);
95
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,127 @@
1
+ /* Round 142 verification: group boxes (R85+R68) gain a drop-
2
+ * shadow lift on hover or pin, mirroring R135's overlay panel
3
+ * hover-elevation. Closes the "interactive surface elevates"
4
+ * vocabulary across canvas-side major surfaces:
5
+ * R18 KPI cards (Overview)
6
+ * R135 overlay panels (recent-signal + legend)
7
+ * R142 group boxes (canvas-level filter)
8
+ *
9
+ * Each carries a soft outward shadow when hovered/selected.
10
+ *
11
+ * States walked (grid layout, three prefix groups):
12
+ * 1. Idle → all 3 boxes: data-group-box-lifted="false", no filter
13
+ * 2. Hover infra label → infra lifted=true, filter set; others stay flat
14
+ * 3. Leave hover → infra restored to flat
15
+ * 4. Pin agents via CustomEvent → agents lifted=true (R63 pin path)
16
+ * 5. Esc → agents back to flat
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
+ const ctx = await browser.newContext({ viewport: { width: 1600, height: 1500 } });
25
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
26
+ await ctx.addInitScript(() => {
27
+ try {
28
+ localStorage.setItem('anet-theme', 'cyber');
29
+ sessionStorage.setItem('anet_v3_auth', '1');
30
+ // R85 group boxes only render in grid layout
31
+ localStorage.setItem('anet-topo-layout', 'grid');
32
+ } catch {}
33
+ });
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, status) => ({
41
+ alias, status, model: 'claude-opus-4', runtime: 'cli-claude-code',
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('agents-a1', 'working'), mk('agents-a2', 'idle'), mk('agents-a3', 'idle'),
47
+ mk('infra-b1', 'working'), mk('infra-b2', 'idle'),
48
+ mk('build-c1', 'idle'), mk('build-c2', 'idle'),
49
+ ] } });
50
+ });
51
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
52
+ await ctx.route('**/api/hub/tasks*', (route) => route.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 === 7, { timeout: 30000 });
57
+ await page.waitForSelector('[data-group]', { timeout: 10000 });
58
+ await page.waitForTimeout(500);
59
+
60
+ const inspectAll = () => page.evaluate(() => {
61
+ const boxes = [...document.querySelectorAll('rect[data-group-box-pinned]')];
62
+ return boxes.map(rect => {
63
+ const parentG = rect.closest('g[data-group]');
64
+ return {
65
+ group: parentG?.getAttribute('data-group'),
66
+ lifted: rect.getAttribute('data-group-box-lifted'),
67
+ filter: rect.getAttribute('filter'),
68
+ pinned: rect.getAttribute('data-group-box-pinned'),
69
+ };
70
+ });
71
+ });
72
+
73
+ const state1 = await inspectAll();
74
+ const idleAll = state1.every(b => b.lifted === 'false' && !b.filter);
75
+
76
+ // Hover infra label
77
+ const infra = state1.find(b => (b.group || '').startsWith('infra'));
78
+ await page.locator(`[data-group-label-hit="${infra.group}"]`).first().hover({ timeout: 3000 });
79
+ await page.waitForTimeout(250);
80
+ const state2 = await inspectAll();
81
+ const infra2 = state2.find(b => b.group === infra.group);
82
+ const othersFlat2 = state2.filter(b => b.group !== infra.group).every(b => b.lifted === 'false' && !b.filter);
83
+
84
+ // Mouse away → restore
85
+ await page.mouse.move(20, 20);
86
+ await page.waitForTimeout(250);
87
+ const state3 = await inspectAll();
88
+ const infra3 = state3.find(b => b.group === infra.group);
89
+
90
+ // Pin agents via CustomEvent
91
+ const agents = state1.find(b => (b.group || '').startsWith('agents'));
92
+ await page.evaluate((key) => {
93
+ window.dispatchEvent(new CustomEvent('anet:topo-pin', { detail: { kind: 'group', value: key } }));
94
+ }, agents.group);
95
+ await page.waitForTimeout(250);
96
+ const state4 = await inspectAll();
97
+ const agents4 = state4.find(b => b.group === agents.group);
98
+
99
+ // Esc clears pin
100
+ await page.keyboard.press('Escape');
101
+ await page.waitForTimeout(250);
102
+ const state5 = await inspectAll();
103
+ const agents5 = state5.find(b => b.group === agents.group);
104
+
105
+ await browser.close();
106
+
107
+ const liftFilter = 'url(#topo-groupbox-lift)';
108
+ const results = {
109
+ s1_idleAllFlat: idleAll,
110
+
111
+ s2_infraLifted: infra2?.lifted === 'true',
112
+ s2_infraHasFilter: infra2?.filter === liftFilter,
113
+ s2_othersStillFlat: othersFlat2,
114
+
115
+ s3_infraRestored: infra3?.lifted === 'false' && !infra3.filter,
116
+
117
+ s4_agentsLifted: agents4?.lifted === 'true',
118
+ s4_agentsHasFilter: agents4?.filter === liftFilter,
119
+ s4_agentsPinnedAttr: agents4?.pinned === 'true',
120
+
121
+ s5_agentsCleared: agents5?.lifted === 'false' && !agents5.filter,
122
+ };
123
+ const ok = Object.values(results).every(Boolean);
124
+ console.log(`${ok ? '✅' : '❌'} groupbox hover-lift:`, JSON.stringify(results),
125
+ `\n s1=`, state1,
126
+ `\n infra2=`, infra2, ` agents4=`, agents4, ` agents5=`, agents5);
127
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,106 @@
1
+ /* Round 248 verification: two more theme-toggle snap closures:
2
+ *
3
+ * 1. Group box <rect> (#111 / R66): existing transition list
4
+ * 'stroke + stroke-width + fill-opacity + filter 200ms' grows
5
+ * to include 'fill 200ms'. The rect's theme-driven fill
6
+ * (isLight ? slate-900 : indigo-300) no longer snaps.
7
+ *
8
+ * 2. Hub center core <circle> (the visual anchor at canvas
9
+ * centre, fill=isLight ? emerald-600 : emerald-500): had NO
10
+ * transition at all — most prominent element on canvas was
11
+ * flipping in one frame while everything around it eased.
12
+ * R248 adds inline 'fill 200ms ease-out' + data-topo-hub-
13
+ * core attr for test introspection.
14
+ *
15
+ * Test scope:
16
+ * - Hub core probe ([data-topo-hub-core]): present, has fill
17
+ * attribute, transition contains 'fill 200ms' (or 0.2s)
18
+ * - Group box probe ([data-group-label]) → climb to its
19
+ * <rect> sibling — needs grid layout + ≥2 prefix-shared
20
+ * aliases. Probe its style.transition for 'fill 200ms'
21
+ * alongside the existing R66 properties.
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
+ // Hub core renders only in ring layout; group boxes only in grid.
29
+ // Two contexts back-to-back.
30
+ async function setupLayout(layout) {
31
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
32
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
33
+ await ctx.addInitScript((l) => {
34
+ try {
35
+ localStorage.setItem('anet-theme', 'cyber');
36
+ sessionStorage.setItem('anet_v3_auth', '1');
37
+ localStorage.setItem('anet-topo-layout', l);
38
+ } catch {}
39
+ }, layout);
40
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
41
+ await ctx.route('**/api/hub/status*', async (route) => {
42
+ const r = await route.fetch();
43
+ const b = await r.json();
44
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
45
+ const mk = (alias) => ({
46
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
47
+ network_id: nid, project_dir: null,
48
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
49
+ });
50
+ // 3 alpha-prefix + 1 delta — R106 cluster makes ONE alpha group box in grid
51
+ await route.fulfill({ response: r, json: { ...b, sessions: [
52
+ mk('alpha-1'), mk('alpha-2'), mk('alpha-3'), mk('delta'),
53
+ ] } });
54
+ });
55
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: [] } }));
56
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.fulfill({ json: { tasks: [] } }));
57
+ const page = await ctx.newPage();
58
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
59
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
60
+ await page.waitForTimeout(400);
61
+ return page;
62
+ }
63
+
64
+ // Scenario A: ring → hub core
65
+ const pageRing = await setupLayout('ring');
66
+ await pageRing.waitForSelector('[data-topo-hub-core]', { timeout: 10000, state: 'attached' });
67
+ const hub = await pageRing.evaluate(() => {
68
+ const el = document.querySelector('[data-topo-hub-core]');
69
+ return el ? { fill: el.getAttribute('fill'), transition: el.style.transition } : null;
70
+ });
71
+ await pageRing.close();
72
+
73
+ // Scenario B: grid → group box <rect>
74
+ const pageGrid = await setupLayout('grid');
75
+ // Wait for group label (signals group box has been mounted alongside)
76
+ await pageGrid.waitForSelector('[data-group-label]', { timeout: 10000, state: 'attached' });
77
+ const groupBox = await pageGrid.evaluate(() => {
78
+ const allRects = Array.from(document.querySelectorAll('rect'));
79
+ const found = allRects.find(r => r.getAttribute('rx') === '14'
80
+ && r.style && /fill\s+200ms/.test(r.style.transition || ''));
81
+ return found ? { fill: found.getAttribute('fill'), transition: found.style.transition } : null;
82
+ });
83
+ await pageGrid.close();
84
+
85
+ const out = { hub, groupBox };
86
+ await browser.close();
87
+
88
+ const hasProp = (s, prop) => new RegExp(`${prop}\\s+(?:200ms|0\\.2s)`).test(s || '');
89
+
90
+ const results = {
91
+ hub_present: out.hub !== null,
92
+ hub_has_fill_attr: typeof out.hub?.fill === 'string' && out.hub.fill.length > 0,
93
+ hub_transition_has_fill: hasProp(out.hub?.transition, 'fill'),
94
+
95
+ groupbox_present: out.groupBox !== null,
96
+ groupbox_has_fill_attr: typeof out.groupBox?.fill === 'string' && out.groupBox.fill.length > 0,
97
+ groupbox_transition_stroke: hasProp(out.groupBox?.transition, 'stroke'),
98
+ groupbox_transition_fill_op: hasProp(out.groupBox?.transition, 'fill-opacity'),
99
+ groupbox_transition_filter: hasProp(out.groupBox?.transition, 'filter'),
100
+ groupbox_transition_fill: hasProp(out.groupBox?.transition, 'fill'),
101
+ };
102
+ const ok = Object.values(results).every(Boolean);
103
+ console.log(`${ok ? '✅' : '❌'} groupbox + hubcore ease:`, JSON.stringify(results),
104
+ '\n hub: ', out.hub,
105
+ '\n groupBox:', out.groupBox);
106
+ process.exit(ok ? 0 : 1);