@sleep2agi/agent-network-dashboard 0.4.6-preview.30 → 0.4.6-preview.301

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 (469) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +41 -41
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/server/app/_global-error.html +1 -1
  7. package/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +2 -2
  15. package/.next/server/app/_not-found.rsc +12 -12
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/admin.html +2 -2
  24. package/.next/server/app/admin.rsc +14 -14
  25. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  26. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  27. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  28. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  30. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  31. package/.next/server/app/index.html +2 -2
  32. package/.next/server/app/index.rsc +14 -14
  33. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  34. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  35. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  36. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  37. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  38. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  39. package/.next/server/app/login.html +2 -2
  40. package/.next/server/app/login.rsc +14 -14
  41. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  42. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  43. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  44. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  46. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  47. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/logs.html +2 -2
  49. package/.next/server/app/logs.rsc +14 -14
  50. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  51. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  52. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  53. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  55. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  56. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/messages.html +2 -2
  58. package/.next/server/app/messages.rsc +14 -14
  59. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  60. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  61. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  62. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  64. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  65. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/node.html +2 -2
  67. package/.next/server/app/node.rsc +14 -14
  68. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  69. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  70. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  71. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  73. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  74. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/nodes.html +2 -2
  76. package/.next/server/app/nodes.rsc +14 -14
  77. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  78. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  79. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  80. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  82. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  83. package/.next/server/app/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs.html +2 -2
  86. package/.next/server/app/server-logs.rsc +14 -14
  87. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  88. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  89. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  90. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  91. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  92. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  93. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/settings/networks.html +2 -2
  95. package/.next/server/app/settings/networks.rsc +14 -14
  96. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  97. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  98. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  99. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  100. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  101. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  102. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  103. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/settings/tokens.html +2 -2
  106. package/.next/server/app/settings/tokens.rsc +14 -14
  107. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  108. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  109. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  110. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  111. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  112. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  113. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  114. package/.next/server/app/settings.html +2 -2
  115. package/.next/server/app/settings.rsc +14 -14
  116. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  117. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  118. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  119. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  120. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  121. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  122. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/tasks.html +2 -2
  125. package/.next/server/app/tasks.rsc +14 -14
  126. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  127. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  128. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  129. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  130. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  131. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  132. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js +1 -1
  133. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +1 -1
  134. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js +1 -1
  135. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js.map +1 -1
  136. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js +1 -1
  137. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js.map +1 -1
  138. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +4 -1
  139. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  140. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  141. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  142. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js +1 -1
  143. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js.map +1 -1
  144. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  145. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  146. package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js +1 -1
  147. package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js.map +1 -1
  148. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js +1 -1
  149. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js.map +1 -1
  150. package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js +1 -1
  151. package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js.map +1 -1
  152. package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js +1 -1
  153. package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js.map +1 -1
  154. package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js +1 -1
  155. package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js.map +1 -1
  156. package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js +1 -1
  157. package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js.map +1 -1
  158. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  159. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  160. package/.next/server/middleware-build-manifest.js +3 -3
  161. package/.next/server/pages/404.html +2 -2
  162. package/.next/server/pages/500.html +1 -1
  163. package/.next/server/server-reference-manifest.js +1 -1
  164. package/.next/server/server-reference-manifest.json +1 -1
  165. package/.next/static/chunks/0-mpa_947ipeq.js +1 -0
  166. package/.next/static/chunks/01lmbsd37fybu.js +1 -0
  167. package/.next/static/chunks/03mk4d5n52iri.js +1 -0
  168. package/.next/static/chunks/03o.h6kvmw4l_.js +1 -0
  169. package/.next/static/chunks/03~~oirxz7~vc.js +1 -0
  170. package/.next/static/chunks/05uk96gc~9mni.js +1 -0
  171. package/.next/static/chunks/08kf-.bgs10ze.js +1 -0
  172. package/.next/static/chunks/0ggj5ummw2a84.js +7 -0
  173. package/.next/static/chunks/0glmrdj9112tc.js +7 -0
  174. package/.next/static/chunks/0hrgzg.5syv8j.js +1 -0
  175. package/.next/static/chunks/0rr~8~s13xsw2.js +4 -0
  176. package/.next/static/chunks/0rxrez_cjn.eh.js +1 -0
  177. package/.next/static/chunks/0x8x.29hkju5a.js +1 -0
  178. package/.next/static/chunks/0~mqrvsw~7354.css +2 -0
  179. package/.next/static/chunks/174re5ar~kxne.js +1 -0
  180. package/.next/trace +2 -2
  181. package/.next/trace-build +1 -1
  182. package/app/components/ChatPopover.tsx +38 -2
  183. package/app/components/CommandPalette.tsx +107 -0
  184. package/app/components/HelpOverlay.tsx +126 -0
  185. package/app/components/TopoGraph.tsx +5942 -259
  186. package/app/components/utils.ts +8 -7
  187. package/app/globals.css +110 -5
  188. package/app/lib/time.ts +36 -0
  189. package/package.json +2 -2
  190. package/screenshots/r293-p0/after-full.png +0 -0
  191. package/screenshots/r293-p0/after-topo.png +0 -0
  192. package/screenshots/r293-p0/baseline-full.png +0 -0
  193. package/screenshots/r293-p0/baseline-topo.png +0 -0
  194. package/scripts/help-overlay-topo-test.mjs +44 -0
  195. package/scripts/p0-topo-screenshot.mjs +82 -0
  196. package/scripts/r293-debug.mjs +19 -0
  197. package/scripts/topo-active-filter-pill-test.mjs +101 -0
  198. package/scripts/topo-active-links-chip-hover-test.mjs +103 -0
  199. package/scripts/topo-active-links-chip-nav-test.mjs +117 -0
  200. package/scripts/topo-active-links-chip-self-hover-test.mjs +171 -0
  201. package/scripts/topo-active-links-chip-test.mjs +73 -0
  202. package/scripts/topo-active-links-empty-recede-test.mjs +125 -0
  203. package/scripts/topo-active-links-freshness-test.mjs +113 -0
  204. package/scripts/topo-active-links-tooltip-test.mjs +79 -0
  205. package/scripts/topo-active-pulse-ease-test.mjs +120 -0
  206. package/scripts/topo-auto-fit-test.mjs +73 -0
  207. package/scripts/topo-brand-watermark-test.mjs +106 -0
  208. package/scripts/topo-canvas-aria-test.mjs +108 -0
  209. package/scripts/topo-chat-target-breath-test.mjs +84 -0
  210. package/scripts/topo-chat-target-ring-ease-test.mjs +82 -0
  211. package/scripts/topo-chat-target-ring-fade-test.mjs +124 -0
  212. package/scripts/topo-chip-empty-recede-test.mjs +116 -0
  213. package/scripts/topo-chip-focus-test.mjs +95 -0
  214. package/scripts/topo-chip-pin-mirror-test.mjs +113 -0
  215. package/scripts/topo-chip-row-semantic-gap-test.mjs +97 -0
  216. package/scripts/topo-chip-row-tabular-test.mjs +111 -0
  217. package/scripts/topo-chrome-a11y-test.mjs +107 -0
  218. package/scripts/topo-chrome-icon-stroke-test.mjs +95 -0
  219. package/scripts/topo-chrome-pop-extension-test.mjs +98 -0
  220. package/scripts/topo-chrome-pop-nodesize-test.mjs +88 -0
  221. package/scripts/topo-chrome-press-state-test.mjs +135 -0
  222. package/scripts/topo-chrome-strip-semantic-gap-test.mjs +99 -0
  223. package/scripts/topo-chrome-strip-theme-ease-test.mjs +99 -0
  224. package/scripts/topo-chrome-zoom-level-pop-test.mjs +122 -0
  225. package/scripts/topo-chrome-zoom-pop-test.mjs +130 -0
  226. package/scripts/topo-click-ripple-ease-test.mjs +94 -0
  227. package/scripts/topo-cmdk-edge-clear-test.mjs +109 -0
  228. package/scripts/topo-cmdk-layout-view-test.mjs +108 -0
  229. package/scripts/topo-cmdk-pin-test.mjs +123 -0
  230. package/scripts/topo-cmdk-vendor-clear-test.mjs +133 -0
  231. package/scripts/topo-cmdk-vendor-pin-test.mjs +103 -0
  232. package/scripts/topo-corner-radius-unify-test.mjs +103 -0
  233. package/scripts/topo-cwd-tooltip-test.mjs +64 -0
  234. package/scripts/topo-dblclick-reset-test.mjs +74 -0
  235. package/scripts/topo-dense-alias-fill-transition-test.mjs +133 -0
  236. package/scripts/topo-discrete-zoom-smooth-test.mjs +125 -0
  237. package/scripts/topo-edge-badge-click-pin-test.mjs +94 -0
  238. package/scripts/topo-edge-badge-crossfade-test.mjs +138 -0
  239. package/scripts/topo-edge-badge-hot-test.mjs +115 -0
  240. package/scripts/topo-edge-badge-hover-test.mjs +85 -0
  241. package/scripts/topo-edge-badge-lift-test.mjs +150 -0
  242. package/scripts/topo-edge-badge-ripple-test.mjs +111 -0
  243. package/scripts/topo-edge-badge-stroke-transition-test.mjs +118 -0
  244. package/scripts/topo-edge-badge-text-pin-test.mjs +121 -0
  245. package/scripts/topo-edge-badge-theme-ease-test.mjs +89 -0
  246. package/scripts/topo-edge-count-badge-test.mjs +86 -0
  247. package/scripts/topo-edge-endpoint-highlight-test.mjs +111 -0
  248. package/scripts/topo-edge-fadein-test.mjs +106 -0
  249. package/scripts/topo-edge-focus-test.mjs +113 -0
  250. package/scripts/topo-edge-hover-test.mjs +93 -0
  251. package/scripts/topo-edge-pill-hot-test.mjs +129 -0
  252. package/scripts/topo-edge-pill-test.mjs +105 -0
  253. package/scripts/topo-edge-ping-pulse-retired-test.mjs +119 -0
  254. package/scripts/topo-edge-stroke-ease-test.mjs +100 -0
  255. package/scripts/topo-edge-stroke-transition-test.mjs +132 -0
  256. package/scripts/topo-edge-tooltip-test.mjs +70 -0
  257. package/scripts/topo-empty-state-breath-test.mjs +150 -0
  258. package/scripts/topo-empty-state-crossfade-test.mjs +130 -0
  259. package/scripts/topo-empty-state-recenter-test.mjs +107 -0
  260. package/scripts/topo-endpoint-ring-fade-test.mjs +132 -0
  261. package/scripts/topo-endpoint-ring-test.mjs +93 -0
  262. package/scripts/topo-endpoint-ring-thicken-test.mjs +112 -0
  263. package/scripts/topo-esc-clears-pin-test.mjs +98 -0
  264. package/scripts/topo-filter-pill-aliases-test.mjs +121 -0
  265. package/scripts/topo-filter-pill-body-click-test.mjs +112 -0
  266. package/scripts/topo-filter-pill-count-test.mjs +110 -0
  267. package/scripts/topo-filter-pill-mobile-test.mjs +90 -0
  268. package/scripts/topo-fit-key-test.mjs +106 -0
  269. package/scripts/topo-freshness-chip-test.mjs +74 -0
  270. package/scripts/topo-freshness-chip-transition-test.mjs +80 -0
  271. package/scripts/topo-freshness-conditional-render-test.mjs +92 -0
  272. package/scripts/topo-fullscreen-active-test.mjs +115 -0
  273. package/scripts/topo-group-box-pinned-state-test.mjs +95 -0
  274. package/scripts/topo-group-count-test.mjs +6 -1
  275. package/scripts/topo-group-label-a11y-test.mjs +113 -0
  276. package/scripts/topo-group-label-count-inherit-test.mjs +99 -0
  277. package/scripts/topo-group-label-hover-test.mjs +119 -0
  278. package/scripts/topo-group-label-pin-spacing-test.mjs +136 -0
  279. package/scripts/topo-group-label-tint-test.mjs +99 -0
  280. package/scripts/topo-group-label-title-test.mjs +83 -0
  281. package/scripts/topo-group-pin-test.mjs +119 -0
  282. package/scripts/topo-group-pip-fadein-test.mjs +100 -0
  283. package/scripts/topo-group-pip-tabular-test.mjs +95 -0
  284. package/scripts/topo-group-status-pips-test.mjs +103 -0
  285. package/scripts/topo-groupbox-fadein-test.mjs +95 -0
  286. package/scripts/topo-groupbox-hover-lift-test.mjs +127 -0
  287. package/scripts/topo-groupbox-hubcore-ease-test.mjs +106 -0
  288. package/scripts/topo-groupbox-live-test.mjs +100 -0
  289. package/scripts/topo-groupbox-march-busyness-test.mjs +155 -0
  290. package/scripts/topo-groupbox-test.mjs +4 -2
  291. package/scripts/topo-help-overlay-r109-test.mjs +67 -0
  292. package/scripts/topo-help-overlay-r118-test.mjs +69 -0
  293. package/scripts/topo-help-overlay-r123-test.mjs +69 -0
  294. package/scripts/topo-help-overlay-r134-test.mjs +74 -0
  295. package/scripts/topo-help-overlay-r137-test.mjs +81 -0
  296. package/scripts/topo-help-overlay-r141-test.mjs +86 -0
  297. package/scripts/topo-help-overlay-r153-test.mjs +85 -0
  298. package/scripts/topo-help-overlay-r59-test.mjs +67 -0
  299. package/scripts/topo-help-overlay-r65-test.mjs +64 -0
  300. package/scripts/topo-help-overlay-r70-test.mjs +63 -0
  301. package/scripts/topo-help-overlay-r78-test.mjs +59 -0
  302. package/scripts/topo-help-overlay-r81-test.mjs +58 -0
  303. package/scripts/topo-help-overlay-r87-test.mjs +64 -0
  304. package/scripts/topo-help-overlay-r91-test.mjs +65 -0
  305. package/scripts/topo-hot-tail-crossfade-test.mjs +117 -0
  306. package/scripts/topo-html-overlay-padding-unify-test.mjs +120 -0
  307. package/scripts/topo-hub-a11y-test.mjs +112 -0
  308. package/scripts/topo-hub-busyness-test.mjs +100 -0
  309. package/scripts/topo-hub-center-crossfade-test.mjs +124 -0
  310. package/scripts/topo-hub-click-fit-test.mjs +137 -0
  311. package/scripts/topo-hub-digit-scale-test.mjs +129 -0
  312. package/scripts/topo-hub-fadein-test.mjs +88 -0
  313. package/scripts/topo-hub-hover-lift-test.mjs +101 -0
  314. package/scripts/topo-hub-hover-ring-test.mjs +82 -0
  315. package/scripts/topo-hub-pip-theme-ease-test.mjs +116 -0
  316. package/scripts/topo-hub-spoke-ease-test.mjs +120 -0
  317. package/scripts/topo-hub-title-test.mjs +93 -0
  318. package/scripts/topo-hub-working-count-test.mjs +99 -0
  319. package/scripts/topo-ingroup-edge-test.mjs +103 -0
  320. package/scripts/topo-inner-radar-ring-retired-test.mjs +89 -0
  321. package/scripts/topo-kicker-tracking-test.mjs +101 -0
  322. package/scripts/topo-label-card-theme-ease-test.mjs +85 -0
  323. package/scripts/topo-label-lift-test.mjs +119 -0
  324. package/scripts/topo-lastseen-tooltip-test.mjs +59 -0
  325. package/scripts/topo-layout-hover-text-preview-test.mjs +101 -0
  326. package/scripts/topo-layout-key-test.mjs +91 -0
  327. package/scripts/topo-layout-switch-crossfade-test.mjs +126 -0
  328. package/scripts/topo-layout-toggle-border-unify-test.mjs +104 -0
  329. package/scripts/topo-layout-toggle-test.mjs +131 -0
  330. package/scripts/topo-legend-compress-test.mjs +107 -0
  331. package/scripts/topo-legend-count-empty-test.mjs +126 -0
  332. package/scripts/topo-legend-count-tabular-parity-test.mjs +90 -0
  333. package/scripts/topo-legend-count-tabular-test.mjs +87 -0
  334. package/scripts/topo-legend-count-test.mjs +88 -0
  335. package/scripts/topo-legend-count-tier-fill-test.mjs +121 -0
  336. package/scripts/topo-legend-header-symmetric-inset-test.mjs +112 -0
  337. package/scripts/topo-legend-hitbox-swatch-center-test.mjs +93 -0
  338. package/scripts/topo-legend-hover-test.mjs +113 -0
  339. package/scripts/topo-legend-offline-delimiter-test.mjs +84 -0
  340. package/scripts/topo-legend-panel-header-test.mjs +83 -0
  341. package/scripts/topo-legend-pin-ring-fade-test.mjs +112 -0
  342. package/scripts/topo-legend-pin-test.mjs +128 -0
  343. package/scripts/topo-legend-row-hover-lift-test.mjs +109 -0
  344. package/scripts/topo-legend-row-pin-spacing-test.mjs +124 -0
  345. package/scripts/topo-legend-row-tint-test.mjs +95 -0
  346. package/scripts/topo-legend-row-title-test.mjs +102 -0
  347. package/scripts/topo-legend-swatch-grow-test.mjs +116 -0
  348. package/scripts/topo-minimap-a11y-test.mjs +101 -0
  349. package/scripts/topo-minimap-dot-transition-test.mjs +113 -0
  350. package/scripts/topo-minimap-rect-stroke-test.mjs +94 -0
  351. package/scripts/topo-minimap-test.mjs +68 -0
  352. package/scripts/topo-minimap-viewport-smooth-test.mjs +124 -0
  353. package/scripts/topo-mobile-chips-test.mjs +103 -0
  354. package/scripts/topo-monogram-font-test.mjs +112 -0
  355. package/scripts/topo-monogram-stroke-test.mjs +101 -0
  356. package/scripts/topo-node-a11y-test.mjs +107 -0
  357. package/scripts/topo-node-hover-lift-test.mjs +85 -0
  358. package/scripts/topo-node-label-card-elevation-test.mjs +157 -0
  359. package/scripts/topo-node-label-card-stroke-test.mjs +135 -0
  360. package/scripts/topo-node-stroke-transition-test.mjs +103 -0
  361. package/scripts/topo-node-text-fill-transition-test.mjs +119 -0
  362. package/scripts/topo-node-title-peers-test.mjs +107 -0
  363. package/scripts/topo-node-title-test.mjs +101 -0
  364. package/scripts/topo-nodesize-active-hover-test.mjs +109 -0
  365. package/scripts/topo-nodesize-crossfade-test.mjs +141 -0
  366. package/scripts/topo-online-chip-nav-test.mjs +143 -0
  367. package/scripts/topo-orbit-particles-retired-test.mjs +91 -0
  368. package/scripts/topo-overlap-test.mjs +32 -5
  369. package/scripts/topo-panel-elevation-test.mjs +76 -0
  370. package/scripts/topo-panel-fadein-test.mjs +102 -0
  371. package/scripts/topo-panel-hot-tail-fade-test.mjs +132 -0
  372. package/scripts/topo-panel-hover-elevation-test.mjs +118 -0
  373. package/scripts/topo-panel-theme-ease-test.mjs +85 -0
  374. package/scripts/topo-panel-title-fill-ease-test.mjs +91 -0
  375. package/scripts/topo-particle-stagger-test.mjs +82 -0
  376. package/scripts/topo-pin-intersection-color-ease-test.mjs +111 -0
  377. package/scripts/topo-pin-intersection-empty-test.mjs +129 -0
  378. package/scripts/topo-pin-intersection-tabular-test.mjs +96 -0
  379. package/scripts/topo-pin-intersection-test.mjs +121 -0
  380. package/scripts/topo-pin-intersection-warning-crossfade-test.mjs +107 -0
  381. package/scripts/topo-pin-mirror-transition-test.mjs +101 -0
  382. package/scripts/topo-pin-persistence-test.mjs +105 -0
  383. package/scripts/topo-popover-meta-test.mjs +78 -0
  384. package/scripts/topo-prepublish-guard-test.mjs +95 -0
  385. package/scripts/topo-pressure-bar-test.mjs +69 -0
  386. package/scripts/topo-pressure-hover-test.mjs +123 -0
  387. package/scripts/topo-pressure-pin-test.mjs +119 -0
  388. package/scripts/topo-pressure-seg-self-hover-test.mjs +126 -0
  389. package/scripts/topo-pressure-seg-tooltip-test.mjs +98 -0
  390. package/scripts/topo-pressure-seg-transition-test.mjs +107 -0
  391. package/scripts/topo-pulse-dot-crossfade-test.mjs +122 -0
  392. package/scripts/topo-pulse-rate-test.mjs +71 -0
  393. package/scripts/topo-recent-panel-count-freshness-test.mjs +109 -0
  394. package/scripts/topo-recent-panel-count-test.mjs +72 -0
  395. package/scripts/topo-recent-panel-footer-breath-test.mjs +142 -0
  396. package/scripts/topo-recent-panel-hot-header-test.mjs +132 -0
  397. package/scripts/topo-recent-panel-more-crossfade-test.mjs +124 -0
  398. package/scripts/topo-recent-panel-more-cyan-hover-test.mjs +151 -0
  399. package/scripts/topo-recent-panel-more-nav-test.mjs +127 -0
  400. package/scripts/topo-recent-panel-more-test.mjs +106 -0
  401. package/scripts/topo-recent-panel-readability-bump-test.mjs +151 -0
  402. package/scripts/topo-recent-row-count-fill-test.mjs +129 -0
  403. package/scripts/topo-recent-row-fadein-test.mjs +107 -0
  404. package/scripts/topo-recent-row-freshness-test.mjs +102 -0
  405. package/scripts/topo-recent-row-hot-test.mjs +110 -0
  406. package/scripts/topo-recent-row-hover-lift-test.mjs +128 -0
  407. package/scripts/topo-recent-row-hover-test.mjs +122 -0
  408. package/scripts/topo-recent-row-pin-spacing-test.mjs +137 -0
  409. package/scripts/topo-recent-row-pin-test.mjs +126 -0
  410. package/scripts/topo-recent-row-tint-test.mjs +91 -0
  411. package/scripts/topo-recent-row-title-test.mjs +112 -0
  412. package/scripts/topo-recent-row-ts-alpha-test.mjs +100 -0
  413. package/scripts/topo-recent-row-ts-test.mjs +95 -0
  414. package/scripts/topo-recent-row-typography-test.mjs +119 -0
  415. package/scripts/topo-recent-signal-empty-hint-test.mjs +73 -0
  416. package/scripts/topo-recent-signal-empty-test.mjs +63 -0
  417. package/scripts/topo-reduced-motion-test.mjs +77 -0
  418. package/scripts/topo-reset-spin-test.mjs +120 -0
  419. package/scripts/topo-runtime-badge-grow-test.mjs +114 -0
  420. package/scripts/topo-spokes-retired-test.mjs +93 -0
  421. package/scripts/topo-starfield-density-test.mjs +89 -0
  422. package/scripts/topo-status-chip-hover-test.mjs +101 -0
  423. package/scripts/topo-status-chip-tooltip-test.mjs +95 -0
  424. package/scripts/topo-statusbar-chips-self-hover-test.mjs +177 -0
  425. package/scripts/topo-svg-focus-test.mjs +112 -0
  426. package/scripts/topo-sweep-busyness-test.mjs +94 -0
  427. package/scripts/topo-tabular-nums-sweep-test.mjs +93 -0
  428. package/scripts/topo-tier-ring-fadein-test.mjs +107 -0
  429. package/scripts/topo-tier-ring-occupancy-test.mjs +87 -0
  430. package/scripts/topo-tier-ring-test.mjs +69 -0
  431. package/scripts/topo-tier-ring-tint-test.mjs +110 -0
  432. package/scripts/topo-tier-stagger-test.mjs +89 -0
  433. package/scripts/topo-title-block-tight-leading-test.mjs +95 -0
  434. package/scripts/topo-title-tracking-test.mjs +98 -0
  435. package/scripts/topo-toggle-inactive-cyan-hover-test.mjs +110 -0
  436. package/scripts/topo-top-rail-theme-ease-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 +131 -0
  440. package/scripts/topo-vendor-chip-tabular-test.mjs +92 -0
  441. package/scripts/topo-vendor-chip-threshold-test.mjs +78 -0
  442. package/scripts/topo-vendor-letter-tooltip-test.mjs +92 -0
  443. package/scripts/topo-vendor-pill-test.mjs +125 -0
  444. package/scripts/topo-vendor-pin-test.mjs +125 -0
  445. package/scripts/topo-view-smooth-test.mjs +131 -0
  446. package/scripts/topo-watermark-tracking-test.mjs +102 -0
  447. package/scripts/topo-wheel-zoom-test.mjs +79 -0
  448. package/scripts/topo-working-chip-pin-test.mjs +154 -0
  449. package/scripts/topo-working-halo-breath-retired-test.mjs +99 -0
  450. package/scripts/topo-wrapper-flow-arrow-theme-test.mjs +97 -0
  451. package/scripts/topo-wrapper-shadow-theme-ease-test.mjs +89 -0
  452. package/.next/static/chunks/0-7iowzu4.798.js +0 -1
  453. package/.next/static/chunks/02t5jr4shh.hy.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/0j_q__o8p6g83.js +0 -1
  458. package/.next/static/chunks/0jl6_w50zh4l5.js +0 -1
  459. package/.next/static/chunks/0nguf50i8enay.js +0 -1
  460. package/.next/static/chunks/0pl7l_.2im.j0.js +0 -7
  461. package/.next/static/chunks/0r4q73.-1bkyl.js +0 -1
  462. package/.next/static/chunks/0r_c8eccg3z2o.js +0 -1
  463. package/.next/static/chunks/0v5rym8-nhetp.js +0 -1
  464. package/.next/static/chunks/10oi7l-tycgl9.js +0 -1
  465. package/.next/static/chunks/11.s_mjm-snyc.js +0 -1
  466. package/.next/static/chunks/11uk1izuv3~0t.js +0 -1
  467. /package/.next/static/{SjGVu8dTaZVySLlXjmaar → 1doIiYlcqirZ60kZuY_8H}/_buildManifest.js +0 -0
  468. /package/.next/static/{SjGVu8dTaZVySLlXjmaar → 1doIiYlcqirZ60kZuY_8H}/_clientMiddlewareManifest.js +0 -0
  469. /package/.next/static/{SjGVu8dTaZVySLlXjmaar → 1doIiYlcqirZ60kZuY_8H}/_ssgManifest.js +0 -0
