@sleep2agi/agent-network-dashboard 0.4.6-preview.27 → 0.4.6-preview.270

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 (468) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/diagnostics/route-bundle-stats.json +41 -41
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/routes-manifest.json +6 -0
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/_not-found.html +2 -2
  17. package/.next/server/app/_not-found.rsc +12 -12
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  19. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  20. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  23. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  24. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  25. package/.next/server/app/admin.html +2 -2
  26. package/.next/server/app/admin.rsc +14 -14
  27. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  28. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  29. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  30. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  31. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  32. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  33. package/.next/server/app/api/hub/servers/route/app-paths-manifest.json +3 -0
  34. package/.next/server/app/api/hub/servers/route/build-manifest.json +9 -0
  35. package/.next/server/app/api/hub/servers/route/server-reference-manifest.json +4 -0
  36. package/.next/server/app/api/hub/servers/route.js +7 -0
  37. package/.next/server/app/api/hub/servers/route.js.map +5 -0
  38. package/.next/server/app/api/hub/servers/route.js.nft.json +1 -0
  39. package/.next/server/app/api/hub/servers/route_client-reference-manifest.js +3 -0
  40. package/.next/server/app/index.html +2 -2
  41. package/.next/server/app/index.rsc +14 -14
  42. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  43. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  44. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  45. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  46. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  47. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/login.html +2 -2
  49. package/.next/server/app/login.rsc +14 -14
  50. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  51. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  52. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  53. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  55. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  56. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/logs.html +2 -2
  58. package/.next/server/app/logs.rsc +14 -14
  59. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  60. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  61. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  62. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  64. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  65. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/messages.html +2 -2
  67. package/.next/server/app/messages.rsc +14 -14
  68. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  69. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  70. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  71. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  73. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  74. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/node.html +2 -2
  76. package/.next/server/app/node.rsc +14 -14
  77. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  78. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  79. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  80. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  82. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  83. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/nodes.html +2 -2
  85. package/.next/server/app/nodes.rsc +14 -14
  86. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  87. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  88. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  89. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  90. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  91. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  92. package/.next/server/app/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/server-logs.html +2 -2
  95. package/.next/server/app/server-logs.rsc +14 -14
  96. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  97. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  98. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  99. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  100. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  101. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  102. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  103. package/.next/server/app/settings/networks.html +2 -2
  104. package/.next/server/app/settings/networks.rsc +14 -14
  105. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  106. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  107. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  108. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  109. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  110. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  111. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  112. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  113. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  114. package/.next/server/app/settings/tokens.html +2 -2
  115. package/.next/server/app/settings/tokens.rsc +14 -14
  116. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  117. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  118. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  119. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  120. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  121. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  122. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  123. package/.next/server/app/settings.html +2 -2
  124. package/.next/server/app/settings.rsc +14 -14
  125. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  126. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  127. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  128. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  129. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  130. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  131. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  132. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  133. package/.next/server/app/tasks.html +2 -2
  134. package/.next/server/app/tasks.rsc +14 -14
  135. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  136. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  137. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  138. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  139. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  140. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  141. package/.next/server/app-paths-manifest.json +1 -0
  142. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_servers_route_actions_08-m-cs.js +3 -0
  143. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_servers_route_actions_08-m-cs.js.map +1 -0
  144. package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js +3 -0
  145. package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js.map +1 -0
  146. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js +1 -1
  147. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +1 -1
  148. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js +1 -1
  149. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js.map +1 -1
  150. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js +1 -1
  151. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js.map +1 -1
  152. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +4 -1
  153. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  154. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  155. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  156. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js +1 -1
  157. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js.map +1 -1
  158. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  159. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  160. package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js +1 -1
  161. package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js.map +1 -1
  162. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js +1 -1
  163. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js.map +1 -1
  164. package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js +1 -1
  165. package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js.map +1 -1
  166. package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js +1 -1
  167. package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js.map +1 -1
  168. package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js +1 -1
  169. package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js.map +1 -1
  170. package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js +1 -1
  171. package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js.map +1 -1
  172. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  173. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  174. package/.next/server/middleware-build-manifest.js +3 -3
  175. package/.next/server/pages/404.html +2 -2
  176. package/.next/server/pages/500.html +1 -1
  177. package/.next/server/server-reference-manifest.js +1 -1
  178. package/.next/server/server-reference-manifest.json +1 -1
  179. package/.next/static/chunks/0-mpa_947ipeq.js +1 -0
  180. package/.next/static/chunks/0.qg_sd3s8sj~.js +1 -0
  181. package/.next/static/chunks/01lmbsd37fybu.js +1 -0
  182. package/.next/static/chunks/03o.h6kvmw4l_.js +1 -0
  183. package/.next/static/chunks/03~~oirxz7~vc.js +1 -0
  184. package/.next/static/chunks/05uk96gc~9mni.js +1 -0
  185. package/.next/static/chunks/08kf-.bgs10ze.js +1 -0
  186. package/.next/static/chunks/0ggj5ummw2a84.js +7 -0
  187. package/.next/static/chunks/0glmrdj9112tc.js +7 -0
  188. package/.next/static/chunks/0rxrez_cjn.eh.js +1 -0
  189. package/.next/static/chunks/0x8x.29hkju5a.js +1 -0
  190. package/.next/static/chunks/0y.7kwty.hl7h.css +2 -0
  191. package/.next/static/chunks/0zwpi6hqmzpqh.js +1 -0
  192. package/.next/static/chunks/149kodgion8f0.js +4 -0
  193. package/.next/static/chunks/174re5ar~kxne.js +1 -0
  194. package/.next/trace +2 -2
  195. package/.next/trace-build +1 -1
  196. package/.next/types/routes.d.ts +2 -1
  197. package/.next/types/validator.ts +9 -0
  198. package/app/api/hub/servers/route.ts +39 -0
  199. package/app/components/ChatPopover.tsx +38 -2
  200. package/app/components/CommandPalette.tsx +107 -0
  201. package/app/components/HelpOverlay.tsx +126 -0
  202. package/app/components/ServersDrawer.tsx +64 -27
  203. package/app/components/TopoGraph.tsx +5411 -240
  204. package/app/components/utils.ts +8 -7
  205. package/app/globals.css +110 -5
  206. package/app/lib/time.ts +36 -0
  207. package/package.json +2 -2
  208. package/scripts/help-overlay-topo-test.mjs +44 -0
  209. package/scripts/servers-drawer-live-test.mjs +96 -0
  210. package/scripts/topo-active-filter-pill-test.mjs +101 -0
  211. package/scripts/topo-active-links-chip-hover-test.mjs +103 -0
  212. package/scripts/topo-active-links-chip-nav-test.mjs +117 -0
  213. package/scripts/topo-active-links-chip-self-hover-test.mjs +171 -0
  214. package/scripts/topo-active-links-chip-test.mjs +73 -0
  215. package/scripts/topo-active-links-empty-recede-test.mjs +125 -0
  216. package/scripts/topo-active-links-freshness-test.mjs +113 -0
  217. package/scripts/topo-active-links-tooltip-test.mjs +79 -0
  218. package/scripts/topo-active-pulse-ease-test.mjs +120 -0
  219. package/scripts/topo-arrival-ping-test.mjs +87 -0
  220. package/scripts/topo-auto-fit-test.mjs +73 -0
  221. package/scripts/topo-canvas-aria-test.mjs +108 -0
  222. package/scripts/topo-chat-target-breath-test.mjs +84 -0
  223. package/scripts/topo-chat-target-ring-ease-test.mjs +82 -0
  224. package/scripts/topo-chat-target-ring-fade-test.mjs +124 -0
  225. package/scripts/topo-chip-empty-recede-test.mjs +116 -0
  226. package/scripts/topo-chip-focus-test.mjs +95 -0
  227. package/scripts/topo-chip-pin-mirror-test.mjs +113 -0
  228. package/scripts/topo-chip-row-semantic-gap-test.mjs +97 -0
  229. package/scripts/topo-chip-row-tabular-test.mjs +111 -0
  230. package/scripts/topo-chrome-a11y-test.mjs +107 -0
  231. package/scripts/topo-chrome-pop-extension-test.mjs +98 -0
  232. package/scripts/topo-chrome-pop-nodesize-test.mjs +88 -0
  233. package/scripts/topo-chrome-press-state-test.mjs +135 -0
  234. package/scripts/topo-chrome-strip-semantic-gap-test.mjs +99 -0
  235. package/scripts/topo-chrome-zoom-level-pop-test.mjs +122 -0
  236. package/scripts/topo-chrome-zoom-pop-test.mjs +130 -0
  237. package/scripts/topo-click-ripple-ease-test.mjs +94 -0
  238. package/scripts/topo-cmdk-edge-clear-test.mjs +109 -0
  239. package/scripts/topo-cmdk-layout-view-test.mjs +108 -0
  240. package/scripts/topo-cmdk-pin-test.mjs +123 -0
  241. package/scripts/topo-cmdk-vendor-clear-test.mjs +133 -0
  242. package/scripts/topo-cmdk-vendor-pin-test.mjs +103 -0
  243. package/scripts/topo-corner-radius-unify-test.mjs +103 -0
  244. package/scripts/topo-cwd-tooltip-test.mjs +64 -0
  245. package/scripts/topo-dblclick-reset-test.mjs +74 -0
  246. package/scripts/topo-dense-alias-fill-transition-test.mjs +133 -0
  247. package/scripts/topo-discrete-zoom-smooth-test.mjs +125 -0
  248. package/scripts/topo-dispatch-pulse-test.mjs +83 -0
  249. package/scripts/topo-edge-badge-click-pin-test.mjs +94 -0
  250. package/scripts/topo-edge-badge-crossfade-test.mjs +138 -0
  251. package/scripts/topo-edge-badge-hot-test.mjs +115 -0
  252. package/scripts/topo-edge-badge-hover-test.mjs +85 -0
  253. package/scripts/topo-edge-badge-lift-test.mjs +150 -0
  254. package/scripts/topo-edge-badge-ripple-test.mjs +111 -0
  255. package/scripts/topo-edge-badge-stroke-transition-test.mjs +118 -0
  256. package/scripts/topo-edge-badge-text-pin-test.mjs +121 -0
  257. package/scripts/topo-edge-badge-theme-ease-test.mjs +89 -0
  258. package/scripts/topo-edge-count-badge-test.mjs +86 -0
  259. package/scripts/topo-edge-endpoint-highlight-test.mjs +111 -0
  260. package/scripts/topo-edge-fadein-test.mjs +106 -0
  261. package/scripts/topo-edge-focus-test.mjs +113 -0
  262. package/scripts/topo-edge-hover-test.mjs +93 -0
  263. package/scripts/topo-edge-pill-hot-test.mjs +129 -0
  264. package/scripts/topo-edge-pill-test.mjs +105 -0
  265. package/scripts/topo-edge-smil-sync-test.mjs +148 -0
  266. package/scripts/topo-edge-smil-theme-ease-test.mjs +97 -0
  267. package/scripts/topo-edge-stroke-ease-test.mjs +100 -0
  268. package/scripts/topo-edge-stroke-transition-test.mjs +132 -0
  269. package/scripts/topo-edge-tooltip-test.mjs +70 -0
  270. package/scripts/topo-empty-state-breath-test.mjs +150 -0
  271. package/scripts/topo-empty-state-crossfade-test.mjs +130 -0
  272. package/scripts/topo-empty-state-recenter-test.mjs +107 -0
  273. package/scripts/topo-endpoint-ring-fade-test.mjs +132 -0
  274. package/scripts/topo-endpoint-ring-test.mjs +93 -0
  275. package/scripts/topo-endpoint-ring-thicken-test.mjs +112 -0
  276. package/scripts/topo-esc-clears-pin-test.mjs +98 -0
  277. package/scripts/topo-filter-pill-aliases-test.mjs +121 -0
  278. package/scripts/topo-filter-pill-body-click-test.mjs +112 -0
  279. package/scripts/topo-filter-pill-count-test.mjs +110 -0
  280. package/scripts/topo-filter-pill-mobile-test.mjs +90 -0
  281. package/scripts/topo-fit-key-test.mjs +106 -0
  282. package/scripts/topo-freshness-chip-test.mjs +74 -0
  283. package/scripts/topo-freshness-chip-transition-test.mjs +80 -0
  284. package/scripts/topo-fullscreen-active-test.mjs +115 -0
  285. package/scripts/topo-group-box-pinned-state-test.mjs +95 -0
  286. package/scripts/topo-group-count-test.mjs +6 -1
  287. package/scripts/topo-group-label-a11y-test.mjs +113 -0
  288. package/scripts/topo-group-label-count-inherit-test.mjs +99 -0
  289. package/scripts/topo-group-label-hover-test.mjs +119 -0
  290. package/scripts/topo-group-label-pin-spacing-test.mjs +136 -0
  291. package/scripts/topo-group-label-tint-test.mjs +99 -0
  292. package/scripts/topo-group-label-title-test.mjs +83 -0
  293. package/scripts/topo-group-pin-test.mjs +119 -0
  294. package/scripts/topo-group-pip-fadein-test.mjs +100 -0
  295. package/scripts/topo-group-pip-tabular-test.mjs +95 -0
  296. package/scripts/topo-group-status-pips-test.mjs +103 -0
  297. package/scripts/topo-groupbox-fadein-test.mjs +95 -0
  298. package/scripts/topo-groupbox-hover-lift-test.mjs +127 -0
  299. package/scripts/topo-groupbox-hubcore-ease-test.mjs +106 -0
  300. package/scripts/topo-groupbox-live-test.mjs +100 -0
  301. package/scripts/topo-groupbox-march-busyness-test.mjs +155 -0
  302. package/scripts/topo-groupbox-test.mjs +4 -2
  303. package/scripts/topo-halo-breath-ease-test.mjs +92 -0
  304. package/scripts/topo-halo-breath-stagger-test.mjs +89 -0
  305. package/scripts/topo-help-overlay-r109-test.mjs +67 -0
  306. package/scripts/topo-help-overlay-r118-test.mjs +69 -0
  307. package/scripts/topo-help-overlay-r123-test.mjs +69 -0
  308. package/scripts/topo-help-overlay-r134-test.mjs +74 -0
  309. package/scripts/topo-help-overlay-r137-test.mjs +81 -0
  310. package/scripts/topo-help-overlay-r141-test.mjs +86 -0
  311. package/scripts/topo-help-overlay-r153-test.mjs +85 -0
  312. package/scripts/topo-help-overlay-r59-test.mjs +67 -0
  313. package/scripts/topo-help-overlay-r65-test.mjs +64 -0
  314. package/scripts/topo-help-overlay-r70-test.mjs +63 -0
  315. package/scripts/topo-help-overlay-r78-test.mjs +59 -0
  316. package/scripts/topo-help-overlay-r81-test.mjs +58 -0
  317. package/scripts/topo-help-overlay-r87-test.mjs +64 -0
  318. package/scripts/topo-help-overlay-r91-test.mjs +65 -0
  319. package/scripts/topo-hot-tail-crossfade-test.mjs +117 -0
  320. package/scripts/topo-html-overlay-padding-unify-test.mjs +120 -0
  321. package/scripts/topo-hub-a11y-test.mjs +112 -0
  322. package/scripts/topo-hub-busyness-test.mjs +100 -0
  323. package/scripts/topo-hub-center-crossfade-test.mjs +124 -0
  324. package/scripts/topo-hub-click-fit-test.mjs +137 -0
  325. package/scripts/topo-hub-digit-scale-test.mjs +129 -0
  326. package/scripts/topo-hub-fadein-test.mjs +88 -0
  327. package/scripts/topo-hub-hover-lift-test.mjs +101 -0
  328. package/scripts/topo-hub-hover-ring-test.mjs +82 -0
  329. package/scripts/topo-hub-pip-theme-ease-test.mjs +116 -0
  330. package/scripts/topo-hub-spoke-ease-test.mjs +120 -0
  331. package/scripts/topo-hub-title-test.mjs +93 -0
  332. package/scripts/topo-hub-working-count-test.mjs +99 -0
  333. package/scripts/topo-ingroup-edge-test.mjs +103 -0
  334. package/scripts/topo-label-card-theme-ease-test.mjs +85 -0
  335. package/scripts/topo-label-lift-test.mjs +119 -0
  336. package/scripts/topo-lastseen-tooltip-test.mjs +59 -0
  337. package/scripts/topo-layout-key-test.mjs +91 -0
  338. package/scripts/topo-layout-switch-crossfade-test.mjs +126 -0
  339. package/scripts/topo-layout-toggle-test.mjs +131 -0
  340. package/scripts/topo-legend-count-empty-test.mjs +126 -0
  341. package/scripts/topo-legend-count-test.mjs +88 -0
  342. package/scripts/topo-legend-count-tier-fill-test.mjs +121 -0
  343. package/scripts/topo-legend-header-symmetric-inset-test.mjs +112 -0
  344. package/scripts/topo-legend-hover-test.mjs +113 -0
  345. package/scripts/topo-legend-panel-header-test.mjs +83 -0
  346. package/scripts/topo-legend-pin-ring-fade-test.mjs +112 -0
  347. package/scripts/topo-legend-pin-test.mjs +128 -0
  348. package/scripts/topo-legend-row-hover-lift-test.mjs +109 -0
  349. package/scripts/topo-legend-row-pin-spacing-test.mjs +124 -0
  350. package/scripts/topo-legend-row-tint-test.mjs +95 -0
  351. package/scripts/topo-legend-row-title-test.mjs +102 -0
  352. package/scripts/topo-legend-swatch-grow-test.mjs +116 -0
  353. package/scripts/topo-minimap-a11y-test.mjs +101 -0
  354. package/scripts/topo-minimap-dot-transition-test.mjs +113 -0
  355. package/scripts/topo-minimap-test.mjs +68 -0
  356. package/scripts/topo-minimap-viewport-smooth-test.mjs +124 -0
  357. package/scripts/topo-mobile-chips-test.mjs +103 -0
  358. package/scripts/topo-node-a11y-test.mjs +107 -0
  359. package/scripts/topo-node-hover-lift-test.mjs +85 -0
  360. package/scripts/topo-node-label-card-elevation-test.mjs +157 -0
  361. package/scripts/topo-node-label-card-stroke-test.mjs +135 -0
  362. package/scripts/topo-node-stroke-transition-test.mjs +103 -0
  363. package/scripts/topo-node-text-fill-transition-test.mjs +119 -0
  364. package/scripts/topo-node-title-peers-test.mjs +107 -0
  365. package/scripts/topo-node-title-test.mjs +101 -0
  366. package/scripts/topo-nodesize-active-hover-test.mjs +109 -0
  367. package/scripts/topo-nodesize-crossfade-test.mjs +141 -0
  368. package/scripts/topo-online-chip-nav-test.mjs +143 -0
  369. package/scripts/topo-orbit-busyness-test.mjs +96 -0
  370. package/scripts/topo-orbit-opacity-bucket-test.mjs +124 -0
  371. package/scripts/topo-overlap-test.mjs +32 -5
  372. package/scripts/topo-pan-cursor-test.mjs +62 -0
  373. package/scripts/topo-panel-elevation-test.mjs +76 -0
  374. package/scripts/topo-panel-fadein-test.mjs +102 -0
  375. package/scripts/topo-panel-hot-tail-fade-test.mjs +132 -0
  376. package/scripts/topo-panel-hover-elevation-test.mjs +118 -0
  377. package/scripts/topo-panel-theme-ease-test.mjs +85 -0
  378. package/scripts/topo-particle-stagger-test.mjs +82 -0
  379. package/scripts/topo-pin-intersection-color-ease-test.mjs +111 -0
  380. package/scripts/topo-pin-intersection-empty-test.mjs +129 -0
  381. package/scripts/topo-pin-intersection-tabular-test.mjs +96 -0
  382. package/scripts/topo-pin-intersection-test.mjs +121 -0
  383. package/scripts/topo-pin-intersection-warning-crossfade-test.mjs +107 -0
  384. package/scripts/topo-pin-mirror-transition-test.mjs +101 -0
  385. package/scripts/topo-pin-persistence-test.mjs +105 -0
  386. package/scripts/topo-ping-pulse-ease-test.mjs +96 -0
  387. package/scripts/topo-popover-meta-test.mjs +78 -0
  388. package/scripts/topo-prepublish-guard-test.mjs +95 -0
  389. package/scripts/topo-pressure-bar-test.mjs +69 -0
  390. package/scripts/topo-pressure-hover-test.mjs +123 -0
  391. package/scripts/topo-pressure-pin-test.mjs +119 -0
  392. package/scripts/topo-pressure-seg-self-hover-test.mjs +126 -0
  393. package/scripts/topo-pressure-seg-tooltip-test.mjs +98 -0
  394. package/scripts/topo-pressure-seg-transition-test.mjs +107 -0
  395. package/scripts/topo-pulse-dot-crossfade-test.mjs +122 -0
  396. package/scripts/topo-pulse-rate-test.mjs +71 -0
  397. package/scripts/topo-recent-panel-count-freshness-test.mjs +109 -0
  398. package/scripts/topo-recent-panel-count-test.mjs +72 -0
  399. package/scripts/topo-recent-panel-footer-breath-test.mjs +142 -0
  400. package/scripts/topo-recent-panel-hot-header-test.mjs +132 -0
  401. package/scripts/topo-recent-panel-more-crossfade-test.mjs +124 -0
  402. package/scripts/topo-recent-panel-more-cyan-hover-test.mjs +151 -0
  403. package/scripts/topo-recent-panel-more-nav-test.mjs +127 -0
  404. package/scripts/topo-recent-panel-more-test.mjs +106 -0
  405. package/scripts/topo-recent-panel-readability-bump-test.mjs +151 -0
  406. package/scripts/topo-recent-row-count-fill-test.mjs +129 -0
  407. package/scripts/topo-recent-row-fadein-test.mjs +107 -0
  408. package/scripts/topo-recent-row-freshness-test.mjs +102 -0
  409. package/scripts/topo-recent-row-hot-test.mjs +110 -0
  410. package/scripts/topo-recent-row-hover-lift-test.mjs +128 -0
  411. package/scripts/topo-recent-row-hover-test.mjs +122 -0
  412. package/scripts/topo-recent-row-pin-spacing-test.mjs +137 -0
  413. package/scripts/topo-recent-row-pin-test.mjs +126 -0
  414. package/scripts/topo-recent-row-tint-test.mjs +91 -0
  415. package/scripts/topo-recent-row-title-test.mjs +112 -0
  416. package/scripts/topo-recent-row-ts-alpha-test.mjs +100 -0
  417. package/scripts/topo-recent-row-ts-test.mjs +95 -0
  418. package/scripts/topo-recent-row-typography-test.mjs +119 -0
  419. package/scripts/topo-recent-signal-empty-hint-test.mjs +73 -0
  420. package/scripts/topo-recent-signal-empty-test.mjs +63 -0
  421. package/scripts/topo-reduced-motion-test.mjs +77 -0
  422. package/scripts/topo-reset-spin-test.mjs +120 -0
  423. package/scripts/topo-runtime-badge-grow-test.mjs +114 -0
  424. package/scripts/topo-spoke-busyness-test.mjs +110 -0
  425. package/scripts/topo-spoke-tint-test.mjs +96 -0
  426. package/scripts/topo-status-chip-hover-test.mjs +101 -0
  427. package/scripts/topo-status-chip-tooltip-test.mjs +95 -0
  428. package/scripts/topo-statusbar-chips-self-hover-test.mjs +177 -0
  429. package/scripts/topo-svg-focus-test.mjs +112 -0
  430. package/scripts/topo-sweep-busyness-test.mjs +94 -0
  431. package/scripts/topo-tabular-nums-sweep-test.mjs +93 -0
  432. package/scripts/topo-tier-ring-fadein-test.mjs +107 -0
  433. package/scripts/topo-tier-ring-occupancy-test.mjs +87 -0
  434. package/scripts/topo-tier-ring-test.mjs +69 -0
  435. package/scripts/topo-tier-ring-tint-test.mjs +110 -0
  436. package/scripts/topo-tier-stagger-test.mjs +89 -0
  437. package/scripts/topo-tz-ghost-test.mjs +69 -0
  438. package/scripts/topo-vendor-chip-hover-test.mjs +111 -0
  439. package/scripts/topo-vendor-chip-self-hover-test.mjs +130 -0
  440. package/scripts/topo-vendor-chip-tabular-test.mjs +89 -0
  441. package/scripts/topo-vendor-letter-tooltip-test.mjs +92 -0
  442. package/scripts/topo-vendor-pill-test.mjs +125 -0
  443. package/scripts/topo-vendor-pin-test.mjs +125 -0
  444. package/scripts/topo-view-smooth-test.mjs +131 -0
  445. package/scripts/topo-wheel-zoom-test.mjs +79 -0
  446. package/scripts/topo-working-chip-pin-test.mjs +154 -0
  447. package/scripts/topo-working-halo-breath-test.mjs +86 -0
  448. package/scripts/topo-wrapper-flow-arrow-theme-test.mjs +97 -0
  449. package/scripts/topo-wrapper-shadow-theme-ease-test.mjs +89 -0
  450. package/scripts/topo-zoom-keys-test.mjs +89 -0
  451. package/.next/static/chunks/0-7iowzu4.798.js +0 -1
  452. package/.next/static/chunks/02t5jr4shh.hy.js +0 -1
  453. package/.next/static/chunks/04ppgi-mphhr6.js +0 -1
  454. package/.next/static/chunks/09nd.t_hd.y6t.css +0 -2
  455. package/.next/static/chunks/0_qfg5s5uzs7..js +0 -7
  456. package/.next/static/chunks/0elalb9n_.~di.js +0 -1
  457. package/.next/static/chunks/0fqy_0wj_1xa7.js +0 -7
  458. package/.next/static/chunks/0j_q__o8p6g83.js +0 -1
  459. package/.next/static/chunks/0jl6_w50zh4l5.js +0 -1
  460. package/.next/static/chunks/0r4q73.-1bkyl.js +0 -1
  461. package/.next/static/chunks/0r_c8eccg3z2o.js +0 -1
  462. package/.next/static/chunks/0zs511wcsq~0j.js +0 -1
  463. package/.next/static/chunks/10oi7l-tycgl9.js +0 -1
  464. package/.next/static/chunks/11uk1izuv3~0t.js +0 -1
  465. package/.next/static/chunks/15gj.t675_yw3.js +0 -1
  466. /package/.next/static/{kk1qr2sXSqVvoqBT7fk1x → zfTklPhuNFS7pf_LmLAvq}/_buildManifest.js +0 -0
  467. /package/.next/static/{kk1qr2sXSqVvoqBT7fk1x → zfTklPhuNFS7pf_LmLAvq}/_clientMiddlewareManifest.js +0 -0
  468. /package/.next/static/{kk1qr2sXSqVvoqBT7fk1x → zfTklPhuNFS7pf_LmLAvq}/_ssgManifest.js +0 -0
