@sleep2agi/agent-network-dashboard 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (493) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +2 -0
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/diagnostics/route-bundle-stats.json +31 -31
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/routes-manifest.json +16 -0
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/_not-found.html +2 -2
  17. package/.next/server/app/_not-found.rsc +12 -12
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  19. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  20. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  23. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  24. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  25. package/.next/server/app/admin.html +2 -2
  26. package/.next/server/app/admin.rsc +14 -14
  27. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  28. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  29. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  30. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  31. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  32. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  33. package/.next/server/app/api/hub/server/[hostname]/agents/route/app-paths-manifest.json +3 -0
  34. package/.next/server/app/api/hub/server/[hostname]/agents/route/build-manifest.json +9 -0
  35. package/.next/server/app/api/hub/server/[hostname]/agents/route/server-reference-manifest.json +4 -0
  36. package/.next/server/app/api/hub/server/[hostname]/agents/route.js +7 -0
  37. package/.next/server/app/api/hub/server/[hostname]/agents/route.js.map +5 -0
  38. package/.next/server/app/api/hub/server/[hostname]/agents/route.js.nft.json +1 -0
  39. package/.next/server/app/api/hub/server/[hostname]/agents/route_client-reference-manifest.js +3 -0
  40. package/.next/server/app/api/hub/server/[hostname]/health/route/app-paths-manifest.json +3 -0
  41. package/.next/server/app/api/hub/server/[hostname]/health/route/build-manifest.json +9 -0
  42. package/.next/server/app/api/hub/server/[hostname]/health/route/server-reference-manifest.json +4 -0
  43. package/.next/server/app/api/hub/server/[hostname]/health/route.js +7 -0
  44. package/.next/server/app/api/hub/server/[hostname]/health/route.js.map +5 -0
  45. package/.next/server/app/api/hub/server/[hostname]/health/route.js.nft.json +1 -0
  46. package/.next/server/app/api/hub/server/[hostname]/health/route_client-reference-manifest.js +3 -0
  47. package/.next/server/app/index.html +2 -2
  48. package/.next/server/app/index.rsc +14 -14
  49. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  50. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  51. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  52. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  53. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app/login.html +2 -2
  56. package/.next/server/app/login.rsc +14 -14
  57. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  58. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  59. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  60. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  61. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  62. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  63. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  64. package/.next/server/app/logs.html +2 -2
  65. package/.next/server/app/logs.rsc +14 -14
  66. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  67. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  68. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  69. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  70. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  71. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  72. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  73. package/.next/server/app/messages.html +2 -2
  74. package/.next/server/app/messages.rsc +14 -14
  75. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  76. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  77. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  78. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  79. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  80. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  81. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  82. package/.next/server/app/node.html +2 -2
  83. package/.next/server/app/node.rsc +14 -14
  84. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  85. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  86. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  87. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  88. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  89. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  90. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  91. package/.next/server/app/nodes.html +2 -2
  92. package/.next/server/app/nodes.rsc +14 -14
  93. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  94. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  95. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  96. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  97. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  98. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  99. package/.next/server/app/page_client-reference-manifest.js +1 -1
  100. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  101. package/.next/server/app/server-logs.html +2 -2
  102. package/.next/server/app/server-logs.rsc +14 -14
  103. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  104. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  105. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  106. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  107. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  108. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  109. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  110. package/.next/server/app/settings/networks.html +2 -2
  111. package/.next/server/app/settings/networks.rsc +14 -14
  112. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  113. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  114. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  115. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  116. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  117. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  118. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  119. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  120. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  121. package/.next/server/app/settings/tokens.html +2 -2
  122. package/.next/server/app/settings/tokens.rsc +14 -14
  123. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  124. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  125. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  126. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  127. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  128. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  129. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  130. package/.next/server/app/settings.html +2 -2
  131. package/.next/server/app/settings.rsc +14 -14
  132. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  133. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  134. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  135. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  136. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  137. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  138. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  139. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  140. package/.next/server/app/tasks.html +2 -2
  141. package/.next/server/app/tasks.rsc +14 -14
  142. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  143. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  144. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  145. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  146. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  147. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  148. package/.next/server/app-paths-manifest.json +2 -0
  149. package/.next/server/chunks/0dk5_next-internal_server_app_api_hub_server_[hostname]_agents_route_actions_0vr92s4.js +3 -0
  150. package/.next/server/chunks/0dk5_next-internal_server_app_api_hub_server_[hostname]_agents_route_actions_0vr92s4.js.map +1 -0
  151. package/.next/server/chunks/0dk5_next-internal_server_app_api_hub_server_[hostname]_health_route_actions_0vr-bv5.js +3 -0
  152. package/.next/server/chunks/0dk5_next-internal_server_app_api_hub_server_[hostname]_health_route_actions_0vr-bv5.js.map +1 -0
  153. package/.next/server/chunks/[root-of-the-server]__05nyivw._.js +3 -0
  154. package/.next/server/chunks/[root-of-the-server]__05nyivw._.js.map +1 -0
  155. package/.next/server/chunks/[root-of-the-server]__0floe3h._.js +3 -0
  156. package/.next/server/chunks/[root-of-the-server]__0floe3h._.js.map +1 -0
  157. package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js +1 -1
  158. package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js.map +1 -1
  159. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +4 -2
  160. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  161. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  162. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  163. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  164. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  165. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  166. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  167. package/.next/server/middleware-build-manifest.js +3 -3
  168. package/.next/server/pages/404.html +2 -2
  169. package/.next/server/pages/500.html +1 -1
  170. package/.next/server/server-reference-manifest.js +1 -1
  171. package/.next/server/server-reference-manifest.json +1 -1
  172. package/.next/static/chunks/0aauz~36q5n2a.css +2 -0
  173. package/.next/static/chunks/0f~0kgy2pycyc.js +1 -0
  174. package/.next/static/chunks/0l_~q07bhpkcx.js +7 -0
  175. package/.next/static/chunks/1175-dp18a1e_.js +1 -0
  176. package/.next/static/chunks/1552j0415oeal.js +4 -0
  177. package/.next/trace +2 -2
  178. package/.next/trace-build +1 -1
  179. package/.next/types/routes.d.ts +3 -1
  180. package/.next/types/validator.ts +18 -0
  181. package/app/api/hub/server/[hostname]/agents/route.ts +62 -0
  182. package/app/api/hub/server/[hostname]/health/route.ts +58 -0
  183. package/app/api/hub/servers/route.ts +88 -1
  184. package/app/components/CommandPalette.tsx +107 -0
  185. package/app/components/HelpOverlay.tsx +109 -1
  186. package/app/components/ServersDrawer.tsx +181 -5
  187. package/app/components/TopoGraph.tsx +6409 -286
  188. package/app/globals.css +110 -5
  189. package/app/lib/vendorIdentity.ts +14 -3
  190. package/docs/rfc/v0.10.0-hero3-dashboard-upgrade.md +230 -0
  191. package/package.json +2 -2
  192. package/public/sleep2agi-logo.svg +11 -0
  193. package/public/vendors/claude.svg +11 -0
  194. package/public/vendors/minimax.svg +11 -0
  195. package/public/vendors/openai.svg +12 -0
  196. package/screenshots/r293-p0/after-full.png +0 -0
  197. package/screenshots/r293-p0/after-topo.png +0 -0
  198. package/screenshots/r293-p0/baseline-full.png +0 -0
  199. package/screenshots/r293-p0/baseline-topo.png +0 -0
  200. package/screenshots/r293-p0/logo-integrated-full.png +0 -0
  201. package/screenshots/r293-p0/logo-integrated-topo.png +0 -0
  202. package/screenshots/r293-p0/servers-drawer-v0.10.0-full.png +0 -0
  203. package/screenshots/r293-p0/servers-drawer-v0.10.0.png +0 -0
  204. package/screenshots/r293-p0/wave1-after-full.png +0 -0
  205. package/screenshots/r293-p0/wave1-after-topo.png +0 -0
  206. package/scripts/p0-servers-drawer-screenshot.mjs +99 -0
  207. package/scripts/p0-topo-screenshot.mjs +82 -0
  208. package/scripts/topo-active-filter-pill-test.mjs +101 -0
  209. package/scripts/topo-active-links-chip-hover-test.mjs +103 -0
  210. package/scripts/topo-active-links-chip-nav-test.mjs +117 -0
  211. package/scripts/topo-active-links-chip-self-hover-test.mjs +171 -0
  212. package/scripts/topo-active-links-empty-recede-test.mjs +125 -0
  213. package/scripts/topo-active-links-freshness-test.mjs +113 -0
  214. package/scripts/topo-active-links-tooltip-test.mjs +79 -0
  215. package/scripts/topo-active-pulse-ease-test.mjs +120 -0
  216. package/scripts/topo-brand-logo-transition-test.mjs +94 -0
  217. package/scripts/topo-brand-watermark-test.mjs +97 -0
  218. package/scripts/topo-canvas-aria-test.mjs +108 -0
  219. package/scripts/topo-canvas-brand-fade-test.mjs +87 -0
  220. package/scripts/topo-chat-target-breath-test.mjs +84 -0
  221. package/scripts/topo-chat-target-ring-ease-test.mjs +82 -0
  222. package/scripts/topo-chat-target-ring-fade-test.mjs +124 -0
  223. package/scripts/topo-chip-empty-recede-test.mjs +116 -0
  224. package/scripts/topo-chip-focus-test.mjs +95 -0
  225. package/scripts/topo-chip-pin-mirror-test.mjs +113 -0
  226. package/scripts/topo-chip-row-gap-test.mjs +90 -0
  227. package/scripts/topo-chip-row-semantic-gap-test.mjs +97 -0
  228. package/scripts/topo-chip-row-tabular-test.mjs +111 -0
  229. package/scripts/topo-chip-unit-demotion-test.mjs +93 -0
  230. package/scripts/topo-chiprow-data-weight-test.mjs +94 -0
  231. package/scripts/topo-chrome-a11y-test.mjs +107 -0
  232. package/scripts/topo-chrome-active-weight-test.mjs +104 -0
  233. package/scripts/topo-chrome-focus-ring-unify-test.mjs +100 -0
  234. package/scripts/topo-chrome-gap-test.mjs +73 -0
  235. package/scripts/topo-chrome-icon-stroke-test.mjs +95 -0
  236. package/scripts/topo-chrome-inactive-color-test.mjs +90 -0
  237. package/scripts/topo-chrome-pop-extension-test.mjs +98 -0
  238. package/scripts/topo-chrome-pop-nodesize-test.mjs +88 -0
  239. package/scripts/topo-chrome-press-state-test.mjs +135 -0
  240. package/scripts/topo-chrome-strip-semantic-gap-test.mjs +99 -0
  241. package/scripts/topo-chrome-strip-theme-ease-test.mjs +99 -0
  242. package/scripts/topo-chrome-zoom-level-pop-test.mjs +122 -0
  243. package/scripts/topo-chrome-zoom-pop-test.mjs +130 -0
  244. package/scripts/topo-chrome-zoom-readout-weight-test.mjs +81 -0
  245. package/scripts/topo-click-ripple-ease-test.mjs +94 -0
  246. package/scripts/topo-cmdk-edge-clear-test.mjs +109 -0
  247. package/scripts/topo-cmdk-layout-view-test.mjs +108 -0
  248. package/scripts/topo-cmdk-pin-test.mjs +123 -0
  249. package/scripts/topo-cmdk-vendor-clear-test.mjs +133 -0
  250. package/scripts/topo-cmdk-vendor-pin-test.mjs +103 -0
  251. package/scripts/topo-corner-radius-unify-test.mjs +103 -0
  252. package/scripts/topo-dense-alias-fill-transition-test.mjs +133 -0
  253. package/scripts/topo-discrete-zoom-smooth-test.mjs +125 -0
  254. package/scripts/topo-edge-badge-click-pin-test.mjs +94 -0
  255. package/scripts/topo-edge-badge-crossfade-test.mjs +138 -0
  256. package/scripts/topo-edge-badge-hot-test.mjs +115 -0
  257. package/scripts/topo-edge-badge-hover-test.mjs +85 -0
  258. package/scripts/topo-edge-badge-lift-test.mjs +150 -0
  259. package/scripts/topo-edge-badge-ripple-test.mjs +111 -0
  260. package/scripts/topo-edge-badge-stroke-transition-test.mjs +118 -0
  261. package/scripts/topo-edge-badge-text-pin-test.mjs +121 -0
  262. package/scripts/topo-edge-badge-theme-ease-test.mjs +89 -0
  263. package/scripts/topo-edge-count-badge-test.mjs +86 -0
  264. package/scripts/topo-edge-endpoint-highlight-test.mjs +111 -0
  265. package/scripts/topo-edge-fadein-test.mjs +106 -0
  266. package/scripts/topo-edge-focus-test.mjs +113 -0
  267. package/scripts/topo-edge-hover-test.mjs +13 -5
  268. package/scripts/topo-edge-pill-hot-test.mjs +129 -0
  269. package/scripts/topo-edge-pill-test.mjs +105 -0
  270. package/scripts/topo-edge-ping-pulse-retired-test.mjs +119 -0
  271. package/scripts/topo-edge-stroke-ease-test.mjs +100 -0
  272. package/scripts/topo-edge-stroke-transition-test.mjs +132 -0
  273. package/scripts/topo-empty-hint-tracking-test.mjs +84 -0
  274. package/scripts/topo-empty-state-breath-test.mjs +150 -0
  275. package/scripts/topo-empty-state-crossfade-test.mjs +130 -0
  276. package/scripts/topo-empty-state-recenter-test.mjs +107 -0
  277. package/scripts/topo-empty-subhint-tracking-test.mjs +87 -0
  278. package/scripts/topo-endpoint-ring-fade-test.mjs +132 -0
  279. package/scripts/topo-endpoint-ring-test.mjs +93 -0
  280. package/scripts/topo-endpoint-ring-thicken-test.mjs +112 -0
  281. package/scripts/topo-esc-clears-pin-test.mjs +98 -0
  282. package/scripts/topo-filter-pill-aliases-test.mjs +121 -0
  283. package/scripts/topo-filter-pill-body-click-test.mjs +112 -0
  284. package/scripts/topo-filter-pill-count-tabular-test.mjs +135 -0
  285. package/scripts/topo-filter-pill-count-test.mjs +110 -0
  286. package/scripts/topo-filter-pill-mobile-test.mjs +90 -0
  287. package/scripts/topo-freshness-chip-transition-test.mjs +80 -0
  288. package/scripts/topo-freshness-chip-weight-test.mjs +90 -0
  289. package/scripts/topo-freshness-conditional-render-test.mjs +92 -0
  290. package/scripts/topo-fullscreen-active-test.mjs +115 -0
  291. package/scripts/topo-group-box-pinned-state-test.mjs +95 -0
  292. package/scripts/topo-group-count-test.mjs +6 -1
  293. package/scripts/topo-group-label-a11y-test.mjs +113 -0
  294. package/scripts/topo-group-label-count-inherit-test.mjs +99 -0
  295. package/scripts/topo-group-label-hover-test.mjs +119 -0
  296. package/scripts/topo-group-label-pin-spacing-test.mjs +136 -0
  297. package/scripts/topo-group-label-tint-test.mjs +99 -0
  298. package/scripts/topo-group-label-title-test.mjs +83 -0
  299. package/scripts/topo-group-pin-test.mjs +119 -0
  300. package/scripts/topo-group-pip-fadein-test.mjs +104 -0
  301. package/scripts/topo-group-pip-singletier-drop-test.mjs +131 -0
  302. package/scripts/topo-group-pip-tabular-test.mjs +95 -0
  303. package/scripts/topo-group-status-pips-test.mjs +103 -0
  304. package/scripts/topo-groupbox-fadein-test.mjs +95 -0
  305. package/scripts/topo-groupbox-hover-lift-test.mjs +127 -0
  306. package/scripts/topo-groupbox-hubcore-ease-test.mjs +106 -0
  307. package/scripts/topo-groupbox-live-test.mjs +100 -0
  308. package/scripts/topo-groupbox-march-busyness-test.mjs +155 -0
  309. package/scripts/topo-groupbox-test.mjs +4 -2
  310. package/scripts/topo-header-gap-test.mjs +94 -0
  311. package/scripts/topo-help-overlay-r109-test.mjs +67 -0
  312. package/scripts/topo-help-overlay-r118-test.mjs +69 -0
  313. package/scripts/topo-help-overlay-r123-test.mjs +69 -0
  314. package/scripts/topo-help-overlay-r134-test.mjs +74 -0
  315. package/scripts/topo-help-overlay-r137-test.mjs +81 -0
  316. package/scripts/topo-help-overlay-r141-test.mjs +86 -0
  317. package/scripts/topo-help-overlay-r153-test.mjs +85 -0
  318. package/scripts/topo-help-overlay-r59-test.mjs +67 -0
  319. package/scripts/topo-help-overlay-r65-test.mjs +64 -0
  320. package/scripts/topo-help-overlay-r70-test.mjs +63 -0
  321. package/scripts/topo-help-overlay-r78-test.mjs +59 -0
  322. package/scripts/topo-help-overlay-r81-test.mjs +58 -0
  323. package/scripts/topo-help-overlay-r87-test.mjs +64 -0
  324. package/scripts/topo-help-overlay-r91-test.mjs +65 -0
  325. package/scripts/topo-hot-tail-crossfade-test.mjs +117 -0
  326. package/scripts/topo-html-overlay-padding-unify-test.mjs +120 -0
  327. package/scripts/topo-hub-a11y-test.mjs +112 -0
  328. package/scripts/topo-hub-busyness-test.mjs +100 -0
  329. package/scripts/topo-hub-center-crossfade-test.mjs +124 -0
  330. package/scripts/topo-hub-click-fit-test.mjs +137 -0
  331. package/scripts/topo-hub-digit-scale-test.mjs +129 -0
  332. package/scripts/topo-hub-fadein-test.mjs +88 -0
  333. package/scripts/topo-hub-hover-lift-test.mjs +101 -0
  334. package/scripts/topo-hub-hover-ring-test.mjs +82 -0
  335. package/scripts/topo-hub-pip-theme-ease-test.mjs +116 -0
  336. package/scripts/topo-hub-spoke-ease-test.mjs +120 -0
  337. package/scripts/topo-hub-title-test.mjs +7 -2
  338. package/scripts/topo-hub-working-count-test.mjs +99 -0
  339. package/scripts/topo-ingroup-edge-test.mjs +103 -0
  340. package/scripts/topo-inner-radar-ring-retired-test.mjs +89 -0
  341. package/scripts/topo-kicker-color-test.mjs +93 -0
  342. package/scripts/topo-kicker-tracking-test.mjs +101 -0
  343. package/scripts/topo-kicker-weight-test.mjs +98 -0
  344. package/scripts/topo-label-card-theme-ease-test.mjs +85 -0
  345. package/scripts/topo-label-lift-test.mjs +5 -1
  346. package/scripts/topo-layout-hover-text-preview-test.mjs +101 -0
  347. package/scripts/topo-layout-switch-crossfade-test.mjs +126 -0
  348. package/scripts/topo-layout-toggle-border-unify-test.mjs +104 -0
  349. package/scripts/topo-layout-toggle-test.mjs +131 -0
  350. package/scripts/topo-layout-trailer-mr-test.mjs +82 -0
  351. package/scripts/topo-legend-compress-test.mjs +107 -0
  352. package/scripts/topo-legend-count-empty-test.mjs +126 -0
  353. package/scripts/topo-legend-count-tabular-parity-test.mjs +90 -0
  354. package/scripts/topo-legend-count-tabular-test.mjs +87 -0
  355. package/scripts/topo-legend-count-test.mjs +88 -0
  356. package/scripts/topo-legend-count-tier-fill-test.mjs +121 -0
  357. package/scripts/topo-legend-count-weight-test.mjs +86 -0
  358. package/scripts/topo-legend-header-symmetric-inset-test.mjs +112 -0
  359. package/scripts/topo-legend-hitbox-swatch-center-test.mjs +93 -0
  360. package/scripts/topo-legend-hover-test.mjs +113 -0
  361. package/scripts/topo-legend-offline-delimiter-test.mjs +84 -0
  362. package/scripts/topo-legend-offline-label-simplify-test.mjs +86 -0
  363. package/scripts/topo-legend-panel-count-weight-test.mjs +89 -0
  364. package/scripts/topo-legend-panel-header-test.mjs +83 -0
  365. package/scripts/topo-legend-pin-ring-fade-test.mjs +112 -0
  366. package/scripts/topo-legend-pin-test.mjs +128 -0
  367. package/scripts/topo-legend-row-hover-lift-test.mjs +109 -0
  368. package/scripts/topo-legend-row-labels-simplify-test.mjs +80 -0
  369. package/scripts/topo-legend-row-pin-spacing-test.mjs +124 -0
  370. package/scripts/topo-legend-row-tint-test.mjs +95 -0
  371. package/scripts/topo-legend-row-title-test.mjs +102 -0
  372. package/scripts/topo-legend-swatch-grow-test.mjs +116 -0
  373. package/scripts/topo-legend-swatch-radius-test.mjs +78 -0
  374. package/scripts/topo-minimap-a11y-test.mjs +101 -0
  375. package/scripts/topo-minimap-dot-transition-test.mjs +113 -0
  376. package/scripts/topo-minimap-radius-test.mjs +88 -0
  377. package/scripts/topo-minimap-rect-stroke-test.mjs +94 -0
  378. package/scripts/topo-minimap-viewport-smooth-test.mjs +124 -0
  379. package/scripts/topo-monogram-font-test.mjs +112 -0
  380. package/scripts/topo-monogram-stroke-test.mjs +101 -0
  381. package/scripts/topo-more-flows-letterspacing-test.mjs +111 -0
  382. package/scripts/topo-node-a11y-test.mjs +107 -0
  383. package/scripts/topo-node-alias-chat-pin-test.mjs +95 -0
  384. package/scripts/topo-node-hover-lift-test.mjs +85 -0
  385. package/scripts/topo-node-label-card-elevation-test.mjs +157 -0
  386. package/scripts/topo-node-label-card-stroke-test.mjs +135 -0
  387. package/scripts/topo-node-stroke-transition-test.mjs +103 -0
  388. package/scripts/topo-node-text-fill-transition-test.mjs +119 -0
  389. package/scripts/topo-node-title-peers-test.mjs +107 -0
  390. package/scripts/topo-node-title-test.mjs +101 -0
  391. package/scripts/topo-nodesize-active-hover-test.mjs +109 -0
  392. package/scripts/topo-nodesize-crossfade-test.mjs +141 -0
  393. package/scripts/topo-online-chip-nav-test.mjs +143 -0
  394. package/scripts/topo-orbit-particles-retired-test.mjs +91 -0
  395. package/scripts/topo-panel-count-unit-test.mjs +112 -0
  396. package/scripts/topo-panel-elevation-test.mjs +76 -0
  397. package/scripts/topo-panel-fadein-test.mjs +102 -0
  398. package/scripts/topo-panel-hot-tail-fade-test.mjs +132 -0
  399. package/scripts/topo-panel-hover-elevation-test.mjs +118 -0
  400. package/scripts/topo-panel-rx-test.mjs +90 -0
  401. package/scripts/topo-panel-theme-ease-test.mjs +85 -0
  402. package/scripts/topo-panel-title-fill-ease-test.mjs +91 -0
  403. package/scripts/topo-panel-title-tracking-test.mjs +78 -0
  404. package/scripts/topo-particle-stagger-test.mjs +82 -0
  405. package/scripts/topo-pin-intersection-color-ease-test.mjs +111 -0
  406. package/scripts/topo-pin-intersection-empty-test.mjs +129 -0
  407. package/scripts/topo-pin-intersection-tabular-test.mjs +96 -0
  408. package/scripts/topo-pin-intersection-test.mjs +121 -0
  409. package/scripts/topo-pin-intersection-warning-crossfade-test.mjs +107 -0
  410. package/scripts/topo-pin-mirror-transition-test.mjs +101 -0
  411. package/scripts/topo-pin-persistence-test.mjs +105 -0
  412. package/scripts/topo-prefix-opacity-test.mjs +107 -0
  413. package/scripts/topo-prepublish-guard-test.mjs +95 -0
  414. package/scripts/topo-pressure-hover-test.mjs +123 -0
  415. package/scripts/topo-pressure-pin-test.mjs +119 -0
  416. package/scripts/topo-pressure-seg-self-hover-test.mjs +126 -0
  417. package/scripts/topo-pressure-seg-tooltip-test.mjs +98 -0
  418. package/scripts/topo-pressure-seg-transition-test.mjs +107 -0
  419. package/scripts/topo-pulse-dot-crossfade-test.mjs +122 -0
  420. package/scripts/topo-pulse-dot-retired-test.mjs +86 -0
  421. package/scripts/topo-recent-panel-count-freshness-test.mjs +109 -0
  422. package/scripts/topo-recent-panel-count-test.mjs +72 -0
  423. package/scripts/topo-recent-panel-count-weight-test.mjs +91 -0
  424. package/scripts/topo-recent-panel-footer-breath-test.mjs +142 -0
  425. package/scripts/topo-recent-panel-hot-header-test.mjs +132 -0
  426. package/scripts/topo-recent-panel-hot-tabular-test.mjs +113 -0
  427. package/scripts/topo-recent-panel-more-crossfade-test.mjs +124 -0
  428. package/scripts/topo-recent-panel-more-cyan-hover-test.mjs +151 -0
  429. package/scripts/topo-recent-panel-more-nav-test.mjs +127 -0
  430. package/scripts/topo-recent-panel-more-test.mjs +106 -0
  431. package/scripts/topo-recent-panel-readability-bump-test.mjs +151 -0
  432. package/scripts/topo-recent-row-count-fill-test.mjs +129 -0
  433. package/scripts/topo-recent-row-count-weight-test.mjs +112 -0
  434. package/scripts/topo-recent-row-fadein-test.mjs +107 -0
  435. package/scripts/topo-recent-row-freshness-test.mjs +102 -0
  436. package/scripts/topo-recent-row-hot-test.mjs +110 -0
  437. package/scripts/topo-recent-row-hover-lift-test.mjs +128 -0
  438. package/scripts/topo-recent-row-hover-test.mjs +122 -0
  439. package/scripts/topo-recent-row-pin-spacing-test.mjs +137 -0
  440. package/scripts/topo-recent-row-pin-test.mjs +126 -0
  441. package/scripts/topo-recent-row-tint-test.mjs +91 -0
  442. package/scripts/topo-recent-row-title-test.mjs +112 -0
  443. package/scripts/topo-recent-row-ts-alpha-test.mjs +100 -0
  444. package/scripts/topo-recent-row-ts-tabular-test.mjs +108 -0
  445. package/scripts/topo-recent-row-ts-test.mjs +95 -0
  446. package/scripts/topo-recent-row-typography-test.mjs +119 -0
  447. package/scripts/topo-recent-signal-empty-hint-test.mjs +73 -0
  448. package/scripts/topo-reset-spin-test.mjs +120 -0
  449. package/scripts/topo-runtime-badge-grow-test.mjs +114 -0
  450. package/scripts/topo-spokes-retired-test.mjs +93 -0
  451. package/scripts/topo-starfield-density-test.mjs +89 -0
  452. package/scripts/topo-status-chip-hover-test.mjs +101 -0
  453. package/scripts/topo-status-chip-tooltip-test.mjs +95 -0
  454. package/scripts/topo-statusbar-chips-self-hover-test.mjs +177 -0
  455. package/scripts/topo-svg-focus-test.mjs +112 -0
  456. package/scripts/topo-sweep-busyness-test.mjs +94 -0
  457. package/scripts/topo-tabular-nums-sweep-test.mjs +93 -0
  458. package/scripts/topo-tier-guide-dashes-test.mjs +86 -0
  459. package/scripts/topo-tier-ring-fadein-test.mjs +107 -0
  460. package/scripts/topo-tier-ring-occupancy-test.mjs +87 -0
  461. package/scripts/topo-tier-ring-test.mjs +69 -0
  462. package/scripts/topo-tier-ring-tint-test.mjs +110 -0
  463. package/scripts/topo-tier-stagger-test.mjs +89 -0
  464. package/scripts/topo-title-block-tight-leading-test.mjs +95 -0
  465. package/scripts/topo-title-tracking-test.mjs +98 -0
  466. package/scripts/topo-titleblock-gap-test.mjs +86 -0
  467. package/scripts/topo-titleblock-margin-test.mjs +83 -0
  468. package/scripts/topo-toggle-inactive-cyan-hover-test.mjs +110 -0
  469. package/scripts/topo-top-rail-theme-ease-test.mjs +89 -0
  470. package/scripts/topo-vendor-chip-hover-test.mjs +111 -0
  471. package/scripts/topo-vendor-chip-self-hover-test.mjs +131 -0
  472. package/scripts/topo-vendor-chip-tabular-test.mjs +92 -0
  473. package/scripts/topo-vendor-chip-threshold-test.mjs +78 -0
  474. package/scripts/topo-vendor-chip-weight-test.mjs +87 -0
  475. package/scripts/topo-vendor-count-suffix-test.mjs +95 -0
  476. package/scripts/topo-vendor-letter-tooltip-test.mjs +92 -0
  477. package/scripts/topo-vendor-pill-test.mjs +125 -0
  478. package/scripts/topo-vendor-pin-test.mjs +125 -0
  479. package/scripts/topo-view-smooth-test.mjs +131 -0
  480. package/scripts/topo-watermark-tracking-test.mjs +99 -0
  481. package/scripts/topo-working-chip-pin-test.mjs +154 -0
  482. package/scripts/topo-working-halo-breath-retired-test.mjs +99 -0
  483. package/scripts/topo-wrapper-flow-arrow-theme-test.mjs +97 -0
  484. package/scripts/topo-wrapper-radius-test.mjs +73 -0
  485. package/scripts/topo-wrapper-shadow-theme-ease-test.mjs +89 -0
  486. package/.next/static/chunks/088ouqy7tekty.js +0 -2
  487. package/.next/static/chunks/0hyl7mdapsz38.js +0 -1
  488. package/.next/static/chunks/0mqwn-l7wtcdo.js +0 -7
  489. package/.next/static/chunks/127kw7~wsnyu7.css +0 -2
  490. package/.next/static/chunks/16yfxp9ak2gdm.js +0 -1
  491. /package/.next/static/{23MxP0RBVA4BaOJ_HMYmM → 424zX3i80AJ_wHF_otKuP}/_buildManifest.js +0 -0
  492. /package/.next/static/{23MxP0RBVA4BaOJ_HMYmM → 424zX3i80AJ_wHF_otKuP}/_clientMiddlewareManifest.js +0 -0
  493. /package/.next/static/{23MxP0RBVA4BaOJ_HMYmM → 424zX3i80AJ_wHF_otKuP}/_ssgManifest.js +0 -0
