@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,73 @@
1
+ /* Round 110 verification: recent-signal empty placeholder gains a
2
+ * sub-text hint so the empty state reads as intentional invitation
3
+ * rather than a stuck-loading state. Primary line "no flow yet"
4
+ * stays at the same position semantics (now y=54 instead of y=60
5
+ * to balance the two-line stack within the panel); sub-line at
6
+ * y=68 says "send a message between agents".
7
+ *
8
+ * Verifies:
9
+ * - both lines render when flowLinks is empty
10
+ * - both lines disappear when flowLinks has content
11
+ * - data-recent-signal-empty + data-recent-signal-empty-hint
12
+ * attributes are stable test hooks
13
+ */
14
+ import { chromium } from 'playwright';
15
+ import { readFileSync } from 'node:fs';
16
+
17
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
18
+ const browser = await chromium.launch({ headless: true });
19
+ const fresh = new Date(Date.now() - 30 * 1000).toISOString();
20
+
21
+ const probe = async (withFlow) => {
22
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1200 } });
23
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
24
+ await ctx.addInitScript(() => {
25
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
26
+ });
27
+ await ctx.route('**/api/hub/status*', async (route) => {
28
+ const r = await route.fetch();
29
+ const b = await r.json();
30
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
31
+ const mk = (alias) => ({
32
+ alias, status: 'idle', model: 'claude-opus-4', runtime: 'cli-claude-code',
33
+ network_id: nid, project_dir: null,
34
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
35
+ });
36
+ await route.fulfill({ response: r, json: { ...b, sessions: [mk('alpha'), mk('beta')] } });
37
+ });
38
+ const now = Date.now();
39
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: withFlow ? [
40
+ { id: 'm1', from_alias: 'alpha', to_alias: 'beta', content: 'hi',
41
+ network_id: 'default', created_at: new Date(now - 10000).toISOString() },
42
+ ] : [] } }));
43
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
44
+ const page = await ctx.newPage();
45
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
46
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 2, { timeout: 30000 });
47
+ await page.waitForTimeout(600);
48
+ const data = await page.evaluate(() => ({
49
+ empty: document.querySelector('[data-recent-signal-empty]')?.textContent || null,
50
+ emptyHint: document.querySelector('[data-recent-signal-empty-hint]')?.textContent || null,
51
+ rowCount: document.querySelectorAll('[data-recent-row]').length,
52
+ }));
53
+ await ctx.close();
54
+ return data;
55
+ };
56
+
57
+ const empty = await probe(false);
58
+ const full = await probe(true);
59
+ await browser.close();
60
+
61
+ const results = {
62
+ empty_primaryLineShown: empty.empty === 'no flow yet',
63
+ empty_hintShown: /^send a message between agents$/.test(empty.emptyHint || ''),
64
+ empty_noRows: empty.rowCount === 0,
65
+ full_primaryGone: full.empty === null,
66
+ full_hintGone: full.emptyHint === null,
67
+ full_rowsRendered: full.rowCount >= 1,
68
+ };
69
+ const ok = Object.values(results).every(Boolean);
70
+ console.log(`${ok ? '✅' : '❌'} recent-signal empty hint:`, JSON.stringify(results),
71
+ `\n empty=`, empty,
72
+ `\n full=`, full);
73
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,63 @@
1
+ /* Round 45 verification: recent-signal panel shows "no flow yet" placeholder
2
+ * when flowLinks is empty; renders normal link rows when populated. */
3
+ import { chromium } from 'playwright';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
7
+ const browser = await chromium.launch({ headless: true });
8
+
9
+ async function probe({ msgs }) {
10
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
11
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
12
+ await ctx.addInitScript(() => {
13
+ try {
14
+ localStorage.setItem('anet-theme', 'cyber');
15
+ localStorage.setItem('anet-topo-layout', 'grid');
16
+ sessionStorage.setItem('anet_v3_auth', '1');
17
+ } catch {}
18
+ });
19
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
20
+ await ctx.route('**/api/hub/status*', async (route) => {
21
+ const r = await route.fetch();
22
+ const b = await r.json();
23
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
24
+ const sessions = ['a', 'b'].map(a => ({
25
+ alias: a, status: 'idle', network_id: nid, project_dir: null,
26
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
27
+ }));
28
+ await route.fulfill({ response: r, json: { ...b, sessions } });
29
+ });
30
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: msgs } }));
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(500);
36
+ const data = await page.evaluate(() => {
37
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
38
+ const empty = svg.querySelector('text[data-recent-signal-empty]');
39
+ // count flow-link description rows in the recent-signal panel (translate(16,16))
40
+ const panel = [...svg.querySelectorAll(':scope > g[transform^="translate(16, 16)"]')][0];
41
+ const linkRows = panel ? [...panel.querySelectorAll('text')].filter(t => /->.*\/.*\//.test(t.textContent || '')).length : 0;
42
+ return { emptyShown: !!empty, emptyText: empty?.textContent || null, linkRows };
43
+ });
44
+ await ctx.close();
45
+ return data;
46
+ }
47
+
48
+ const noFlow = await probe({ msgs: [] });
49
+ const withFlow = await probe({ msgs: [
50
+ { from_alias: 'a', to_alias: 'b', content: 'hello', created_at: new Date().toISOString() },
51
+ ] });
52
+ await browser.close();
53
+
54
+ const results = {
55
+ emptyShowsPlaceholder: noFlow.emptyShown === true && /no flow yet/i.test(noFlow.emptyText || ''),
56
+ emptyHidesLinkRows: noFlow.linkRows === 0,
57
+ populatedHidesPlaceholder: withFlow.emptyShown === false,
58
+ populatedShowsLinkRows: withFlow.linkRows >= 1,
59
+ };
60
+ const ok = Object.values(results).every(Boolean);
61
+ console.log(`${ok ? '✅' : '❌'} recent-signal empty state:`, JSON.stringify(results),
62
+ `\n noFlow=`, noFlow, `\n withFlow=`, withFlow);
63
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,77 @@
1
+ /* Round 36 verification: prefers-reduced-motion disables all SMIL
2
+ * animations in the topology. Round 29 zeroed CSS via media query;
3
+ * SVG <animate> / <animateMotion> elements need JS to opt out and
4
+ * this round wires that via the useReducedMotion hook. */
5
+ import { chromium } from 'playwright';
6
+ import { readFileSync } from 'node:fs';
7
+
8
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
9
+ const browser = await chromium.launch({ headless: true });
10
+
11
+ async function probe({ reduce, layout }) {
12
+ const ctx = await browser.newContext({
13
+ viewport: { width: 1280, height: 1600 },
14
+ reducedMotion: reduce ? 'reduce' : 'no-preference',
15
+ });
16
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
17
+ await ctx.addInitScript((lay) => {
18
+ try {
19
+ localStorage.setItem('anet-theme', 'cyber');
20
+ localStorage.setItem('anet-topo-layout', lay);
21
+ localStorage.setItem('anet-topo-view', JSON.stringify({ zoom: 1, x: 0, y: 0 }));
22
+ sessionStorage.setItem('anet_v3_auth', '1');
23
+ } catch {}
24
+ }, layout);
25
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
26
+ await ctx.route('**/api/hub/status*', async (route) => {
27
+ const r = await route.fetch();
28
+ const b = await r.json();
29
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
30
+ // Include a 'working' status so the working pulse path renders.
31
+ const sessions = [
32
+ { alias: 'wkr', status: 'working', network_id: nid, project_dir: null,
33
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh },
34
+ { alias: 'idl', 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
+ // Inject a tiny message so a flow link + particle render.
40
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [
41
+ { from_alias: 'wkr', to_alias: 'idl', content: 'hi', created_at: fresh },
42
+ ] } }));
43
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
44
+ const page = await ctx.newPage();
45
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
46
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 2, { timeout: 30000 });
47
+ await page.waitForTimeout(600);
48
+ // Click a node so the click ripple is invoked (it should NOT render under reduced motion).
49
+ await page.locator('g[data-node="wkr"]').click().catch(() => {});
50
+ await page.waitForTimeout(120);
51
+ const counts = await page.evaluate(() => {
52
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
53
+ return {
54
+ animate: svg.querySelectorAll('animate').length,
55
+ animateMotion: svg.querySelectorAll('animateMotion').length,
56
+ };
57
+ });
58
+ await ctx.close();
59
+ return counts;
60
+ }
61
+
62
+ // Baseline (no reduce) — many SMIL elements expected.
63
+ const normalRing = await probe({ reduce: false, layout: 'ring' });
64
+ // Reduced motion — every SMIL element should be gone.
65
+ const reducedRing = await probe({ reduce: true, layout: 'ring' });
66
+ const reducedGrid = await probe({ reduce: true, layout: 'grid' });
67
+
68
+ await browser.close();
69
+ const results = {
70
+ baselineHasAnimations: normalRing.animate + normalRing.animateMotion > 0,
71
+ reducedRingDropsAll: reducedRing.animate === 0 && reducedRing.animateMotion === 0,
72
+ reducedGridDropsAll: reducedGrid.animate === 0 && reducedGrid.animateMotion === 0,
73
+ };
74
+ const ok = Object.values(results).every(Boolean);
75
+ console.log(`${ok ? '✅' : '❌'} reduced motion:`, JSON.stringify(results),
76
+ `\n normalRing=`, normalRing, `\n reducedRing=`, reducedRing, `\n reducedGrid=`, reducedGrid);
77
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,120 @@
1
+ /* Round 184 verification: chrome reset button gets a one-shot
2
+ * icon spin on click — visual confirmation that the click
3
+ * registered.
4
+ *
5
+ * Pre-R184 the reset button triggered R168 smoothView crossfade
6
+ * on the canvas but the button itself had no on-button feedback.
7
+ * R184 arms resetSpinning for 460ms on click; the SVG icon
8
+ * picks up `.anet-reset-spin` CSS animation (450ms ease-out,
9
+ * -360°).
10
+ *
11
+ * Test:
12
+ * 1. Idle: data-topo-chrome-reset-spinning='false',
13
+ * SVG icon has NO 'anet-reset-spin' className
14
+ * 2. Click reset → flag='true' within 50ms,
15
+ * SVG icon has 'anet-reset-spin' className
16
+ * 3. Wait 500ms → flag='false', className removed
17
+ * 4. CSS animation keyframe 'anet-reset-spin' exists in stylesheet
18
+ * (rotates -360deg)
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
+ const browser = await chromium.launch({ headless: true });
25
+
26
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
27
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
28
+ await ctx.addInitScript(() => {
29
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
30
+ });
31
+
32
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
33
+ await ctx.route('**/api/hub/status*', async (route) => {
34
+ const r = await route.fetch();
35
+ const b = await r.json();
36
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
37
+ const mk = (alias) => ({
38
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
39
+ network_id: nid, project_dir: null,
40
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
41
+ });
42
+ await route.fulfill({ response: r, json: { ...b, sessions: [mk('alpha'), mk('beta'), mk('gamma')] } });
43
+ });
44
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
45
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
46
+
47
+ const page = await ctx.newPage();
48
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
49
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 3, { timeout: 30000 });
50
+ await page.waitForSelector('[data-topo-chrome-reset]', { timeout: 10000 });
51
+ await page.waitForTimeout(400);
52
+
53
+ const probe = () => page.evaluate(() => {
54
+ const btn = document.querySelector('[data-topo-chrome-reset]');
55
+ const icon = document.querySelector('[data-topo-chrome-reset-icon]');
56
+ return {
57
+ btnSpinningAttr: btn?.getAttribute('data-topo-chrome-reset-spinning'),
58
+ iconClass: icon?.getAttribute('class') || '',
59
+ };
60
+ });
61
+
62
+ const idle = await probe();
63
+
64
+ // First zoom in so reset has visible effect (not strictly needed for the
65
+ // spin test, but exercises the resetView path alongside the spin)
66
+ await page.keyboard.press('+');
67
+ await page.keyboard.press('+');
68
+ await page.waitForTimeout(200);
69
+
70
+ // Click reset button — spin should arm
71
+ await page.locator('[data-topo-chrome-reset]').click();
72
+ await page.waitForTimeout(50);
73
+ const duringSpin = await probe();
74
+
75
+ // Wait past the 460ms window
76
+ await page.waitForTimeout(500);
77
+ const afterSpin = await probe();
78
+
79
+ // Verify the CSS keyframe exists in the stylesheet (rotates negatively)
80
+ const cssCheck = await page.evaluate(() => {
81
+ for (const sheet of document.styleSheets) {
82
+ let rules;
83
+ try { rules = sheet.cssRules; } catch { continue; }
84
+ if (!rules) continue;
85
+ for (const rule of rules) {
86
+ // CSSKeyframesRule has .name and .cssText listing keyframes
87
+ if (rule.type === 7 /* KEYFRAMES_RULE */ && rule.name === 'anet-reset-spin') {
88
+ return {
89
+ found: true,
90
+ cssText: rule.cssText,
91
+ };
92
+ }
93
+ }
94
+ }
95
+ return { found: false, cssText: '' };
96
+ });
97
+
98
+ await browser.close();
99
+
100
+ const results = {
101
+ idle_attr_false: idle.btnSpinningAttr === 'false',
102
+ idle_no_spin_class: !idle.iconClass.includes('anet-reset-spin'),
103
+
104
+ spin_attr_true: duringSpin.btnSpinningAttr === 'true',
105
+ spin_has_class: duringSpin.iconClass.includes('anet-reset-spin'),
106
+
107
+ after_attr_false: afterSpin.btnSpinningAttr === 'false',
108
+ after_no_spin_class: !afterSpin.iconClass.includes('anet-reset-spin'),
109
+
110
+ keyframe_found: cssCheck.found,
111
+ keyframe_negative_360: cssCheck.cssText.includes('-360deg') ||
112
+ cssCheck.cssText.includes('rotate(-360'),
113
+ };
114
+ const ok = Object.values(results).every(Boolean);
115
+ console.log(`${ok ? '✅' : '❌'} reset-spin:`, JSON.stringify(results),
116
+ `\n idle:`, idle,
117
+ `\n duringSpin:`, duringSpin,
118
+ `\n afterSpin:`, afterSpin,
119
+ `\n keyframe:`, cssCheck);
120
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,114 @@
1
+ /* Round 208 verification: runtime badge (CLI/SDK/HTTP icon at the
2
+ * bottom-right of each node avatar) grows r + thickens stroke on
3
+ * parent-node hover. Completes the micro-lift radius-axis family:
4
+ * R177 hub ring r 14 → 17 on hub-hover
5
+ * R197 legend swatch r 5.5 → 7 on row-hover/pin
6
+ * R208 runtime badge r 7 → 8 on node-hover (online)
7
+ * (r 5.5 → 6.5 offline)
8
+ *
9
+ * Test (cyber dark, 4 working sessions with runtime='cli-claude-code'
10
+ * which maps to a runtime identity → badge renders):
11
+ * 1. 4 [data-runtime-badge] badges present (one per node).
12
+ * 2. Idle: all data-runtime-badge-active='false', computed r ≈ 7,
13
+ * strokeWidth ≈ 1.5.
14
+ * 3. Transition includes 'r 150ms' AND 'stroke-width 150ms'.
15
+ * 4. Hover g[data-node="alpha"] → wait past 180ms.
16
+ * 5. alpha badge: active='true', r ≈ 8, strokeWidth ≈ 2.
17
+ * 6. Other (non-hovered) badges stay active='false', r ≈ 7,
18
+ * strokeWidth ≈ 1.5.
19
+ * 7. Move away — alpha badge back to 'false', r ≈ 7.
20
+ */
21
+ import { chromium } from 'playwright';
22
+ import { readFileSync } from 'node:fs';
23
+
24
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
25
+ const browser = await chromium.launch({ headless: true });
26
+
27
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
28
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
29
+ await ctx.addInitScript(() => {
30
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
31
+ });
32
+
33
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
34
+ await ctx.route('**/api/hub/status*', async (route) => {
35
+ const r = await route.fetch();
36
+ const b = await r.json();
37
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
38
+ const mk = (alias) => ({
39
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
40
+ network_id: nid, project_dir: null,
41
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
42
+ });
43
+ await route.fulfill({ response: r, json: { ...b, sessions: [
44
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
45
+ ] } });
46
+ });
47
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: [] } }));
48
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.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(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
53
+ // SVG <circle> doesn't pass Playwright's default visibility check the
54
+ // same way HTML elements do — wait for DOM attachment instead.
55
+ await page.waitForSelector('[data-runtime-badge]', { timeout: 10000, state: 'attached' });
56
+ await page.waitForTimeout(300);
57
+
58
+ const probeAll = async () => page.evaluate(() => {
59
+ return [...document.querySelectorAll('[data-runtime-badge]')].map(el => {
60
+ const cs = getComputedStyle(el);
61
+ return {
62
+ alias: el.getAttribute('data-runtime-badge'),
63
+ active: el.getAttribute('data-runtime-badge-active'),
64
+ r: parseFloat(cs.r),
65
+ strokeWidth: parseFloat(cs.strokeWidth),
66
+ transition: el.style.transition,
67
+ };
68
+ });
69
+ });
70
+
71
+ const idle = await probeAll();
72
+
73
+ await page.hover('g[data-node="alpha"]');
74
+ await page.waitForTimeout(200);
75
+ const hovered = await probeAll();
76
+
77
+ await page.mouse.move(0, 0);
78
+ await page.waitForTimeout(200);
79
+ const settled = await probeAll();
80
+
81
+ await browser.close();
82
+
83
+ const idleAlpha = idle.find(b => b.alias === 'alpha');
84
+ const idleBeta = idle.find(b => b.alias === 'beta');
85
+ const hoverAlpha = hovered.find(b => b.alias === 'alpha');
86
+ const hoverBeta = hovered.find(b => b.alias === 'beta');
87
+ const settledAlpha = settled.find(b => b.alias === 'alpha');
88
+
89
+ const near = (got, want, tol = 0.4) => Math.abs(got - want) < tol;
90
+
91
+ const results = {
92
+ four_badges_present: idle.length === 4,
93
+ idle_all_inactive: idle.every(b => b.active === 'false'),
94
+ idle_all_r_7: idle.every(b => near(b.r, 7)),
95
+ idle_all_stroke_1p5: idle.every(b => near(b.strokeWidth, 1.5)),
96
+ transition_has_r: idle.every(b => /\br\s+(?:150ms|0\.15s)/.test(b.transition || '')),
97
+ transition_has_stroke_width: idle.every(b => /stroke-width\s+(?:150ms|0\.15s)/.test(b.transition || '')),
98
+
99
+ hover_alpha_active: hoverAlpha?.active === 'true',
100
+ hover_alpha_r_grown: near(hoverAlpha?.r ?? 0, 8),
101
+ hover_alpha_stroke_thicker: near(hoverAlpha?.strokeWidth ?? 0, 2),
102
+ hover_beta_stays_inactive: hoverBeta?.active === 'false',
103
+ hover_beta_r_unchanged: near(hoverBeta?.r ?? 0, 7),
104
+ hover_beta_stroke_unchanged: near(hoverBeta?.strokeWidth ?? 0, 1.5),
105
+
106
+ settled_alpha_inactive: settledAlpha?.active === 'false',
107
+ settled_alpha_r_back: near(settledAlpha?.r ?? 0, 7),
108
+ };
109
+ const ok = Object.values(results).every(Boolean);
110
+ console.log(`${ok ? '✅' : '❌'} runtime badge grow:`, JSON.stringify(results),
111
+ `\n idle:`, idle,
112
+ `\n hovered:`, hovered,
113
+ `\n settled:`, settled);
114
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,110 @@
1
+ /* Round 145 verification: R46 idle-spoke flow rate buckets on
2
+ * workingCount, completing the 4-layer busyness-driven motion
3
+ * family:
4
+ * R84 hub breath (centre)
5
+ * R131 outer-ring orbit (periphery)
6
+ * R132 groupbox march (per-team, grid layout)
7
+ * R145 idle-spoke flow (ring layout, hub→nodes) — new
8
+ *
9
+ * Bucket ladder shared across the four layers (same R84
10
+ * thresholds):
11
+ * workingCount === 0 → bucket 0
12
+ * workingCount in [1, 2] → bucket 1
13
+ * workingCount in [3, 5] → bucket 2
14
+ * workingCount >= 6 → bucket 3
15
+ *
16
+ * Idle-spoke durations per bucket: 2.8 / 2.4 / 2.0 / 1.6 seconds
17
+ * (R46 baseline 2.4s is bucket 1).
18
+ *
19
+ * Spokes only render in ring layout. Active spokes (carrying flow
20
+ * traffic) don't get the animation — they're solid bright strokes.
21
+ * Only idle spokes carry the .anet-topo-spoke-flow class and the
22
+ * --spoke-dur var. The test seeds 0 messages so all spokes are idle.
23
+ */
24
+ import { chromium } from 'playwright';
25
+ import { readFileSync } from 'node:fs';
26
+
27
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
28
+
29
+ async function probe(workingCount, totalCount) {
30
+ const browser = await chromium.launch({ headless: true });
31
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
32
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
33
+ await ctx.addInitScript(() => {
34
+ try {
35
+ localStorage.setItem('anet-theme', 'cyber');
36
+ sessionStorage.setItem('anet_v3_auth', '1');
37
+ // ring layout is R145's target — it's also the default, but
38
+ // pinning explicitly defends against future default flips.
39
+ localStorage.setItem('anet-topo-layout', 'ring');
40
+ } catch {}
41
+ });
42
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
43
+ await ctx.route('**/api/hub/status*', async (route) => {
44
+ const r = await route.fetch();
45
+ const b = await r.json();
46
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
47
+ const sessions = [];
48
+ for (let i = 0; i < totalCount; i++) {
49
+ sessions.push({
50
+ alias: `node${i}`,
51
+ status: i < workingCount ? 'working' : 'idle',
52
+ model: 'claude-opus-4', runtime: 'cli-claude-code',
53
+ network_id: nid, project_dir: null,
54
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
55
+ });
56
+ }
57
+ await route.fulfill({ response: r, json: { ...b, sessions } });
58
+ });
59
+ // 0 messages → no active spokes → every spoke is idle (.anet-topo-spoke-flow)
60
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
61
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
62
+
63
+ const page = await ctx.newPage();
64
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
65
+ await page.waitForFunction((n) => document.querySelectorAll('g[data-node]').length === n, totalCount, { timeout: 30000 });
66
+ await page.waitForTimeout(400);
67
+
68
+ const out = await page.evaluate(() => {
69
+ const spokes = [...document.querySelectorAll('path.anet-topo-spoke-flow')];
70
+ return spokes.map(p => ({
71
+ bucket: p.getAttribute('data-topo-spoke-bucket'),
72
+ dur: p.getAttribute('data-topo-spoke-dur'),
73
+ // Resolved animation-duration from computed style — confirms
74
+ // the CSS var bridge through to the cascade.
75
+ cssDur: getComputedStyle(p).getPropertyValue('animation-duration').trim(),
76
+ }));
77
+ });
78
+ await browser.close();
79
+ return out;
80
+ }
81
+
82
+ // One probe per bucket
83
+ const b0 = await probe(0, 4); // 0 working / 4 idle → bucket 0 (2.8s)
84
+ const b1 = await probe(2, 4); // 2 working / 2 idle → bucket 1 (2.4s)
85
+ const b2 = await probe(4, 5); // 4 working / 1 idle → bucket 2 (2.0s)
86
+ const b3 = await probe(7, 8); // 7 working / 1 idle → bucket 3 (1.6s)
87
+
88
+ const consistent = (spokes, bucket, dur) =>
89
+ spokes.length > 0 &&
90
+ spokes.every(s => s.bucket === String(bucket) && s.dur === String(dur) && s.cssDur === `${dur}s`);
91
+
92
+ const results = {
93
+ b0_someSpokes: b0.length > 0,
94
+ b0_bucket0_2_8s: consistent(b0, 0, 2.8),
95
+ b1_bucket1_2_4s: consistent(b1, 1, 2.4),
96
+ b2_bucket2_2_0s: consistent(b2, 2, 2.0),
97
+ b3_bucket3_1_6s: consistent(b3, 3, 1.6),
98
+ // dur strictly decreases as busy climbs
99
+ dur_monotonic:
100
+ Number(b0[0]?.dur) > Number(b1[0]?.dur) &&
101
+ Number(b1[0]?.dur) > Number(b2[0]?.dur) &&
102
+ Number(b2[0]?.dur) > Number(b3[0]?.dur),
103
+ };
104
+ const ok = Object.values(results).every(Boolean);
105
+ console.log(`${ok ? '✅' : '❌'} spoke busyness:`, JSON.stringify(results),
106
+ `\n b0(0w/4)=`, b0.slice(0,1),
107
+ `\n b1(2w/4)=`, b1.slice(0,1),
108
+ `\n b2(4w/5)=`, b2.slice(0,1),
109
+ `\n b3(7w/8)=`, b3.slice(0,1));
110
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,96 @@
1
+ /* Round 240 verification: backdrop spokes (6 radar-style lines)
2
+ * tint to pal.legendAccent on any pinned filter, mirroring R93's
3
+ * tier-ring treatment. Background scaffolding now signals
4
+ * 'filtered mode' as a unified set instead of half-and-half.
5
+ *
6
+ * Cyber theme palette:
7
+ * pal.ringStroke = #164e63 = rgb(22, 78, 99)
8
+ * pal.legendAccent = #67e8f9 = rgb(103, 232, 249)
9
+ *
10
+ * Test scope:
11
+ * - rest state (no pin): all 6 spokes stroke = ringStroke
12
+ * - pinned state: all 6 spokes stroke = legendAccent
13
+ * - transition wiring: style.transition contains 'stroke 200ms'
14
+ * (browser may normalise to '0.2s' — accept either)
15
+ * - data-topo-spoke-tinted attr flips false ↔ true
16
+ *
17
+ * Scenario: 4 working agents (single tier, simple geometry).
18
+ * Click the working pressure-bar segment to pin status=working.
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
+ const browser = await chromium.launch({ headless: true });
25
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
26
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
27
+ await ctx.addInitScript(() => {
28
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
29
+ });
30
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
31
+ await ctx.route('**/api/hub/status*', async (route) => {
32
+ const r = await route.fetch();
33
+ const b = await r.json();
34
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
35
+ const mk = (alias) => ({
36
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
37
+ network_id: nid, project_dir: null,
38
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
39
+ });
40
+ await route.fulfill({ response: r, json: { ...b, sessions: [
41
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
42
+ ] } });
43
+ });
44
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: [] } }));
45
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.fulfill({ json: { tasks: [] } }));
46
+
47
+ const page = await ctx.newPage();
48
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
49
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
50
+ await page.waitForSelector('[data-topo-spoke-angle]', { timeout: 10000, state: 'attached' });
51
+ await page.waitForTimeout(400);
52
+
53
+ const probeSpokes = (page) => page.evaluate(() => {
54
+ const spokes = Array.from(document.querySelectorAll('[data-topo-spoke-angle]'));
55
+ return spokes.map((s) => ({
56
+ angle: s.getAttribute('data-topo-spoke-angle'),
57
+ tinted: s.getAttribute('data-topo-spoke-tinted'),
58
+ fill: getComputedStyle(s).stroke,
59
+ transition: getComputedStyle(s).transition,
60
+ }));
61
+ });
62
+
63
+ const rest = await probeSpokes(page);
64
+
65
+ // Pin working status via pressure-bar segment click
66
+ await page.locator('[data-pressure-seg="working"]').first().click();
67
+ await page.waitForTimeout(400);
68
+ const pinned = await probeSpokes(page);
69
+
70
+ await browser.close();
71
+
72
+ const RING_RGB = 'rgb(22, 78, 99)';
73
+ const ACCENT_RGB = 'rgb(103, 232, 249)';
74
+ // Browser normalises 200ms → 0.2s
75
+ const hasTransition = (s) => /stroke\s+(?:200ms|0\.2s)/.test(s || '');
76
+
77
+ const results = {
78
+ six_spokes: rest.length === 6,
79
+ rest_all_untinted: rest.every(s => s.tinted === 'false'),
80
+ rest_all_ring_stroke: rest.every(s => s.fill === RING_RGB),
81
+ rest_all_has_transition: rest.every(s => hasTransition(s.transition)),
82
+
83
+ pinned_six_spokes: pinned.length === 6,
84
+ pinned_all_tinted: pinned.every(s => s.tinted === 'true'),
85
+ pinned_all_accent: pinned.every(s => s.fill === ACCENT_RGB),
86
+ pinned_all_has_transition: pinned.every(s => hasTransition(s.transition)),
87
+
88
+ // Angles match the original 0/30/60/90/120/150 pattern (sanity)
89
+ angles_correct: JSON.stringify(rest.map(s => s.angle).sort())
90
+ === JSON.stringify(['0','120','150','30','60','90']),
91
+ };
92
+ const ok = Object.values(results).every(Boolean);
93
+ console.log(`${ok ? '✅' : '❌'} spoke tint:`, JSON.stringify(results),
94
+ '\n rest: ', rest.map(s => ({ a: s.angle, t: s.tinted, fill: s.fill })),
95
+ '\n pinned:', pinned.map(s => ({ a: s.angle, t: s.tinted, fill: s.fill })));
96
+ process.exit(ok ? 0 : 1);