@@ -0,0 +1,143 @@
1
+ /* Round 140 verification: the "N online" chip in the chip row
2
+ * gets an onClick that navigates to /nodes when interactive.
3
+ * Mirrors R136 active-links→/messages idiom; complements R139
4
+ * working→pin idiom. The online chip is the third chip-row chip
5
+ * that had cursor:pointer without a click action since R79.
6
+ *
7
+ * Why nav not pin: "online" = working + idle in semantics; there's
8
+ * no single pinnedStatus value that captures both. /nodes is the
9
+ * natural full-list destination.
10
+ *
11
+ * Three states tested:
12
+ * - Empty fleet (onlineCount=0): chip not interactive
13
+ * - 1 online: clickable, role=link, click → /nodes
14
+ * - 5 online (mix of working+idle): same shape, click → /nodes
15
+ *
16
+ * Hover preview still works (R79 highlight all online) — that's
17
+ * a separate gesture verified in topo-active-links-chip-hover-test
18
+ * for the chip-row's hover ladder.
19
+ */
20
+ import { chromium } from 'playwright';
21
+ import { readFileSync } from 'node:fs';
22
+
23
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
24
+
25
+ // status: 'working' | 'idle' | 'offline' — distribute by counts
26
+ async function probe({ working, idle, offline }) {
27
+ const browser = await chromium.launch({ headless: true });
28
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
29
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
30
+ await ctx.addInitScript(() => {
31
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
32
+ });
33
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
34
+ await ctx.route('**/api/hub/status*', async (route) => {
35
+ const r = await route.fetch();
36
+ const b = await r.json();
37
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
38
+ const sessions = [];
39
+ let i = 0;
40
+ for (let n = 0; n < working; n++, i++) sessions.push(mk(i, 'working', fresh, nid));
41
+ for (let n = 0; n < idle; n++, i++) sessions.push(mk(i, 'idle', fresh, nid));
42
+ for (let n = 0; n < offline; n++, i++) sessions.push(mk(i, 'offline', fresh, nid));
43
+ await route.fulfill({ response: r, json: { ...b, sessions } });
44
+ });
45
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
46
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
47
+
48
+ const page = await ctx.newPage();
49
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
50
+ // offline-only fleet won't render g[data-node] (only online ones do),
51
+ // but the chip row still renders because sessions.length > 0.
52
+ await page.waitForSelector('[data-online-chip]', { timeout: 10000 });
53
+ await page.waitForTimeout(400);
54
+
55
+ return { browser, page };
56
+ }
57
+
58
+ function mk(i, status, fresh, nid) {
59
+ return {
60
+ alias: `node${i}`,
61
+ status,
62
+ model: 'claude-opus-4', runtime: 'cli-claude-code',
63
+ network_id: nid, project_dir: null,
64
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
65
+ };
66
+ }
67
+
68
+ // State A: 5 online (3 working / 2 idle)
69
+ const { browser: b1, page: p1 } = await probe({ working: 3, idle: 2, offline: 0 });
70
+ const fiveProbe = await p1.evaluate(() => {
71
+ const chip = document.querySelector('[data-online-chip]');
72
+ return {
73
+ text: chip?.textContent,
74
+ clickable: chip?.getAttribute('data-online-chip-clickable'),
75
+ role: chip?.getAttribute('role'),
76
+ tabindex: chip?.getAttribute('tabindex'),
77
+ cursor: chip?.getAttribute('style')?.includes('cursor: pointer'),
78
+ title: chip?.getAttribute('title'),
79
+ };
80
+ });
81
+ await p1.locator('[data-online-chip]').click();
82
+ await p1.waitForURL('**/nodes', { timeout: 10000 });
83
+ const fiveUrl = p1.url();
84
+ await b1.close();
85
+
86
+ // State B: 1 online (1 idle, 0 working)
87
+ const { browser: b2, page: p2 } = await probe({ working: 0, idle: 1, offline: 0 });
88
+ const oneProbe = await p2.evaluate(() => {
89
+ const chip = document.querySelector('[data-online-chip]');
90
+ return {
91
+ text: chip?.textContent,
92
+ clickable: chip?.getAttribute('data-online-chip-clickable'),
93
+ role: chip?.getAttribute('role'),
94
+ };
95
+ });
96
+ await p2.locator('[data-online-chip]').click();
97
+ await p2.waitForURL('**/nodes', { timeout: 10000 });
98
+ const oneUrl = p2.url();
99
+ await b2.close();
100
+
101
+ // State C: offline-only fleet (2 sessions, all offline → onlineNodes=0)
102
+ // 0-session fleet hits Overview's R52/R70 first-run CTA path, not
103
+ // TopoGraph; offline-only keeps the chip row alive so the non-
104
+ // interactive contract on "0 online" can be verified.
105
+ const { browser: b3, page: p3 } = await probe({ working: 0, idle: 0, offline: 2 });
106
+ const emptyProbe = await p3.evaluate(() => {
107
+ const chip = document.querySelector('[data-online-chip]');
108
+ return {
109
+ text: chip?.textContent,
110
+ clickable: chip?.getAttribute('data-online-chip-clickable'),
111
+ role: chip?.getAttribute('role'),
112
+ tabindex: chip?.getAttribute('tabindex'),
113
+ cursor: chip?.getAttribute('style')?.includes('cursor: pointer'),
114
+ };
115
+ });
116
+ await b3.close();
117
+
118
+ const results = {
119
+ five_text5Online: fiveProbe.text === '5 online',
120
+ five_clickableTrue: fiveProbe.clickable === 'true',
121
+ five_roleLink: fiveProbe.role === 'link',
122
+ five_tabIndex0: fiveProbe.tabindex === '0',
123
+ five_cursorPointer: fiveProbe.cursor === true,
124
+ five_titleMentionsNodes: (fiveProbe.title || '').includes('/nodes'),
125
+ five_navToNodes: /\/nodes$/.test(fiveUrl),
126
+
127
+ one_text1Online: oneProbe.text === '1 online',
128
+ one_clickableTrue: oneProbe.clickable === 'true',
129
+ one_roleLink: oneProbe.role === 'link',
130
+ one_navToNodes: /\/nodes$/.test(oneUrl),
131
+
132
+ empty_text0Online: emptyProbe.text === '0 online',
133
+ empty_clickableFalse: emptyProbe.clickable === 'false',
134
+ empty_noRole: emptyProbe.role === null || emptyProbe.role === undefined,
135
+ empty_noTabIndex: emptyProbe.tabindex === null || emptyProbe.tabindex === undefined,
136
+ empty_noCursor: !emptyProbe.cursor,
137
+ };
138
+ const ok = Object.values(results).every(Boolean);
139
+ console.log(`${ok ? '✅' : '❌'} online chip nav:`, JSON.stringify(results),
140
+ `\n fiveProbe=`, fiveProbe, ` fiveUrl=`, fiveUrl,
141
+ `\n oneProbe=`, oneProbe, ` oneUrl=`, oneUrl,
142
+ `\n emptyProbe=`, emptyProbe);
143
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,96 @@
1
+ /* Round 131 verification: outer-ring orbit period buckets on
2
+ * workingCount, mirroring R84's hub-breath cadence. Idle fleet
3
+ * keeps the original 16s "calm sweep"; as work piles up the
4
+ * orbit accelerates to 14 / 12 / 10s. Both motion layers now
5
+ * read the same underlying "is the network busy" signal.
6
+ *
7
+ * Bucket boundaries identical to R84 (line ~2702):
8
+ * workingCount === 0 → bucket 0, dur 16s
9
+ * workingCount in [1, 2] → bucket 1, dur 14s
10
+ * workingCount in [3, 5] → bucket 2, dur 12s
11
+ * workingCount >= 6 → bucket 3, dur 10s
12
+ *
13
+ * R50 only renders on cyber theme (light is skipped to keep the
14
+ * white surface clean). All probes use cyber.
15
+ *
16
+ * Probe fleet sizes pick one representative per bucket.
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
+
23
+ async function probe(workingCount, totalCount) {
24
+ const browser = await chromium.launch({ headless: true });
25
+ const ctx = await browser.newContext({ viewport: { width: 1400, 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 sessions = [];
36
+ for (let i = 0; i < totalCount; i++) {
37
+ sessions.push({
38
+ alias: `node${i}`,
39
+ status: i < workingCount ? 'working' : 'idle',
40
+ model: 'claude-opus-4', runtime: 'cli-claude-code',
41
+ network_id: nid, project_dir: null,
42
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
43
+ });
44
+ }
45
+ await route.fulfill({ response: r, json: { ...b, sessions } });
46
+ });
47
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
48
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
49
+
50
+ const page = await ctx.newPage();
51
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
52
+ await page.waitForFunction((n) => document.querySelectorAll('g[data-node]').length === n, totalCount, { timeout: 30000 });
53
+ await page.waitForTimeout(400);
54
+
55
+ const out = await page.evaluate(() => {
56
+ const orbits = [...document.querySelectorAll('[data-topo-orbit-bucket]')];
57
+ return orbits.map(g => ({
58
+ bucket: g.getAttribute('data-topo-orbit-bucket'),
59
+ dur: g.getAttribute('data-topo-orbit-dur'),
60
+ animDur: g.querySelector('animateTransform')?.getAttribute('dur'),
61
+ }));
62
+ });
63
+
64
+ await browser.close();
65
+ return out;
66
+ }
67
+
68
+ // Pick one from each bucket
69
+ const b0 = await probe(0, 4); // workingCount=0 → bucket 0, dur 16
70
+ const b1 = await probe(2, 4); // workingCount=2 → bucket 1, dur 14
71
+ const b2 = await probe(4, 5); // workingCount=4 → bucket 2, dur 12
72
+ const b3 = await probe(7, 8); // workingCount=7 → bucket 3, dur 10
73
+
74
+ const allConsistent = (orbits, bucket, dur) =>
75
+ orbits.length === 4 &&
76
+ orbits.every(o => o.bucket === String(bucket) && o.dur === String(dur) && o.animDur === `${dur}s`);
77
+
78
+ const results = {
79
+ b0_fourOrbits: b0.length === 4,
80
+ b0_bucket0_16s: allConsistent(b0, 0, 16),
81
+ b1_bucket1_14s: allConsistent(b1, 1, 14),
82
+ b2_bucket2_12s: allConsistent(b2, 2, 12),
83
+ b3_bucket3_10s: allConsistent(b3, 3, 10),
84
+ // dur strictly decreases as busy increases
85
+ dur_monotonic:
86
+ Number(b0[0]?.dur) > Number(b1[0]?.dur) &&
87
+ Number(b1[0]?.dur) > Number(b2[0]?.dur) &&
88
+ Number(b2[0]?.dur) > Number(b3[0]?.dur),
89
+ };
90
+ const ok = Object.values(results).every(Boolean);
91
+ console.log(`${ok ? '✅' : '❌'} orbit busyness:`, JSON.stringify(results),
92
+ `\n b0(0w/4)=`, b0,
93
+ `\n b1(2w/4)=`, b1,
94
+ `\n b2(4w/5)=`, b2,
95
+ `\n b3(7w/8)=`, b3);
96
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,124 @@
1
+ /* Round 216 verification: outer-ring orbit particle opacity scales
2
+ * with busy bucket alongside R131's speed scaling. Idle fleet → dim
3
+ * particles; busy fleet → bright. transitions on opacity 300ms.
4
+ *
5
+ * Bucket map: workingCount 0 → busy 0; 1-2 → 1; 3-5 → 2; 6+ → 3
6
+ * opacity [0.5, 0.7, 0.85, 1.0]
7
+ *
8
+ * Test scenarios (cyber dark, orbit particles only render in dark):
9
+ * A. 0 working sessions (4 idle): busy=0, opacity≈0.5
10
+ * B. 2 working sessions (2 working+2 idle): busy=1, opacity≈0.7
11
+ * C. 6 working sessions: busy=3, opacity≈1.0
12
+ * Every orbit particle has style.transition: 'opacity 300ms'
13
+ * Every orbit <g> exposes data-topo-orbit-opacity matching the
14
+ * bucket value (test verifies derivation + DOM agreement).
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
+
22
+ async function pageFor(workingN) {
23
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
24
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
25
+ await ctx.addInitScript(() => {
26
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
27
+ });
28
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
29
+ const totalSessions = Math.max(workingN, 4);
30
+ await ctx.route('**/api/hub/status*', async (route) => {
31
+ const r = await route.fetch();
32
+ const b = await r.json();
33
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
34
+ const mk = (alias, status) => ({
35
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
36
+ network_id: nid, project_dir: null,
37
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
38
+ });
39
+ const sessions = [];
40
+ for (let i = 0; i < totalSessions; i++) {
41
+ sessions.push(mk(`node-${(i + 1).toString().padStart(2, '0')}`, i < workingN ? 'working' : 'idle'));
42
+ }
43
+ await route.fulfill({ response: r, json: { ...b, sessions } });
44
+ });
45
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: [] } }));
46
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.fulfill({ json: { tasks: [] } }));
47
+ const page = await ctx.newPage();
48
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
49
+ await page.waitForFunction((n) => document.querySelectorAll('g[data-node]').length === n, totalSessions, { timeout: 30000 });
50
+ await page.waitForSelector('[data-topo-orbit-bucket]', { timeout: 10000, state: 'attached' });
51
+ await page.waitForTimeout(300);
52
+ return page;
53
+ }
54
+
55
+ const probe = async (page) => page.evaluate(() => {
56
+ return [...document.querySelectorAll('[data-topo-orbit-bucket]')].map(el => {
57
+ const inner = el.querySelector('circle');
58
+ return {
59
+ bucket: el.getAttribute('data-topo-orbit-bucket'),
60
+ bucketOpacity: el.getAttribute('data-topo-orbit-opacity'),
61
+ dur: el.getAttribute('data-topo-orbit-dur'),
62
+ circleOpacity: parseFloat(inner?.getAttribute('opacity') || ''),
63
+ transition: inner?.style.transition,
64
+ };
65
+ });
66
+ });
67
+
68
+ // Scenario A: 0 working → busy=0, opacity=0.5
69
+ const pageA = await pageFor(0);
70
+ const probeA = await probe(pageA);
71
+ await pageA.close();
72
+
73
+ // Scenario B: 2 working → busy=1, opacity=0.7
74
+ const pageB = await pageFor(2);
75
+ const probeB = await probe(pageB);
76
+ await pageB.close();
77
+
78
+ // Scenario C: 6 working → busy=3, opacity=1.0
79
+ const pageC = await pageFor(6);
80
+ const probeC = await probe(pageC);
81
+ await pageC.close();
82
+ await browser.close();
83
+
84
+ const near = (got, want, tol = 0.05) => Math.abs(got - want) < tol;
85
+ const hasOpacityT = (s) => /opacity\s+(?:300ms|0\.3s)/.test(s || '');
86
+
87
+ const results = {
88
+ // 4 orbit particles in each scenario (R50 phase array of 4)
89
+ A_four_particles: probeA.length === 4,
90
+ B_four_particles: probeB.length === 4,
91
+ C_four_particles: probeC.length === 4,
92
+
93
+ // Scenario A — idle fleet (busy=0)
94
+ A_bucket_0: probeA.every(p => p.bucket === '0'),
95
+ A_opacity_half: probeA.every(p => near(p.circleOpacity, 0.5)),
96
+ A_data_attr_matches: probeA.every(p => parseFloat(p.bucketOpacity) === p.circleOpacity),
97
+
98
+ // Scenario B — light load (busy=1)
99
+ B_bucket_1: probeB.every(p => p.bucket === '1'),
100
+ B_opacity_0p7: probeB.every(p => near(p.circleOpacity, 0.7)),
101
+
102
+ // Scenario C — heavy load (busy=3)
103
+ C_bucket_3: probeC.every(p => p.bucket === '3'),
104
+ C_opacity_full: probeC.every(p => near(p.circleOpacity, 1.0)),
105
+
106
+ // Brightness actually grew across buckets
107
+ brightness_grew: probeA[0].circleOpacity < probeB[0].circleOpacity
108
+ && probeB[0].circleOpacity < probeC[0].circleOpacity,
109
+
110
+ // Transitions present in every particle of every scenario
111
+ A_all_have_transition: probeA.every(p => hasOpacityT(p.transition)),
112
+ B_all_have_transition: probeB.every(p => hasOpacityT(p.transition)),
113
+ C_all_have_transition: probeC.every(p => hasOpacityT(p.transition)),
114
+
115
+ // R131's existing speed scaling still derives — dur shortens as
116
+ // bucket grows
117
+ speed_scales: parseFloat(probeA[0].dur || '0') > parseFloat(probeC[0].dur || '99'),
118
+ };
119
+ const ok = Object.values(results).every(Boolean);
120
+ console.log(`${ok ? '✅' : '❌'} orbit opacity bucket:`, JSON.stringify(results),
121
+ `\n probeA (0 working):`, probeA.slice(0, 1),
122
+ `\n probeB (2 working):`, probeB.slice(0, 1),
123
+ `\n probeC (6 working):`, probeC.slice(0, 1));
124
+ process.exit(ok ? 0 : 1);
@@ -3,7 +3,10 @@
3
3
  * - node ↔ node — status rings don't intersect