@@ -0,0 +1,79 @@
1
+ /* Round 114 verification: active-links chip tooltip lists the actual
2
+ * flow pairs. Closes the info-density sweep on the last chip-row
3
+ * hover surface (R97-R113 covered everything else). Format:
4
+ * "alpha→beta (3), gamma→delta (1) — hover brightens all"
5
+ *
6
+ * Drive 3 flow pairs of different counts. Verify each pair appears
7
+ * in the title with its count; the trailing action hint says
8
+ * "hover brightens all". Empty-flow fleet → no title.
9
+ */
10
+ import { chromium } from 'playwright';
11
+ import { readFileSync } from 'node:fs';
12
+
13
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
14
+ const browser = await chromium.launch({ headless: true });
15
+ const fresh = new Date(Date.now() - 30 * 1000).toISOString();
16
+
17
+ const probe = async (withFlow) => {
18
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
19
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
20
+ await ctx.addInitScript(() => {
21
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
22
+ });
23
+ await ctx.route('**/api/hub/status*', async (route) => {
24
+ const r = await route.fetch();
25
+ const b = await r.json();
26
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
27
+ const mk = (alias) => ({
28
+ alias, status: 'idle', model: 'claude-opus-4', runtime: 'cli-claude-code',
29
+ network_id: nid, project_dir: null,
30
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
31
+ });
32
+ await route.fulfill({ response: r, json: { ...b, sessions: [mk('alpha'), mk('beta'), mk('gamma'), mk('delta')] } });
33
+ });
34
+ const now = Date.now();
35
+ const mkMsg = (id, from_alias, to_alias, ageMs) => ({
36
+ id, from_alias, to_alias, content: 'hi', network_id: 'default',
37
+ created_at: new Date(now - ageMs).toISOString(),
38
+ });
39
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: withFlow ? [
40
+ mkMsg('m1', 'alpha', 'beta', 30 * 1000),
41
+ mkMsg('m2', 'alpha', 'beta', 40 * 1000),
42
+ mkMsg('m3', 'alpha', 'beta', 50 * 1000),
43
+ mkMsg('m4', 'gamma', 'delta', 60 * 1000),
44
+ ] : [] } }));
45
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
46
+ const page = await ctx.newPage();
47
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
48
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
49
+ await page.waitForTimeout(600);
50
+ const data = await page.evaluate(() => {
51
+ const el = document.querySelector('[data-active-links-chip]');
52
+ return {
53
+ title: el?.getAttribute('title'),
54
+ flowCount: el?.getAttribute('data-active-links-flow-count') || '',
55
+ text: el?.textContent?.trim() || '',
56
+ };
57
+ });
58
+ await ctx.close();
59
+ return data;
60
+ };
61
+
62
+ const empty = await probe(false);
63
+ const full = await probe(true);
64
+ await browser.close();
65
+
66
+ const results = {
67
+ empty_noTitle: empty.title === null,
68
+ empty_zeroCount: empty.flowCount === '0',
69
+ full_titleHasAlphaBeta: /alpha→beta \(3\)/.test(full.title || ''),
70
+ full_titleHasGammaDelta: /gamma→delta \(1\)/.test(full.title || ''),
71
+ full_titleActionHint: /hover brightens all/.test(full.title || ''),
72
+ full_flowCount2: full.flowCount === '2',
73
+ full_chipShowsCount: /2 active link/.test(full.text),
74
+ };
75
+ const ok = Object.values(results).every(Boolean);
76
+ console.log(`${ok ? '✅' : '❌'} active-links tooltip:`, JSON.stringify(results),
77
+ `\n empty=`, empty,
78
+ `\n full=`, full);
79
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,120 @@
1
+ /* Round 243 verification: active-node pulse ring gets two polishes
2
+ * on a single element:
3
+ *
4
+ * 1. Always-mount-opacity-gate wrapper <g> — pulse stays in DOM
5
+ * for all online nodes (when reducedMotion is off); isActive
6
+ * flips wrapper opacity 1 ↔ 0 with 300ms ease-out, instead of
7
+ * conditional mount that snap-restarted SMIL from phase 0.
8
+ *
9
+ * 2. SMIL ease-in-out keySplines on both r + opacity animates —
10
+ * the breath rises and settles at both endpoints instead of
11
+ * linear constant-velocity interp.
12
+ *
13
+ * Scenario: 4 working agents + 5-msg alpha→beta flow.
14
+ * activeAliases = {alpha, beta} (active=true)
15
+ * gamma, delta = active=false
16
+ *
17
+ * Test scope per node:
18
+ * - [data-node-pulse] always present (4 pulses in DOM)
19
+ * - alpha + beta: data-node-pulse-active='true', wrapper opacity=1
20
+ * - gamma + delta: data-node-pulse-active='false', wrapper opacity=0
21
+ * - All 4 wrappers have style.transition with 'opacity 300ms'
22
+ * - All 4 contain two <animate> children (r + opacity)
23
+ * - Each <animate> has calcMode='spline', keyTimes='0;0.5;1',
24
+ * keySplines='0.42 0 0.58 1;0.42 0 0.58 1', dur='2.4s'
25
+ */
26
+ import { chromium } from 'playwright';
27
+ import { readFileSync } from 'node:fs';
28
+
29
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
30
+ const browser = await chromium.launch({ headless: true });
31
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
32
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
33
+ await ctx.addInitScript(() => {
34
+ try {
35
+ localStorage.setItem('anet-theme', 'cyber');
36
+ sessionStorage.setItem('anet_v3_auth', '1');
37
+ localStorage.setItem('anet-topo-layout', 'ring');
38
+ } catch {}
39
+ });
40
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
41
+ await ctx.route('**/api/hub/status*', async (route) => {
42
+ const r = await route.fetch();
43
+ const b = await r.json();
44
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
45
+ const mk = (alias) => ({
46
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
47
+ network_id: nid, project_dir: null,
48
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
49
+ });
50
+ await route.fulfill({ response: r, json: { ...b, sessions: [
51
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
52
+ ] } });
53
+ });
54
+ const now = Date.now();
55
+ const msgs = [];
56
+ for (let i = 0; i < 5; i++) {
57
+ msgs.push({
58
+ id: `m${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
59
+ network_id: 'default', created_at: new Date(now - (1000 + i * 50)).toISOString(),
60
+ });
61
+ }
62
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: msgs } }));
63
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.fulfill({ json: { tasks: [] } }));
64
+
65
+ const page = await ctx.newPage();
66
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
67
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
68
+ await page.waitForSelector('[data-node-pulse]', { timeout: 10000, state: 'attached' });
69
+ await page.waitForTimeout(300);
70
+
71
+ const probe = await page.evaluate(() => {
72
+ const pulses = Array.from(document.querySelectorAll('[data-node-pulse]'));
73
+ return pulses.map((g) => {
74
+ const animates = Array.from(g.querySelectorAll('animate'));
75
+ return {
76
+ alias: g.getAttribute('data-node-pulse'),
77
+ active: g.getAttribute('data-node-pulse-active'),
78
+ opacityAttr: g.getAttribute('opacity'),
79
+ transition: g.style.transition,
80
+ animateCount: animates.length,
81
+ animates: animates.map(a => ({
82
+ attr: a.getAttribute('attributeName'),
83
+ calcMode: a.getAttribute('calcMode'),
84
+ keyTimes: a.getAttribute('keyTimes'),
85
+ keySplines: a.getAttribute('keySplines'),
86
+ dur: a.getAttribute('dur'),
87
+ })),
88
+ };
89
+ });
90
+ });
91
+ await browser.close();
92
+
93
+ const EASE_INOUT = '0.42 0 0.58 1;0.42 0 0.58 1';
94
+ const hasOpTransition = (s) => /opacity\s+(?:300ms|0\.3s)/.test(s || '');
95
+ const activeAliases = new Set(['alpha', 'beta']);
96
+ const expectAlias = (p) => activeAliases.has(p.alias) ? 'true' : 'false';
97
+
98
+ const animateOK = (a) =>
99
+ (a.attr === 'r' || a.attr === 'opacity') &&
100
+ a.calcMode === 'spline' &&
101
+ a.keyTimes === '0;0.5;1' &&
102
+ a.keySplines === EASE_INOUT &&
103
+ a.dur === '2.4s';
104
+
105
+ const results = {
106
+ four_pulses: probe.length === 4,
107
+ all_have_alias: probe.every(p => typeof p.alias === 'string'),
108
+ active_state_matches: probe.every(p => p.active === expectAlias(p)),
109
+ active_pulses_opacity_1: probe.filter(p => p.active === 'true').every(p => p.opacityAttr === '1'),
110
+ inactive_pulses_opacity_0: probe.filter(p => p.active === 'false').every(p => p.opacityAttr === '0'),
111
+ all_have_op_transition: probe.every(p => hasOpTransition(p.transition)),
112
+ all_two_animates: probe.every(p => p.animateCount === 2),
113
+ all_animates_have_r: probe.every(p => p.animates.some(a => a.attr === 'r')),
114
+ all_animates_have_opacity: probe.every(p => p.animates.some(a => a.attr === 'opacity')),
115
+ all_animates_ease_inout: probe.every(p => p.animates.every(animateOK)),
116
+ };
117
+ const ok = Object.values(results).every(Boolean);
118
+ console.log(`${ok ? '✅' : '❌'} active pulse ease+gate:`, JSON.stringify(results),
119
+ '\n pulses:', probe.map(p => ({ a: p.alias, active: p.active, op: p.opacityAttr })));
120
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,73 @@
1
+ /* Round 28 verification: first-paint auto-fit zoom when grid content
2
+ * would overflow viewBox; respects persisted view; doesn't fire on
3
+ * small fleets (no overflow). */
4
+ import { chromium } from 'playwright';
5
+ import { readFileSync } from 'node:fs';
6
+
7
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
8
+ const browser = await chromium.launch({ headless: true });
9
+
10
+ async function probe({ fleetSize, persisted }) {
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(([pers]) => {
14
+ try {
15
+ localStorage.setItem('anet-theme', 'cyber');
16
+ localStorage.setItem('anet-topo-layout', 'grid');
17
+ if (pers) localStorage.setItem('anet-topo-view', JSON.stringify(pers));
18
+ else localStorage.removeItem('anet-topo-view');
19
+ sessionStorage.setItem('anet_v3_auth', '1');
20
+ } catch {}
21
+ }, [persisted]);
22
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
23
+ await ctx.route('**/api/hub/status*', async (route) => {
24
+ const r = await route.fetch();
25
+ const b = await r.json();
26
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
27
+ // Use prefix groups with 5 members each — 7 bands stack vertically
28
+ // and overflow the 680-px viewBox at cellH ≥ 81 (Round 27 floor).
29
+ const aliases = Array.from({ length: fleetSize }, (_, i) => {
30
+ const groups = ['A站', 'B站', 'C站', 'D站', 'E站', 'F站', 'G站', 'H站', 'I站', 'J站'];
31
+ const group = groups[Math.floor(i / 5) % groups.length];
32
+ return `${group}n${i + 1}`;
33
+ });
34
+ const sessions = aliases.map(a => ({
35
+ alias: a, status: 'idle', network_id: nid, project_dir: null,
36
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
37
+ }));
38
+ await route.fulfill({ response: r, json: { ...b, sessions } });
39
+ });
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.waitForFunction((n) => {
44
+ const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
45
+ return !!svg && svg.querySelectorAll('g[data-node]').length === n;
46
+ }, fleetSize, { timeout: 30000 }).catch(() => {});
47
+ await page.waitForTimeout(600);
48
+ const zoom = await page.evaluate(() => {
49
+ const span = document.querySelector('button[aria-label="Zoom in"]').parentElement.querySelector('span[title*="zoom level"]');
50
+ return +span.textContent.replace('%', '');
51
+ });
52
+ await ctx.close();
53
+ return zoom;
54
+ }
55
+
56
+ // 1. Big fleet (35 nodes / 7 bands → overflows ~680-px viewBox at cellH≥81)
57
+ // with no persisted view → auto-fit triggers (zoom < 100%)
58
+ const bigFresh = await probe({ fleetSize: 35 });
59
+ // 2. Big fleet, user has persisted view (zoom=1) → respect it, no auto-fit
60
+ const bigPersisted = await probe({ fleetSize: 35, persisted: { zoom: 1, x: 0, y: 0 } });
61
+ // 3. Small fleet, no persisted view → no overflow, zoom stays 100%
62
+ const smallFresh = await probe({ fleetSize: 6 });
63
+
64
+ await browser.close();
65
+ const results = {
66
+ bigFleetAutoFits: bigFresh < 100 && bigFresh >= 50,
67
+ persistedRespected: bigPersisted === 100,
68
+ smallFleetStays100: smallFresh === 100,
69
+ };
70
+ const ok = Object.values(results).every(Boolean);
71
+ console.log(`${ok ? '✅' : '❌'} auto-fit:`, JSON.stringify(results),
72
+ `bigFresh=${bigFresh}% bigPersisted=${bigPersisted}% smallFresh=${smallFresh}%`);
73
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,106 @@
1
+ /* Round 282 verification: sleep2agi brand watermark at canvas bottom-left.
2
+ *
3
+ * Per Vincent 5215 ask via 通信龙 (logo placement on the canvas), R282
4
+ * adds a low-opacity monospace text "sleep2agi" at viewBox (16, 672)
5
+ * — bottom-left corner, the only fully-empty corner after R275-R281
6
+ * 减法 arc:
7
+ * top-left: recent-signal panel
8
+ * top-right: legend panel
9
+ * bottom-right: chrome strip (HTML overlay)
10
+ * bottom-left: EMPTY → watermark home
11
+ *
12
+ * No icon yet (no sleep2agi-specific crescent asset in public/);
13
+ * text-only watermark is the minimal-risk implementation. R283+
14
+ * can swap in a real logo if Vincent provides asset.
15
+ *
16
+ * Test scope:
17
+ * 1. data-topo-brand-watermark element exists.
18
+ * 2. Text content === "sleep2agi".
19
+ * 3. x === 16, y === 672 (bottom-left, 16-unit inset matching panels).
20
+ * 4. opacity === "0.4" (subtle watermark).
21
+ * 5. fill is set + theme-aware (not 'none', not empty).
22
+ * 6. R281 vendor chip threshold regression: 2-vendor scenario chip absent.
23
+ * 7. R280 spokes still retired (regression).
24
+ */
25
+ import { chromium } from 'playwright';
26
+ import { readFileSync } from 'node:fs';
27
+
28
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
29
+ const browser = await chromium.launch({ headless: true });
30
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
31
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
32
+ await ctx.addInitScript(() => {
33
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
34
+ });
35
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
36
+ await ctx.route('**/api/hub/status*', async (route) => {
37
+ const r = await route.fetch();
38
+ const b = await r.json();
39
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
40
+ // 2-vendor scenario (claude + gpt) so R281 vendor chip should be absent
41
+ const mk = (alias, model) => ({
42
+ alias, status: 'working', model, runtime: 'claude-code-cli',
43
+ network_id: nid, project_dir: null,
44
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
45
+ });
46
+ await route.fulfill({ response: r, json: { ...b, sessions: [
47
+ mk('alpha', 'claude-opus-4'),
48
+ mk('beta', 'gpt-4o'),
49
+ mk('gamma', 'claude-sonnet-4'),
50
+ mk('delta', 'gpt-4'),
51
+ ] } });
52
+ });
53
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
54
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
55
+
56
+ const page = await ctx.newPage();
57
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
58
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
59
+ await page.waitForSelector('[data-topo-brand-watermark]', { timeout: 10000 });
60
+ await page.waitForTimeout(300);
61
+
62
+ /* P0 (Vincent 5222) note: watermark structure expanded from text-only
63
+ * to <g>{moon glyph + wordmark}. Opacity moved to the wrapping <g>
64
+ * (data-topo-brand-watermark-group); text wordmark shifted x=16 → x=34
65
+ * to make room for the crescent glyph rendered at x=16. Probe both. */
66
+ const probe = await page.evaluate(() => {
67
+ const mark = document.querySelector('[data-topo-brand-watermark]');
68
+ const group = document.querySelector('[data-topo-brand-watermark-group]');
69
+ const glyph = document.querySelector('[data-topo-brand-watermark-glyph]');
70
+ const vendorChips = document.querySelectorAll('[data-vendor-letter]');
71
+ const spokes = document.querySelectorAll('[data-topo-spoke-angle]');
72
+ return {
73
+ present: mark !== null,
74
+ text: mark?.textContent ?? null,
75
+ x: mark ? +mark.getAttribute('x') : null,
76
+ y: mark ? +mark.getAttribute('y') : null,
77
+ groupOpacity: group?.getAttribute('opacity') ?? null,
78
+ glyphPresent: glyph !== null,
79
+ fill: mark ? window.getComputedStyle(mark).fill : null,
80
+ vendorChipCount: vendorChips.length,
81
+ spokeCount: spokes.length,
82
+ };
83
+ });
84
+ await browser.close();
85
+
86
+ const results = {
87
+ watermark_present: probe.present,
88
+ watermark_text_is_brand: probe.text === 'sleep2agi',
89
+ watermark_x_shifted_for_glyph: probe.x === 34,
90
+ watermark_y_is_672: probe.y === 672,
91
+ watermark_group_opacity_0_4: probe.groupOpacity === '0.4',
92
+ watermark_glyph_present: probe.glyphPresent,
93
+ watermark_fill_set: probe.fill != null && probe.fill !== '' && probe.fill !== 'none',
94
+ r281_vendor_chip_absent_in_2_vendor: probe.vendorChipCount === 0,
95
+ r280_spokes_absent: probe.spokeCount === 0,
96
+ };
97
+ const ok = Object.values(results).every(Boolean);
98
+ console.log(`${ok ? '✅' : '❌'} brand watermark:`, JSON.stringify(results),
99
+ '\n text:', JSON.stringify(probe.text),
100
+ '\n (x,y):', `(${probe.x}, ${probe.y})`,
101
+ '\n group opacity:', probe.groupOpacity,
102
+ '\n glyph present:', probe.glyphPresent,
103
+ '\n fill:', probe.fill,
104
+ '\n vendor chips (expect 0):', probe.vendorChipCount,
105
+ '\n spokes (expect 0):', probe.spokeCount);
106
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,108 @@
1
+ /* Round 158 verification: main canvas SVG gains aria-label +
2
+ * aria-roledescription.
3
+ *
4
+ * R151-R157 closed a11y across every interactive surface INSIDE
5
+ * the canvas (nodes / group labels / edge badges / recent rows /
6
+ * legend rows / chrome buttons / minimap). The canvas <svg> root
7
+ * itself was a nameless 1000×680 box — a screen-reader user who
8
+ * landed on it before tab-diving into children heard nothing
9
+ * identifying the surface.
10
+ *
11
+ * R158 closes that gap:
12
+ * aria-roledescription="agent network topology"
13
+ * aria-label live-updates with snapshot:
14
+ * "Agent network topology — 5 agents online · 3 working ·
15
+ * 1 offline · 2 active links. Tab to navigate nodes,
16
+ * double-click canvas to reset view."
17
+ *
18
+ * No role override — default SVG role (graphics-document) keeps
19
+ * children navigable; role="img" would flatten the SVG to a
20
+ * single opaque image and hide all the R151-R157 work.
21
+ *
22
+ * Test:
23
+ * 1. Mock 3 working + 2 idle online + 1 offline + 2 active links
24
+ * 2. Assert canvas SVG has both ARIA attrs
25
+ * 3. Assert aria-label contains all four count fragments + gesture hints
26
+ * 4. Assert no role="img" (would defeat children a11y)
27
+ */
28
+ import { chromium } from 'playwright';
29
+ import { readFileSync } from 'node:fs';
30
+
31
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
32
+ const browser = await chromium.launch({ headless: true });
33
+
34
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
35
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
36
+ await ctx.addInitScript(() => {
37
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
38
+ });
39
+
40
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
41
+ const stale = new Date(Date.now() - 10 * 60 * 1000).toISOString();
42
+ await ctx.route('**/api/hub/status*', async (route) => {
43
+ const r = await route.fetch();
44
+ const b = await r.json();
45
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
46
+ const mk = (alias, status, last) => ({
47
+ alias, status, model: 'claude-opus-4', runtime: 'cli-claude-code',
48
+ network_id: nid, project_dir: null,
49
+ created_at: last, updated_at: last, last_seen_at: last,
50
+ });
51
+ await route.fulfill({ response: r, json: { ...b, sessions: [
52
+ mk('agents-a1', 'working', fresh),
53
+ mk('agents-a2', 'working', fresh),
54
+ mk('agents-a3', 'working', fresh),
55
+ mk('infra-b1', 'idle', fresh),
56
+ mk('infra-b2', 'idle', fresh),
57
+ mk('legacy-c1', 'offline', stale),
58
+ ] } });
59
+ });
60
+
61
+ const now = Date.now();
62
+ // 4 a1↔a2 msgs (R100 threshold for edge badge) + 1 a2↔a3 = 2 flowLinks
63
+ const msgs = [];
64
+ for (let i = 0; i < 4; i++) {
65
+ msgs.push({ id: `m${i}`, from_alias: 'agents-a1', to_alias: 'agents-a2', content: 'hi',
66
+ network_id: 'default', created_at: new Date(now - (10000 + i * 500)).toISOString() });
67
+ }
68
+ msgs.push({ id: 'mx', from_alias: 'agents-a2', to_alias: 'agents-a3', content: 'hi',
69
+ network_id: 'default', created_at: new Date(now - 14000).toISOString() });
70
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: msgs } }));
71
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
72
+
73
+ const page = await ctx.newPage();
74
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
75
+ // 5 online + 1 offline = 6 g[data-node]
76
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 6, { timeout: 30000 });
77
+ await page.waitForTimeout(400);
78
+
79
+ const probe = await page.evaluate(() => {
80
+ const el = document.querySelector('[data-topo-canvas-aria]');
81
+ if (!el) return null;
82
+ return {
83
+ tagName: el.tagName.toLowerCase(),
84
+ role: el.getAttribute('role'),
85
+ roledesc: el.getAttribute('aria-roledescription'),
86
+ label: el.getAttribute('aria-label'),
87
+ };
88
+ });
89
+
90
+ await browser.close();
91
+
92
+ const label = probe?.label || '';
93
+ const results = {
94
+ canvas_found: probe !== null,
95
+ is_svg: probe?.tagName === 'svg',
96
+ no_role_img: probe?.role !== 'img',
97
+ has_roledescription: probe?.roledesc === 'agent network topology',
98
+ label_mentions_online: /5 agents online/.test(label),
99
+ label_mentions_working: /3 working/.test(label),
100
+ label_mentions_offline: /1 offline/.test(label),
101
+ label_mentions_links: /2 active links/.test(label),
102
+ label_mentions_tab: /Tab to navigate/.test(label),
103
+ label_mentions_doubleclick: /double-click/.test(label),
104
+ };
105
+ const ok = Object.values(results).every(Boolean);
106
+ console.log(`${ok ? '✅' : '❌'} canvas SVG aria:`, JSON.stringify(results),
107
+ `\n probe=`, probe);
108
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,84 @@
1
+ /* Round 120 verification: chat-target ring breathes when chat is
2
+ * open. R51 added the static ring at radius+14; R120 adds a SMIL
3
+ * opacity pulse (±0.1, 3s) so the active chat session reads as
4
+ * "live" without being intrusive. Gated by reducedMotion.
5
+ *
6
+ * Drive chatAlias by clicking a node to open the popover. Verify:
7
+ * - chat-target ring renders with data-chat-target-breath="on"
8
+ * - the ring has an <animate> child with dur="3s"
9
+ * - reducedMotion suppresses the breath
10
+ */
11
+ import { chromium } from 'playwright';
12
+ import { readFileSync } from 'node:fs';
13
+
14
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
15
+ const browser = await chromium.launch({ headless: true });
16
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
17
+
18
+ const probe = async (reducedMotion) => {
19
+ const ctx = await browser.newContext({
20
+ viewport: { width: 1400, height: 1500 },
21
+ reducedMotion: reducedMotion ? 'reduce' : 'no-preference',
22
+ });
23
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
24
+ await ctx.addInitScript(() => {
25
+ try {
26
+ localStorage.setItem('anet-theme', 'cyber');
27
+ localStorage.setItem('anet-topo-layout', 'ring');
28
+ sessionStorage.setItem('anet_v3_auth', '1');
29
+ } catch {}
30
+ });
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: 'idle', model: 'claude-opus-4', runtime: 'cli-claude-code',
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: [mk('alpha'), mk('beta')] } });
41
+ });
42
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
43
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
44
+ const page = await ctx.newPage();
45
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
46
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 2, { timeout: 30000 });
47
+ await page.waitForTimeout(400);
48
+
49
+ // Open chat by clicking the alpha node.
50
+ await page.locator('g[data-node="alpha"]').click();
51
+ await page.waitForTimeout(500);
52
+
53
+ const data = await page.evaluate(() => {
54
+ const ring = document.querySelector('[data-chat-target-ring]');
55
+ const anim = ring?.querySelector('animate');
56
+ return {
57
+ ringPresent: !!ring,
58
+ breathAttr: ring?.getAttribute('data-chat-target-breath') || '',
59
+ hasAnimate: !!anim,
60
+ dur: anim?.getAttribute('dur') || null,
61
+ };
62
+ });
63
+ await ctx.close();
64
+ return data;
65
+ };
66
+
67
+ const normal = await probe(false);
68
+ const reduced = await probe(true);
69
+ await browser.close();
70
+
71
+ const results = {
72
+ normal_ringPresent: normal.ringPresent === true,
73
+ normal_breathOn: normal.breathAttr === 'on',
74
+ normal_hasAnimate: normal.hasAnimate === true,
75
+ normal_dur3s: normal.dur === '3s',
76
+ reduced_ringPresent: reduced.ringPresent === true,
77
+ reduced_breathOff: reduced.breathAttr === 'off',
78
+ reduced_noAnimate: reduced.hasAnimate === false,
79
+ };
80
+ const ok = Object.values(results).every(Boolean);
81
+ console.log(`${ok ? '✅' : '❌'} chat-target breath:`, JSON.stringify(results),
82
+ `\n normal=`, normal,
83
+ `\n reduced=`, reduced);
84
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,82 @@
1
+ /* Round 242 verification: chat-target ring (R183) transition list
2
+ * grows to include stroke + filter alongside the existing opacity
3
+ * gate, so a chat-target node's status flips (green→teal etc.) and
4
+ * the glow toggle (cyber theme) both ease over 200ms instead of
5
+ * snapping.
6
+ *
7
+ * The ring is always-mounted (R183 idiom) with opacity gated by
8
+ * isChat. Without setting a chat target, opacity stays at 0 — but
9
+ * the transition wiring is in style.transition regardless, so we
10
+ * can probe it without needing to demonstrate the easing live.
11
+ *
12
+ * Test scope per ring:
13
+ * - data-chat-target-ring attr present (probe entry)
14
+ * - data-chat-target-active='false' (no chat at default state)
15
+ * - style.transition includes opacity / stroke / filter
16
+ * each at 200ms (or 0.2s browser-normalised)
17
+ * - stroke attribute present (color from status.primary)
18
+ * - opacity attribute = 0 (gated by isChat=false)
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 {
29
+ localStorage.setItem('anet-theme', 'cyber');
30
+ sessionStorage.setItem('anet_v3_auth', '1');
31
+ localStorage.setItem('anet-topo-layout', 'ring');
32
+ } catch {}
33
+ });
34
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
35
+ await ctx.route('**/api/hub/status*', async (route) => {
36
+ const r = await route.fetch();
37
+ const b = await r.json();
38
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
39
+ const mk = (alias) => ({
40
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
41
+ network_id: nid, project_dir: null,
42
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
43
+ });
44
+ await route.fulfill({ response: r, json: { ...b, sessions: [
45
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
46
+ ] } });
47
+ });
48
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: [] } }));
49
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.fulfill({ json: { tasks: [] } }));
50
+
51
+ const page = await ctx.newPage();
52
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
53
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
54
+ await page.waitForSelector('[data-chat-target-ring]', { timeout: 10000, state: 'attached' });
55
+ await page.waitForTimeout(300);
56
+
57
+ const probe = await page.evaluate(() => {
58
+ const rings = Array.from(document.querySelectorAll('[data-chat-target-ring]'));
59
+ return rings.map((r) => ({
60
+ active: r.getAttribute('data-chat-target-active'),
61
+ stroke: r.getAttribute('stroke'),
62
+ opacity: r.getAttribute('opacity'),
63
+ transition: r.style.transition,
64
+ }));
65
+ });
66
+ await browser.close();
67
+
68
+ const hasProp = (s, prop) => new RegExp(`${prop}\\s+(?:200ms|0\\.2s)`).test(s || '');
69
+
70
+ const results = {
71
+ four_rings: probe.length === 4,
72
+ all_inactive: probe.every(r => r.active === 'false'),
73
+ all_have_stroke: probe.every(r => typeof r.stroke === 'string' && r.stroke.length > 0),
74
+ all_opacity_zero: probe.every(r => r.opacity === '0'),
75
+ all_transition_has_opacity: probe.every(r => hasProp(r.transition, 'opacity')),
76
+ all_transition_has_stroke: probe.every(r => hasProp(r.transition, 'stroke')),
77
+ all_transition_has_filter: probe.every(r => hasProp(r.transition, 'filter')),
78
+ };
79
+ const ok = Object.values(results).every(Boolean);
80
+ console.log(`${ok ? '✅' : '❌'} chat-target ring ease:`, JSON.stringify(results),
81
+ '\n rings:', probe.map(r => ({ a: r.active, stroke: r.stroke, t: r.transition.slice(0, 80) })));
82
+ process.exit(ok ? 0 : 1);