@@ -0,0 +1,107 @@
1
+ /* Round 258 verification: recent-signal panel empty-state re-centered
2
+ * within the post-R256 88-tall panel.
3
+ *
4
+ * Pre-R256 (panel height 84):
5
+ * panel mid-line = (0 + 84) / 2 = 42
6
+ * main "no flow yet" at y=54 → +12 below mid
7
+ * hint "send a..." at y=68 → +26 below mid
8
+ * Optically balanced for an 84-tall panel.
9
+ *
10
+ * Post-R256 (panel height 88) — pre-R258:
11
+ * panel mid-line = (0 + 88) / 2 = 44
12
+ * main at y=54 → +10 below mid (drifted 2px high)
13
+ * hint at y=68 → +24 below mid (drifted 2px high)
14
+ * Empty state visually drifted up after the height bump.
15
+ *
16
+ * Post-R258:
17
+ * main y=54 → y=56 (+2)
18
+ * hint y=68 → y=70 (+2)
19
+ * New offsets: +12 / +26 below mid — restored optical balance.
20
+ *
21
+ * Test scope:
22
+ * 1. Empty state mounts visible (flowLinks.length === 0).
23
+ * 2. Main text y === 56 (was 54).
24
+ * 3. Hint text y === 70 (was 68).
25
+ * 4. Panel rect height === 88 (regression — R256 holds).
26
+ * 5. Offsets-from-mid: (main_y - panel_height/2) === 12 (matches
27
+ * original R45 optical placement).
28
+ * 6. R200 SMIL breath continues — animate child still attached.
29
+ * 7. Hint baseline still sits 12px above the footer (visible when
30
+ * flowLinks.length > 3 — but in empty state footer is invisible
31
+ * anyway; just verify y=70 < 82).
32
+ */
33
+ import { chromium } from 'playwright';
34
+ import { readFileSync } from 'node:fs';
35
+
36
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
37
+ const browser = await chromium.launch({ headless: true });
38
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
39
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
40
+ await ctx.addInitScript(() => {
41
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
42
+ });
43
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
44
+ await ctx.route('**/api/hub/status*', async (route) => {
45
+ const r = await route.fetch();
46
+ const b = await r.json();
47
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
48
+ const mk = (alias) => ({
49
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
50
+ network_id: nid, project_dir: null,
51
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
52
+ });
53
+ await route.fulfill({ response: r, json: { ...b, sessions: [
54
+ mk('alpha'), mk('beta'),
55
+ ] } });
56
+ });
57
+ // Zero messages → flowLinks.length === 0 → empty state visible.
58
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: [] } }));
59
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.fulfill({ json: { tasks: [] } }));
60
+
61
+ const page = await ctx.newPage();
62
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
63
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 2, { timeout: 30000 });
64
+ await page.waitForSelector('[data-recent-signal-empty]', { timeout: 10000 });
65
+ await page.waitForSelector('[data-recent-signal-empty-hint]', { timeout: 10000 });
66
+ await page.waitForTimeout(300);
67
+
68
+ const probe = await page.evaluate(() => {
69
+ const panelG = document.querySelector('[data-topo-panel="recent"]');
70
+ const rect = panelG?.querySelector('rect');
71
+ const wrap = document.querySelector('[data-recent-signal-empty-wrapper]');
72
+ const main = document.querySelector('[data-recent-signal-empty]');
73
+ const hint = document.querySelector('[data-recent-signal-empty-hint]');
74
+ const mainAnim = main?.querySelector('animate');
75
+ const hintAnim = hint?.querySelector('animate');
76
+ return {
77
+ panelH: rect ? +rect.getAttribute('height') : null,
78
+ wrapperVisible: wrap?.getAttribute('data-recent-signal-empty-visible'),
79
+ mainY: main ? +main.getAttribute('y') : null,
80
+ hintY: hint ? +hint.getAttribute('y') : null,
81
+ mainHasBreath: !!mainAnim,
82
+ hintHasBreath: !!hintAnim,
83
+ };
84
+ });
85
+ await browser.close();
86
+
87
+ const midLine = probe.panelH != null ? probe.panelH / 2 : null;
88
+ const mainOffsetMid = (probe.mainY != null && midLine != null) ? probe.mainY - midLine : null;
89
+ const hintOffsetMid = (probe.hintY != null && midLine != null) ? probe.hintY - midLine : null;
90
+
91
+ const results = {
92
+ panel_h_88: probe.panelH === 88,
93
+ wrapper_empty_visible_true: probe.wrapperVisible === 'true',
94
+ main_y_moved_to_56: probe.mainY === 56,
95
+ hint_y_moved_to_70: probe.hintY === 70,
96
+ main_offset_from_mid_is_12: mainOffsetMid === 12,
97
+ hint_offset_from_mid_is_26: hintOffsetMid === 26,
98
+ main_smil_breath_present: probe.mainHasBreath === true,
99
+ hint_smil_breath_present: probe.hintHasBreath === true,
100
+ hint_above_footer_y82: probe.hintY != null && probe.hintY < 82,
101
+ };
102
+ const ok = Object.values(results).every(Boolean);
103
+ console.log(`${ok ? '✅' : '❌'} recent panel empty-state re-center:`, JSON.stringify(results),
104
+ '\n panelH:', probe.panelH, 'midLine:', midLine,
105
+ '\n mainY:', probe.mainY, '(offset-from-mid', mainOffsetMid, ')',
106
+ '\n hintY:', probe.hintY, '(offset-from-mid', hintOffsetMid, ')');
107
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,87 @@
1
+ /* Round 304 verification: secondary instructional hint
2
+ * 'send a message between agents' gains letterSpacing='0.15'.
3
+ * Extends the R301/R302 editorial-spacing-by-loudness hierarchy
4
+ * one layer down — quietest authored text in the panel.
5
+ *
6
+ * Contract:
7
+ * - [data-recent-signal-empty-hint] attr letter-spacing === '0.15'.
8
+ * - Text content unchanged.
9
+ * - opacity 0.45 + fontSize 9 preserved.
10
+ * - R302 main empty hint ls=0.2 still in place (regression).
11
+ * - R301 panel titles ls=0.3 + R300 kicker fw=500 intact.
12
+ */
13
+ import { chromium } from 'playwright';
14
+ import { readFileSync } from 'node:fs';
15
+
16
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
17
+ const browser = await chromium.launch({ headless: true });
18
+ const ctx = await browser.newContext({ viewport: { width: 1500, 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
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
24
+ await ctx.route('**/api/hub/status*', async (route) => {
25
+ const r = await route.fetch();
26
+ const b = await r.json();
27
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
28
+ const mk = (alias, model) => ({
29
+ alias, status: 'working', model, runtime: 'claude-code-cli',
30
+ network_id: nid, project_dir: null,
31
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
32
+ });
33
+ await route.fulfill({ response: r, json: { ...b, sessions: [
34
+ mk('alpha', 'claude-opus-4'),
35
+ mk('beta', 'gpt-4o'),
36
+ ] } });
37
+ });
38
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
39
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
40
+
41
+ const page = await ctx.newPage();
42
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
43
+ await page.waitForSelector('[data-recent-signal-empty-hint]', { timeout: 15000 });
44
+ await page.waitForTimeout(300);
45
+
46
+ const probe = await page.evaluate(() => {
47
+ const hint = document.querySelector('[data-recent-signal-empty-hint]');
48
+ const empty = document.querySelector('[data-recent-signal-empty]');
49
+ const recentTitle = document.querySelector('[data-recent-panel-title]');
50
+ const legendTitle = document.querySelector('[data-legend-panel-title]');
51
+ const kicker = document.querySelector('[data-topo-section-kicker]');
52
+ return {
53
+ hintLs: hint?.getAttribute('letter-spacing') ?? null,
54
+ hintText: hint?.textContent?.trim() ?? null,
55
+ hintFontSize: hint?.getAttribute('font-size') ?? null,
56
+ hintOpacity: hint?.getAttribute('opacity') ?? null,
57
+ emptyLs: empty?.getAttribute('letter-spacing') ?? null,
58
+ recentTitleLs: recentTitle?.getAttribute('letter-spacing') ?? null,
59
+ legendTitleLs: legendTitle?.getAttribute('letter-spacing') ?? null,
60
+ kickerFw: kicker ? getComputedStyle(kicker).fontWeight : null,
61
+ pulseCount: document.querySelectorAll('[data-pulse-wrapper]').length,
62
+ tierDashes: (() => {
63
+ const t = document.querySelector('[data-tier-ring]');
64
+ return t?.getAttribute('stroke-dasharray') ?? null;
65
+ })(),
66
+ };
67
+ });
68
+ await browser.close();
69
+
70
+ const results = {
71
+ subhint_ls_0_15: probe.hintLs === '0.15',
72
+ subhint_text_kept: (probe.hintText || '').startsWith('send a message between agents'),
73
+ subhint_font_size_9: probe.hintFontSize === '9',
74
+ subhint_opacity_0_45: String(probe.hintOpacity) === '0.45',
75
+ r302_main_hint_ls_0_2: probe.emptyLs === '0.2',
76
+ r301_recent_title_ls: probe.recentTitleLs === '0.3',
77
+ r301_legend_title_ls: probe.legendTitleLs === '0.3',
78
+ r300_kicker_fw_500: String(probe.kickerFw) === '500',
79
+ r303_tier_dashes_2_6: probe.tierDashes === '2 6',
80
+ r294_pulse_absent: probe.pulseCount === 0,
81
+ };
82
+ const ok = Object.values(results).every(Boolean);
83
+ console.log(`${ok ? '✅' : '❌'} empty subhint tracking:`, JSON.stringify(results),
84
+ '\n sub-hint letter-spacing:', probe.hintLs,
85
+ '\n main hint letter-spacing (R302):', probe.emptyLs,
86
+ '\n panel titles ls (R301):', probe.recentTitleLs, '/', probe.legendTitleLs);
87
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,132 @@
1
+ /* Round 182 verification: edge-endpoint emphasis ring (R49/R111)
2
+ * fades in/out instead of mount/unmount.
3
+ *
4
+ * Pre-R182 the ring mounted only when the node was in the
5
+ * hoveredEdgeEndpoints set — the transition style was already
6
+ * there but the mount/unmount sequence defeated it. R182 makes
7
+ * the ring always-mounted with opacity gated by the set, so the
8
+ * transition actually fires on hover entry/exit.
9
+ *
10
+ * Also escapes the R51 overlap-test sentinel:
11
+ * strokeWidth: 1.5 → 1.6 (visually imperceptible, doesn't
12
+ * match the exact-string CSS selector that picks the status
13
+ * ring inside g[data-node]).
14
+ *
15
+ * Test:
16
+ * 1. 4 sessions + a 4-msg flow alpha→beta (so badge renders)
17
+ * 2. Idle: ring exists on alpha + beta + gamma + delta nodes,
18
+ * all at opacity=0
19
+ * 3. Hover the edge badge (propagates to hoveredEdgeKey →
20
+ * hoveredEdgeEndpoints set with alpha + beta)
21
+ * 4. Rings on alpha + beta: opacity ≈ 0.85 (cyber)
22
+ * 5. Rings on gamma + delta: still opacity=0
23
+ * 6. Mouseleave: all four back to opacity=0
24
+ * 7. All rings carry 'opacity 180ms ease-out' transition
25
+ * 8. All rings strokeWidth='1.6' (not 1.5)
26
+ */
27
+ import { chromium } from 'playwright';
28
+ import { readFileSync } from 'node:fs';
29
+
30
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
31
+ const browser = await chromium.launch({ headless: true });
32
+
33
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
34
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
35
+ await ctx.addInitScript(() => {
36
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
37
+ });
38
+
39
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
40
+ await ctx.route('**/api/hub/status*', async (route) => {
41
+ const r = await route.fetch();
42
+ const b = await r.json();
43
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
44
+ const mk = (alias) => ({
45
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
46
+ network_id: nid, project_dir: null,
47
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
48
+ });
49
+ // 4 sessions so alpha/beta sit 90° apart (avoid hub overlap with badge midpoint)
50
+ await route.fulfill({ response: r, json: { ...b, sessions: [
51
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
52
+ ] } });
53
+ });
54
+
55
+ const now = Date.now();
56
+ const msgs = [];
57
+ for (let i = 0; i < 4; i++) {
58
+ msgs.push({ id: `m${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
59
+ network_id: 'default', created_at: new Date(now - (10000 + i * 500)).toISOString() });
60
+ }
61
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: msgs } }));
62
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
63
+
64
+ const page = await ctx.newPage();
65
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
66
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
67
+ await page.waitForSelector('[data-edge-endpoint-ring]', { timeout: 10000 });
68
+ await page.waitForSelector('[data-edge-count-badge]', { timeout: 10000 });
69
+ await page.waitForTimeout(400);
70
+
71
+ const probeRings = () => page.evaluate(() => {
72
+ const rings = [...document.querySelectorAll('g[data-node] [data-edge-endpoint-ring]')];
73
+ return rings.map(r => {
74
+ const node = r.closest('g[data-node]');
75
+ return {
76
+ alias: node?.getAttribute('data-node'),
77
+ opacity: parseFloat(r.getAttribute('opacity') || ''),
78
+ activeAttr: r.getAttribute('data-edge-endpoint-active'),
79
+ strokeWidth: r.getAttribute('stroke-width'),
80
+ transition: r.style.transition || getComputedStyle(r).transition,
81
+ };
82
+ });
83
+ });
84
+
85
+ const idle = await probeRings();
86
+
87
+ // Hover the edge midpoint badge (propagates to hoveredEdgeKey)
88
+ await page.locator('[data-edge-count-badge]').hover();
89
+ await page.waitForTimeout(300);
90
+ const hovered = await probeRings();
91
+
92
+ // Move cursor away
93
+ await page.mouse.move(10, 10);
94
+ await page.waitForTimeout(300);
95
+ const idleAgain = await probeRings();
96
+
97
+ await browser.close();
98
+
99
+ const findRing = (rings, alias) => rings.find(r => r.alias === alias);
100
+ const hasOpacityTransition = (s) =>
101
+ (s.transition || '').includes('opacity 180ms') ||
102
+ /opacity\s+0\.18s|opacity\s+180ms/.test(s.transition || '');
103
+
104
+ const results = {
105
+ four_rings_present: idle.length === 4,
106
+ all_have_strokewidth_1p6: idle.every(r => r.strokeWidth === '1.6'),
107
+ none_match_15_sentinel: idle.every(r => r.strokeWidth !== '1.5'),
108
+ all_have_transition: idle.every(r => hasOpacityTransition(r)),
109
+
110
+ idle_all_opacity_0: idle.every(r => r.opacity === 0),
111
+ idle_all_active_false: idle.every(r => r.activeAttr === 'false'),
112
+
113
+ // Hover badge → alpha + beta active, gamma + delta inactive
114
+ hover_alpha_active: findRing(hovered, 'alpha')?.activeAttr === 'true',
115
+ hover_alpha_opacity_0p85: Math.abs((findRing(hovered, 'alpha')?.opacity ?? 0) - 0.85) < 0.05,
116
+ hover_beta_active: findRing(hovered, 'beta')?.activeAttr === 'true',
117
+ hover_beta_opacity_0p85: Math.abs((findRing(hovered, 'beta')?.opacity ?? 0) - 0.85) < 0.05,
118
+ hover_gamma_inactive: findRing(hovered, 'gamma')?.activeAttr === 'false',
119
+ hover_gamma_opacity_0: findRing(hovered, 'gamma')?.opacity === 0,
120
+ hover_delta_inactive: findRing(hovered, 'delta')?.activeAttr === 'false',
121
+ hover_delta_opacity_0: findRing(hovered, 'delta')?.opacity === 0,
122
+
123
+ // Mouseleave → all back to 0
124
+ idle_again_all_opacity_0: idleAgain.every(r => r.opacity === 0),
125
+ idle_again_all_inactive: idleAgain.every(r => r.activeAttr === 'false'),
126
+ };
127
+ const ok = Object.values(results).every(Boolean);
128
+ console.log(`${ok ? '✅' : '❌'} endpoint-ring fade:`, JSON.stringify(results),
129
+ `\n idle:`, idle,
130
+ `\n hovered:`, hovered,
131
+ `\n idleAgain:`, idleAgain);
132
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,93 @@
1
+ /* Round 111 verification: when an edge is hovered, BOTH endpoint
2
+ * nodes get an accent stroke ring (data-edge-endpoint-ring) while
3
+ * non-endpoint nodes don't. R49 already kept endpoints at opacity 1
4
+ * but offered no positive cue; the ring closes that.
5
+ *
6
+ * Fleet: alpha → beta with 4 messages (sole flow), gamma idle (no
7
+ * flows). Hover the edge → alpha + beta each carry a ring; gamma
8
+ * carries none. Mouseleave → all rings vanish.
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
+
16
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
17
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
18
+ await ctx.addInitScript(() => {
19
+ try {
20
+ localStorage.setItem('anet-theme', 'cyber');
21
+ localStorage.setItem('anet-topo-layout', 'ring');
22
+ sessionStorage.setItem('anet_v3_auth', '1');
23
+ } catch {}
24
+ });
25
+
26
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
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'), mk('gamma')] } });
37
+ });
38
+
39
+ const now = Date.now();
40
+ const mkMsg = (id, from_alias, to_alias, ageMs) => ({
41
+ id, from_alias, to_alias, content: 'hi', network_id: 'default',
42
+ created_at: new Date(now - ageMs).toISOString(),
43
+ });
44
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [
45
+ mkMsg('m1', 'alpha', 'beta', 30 * 1000),
46
+ mkMsg('m2', 'alpha', 'beta', 40 * 1000),
47
+ mkMsg('m3', 'alpha', 'beta', 50 * 1000),
48
+ mkMsg('m4', 'alpha', 'beta', 60 * 1000),
49
+ ] } }));
50
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
51
+ const page = await ctx.newPage();
52
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
53
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 3, { timeout: 30000 });
54
+ await page.waitForTimeout(400);
55
+
56
+ const ringByAlias = () => page.evaluate(() => {
57
+ const out = {};
58
+ for (const alias of ['alpha', 'beta', 'gamma']) {
59
+ const g = document.querySelector(`g[data-node="${alias}"]`);
60
+ out[alias] = !!g?.querySelector('[data-edge-endpoint-ring]');
61
+ }
62
+ return out;
63
+ });
64
+
65
+ const before = await ringByAlias();
66
+
67
+ // Hover the edge hitbox (the invisible 16-px stroke that owns the
68
+ // hover surface per R48). The recent-signal row provides a stable
69
+ // way to set hoveredEdgeKey without finding the path on the canvas.
70
+ await page.locator('[data-recent-row="alpha->beta"]').hover();
71
+ await page.waitForTimeout(300);
72
+ const onHover = await ringByAlias();
73
+
74
+ // Move away.
75
+ await page.mouse.move(10, 10);
76
+ await page.waitForTimeout(300);
77
+ const afterLeave = await ringByAlias();
78
+
79
+ await browser.close();
80
+
81
+ const results = {
82
+ before_noneHaveRing: !before.alpha && !before.beta && !before.gamma,
83
+ hover_alphaRing: onHover.alpha === true,
84
+ hover_betaRing: onHover.beta === true,
85
+ hover_gammaNoRing: onHover.gamma === false,
86
+ leave_allClear: !afterLeave.alpha && !afterLeave.beta && !afterLeave.gamma,
87
+ };
88
+ const ok = Object.values(results).every(Boolean);
89
+ console.log(`${ok ? '✅' : '❌'} edge-endpoint ring:`, JSON.stringify(results),
90
+ `\n before=`, before,
91
+ `\n onHover=`, onHover,
92
+ `\n afterLeave=`, afterLeave);
93
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,112 @@
1
+ /* Round 233 verification: edge endpoint ring (R111/R182) gains a
2
+ * stroke-width thicken (1.6 → 2.4) on edge hover, completing the
3
+ * hover-elevation gesture across the whole edge surface.
4
+ *
5
+ * Test scope:
6
+ * - Rest state (no edge hovered): every endpoint ring has
7
+ * stroke-width=1.6, data-edge-endpoint-active='false', opacity=0
8
+ * - Transition wiring: style.transition includes 'stroke-width
9
+ * 180ms', so when isEndpoint flips the value eases (not snaps)
10
+ * - Hover state (dispatched mouseenter on edge hitbox): the two
11
+ * endpoint nodes' rings flip to stroke-width=2.4, data-edge-
12
+ * endpoint-active='true', and opacity > 0
13
+ *
14
+ * Scenario: 4 working agents + 1 flow alpha→beta count=5 so a
15
+ * single edge is present with two well-defined endpoints.
16
+ */
17
+ import { chromium } from 'playwright';
18
+ import { readFileSync } from 'node:fs';
19
+
20
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
21
+ const browser = await chromium.launch({ headless: true });
22
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
23
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
24
+ await ctx.addInitScript(() => {
25
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
26
+ });
27
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
28
+ await ctx.route('**/api/hub/status*', async (route) => {
29
+ const r = await route.fetch();
30
+ const b = await r.json();
31
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
32
+ const mk = (alias) => ({
33
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
34
+ 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
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
39
+ ] } });
40
+ });
41
+ const now = Date.now();
42
+ const msgs = [];
43
+ for (let i = 0; i < 5; i++) {
44
+ msgs.push({
45
+ id: `m${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
46
+ network_id: 'default', created_at: new Date(now - (1000 + i * 50)).toISOString(),
47
+ });
48
+ }
49
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: msgs } }));
50
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.fulfill({ json: { tasks: [] } }));
51
+
52
+ const page = await ctx.newPage();
53
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
54
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
55
+ await page.waitForSelector('[data-edge-endpoint-ring]', { timeout: 10000, state: 'attached' });
56
+ await page.waitForTimeout(300);
57
+
58
+ const probeAll = (page) => page.evaluate(() => {
59
+ const rings = Array.from(document.querySelectorAll('[data-edge-endpoint-ring]'));
60
+ return rings.map((r) => ({
61
+ active: r.getAttribute('data-edge-endpoint-active'),
62
+ strokeAttr: r.getAttribute('stroke-width'),
63
+ strokeData: r.getAttribute('data-edge-endpoint-ring-stroke-width'),
64
+ transition: r.style.transition,
65
+ opacity: parseFloat(r.getAttribute('opacity') || ''),
66
+ }));
67
+ });
68
+
69
+ // Rest state ---
70
+ const rest = await probeAll(page);
71
+
72
+ // Hover state — recent-signal panel row also flips hoveredEdgeKey
73
+ // (see "Hover a row → set hoveredEdgeKey" comment in TopoGraph
74
+ // around line 5197). The row is a robust hover target — discrete
75
+ // rect bbox, plain HTML coords through the SVG. Hovering the row
76
+ // for the alpha→beta flow drives hoveredEdgeEndpoints = {alpha,
77
+ // beta}, which is exactly the test condition we need.
78
+ const recentRow = await page.locator('[data-recent-row]').first();
79
+ await recentRow.hover({ force: true });
80
+ await page.waitForTimeout(300);
81
+ const hover = await probeAll(page);
82
+
83
+ await browser.close();
84
+
85
+ const restAllInactive = rest.every(r => r.active === 'false');
86
+ const restStrokes = rest.map(r => r.strokeAttr);
87
+ const restAll16 = restStrokes.every(s => s === '1.6');
88
+ const restTransition = rest.every(r => /stroke-width\s+180ms/.test(r.transition || ''));
89
+
90
+ const hoverActiveCount = hover.filter(r => r.active === 'true').length;
91
+ const hoverActiveRings = hover.filter(r => r.active === 'true');
92
+ const hoverActiveStrokes = hoverActiveRings.map(r => r.strokeAttr);
93
+ const hoverActiveAll24 = hoverActiveStrokes.length > 0 && hoverActiveStrokes.every(s => s === '2.4');
94
+ const hoverActiveDataStr = hoverActiveRings.every(r => r.strokeData === '2.4');
95
+ const hoverActiveOpacityHigh = hoverActiveRings.every(r => r.opacity > 0.5);
96
+ const hoverInactiveStillStable = hover.filter(r => r.active === 'false').every(r => r.strokeAttr === '1.6');
97
+
98
+ const results = {
99
+ rest_all_inactive: restAllInactive,
100
+ rest_all_stroke_16: restAll16,
101
+ rest_all_has_transition: restTransition,
102
+ hover_two_endpoints: hoverActiveCount === 2,
103
+ hover_active_stroke_24: hoverActiveAll24,
104
+ hover_data_attr_24: hoverActiveDataStr,
105
+ hover_active_opacity_hi: hoverActiveOpacityHigh,
106
+ hover_inactive_still_16: hoverInactiveStillStable,
107
+ };
108
+ const ok = Object.values(results).every(Boolean);
109
+ console.log(`${ok ? '✅' : '❌'} endpoint ring thicken:`, JSON.stringify(results),
110
+ '\n rest:', rest.map(r => ({ a: r.active, s: r.strokeAttr })),
111
+ '\n hover:', hover.map(r => ({ a: r.active, s: r.strokeAttr, o: r.opacity })));
112
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,98 @@
1
+ /* Round 62 verification: pressing Esc clears pinnedStatus when no chat
2
+ * is open. Composes cleanly with the R136 ChatPopover Esc handler
3
+ * (which only mounts while chatAlias is set).
4
+ *
5
+ * - Pin via legend row (R61) → assert filter active.
6
+ * - Press Esc → assert pin released, all nodes restore.
7
+ * - aria-pressed flips back to false.
8
+ */
9
+ import { chromium } from 'playwright';
10
+ import { readFileSync } from 'node:fs';
11
+
12
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
13
+ const browser = await chromium.launch({ headless: true });
14
+
15
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
16
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
17
+ await ctx.addInitScript(() => {
18
+ try {
19
+ localStorage.setItem('anet-theme', 'cyber');
20
+ localStorage.setItem('anet-topo-layout', 'ring');
21
+ sessionStorage.setItem('anet_v3_auth', '1');
22
+ } catch {}
23
+ });
24
+
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
+ const sessions = [
31
+ { alias: 'wkr', status: 'working', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
32
+ { alias: 'idl1', status: 'idle', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
33
+ { alias: 'idl2', status: 'idle', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
34
+ ];
35
+ await route.fulfill({ response: r, json: { ...b, sessions } });
36
+ });
37
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
38
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
39
+ const page = await ctx.newPage();
40
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
41
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 3, { timeout: 30000 });
42
+ await page.waitForTimeout(500);
43
+
44
+ const opacities = () => page.evaluate(() => {
45
+ const o = {};
46
+ for (const a of ['wkr', 'idl1', 'idl2']) {
47
+ const g = document.querySelector(`g[data-node="${a}"]`);
48
+ o[a] = g ? +(g.style.opacity || '1') : null;
49
+ }
50
+ return o;
51
+ });
52
+ const ariaPressed = (status) => page.evaluate(s => {
53
+ const g = document.querySelector(`g[data-legend-status="${s}"]`);
54
+ return g?.getAttribute('aria-pressed');
55
+ }, status);
56
+
57
+ const before = await opacities();
58
+
59
+ // Pin working via legend row click.
60
+ await page.locator('g[data-legend-status="working"]').first().click({ force: true });
61
+ // Move mouse away so we observe pin-only (not hover-driven) filter.
62
+ await page.mouse.move(10, 10);
63
+ await page.waitForTimeout(250);
64
+ const afterPin = await opacities();
65
+ const ariaAfterPin = await ariaPressed('working');
66
+
67
+ // Press Esc. Body should have focus so the document-level keydown
68
+ // handler fires.
69
+ await page.evaluate(() => document.activeElement && typeof document.activeElement.blur === 'function' && document.activeElement.blur());
70
+ await page.keyboard.press('Escape');
71
+ await page.waitForTimeout(250);
72
+ const afterEsc = await opacities();
73
+ const ariaAfterEsc = await ariaPressed('working');
74
+
75
+ // Press Esc again — should be a no-op (no pin, no chat).
76
+ await page.keyboard.press('Escape');
77
+ await page.waitForTimeout(150);
78
+ const afterEsc2 = await opacities();
79
+
80
+ await browser.close();
81
+
82
+ const bright = (v) => v != null && v >= 0.55;
83
+ const dim = (v) => v != null && v < 0.4;
84
+ const results = {
85
+ before_baseline: bright(before.wkr) && bright(before.idl1) && bright(before.idl2),
86
+ pin_keepsWorking: bright(afterPin.wkr),
87
+ pin_dimsIdle: dim(afterPin.idl1) && dim(afterPin.idl2),
88
+ pin_ariaTrue: ariaAfterPin === 'true',
89
+ esc_releasesPin: bright(afterEsc.wkr) && bright(afterEsc.idl1) && bright(afterEsc.idl2),
90
+ esc_ariaBackToFalse: ariaAfterEsc === 'false',
91
+ escAgain_isNoOp: bright(afterEsc2.wkr) && bright(afterEsc2.idl1),
92
+ };
93
+ const ok = Object.values(results).every(Boolean);
94
+ console.log(`${ok ? '✅' : '❌'} esc clears pin:`, JSON.stringify(results),
95
+ `\n before=`, before,
96
+ `\n afterPin=`, afterPin, ` aria=${ariaAfterPin}`,
97
+ `\n afterEsc=`, afterEsc, ` aria=${ariaAfterEsc}`);
98
+ process.exit(ok ? 0 : 1);