4
4
  * - node ↔ panel — no node circle intersects an overlay panel rect
5
5
  * - node ↔ group-label — no group-box label text covers a node
6
- * - group-box ↔ box — no two group boxes overlap */
6
+ * - group-box ↔ box — no two group boxes overlap
7
+ * - label ↔ label — no two text labels overlap (Round 27 — caught
8
+ * the dense-node-label vs group-label collision
9
+ * Vincent reported in preview.29) */
7
10
  import { chromium } from 'playwright';
8
11
  import { readFileSync, mkdirSync } from 'node:fs';
9
12
 
@@ -80,11 +83,24 @@ async function check(layout) {
80
83
  label: { x: bb.x, y: bb.y, w: bb.width, h: bb.height },
81
84
  };
82
85
  });
83
- return { nodes, panels, groups };
86
+ // Round 27 / P0: every visible label bbox (node alias text + group
87
+ // headings) so we can assert label↔label clearance. Skip elements
88
+ // with empty bboxes (e.g., text inside an unrendered <g>).
89
+ const allLabels = [];
90
+ for (const t of svg.querySelectorAll('g[data-node] text, g[data-group] text')) {
91
+ const bb = t.getBBox();
92
+ if (bb.width === 0 || bb.height === 0) continue;
93
+ allLabels.push({
94
+ kind: t.closest('g[data-group]') ? 'group' : 'node',
95
+ owner: (t.closest('g[data-group]')?.getAttribute('data-group')) || (t.closest('g[data-node]')?.getAttribute('data-node')) || '?',
96
+ x: bb.x, y: bb.y, w: bb.width, h: bb.height,
97
+ });
98
+ }
99
+ return { nodes, panels, groups, allLabels };
84
100
  });
85
101
 
86
102
  const TOL = 2; // px slack — touching edges is fine, real overlap is not
87
- let nodeNode = 0, nodePanel = 0, nodeLabel = 0, boxBox = 0;
103
+ let nodeNode = 0, nodePanel = 0, nodeLabel = 0, boxBox = 0, labelLabel = 0;
88
104
 
89
105
  // node ↔ node
90
106
  for (let i = 0; i < data.nodes.length; i++) {
@@ -119,12 +135,23 @@ async function check(layout) {
119
135
  overlaps1D(a.y, a.y + a.h, b.y, b.y + b.h, TOL)) boxBox++;
120
136
  }
121
137
  }
138
+ // label ↔ label (Round 27)
139
+ // Same-node node-labels (e.g. alias + sub-status in the full card) are
140
+ // intentionally close and adjacent — skip pairs that share an owner.
141
+ for (let i = 0; i < data.allLabels.length; i++) {
142
+ for (let j = i + 1; j < data.allLabels.length; j++) {
143
+ const a = data.allLabels[i], b = data.allLabels[j];
144
+ if (a.owner === b.owner) continue;
145
+ if (overlaps1D(a.x, a.x + a.w, b.x, b.x + b.w, TOL) &&
146
+ overlaps1D(a.y, a.y + a.h, b.y, b.y + b.h, TOL)) labelLabel++;
147
+ }
148
+ }
122
149
 
123
150
  await page.screenshot({ path: `/tmp/anet-issue-112/overlap-${layout}.png` });
124
151
  await ctx.close();
125
- const ok = nodeNode === 0 && nodePanel === 0 && nodeLabel === 0 && boxBox === 0;
152
+ const ok = nodeNode === 0 && nodePanel === 0 && nodeLabel === 0 && boxBox === 0 && labelLabel === 0;
126
153
  console.log(`${ok ? '✅' : '❌'} [${layout}] nodes=${data.nodes.length} groups=${data.groups.length} — ` +
127
- `node↔node:${nodeNode} node↔panel:${nodePanel} node↔label:${nodeLabel} box↔box:${boxBox}`);
154
+ `node↔node:${nodeNode} node↔panel:${nodePanel} node↔label:${nodeLabel} box↔box:${boxBox} label↔label:${labelLabel}`);
128
155
  return ok;
129
156
  }
130
157
 
@@ -0,0 +1,62 @@
1
+ /* Round 21 verification: SVG cursor flips from grab → grabbing during
2
+ * a pan drag and back to grab on release. */
3
+ import { chromium } from 'playwright';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
7
+ const browser = await chromium.launch({ headless: true });
8
+ // Need a tall enough viewport to keep the SVG ABOVE the fold so real
9
+ // pointer events land — at 1280×900 the canvas starts at ~y=815 (the
10
+ // /admin hero + StatsBar above push it below the viewport bottom).
11
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 1600 } });
12
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
13
+ await ctx.addInitScript(() => {
14
+ try {
15
+ localStorage.setItem('anet-theme', 'cyber');
16
+ localStorage.setItem('anet-topo-layout', 'grid');
17
+ sessionStorage.setItem('anet_v3_auth', '1');
18
+ } catch {}
19
+ });
20
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
21
+ await ctx.route('**/api/hub/status*', async (route) => {
22
+ const r = await route.fetch();
23
+ const b = await r.json();
24
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
25
+ const sessions = ['a', 'b'].map(a => ({
26
+ alias: a, status: 'idle', network_id: nid, project_dir: null,
27
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
28
+ }));
29
+ await route.fulfill({ response: r, json: { ...b, sessions } });
30
+ });
31
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
32
+ const page = await ctx.newPage();
33
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
34
+ await page.waitForSelector('svg[viewBox="0 0 1000 680"]', { timeout: 30000 });
35
+ await page.waitForTimeout(400);
36
+
37
+ const cursorAt = () => page.$eval('svg[viewBox="0 0 1000 680"]', el => el.style.cursor);
38
+ const rest = await cursorAt();
39
+
40
+ // pick a pan-safe target far from any node — top-left empty canvas region
41
+ const box = await page.$eval('svg[viewBox="0 0 1000 680"]', el => el.getBoundingClientRect());
42
+ const sx = box.x + box.width * 0.08;
43
+ const sy = box.y + box.height * 0.40;
44
+
45
+ await page.mouse.move(sx, sy);
46
+ await page.mouse.down();
47
+ await page.mouse.move(sx + 40, sy + 30);
48
+ await page.waitForTimeout(50);
49
+ const during = await cursorAt();
50
+ await page.mouse.up();
51
+ await page.waitForTimeout(50);
52
+ const after = await cursorAt();
53
+
54
+ await browser.close();
55
+ const results = {
56
+ restIsGrab: rest === 'grab',
57
+ duringIsGrabbing: during === 'grabbing',
58
+ afterIsGrab: after === 'grab',
59
+ };
60
+ const ok = Object.values(results).every(Boolean);
61
+ console.log(`${ok ? '✅' : '❌'} pan cursor:`, JSON.stringify(results), `rest=${rest} during=${during} after=${after}`);
62
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,76 @@
1
+ /* Round 57 verification: recent-signal + legend panels carry a drop-
2
+ * shadow filter so they read as floating cards instead of pasted-on
3
+ * graphics — especially important on light theme where panel fill is
4
+ * near-white on a near-white canvas.
5
+ *
6
+ * - Both panels render a <rect> with `data-topo-panel-elevation`.
7
+ * - The rect's `style.filter` contains "drop-shadow(".
8
+ * - Light vs cyber themes use different shadow alpha (light tuned for
9
+ * contrast against white canvas; cyber for subtle depth on dark).
10
+ * Verify the strings differ so the theme switch isn't a no-op.
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
+
18
+ async function probe(theme) {
19
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 1500 } });
20
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
21
+ await ctx.addInitScript((t) => {
22
+ try {
23
+ localStorage.setItem('anet-theme', t);
24
+ localStorage.setItem('anet-topo-layout', 'ring');
25
+ sessionStorage.setItem('anet_v3_auth', '1');
26
+ } catch {}
27
+ }, theme);
28
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
29
+ await ctx.route('**/api/hub/status*', async (route) => {
30
+ const r = await route.fetch();
31
+ const b = await r.json();
32
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
33
+ const sessions = ['alpha', 'beta'].map(a => ({
34
+ alias: a, status: 'idle', network_id: nid, project_dir: null,
35
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
36
+ }));
37
+ await route.fulfill({ response: r, json: { ...b, sessions } });
38
+ });
39
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
40
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
41
+ const page = await ctx.newPage();
42
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
43
+ await page.waitForSelector('rect[data-topo-panel-elevation="recent"]', { timeout: 30000 });
44
+ await page.waitForTimeout(400);
45
+ const filters = await page.evaluate(() => {
46
+ const recent = document.querySelector('rect[data-topo-panel-elevation="recent"]');
47
+ const legend = document.querySelector('rect[data-topo-panel-elevation="legend"]');
48
+ return {
49
+ recent: recent?.getAttribute('style') || '',
50
+ legend: legend?.getAttribute('style') || '',
51
+ recentComputed: recent ? getComputedStyle(recent).filter : null,
52
+ legendComputed: legend ? getComputedStyle(legend).filter : null,
53
+ };
54
+ });
55
+ await ctx.close();
56
+ return filters;
57
+ }
58
+
59
+ const light = await probe('light');
60
+ const cyber = await probe('cyber');
61
+ await browser.close();
62
+
63
+ const hasShadow = (s) => /drop-shadow\(/.test(s || '');
64
+ const results = {
65
+ light_recent_hasShadow: hasShadow(light.recent),
66
+ light_legend_hasShadow: hasShadow(light.legend),
67
+ cyber_recent_hasShadow: hasShadow(cyber.recent),
68
+ cyber_legend_hasShadow: hasShadow(cyber.legend),
69
+ themes_differ: light.recent !== cyber.recent,
70
+ computed_resolves: hasShadow(light.recentComputed) && hasShadow(cyber.recentComputed),
71
+ };
72
+ const ok = Object.values(results).every(Boolean);
73
+ console.log(`${ok ? '✅' : '❌'} panel elevation:`, JSON.stringify(results),
74
+ `\n light.recent=${JSON.stringify(light.recent)}`,
75
+ `\n cyber.recent=${JSON.stringify(cyber.recent)}`);
76
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,102 @@
1
+ /* Round 175 verification: corner panels fade-in after the
2
+ * canvas content reveal.
3
+ *
4
+ * Pre-R175 the recent-signal (top-left) + legend (top-right)
5
+ * panels appeared instantly while nodes/edges/group boxes/tier
6
+ * rings staggered in around them. The panels felt like 'already
7
+ * there' before the content showed up.
8
+ *
9
+ * R175 adds .anet-fade-in + 700ms / 800ms animation-delay on
10
+ * the two panel <g> wrappers — fifth surface in the first-paint
11
+ * reveal family (after R9 nodes / R172 edges / R173 group boxes
12
+ * / R174 tier rings).
13
+ *
14
+ * Test:
15
+ * 1. Mock 3 sessions in ring layout
16
+ * 2. Probe [data-topo-panel="recent"] and [="legend"]
17
+ * 3. Both have .anet-fade-in className
18
+ * 4. recent panel animationDelay = 700ms
19
+ * 5. legend panel animationDelay = 800ms
20
+ * 6. data-topo-panel-fade-delay attributes match
21
+ */
22
+ import { chromium } from 'playwright';
23
+ import { readFileSync } from 'node:fs';
24
+
25
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
26
+ const browser = await chromium.launch({ headless: true });
27
+
28
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
29
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
30
+ await ctx.addInitScript(() => {
31
+ try {
32
+ localStorage.setItem('anet-theme', 'cyber');
33
+ sessionStorage.setItem('anet_v3_auth', '1');
34
+ localStorage.setItem('anet-topo-layout', 'ring');
35
+ } catch {}
36
+ });
37
+
38
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
39
+ await ctx.route('**/api/hub/status*', async (route) => {
40
+ const r = await route.fetch();
41
+ const b = await r.json();
42
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
43
+ const mk = (alias) => ({
44
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
45
+ network_id: nid, project_dir: null,
46
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
47
+ });
48
+ await route.fulfill({ response: r, json: { ...b, sessions: [mk('alpha'), mk('beta'), mk('gamma')] } });
49
+ });
50
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
51
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
52
+
53
+ const page = await ctx.newPage();
54
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
55
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 3, { timeout: 30000 });
56
+ await page.waitForSelector('[data-topo-panel="recent"]', { timeout: 10000 });
57
+ await page.waitForSelector('[data-topo-panel="legend"]', { timeout: 10000 });
58
+ await page.waitForTimeout(400);
59
+
60
+ const probe = await page.evaluate(() => {
61
+ const grab = (sel) => {
62
+ const el = document.querySelector(sel);
63
+ if (!el) return null;
64
+ return {
65
+ hasFadeClass: (el.getAttribute('class') || '').includes('anet-fade-in'),
66
+ fadeDelayAttr: parseFloat(el.getAttribute('data-topo-panel-fade-delay') || ''),
67
+ animationDelay: el.style.animationDelay || getComputedStyle(el).animationDelay,
68
+ };
69
+ };
70
+ return {
71
+ recent: grab('[data-topo-panel="recent"]'),
72
+ legend: grab('[data-topo-panel="legend"]'),
73
+ };
74
+ });
75
+
76
+ await browser.close();
77
+
78
+ const parseDelay = (s) => {
79
+ if (!s) return null;
80
+ if (s.endsWith('ms')) return parseFloat(s);
81
+ if (s.endsWith('s')) return parseFloat(s) * 1000;
82
+ return parseFloat(s);
83
+ };
84
+ const recentDelay = parseDelay(probe.recent?.animationDelay);
85
+ const legendDelay = parseDelay(probe.legend?.animationDelay);
86
+
87
+ const results = {
88
+ recent_panel_found: probe.recent !== null,
89
+ legend_panel_found: probe.legend !== null,
90
+ recent_has_fade_class: probe.recent?.hasFadeClass === true,
91
+ legend_has_fade_class: probe.legend?.hasFadeClass === true,
92
+ recent_delay_attr_700: probe.recent?.fadeDelayAttr === 700,
93
+ legend_delay_attr_800: probe.legend?.fadeDelayAttr === 800,
94
+ recent_anim_delay_700ms: recentDelay === 700,
95
+ legend_anim_delay_800ms: legendDelay === 800,
96
+ legend_lags_recent_100ms: legendDelay - recentDelay === 100,
97
+ };
98
+ const ok = Object.values(results).every(Boolean);
99
+ console.log(`${ok ? '✅' : '❌'} panel fade-in:`, JSON.stringify(results),
100
+ `\n recent =`, probe.recent, `delay=${recentDelay}`,
101
+ `\n legend =`, probe.legend, `delay=${legendDelay}`);
102
+ process.exit(ok ? 0 : 1);