@sleep2agi/agent-network-dashboard 0.4.6-preview.33 → 0.4.6-preview.331

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 (496) 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/03o.h6kvmw4l_.js +1 -0
  168. package/.next/static/chunks/03~~oirxz7~vc.js +1 -0
  169. package/.next/static/chunks/05uk96gc~9mni.js +1 -0
  170. package/.next/static/chunks/07lsi~s-c0wvh.css +2 -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/0mpa-fgyi2k78.js +1 -0
  175. package/.next/static/chunks/0nlhul7k4hwil.js +1 -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/174re5ar~kxne.js +1 -0
  179. package/.next/static/chunks/1851sbdemt00t.js +4 -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 +111 -1
  185. package/app/components/TopoGraph.tsx +6197 -262
  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/app/lib/vendorIdentity.ts +14 -3
  190. package/package.json +2 -2
  191. package/public/sleep2agi-logo.svg +11 -0
  192. package/public/vendors/claude.svg +11 -0
  193. package/public/vendors/minimax.svg +11 -0
  194. package/public/vendors/openai.svg +12 -0
  195. package/screenshots/r293-p0/after-full.png +0 -0
  196. package/screenshots/r293-p0/after-topo.png +0 -0
  197. package/screenshots/r293-p0/baseline-full.png +0 -0
  198. package/screenshots/r293-p0/baseline-topo.png +0 -0
  199. package/screenshots/r293-p0/logo-integrated-full.png +0 -0
  200. package/screenshots/r293-p0/logo-integrated-topo.png +0 -0
  201. package/screenshots/r293-p0/v0.10.0-hero3-rfc.md +230 -0
  202. package/scripts/p0-topo-screenshot.mjs +82 -0
  203. package/scripts/topo-active-filter-pill-test.mjs +101 -0
  204. package/scripts/topo-active-links-chip-hover-test.mjs +103 -0
  205. package/scripts/topo-active-links-chip-nav-test.mjs +117 -0
  206. package/scripts/topo-active-links-chip-self-hover-test.mjs +171 -0
  207. package/scripts/topo-active-links-chip-test.mjs +73 -0
  208. package/scripts/topo-active-links-empty-recede-test.mjs +125 -0
  209. package/scripts/topo-active-links-freshness-test.mjs +113 -0
  210. package/scripts/topo-active-links-tooltip-test.mjs +79 -0
  211. package/scripts/topo-active-pulse-ease-test.mjs +120 -0
  212. package/scripts/topo-auto-fit-test.mjs +73 -0
  213. package/scripts/topo-brand-logo-transition-test.mjs +94 -0
  214. package/scripts/topo-brand-watermark-test.mjs +97 -0
  215. package/scripts/topo-canvas-aria-test.mjs +108 -0
  216. package/scripts/topo-chat-target-breath-test.mjs +84 -0
  217. package/scripts/topo-chat-target-ring-ease-test.mjs +82 -0
  218. package/scripts/topo-chat-target-ring-fade-test.mjs +124 -0
  219. package/scripts/topo-chip-empty-recede-test.mjs +116 -0
  220. package/scripts/topo-chip-focus-test.mjs +95 -0
  221. package/scripts/topo-chip-pin-mirror-test.mjs +113 -0
  222. package/scripts/topo-chip-row-semantic-gap-test.mjs +97 -0
  223. package/scripts/topo-chip-row-tabular-test.mjs +111 -0
  224. package/scripts/topo-chiprow-data-weight-test.mjs +94 -0
  225. package/scripts/topo-chrome-a11y-test.mjs +107 -0
  226. package/scripts/topo-chrome-focus-ring-unify-test.mjs +100 -0
  227. package/scripts/topo-chrome-icon-stroke-test.mjs +95 -0
  228. package/scripts/topo-chrome-inactive-color-test.mjs +90 -0
  229. package/scripts/topo-chrome-pop-extension-test.mjs +98 -0
  230. package/scripts/topo-chrome-pop-nodesize-test.mjs +88 -0
  231. package/scripts/topo-chrome-press-state-test.mjs +135 -0
  232. package/scripts/topo-chrome-strip-semantic-gap-test.mjs +99 -0
  233. package/scripts/topo-chrome-strip-theme-ease-test.mjs +99 -0
  234. package/scripts/topo-chrome-zoom-level-pop-test.mjs +122 -0
  235. package/scripts/topo-chrome-zoom-pop-test.mjs +130 -0
  236. package/scripts/topo-chrome-zoom-readout-weight-test.mjs +81 -0
  237. package/scripts/topo-click-ripple-ease-test.mjs +94 -0
  238. package/scripts/topo-cmdk-edge-clear-test.mjs +109 -0
  239. package/scripts/topo-cmdk-layout-view-test.mjs +108 -0
  240. package/scripts/topo-cmdk-pin-test.mjs +123 -0
  241. package/scripts/topo-cmdk-vendor-clear-test.mjs +133 -0
  242. package/scripts/topo-cmdk-vendor-pin-test.mjs +103 -0
  243. package/scripts/topo-corner-radius-unify-test.mjs +103 -0
  244. package/scripts/topo-cwd-tooltip-test.mjs +64 -0
  245. package/scripts/topo-dblclick-reset-test.mjs +74 -0
  246. package/scripts/topo-dense-alias-fill-transition-test.mjs +133 -0
  247. package/scripts/topo-discrete-zoom-smooth-test.mjs +125 -0
  248. package/scripts/topo-edge-badge-click-pin-test.mjs +94 -0
  249. package/scripts/topo-edge-badge-crossfade-test.mjs +138 -0
  250. package/scripts/topo-edge-badge-hot-test.mjs +115 -0
  251. package/scripts/topo-edge-badge-hover-test.mjs +85 -0
  252. package/scripts/topo-edge-badge-lift-test.mjs +150 -0
  253. package/scripts/topo-edge-badge-ripple-test.mjs +111 -0
  254. package/scripts/topo-edge-badge-stroke-transition-test.mjs +118 -0
  255. package/scripts/topo-edge-badge-text-pin-test.mjs +121 -0
  256. package/scripts/topo-edge-badge-theme-ease-test.mjs +89 -0
  257. package/scripts/topo-edge-count-badge-test.mjs +86 -0
  258. package/scripts/topo-edge-endpoint-highlight-test.mjs +111 -0
  259. package/scripts/topo-edge-fadein-test.mjs +106 -0
  260. package/scripts/topo-edge-focus-test.mjs +113 -0
  261. package/scripts/topo-edge-hover-test.mjs +93 -0
  262. package/scripts/topo-edge-pill-hot-test.mjs +129 -0
  263. package/scripts/topo-edge-pill-test.mjs +105 -0
  264. package/scripts/topo-edge-ping-pulse-retired-test.mjs +119 -0
  265. package/scripts/topo-edge-stroke-ease-test.mjs +100 -0
  266. package/scripts/topo-edge-stroke-transition-test.mjs +132 -0
  267. package/scripts/topo-edge-tooltip-test.mjs +70 -0
  268. package/scripts/topo-empty-hint-tracking-test.mjs +84 -0
  269. package/scripts/topo-empty-state-breath-test.mjs +150 -0
  270. package/scripts/topo-empty-state-crossfade-test.mjs +130 -0
  271. package/scripts/topo-empty-state-recenter-test.mjs +107 -0
  272. package/scripts/topo-empty-subhint-tracking-test.mjs +87 -0
  273. package/scripts/topo-endpoint-ring-fade-test.mjs +132 -0
  274. package/scripts/topo-endpoint-ring-test.mjs +93 -0
  275. package/scripts/topo-endpoint-ring-thicken-test.mjs +112 -0
  276. package/scripts/topo-esc-clears-pin-test.mjs +98 -0
  277. package/scripts/topo-filter-pill-aliases-test.mjs +121 -0
  278. package/scripts/topo-filter-pill-body-click-test.mjs +112 -0
  279. package/scripts/topo-filter-pill-count-test.mjs +110 -0
  280. package/scripts/topo-filter-pill-mobile-test.mjs +90 -0
  281. package/scripts/topo-fit-key-test.mjs +106 -0
  282. package/scripts/topo-freshness-chip-test.mjs +74 -0
  283. package/scripts/topo-freshness-chip-transition-test.mjs +80 -0
  284. package/scripts/topo-freshness-chip-weight-test.mjs +90 -0
  285. package/scripts/topo-freshness-conditional-render-test.mjs +92 -0
  286. package/scripts/topo-fullscreen-active-test.mjs +115 -0
  287. package/scripts/topo-group-box-pinned-state-test.mjs +95 -0
  288. package/scripts/topo-group-count-test.mjs +6 -1
  289. package/scripts/topo-group-label-a11y-test.mjs +113 -0
  290. package/scripts/topo-group-label-count-inherit-test.mjs +99 -0
  291. package/scripts/topo-group-label-hover-test.mjs +119 -0
  292. package/scripts/topo-group-label-pin-spacing-test.mjs +136 -0
  293. package/scripts/topo-group-label-tint-test.mjs +99 -0
  294. package/scripts/topo-group-label-title-test.mjs +83 -0
  295. package/scripts/topo-group-pin-test.mjs +119 -0
  296. package/scripts/topo-group-pip-fadein-test.mjs +100 -0
  297. package/scripts/topo-group-pip-tabular-test.mjs +95 -0
  298. package/scripts/topo-group-status-pips-test.mjs +103 -0
  299. package/scripts/topo-groupbox-fadein-test.mjs +95 -0
  300. package/scripts/topo-groupbox-hover-lift-test.mjs +127 -0
  301. package/scripts/topo-groupbox-hubcore-ease-test.mjs +106 -0
  302. package/scripts/topo-groupbox-live-test.mjs +100 -0
  303. package/scripts/topo-groupbox-march-busyness-test.mjs +155 -0
  304. package/scripts/topo-groupbox-test.mjs +4 -2
  305. package/scripts/topo-help-overlay-r109-test.mjs +67 -0
  306. package/scripts/topo-help-overlay-r118-test.mjs +69 -0
  307. package/scripts/topo-help-overlay-r123-test.mjs +69 -0
  308. package/scripts/topo-help-overlay-r134-test.mjs +74 -0
  309. package/scripts/topo-help-overlay-r137-test.mjs +81 -0
  310. package/scripts/topo-help-overlay-r141-test.mjs +86 -0
  311. package/scripts/topo-help-overlay-r153-test.mjs +85 -0
  312. package/scripts/topo-help-overlay-r59-test.mjs +67 -0
  313. package/scripts/topo-help-overlay-r65-test.mjs +64 -0
  314. package/scripts/topo-help-overlay-r70-test.mjs +63 -0
  315. package/scripts/topo-help-overlay-r78-test.mjs +59 -0
  316. package/scripts/topo-help-overlay-r81-test.mjs +58 -0
  317. package/scripts/topo-help-overlay-r87-test.mjs +64 -0
  318. package/scripts/topo-help-overlay-r91-test.mjs +65 -0
  319. package/scripts/topo-hot-tail-crossfade-test.mjs +117 -0
  320. package/scripts/topo-html-overlay-padding-unify-test.mjs +120 -0
  321. package/scripts/topo-hub-a11y-test.mjs +112 -0
  322. package/scripts/topo-hub-busyness-test.mjs +100 -0
  323. package/scripts/topo-hub-center-crossfade-test.mjs +124 -0
  324. package/scripts/topo-hub-click-fit-test.mjs +137 -0
  325. package/scripts/topo-hub-digit-scale-test.mjs +129 -0
  326. package/scripts/topo-hub-fadein-test.mjs +88 -0
  327. package/scripts/topo-hub-hover-lift-test.mjs +101 -0
  328. package/scripts/topo-hub-hover-ring-test.mjs +82 -0
  329. package/scripts/topo-hub-pip-theme-ease-test.mjs +116 -0
  330. package/scripts/topo-hub-spoke-ease-test.mjs +120 -0
  331. package/scripts/topo-hub-title-test.mjs +93 -0
  332. package/scripts/topo-hub-working-count-test.mjs +99 -0
  333. package/scripts/topo-ingroup-edge-test.mjs +103 -0
  334. package/scripts/topo-inner-radar-ring-retired-test.mjs +89 -0
  335. package/scripts/topo-kicker-color-test.mjs +93 -0
  336. package/scripts/topo-kicker-tracking-test.mjs +101 -0
  337. package/scripts/topo-kicker-weight-test.mjs +98 -0
  338. package/scripts/topo-label-card-theme-ease-test.mjs +85 -0
  339. package/scripts/topo-label-lift-test.mjs +119 -0
  340. package/scripts/topo-lastseen-tooltip-test.mjs +59 -0
  341. package/scripts/topo-layout-hover-text-preview-test.mjs +101 -0
  342. package/scripts/topo-layout-key-test.mjs +91 -0
  343. package/scripts/topo-layout-switch-crossfade-test.mjs +126 -0
  344. package/scripts/topo-layout-toggle-border-unify-test.mjs +104 -0
  345. package/scripts/topo-layout-toggle-test.mjs +131 -0
  346. package/scripts/topo-legend-compress-test.mjs +107 -0
  347. package/scripts/topo-legend-count-empty-test.mjs +126 -0
  348. package/scripts/topo-legend-count-tabular-parity-test.mjs +90 -0
  349. package/scripts/topo-legend-count-tabular-test.mjs +87 -0
  350. package/scripts/topo-legend-count-test.mjs +88 -0
  351. package/scripts/topo-legend-count-tier-fill-test.mjs +121 -0
  352. package/scripts/topo-legend-count-weight-test.mjs +86 -0
  353. package/scripts/topo-legend-header-symmetric-inset-test.mjs +112 -0
  354. package/scripts/topo-legend-hitbox-swatch-center-test.mjs +93 -0
  355. package/scripts/topo-legend-hover-test.mjs +113 -0
  356. package/scripts/topo-legend-offline-delimiter-test.mjs +84 -0
  357. package/scripts/topo-legend-offline-label-simplify-test.mjs +86 -0
  358. package/scripts/topo-legend-panel-count-weight-test.mjs +89 -0
  359. package/scripts/topo-legend-panel-header-test.mjs +83 -0
  360. package/scripts/topo-legend-pin-ring-fade-test.mjs +112 -0
  361. package/scripts/topo-legend-pin-test.mjs +128 -0
  362. package/scripts/topo-legend-row-hover-lift-test.mjs +109 -0
  363. package/scripts/topo-legend-row-labels-simplify-test.mjs +80 -0
  364. package/scripts/topo-legend-row-pin-spacing-test.mjs +124 -0
  365. package/scripts/topo-legend-row-tint-test.mjs +95 -0
  366. package/scripts/topo-legend-row-title-test.mjs +102 -0
  367. package/scripts/topo-legend-swatch-grow-test.mjs +116 -0
  368. package/scripts/topo-legend-swatch-radius-test.mjs +78 -0
  369. package/scripts/topo-minimap-a11y-test.mjs +101 -0
  370. package/scripts/topo-minimap-dot-transition-test.mjs +113 -0
  371. package/scripts/topo-minimap-rect-stroke-test.mjs +94 -0
  372. package/scripts/topo-minimap-test.mjs +68 -0
  373. package/scripts/topo-minimap-viewport-smooth-test.mjs +124 -0
  374. package/scripts/topo-mobile-chips-test.mjs +103 -0
  375. package/scripts/topo-monogram-font-test.mjs +112 -0
  376. package/scripts/topo-monogram-stroke-test.mjs +101 -0
  377. package/scripts/topo-node-a11y-test.mjs +107 -0
  378. package/scripts/topo-node-alias-chat-pin-test.mjs +95 -0
  379. package/scripts/topo-node-hover-lift-test.mjs +85 -0
  380. package/scripts/topo-node-label-card-elevation-test.mjs +157 -0
  381. package/scripts/topo-node-label-card-stroke-test.mjs +135 -0
  382. package/scripts/topo-node-stroke-transition-test.mjs +103 -0
  383. package/scripts/topo-node-text-fill-transition-test.mjs +119 -0
  384. package/scripts/topo-node-title-peers-test.mjs +107 -0
  385. package/scripts/topo-node-title-test.mjs +101 -0
  386. package/scripts/topo-nodesize-active-hover-test.mjs +109 -0
  387. package/scripts/topo-nodesize-crossfade-test.mjs +141 -0
  388. package/scripts/topo-online-chip-nav-test.mjs +143 -0
  389. package/scripts/topo-orbit-particles-retired-test.mjs +91 -0
  390. package/scripts/topo-overlap-test.mjs +32 -5
  391. package/scripts/topo-panel-elevation-test.mjs +76 -0
  392. package/scripts/topo-panel-fadein-test.mjs +102 -0
  393. package/scripts/topo-panel-hot-tail-fade-test.mjs +132 -0
  394. package/scripts/topo-panel-hover-elevation-test.mjs +118 -0
  395. package/scripts/topo-panel-theme-ease-test.mjs +85 -0
  396. package/scripts/topo-panel-title-fill-ease-test.mjs +91 -0
  397. package/scripts/topo-panel-title-tracking-test.mjs +78 -0
  398. package/scripts/topo-particle-stagger-test.mjs +82 -0
  399. package/scripts/topo-pin-intersection-color-ease-test.mjs +111 -0
  400. package/scripts/topo-pin-intersection-empty-test.mjs +129 -0
  401. package/scripts/topo-pin-intersection-tabular-test.mjs +96 -0
  402. package/scripts/topo-pin-intersection-test.mjs +121 -0
  403. package/scripts/topo-pin-intersection-warning-crossfade-test.mjs +107 -0
  404. package/scripts/topo-pin-mirror-transition-test.mjs +101 -0
  405. package/scripts/topo-pin-persistence-test.mjs +105 -0
  406. package/scripts/topo-popover-meta-test.mjs +78 -0
  407. package/scripts/topo-prepublish-guard-test.mjs +95 -0
  408. package/scripts/topo-pressure-bar-test.mjs +69 -0
  409. package/scripts/topo-pressure-hover-test.mjs +123 -0
  410. package/scripts/topo-pressure-pin-test.mjs +119 -0
  411. package/scripts/topo-pressure-seg-self-hover-test.mjs +126 -0
  412. package/scripts/topo-pressure-seg-tooltip-test.mjs +98 -0
  413. package/scripts/topo-pressure-seg-transition-test.mjs +107 -0
  414. package/scripts/topo-pulse-dot-crossfade-test.mjs +122 -0
  415. package/scripts/topo-pulse-dot-retired-test.mjs +86 -0
  416. package/scripts/topo-recent-panel-count-freshness-test.mjs +109 -0
  417. package/scripts/topo-recent-panel-count-test.mjs +72 -0
  418. package/scripts/topo-recent-panel-count-weight-test.mjs +91 -0
  419. package/scripts/topo-recent-panel-footer-breath-test.mjs +142 -0
  420. package/scripts/topo-recent-panel-hot-header-test.mjs +132 -0
  421. package/scripts/topo-recent-panel-more-crossfade-test.mjs +124 -0
  422. package/scripts/topo-recent-panel-more-cyan-hover-test.mjs +151 -0
  423. package/scripts/topo-recent-panel-more-nav-test.mjs +127 -0
  424. package/scripts/topo-recent-panel-more-test.mjs +106 -0
  425. package/scripts/topo-recent-panel-readability-bump-test.mjs +151 -0
  426. package/scripts/topo-recent-row-count-fill-test.mjs +129 -0
  427. package/scripts/topo-recent-row-fadein-test.mjs +107 -0
  428. package/scripts/topo-recent-row-freshness-test.mjs +102 -0
  429. package/scripts/topo-recent-row-hot-test.mjs +110 -0
  430. package/scripts/topo-recent-row-hover-lift-test.mjs +128 -0
  431. package/scripts/topo-recent-row-hover-test.mjs +122 -0
  432. package/scripts/topo-recent-row-pin-spacing-test.mjs +137 -0
  433. package/scripts/topo-recent-row-pin-test.mjs +126 -0
  434. package/scripts/topo-recent-row-tint-test.mjs +91 -0
  435. package/scripts/topo-recent-row-title-test.mjs +112 -0
  436. package/scripts/topo-recent-row-ts-alpha-test.mjs +100 -0
  437. package/scripts/topo-recent-row-ts-test.mjs +95 -0
  438. package/scripts/topo-recent-row-typography-test.mjs +119 -0
  439. package/scripts/topo-recent-signal-empty-hint-test.mjs +73 -0
  440. package/scripts/topo-recent-signal-empty-test.mjs +63 -0
  441. package/scripts/topo-reduced-motion-test.mjs +77 -0
  442. package/scripts/topo-reset-spin-test.mjs +120 -0
  443. package/scripts/topo-runtime-badge-grow-test.mjs +114 -0
  444. package/scripts/topo-spokes-retired-test.mjs +93 -0
  445. package/scripts/topo-starfield-density-test.mjs +89 -0
  446. package/scripts/topo-status-chip-hover-test.mjs +101 -0
  447. package/scripts/topo-status-chip-tooltip-test.mjs +95 -0
  448. package/scripts/topo-statusbar-chips-self-hover-test.mjs +177 -0
  449. package/scripts/topo-svg-focus-test.mjs +112 -0
  450. package/scripts/topo-sweep-busyness-test.mjs +94 -0
  451. package/scripts/topo-tabular-nums-sweep-test.mjs +93 -0
  452. package/scripts/topo-tier-guide-dashes-test.mjs +86 -0
  453. package/scripts/topo-tier-ring-fadein-test.mjs +107 -0
  454. package/scripts/topo-tier-ring-occupancy-test.mjs +87 -0
  455. package/scripts/topo-tier-ring-test.mjs +69 -0
  456. package/scripts/topo-tier-ring-tint-test.mjs +110 -0
  457. package/scripts/topo-tier-stagger-test.mjs +89 -0
  458. package/scripts/topo-title-block-tight-leading-test.mjs +95 -0
  459. package/scripts/topo-title-tracking-test.mjs +98 -0
  460. package/scripts/topo-titleblock-gap-test.mjs +86 -0
  461. package/scripts/topo-titleblock-margin-test.mjs +83 -0
  462. package/scripts/topo-toggle-inactive-cyan-hover-test.mjs +110 -0
  463. package/scripts/topo-top-rail-theme-ease-test.mjs +89 -0
  464. package/scripts/topo-tz-ghost-test.mjs +69 -0
  465. package/scripts/topo-vendor-chip-hover-test.mjs +111 -0
  466. package/scripts/topo-vendor-chip-self-hover-test.mjs +131 -0
  467. package/scripts/topo-vendor-chip-tabular-test.mjs +92 -0
  468. package/scripts/topo-vendor-chip-threshold-test.mjs +78 -0
  469. package/scripts/topo-vendor-chip-weight-test.mjs +87 -0
  470. package/scripts/topo-vendor-letter-tooltip-test.mjs +92 -0
  471. package/scripts/topo-vendor-pill-test.mjs +125 -0
  472. package/scripts/topo-vendor-pin-test.mjs +125 -0
  473. package/scripts/topo-view-smooth-test.mjs +131 -0
  474. package/scripts/topo-watermark-tracking-test.mjs +99 -0
  475. package/scripts/topo-working-chip-pin-test.mjs +154 -0
  476. package/scripts/topo-working-halo-breath-retired-test.mjs +99 -0
  477. package/scripts/topo-wrapper-flow-arrow-theme-test.mjs +97 -0
  478. package/scripts/topo-wrapper-shadow-theme-ease-test.mjs +89 -0
  479. package/.next/static/chunks/0-7iowzu4.798.js +0 -1
  480. package/.next/static/chunks/02t5jr4shh.hy.js +0 -1
  481. package/.next/static/chunks/09nd.t_hd.y6t.css +0 -2
  482. package/.next/static/chunks/0_8snedeata9k.js +0 -1
  483. package/.next/static/chunks/0_qfg5s5uzs7..js +0 -7
  484. package/.next/static/chunks/0elalb9n_.~di.js +0 -1
  485. package/.next/static/chunks/0j_q__o8p6g83.js +0 -1
  486. package/.next/static/chunks/0jl6_w50zh4l5.js +0 -1
  487. package/.next/static/chunks/0kwf..oq7.27x.js +0 -7
  488. package/.next/static/chunks/0r4q73.-1bkyl.js +0 -1
  489. package/.next/static/chunks/0r_c8eccg3z2o.js +0 -1
  490. package/.next/static/chunks/0sjqjenh7~k-j.js +0 -1
  491. package/.next/static/chunks/0y8v4grez.a76.js +0 -1
  492. package/.next/static/chunks/10oi7l-tycgl9.js +0 -1
  493. package/.next/static/chunks/11uk1izuv3~0t.js +0 -1
  494. /package/.next/static/{Q3_3F7glo-M3CFrXA8POE → 3PMYuA9TBRow9F7QeNZc7}/_buildManifest.js +0 -0
  495. /package/.next/static/{Q3_3F7glo-M3CFrXA8POE → 3PMYuA9TBRow9F7QeNZc7}/_clientMiddlewareManifest.js +0 -0
  496. /package/.next/static/{Q3_3F7glo-M3CFrXA8POE → 3PMYuA9TBRow9F7QeNZc7}/_ssgManifest.js +0 -0
@@ -0,0 +1,93 @@
1
+ /* Round 271 verification: legend row hitbox centers exactly on the
2
+ * swatch dot's cy.
3
+ *
4
+ * Pre-R271:
5
+ * Hitbox y = row.y0 - 12, height = 22 → center at row.y0 - 1
6
+ * Swatch cy = row.y0
7
+ * ⇒ Swatch sits 1 px above hitbox center (asymmetric tint band)
8
+ *
9
+ * Post-R271:
10
+ * Hitbox y = row.y0 - 11, height = 22 → center at row.y0
11
+ * Swatch cy = row.y0
12
+ * ⇒ Swatch sits exactly at hitbox center (symmetric tint band)
13
+ *
14
+ * Test scope:
15
+ * 1. Hover the working row → tint rect appears (data-legend-row-tinted='hover').
16
+ * 2. Tinted rect's getBBox.y === row.y0 - 11 (for working row.y0=32 → y=21).
17
+ * 3. Tinted rect's getBBox.height === 22 (unchanged).
18
+ * 4. Tinted rect's vertical center === swatch cy.
19
+ * 5. R270 fullscreen inactive cyan hover regression intact.
20
+ */
21
+ import { chromium } from 'playwright';
22
+ import { readFileSync } from 'node:fs';
23
+
24
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
25
+ const browser = await chromium.launch({ headless: true });
26
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
27
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
28
+ await ctx.addInitScript(() => {
29
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
30
+ });
31
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
32
+ await ctx.route('**/api/hub/status*', async (route) => {
33
+ const r = await route.fetch();
34
+ const b = await r.json();
35
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
36
+ const mk = (alias) => ({
37
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
38
+ network_id: nid, project_dir: null,
39
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
40
+ });
41
+ await route.fulfill({ response: r, json: { ...b, sessions: [
42
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
43
+ ] } });
44
+ });
45
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
46
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
47
+
48
+ const page = await ctx.newPage();
49
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
50
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
51
+ await page.waitForSelector('[data-legend-status="working"]', { timeout: 10000 });
52
+ await page.waitForSelector('[data-legend-swatch="working"]', { timeout: 10000 });
53
+ await page.waitForSelector('[data-topo-chrome-fullscreen]', { timeout: 10000 });
54
+ await page.waitForTimeout(300);
55
+
56
+ // Hover the working row to force the tint rect to render
57
+ await page.locator('[data-legend-status="working"]').hover();
58
+ await page.waitForTimeout(250);
59
+
60
+ const probe = await page.evaluate(() => {
61
+ const row = document.querySelector('[data-legend-status="working"]');
62
+ if (!row) return null;
63
+ // The hitbox rect is the FIRST <rect> inside the row group
64
+ const rect = row.querySelector('rect');
65
+ const swatch = row.querySelector('[data-legend-swatch="working"]');
66
+ const fullscreen = document.querySelector('[data-topo-chrome-fullscreen]');
67
+ return {
68
+ rectY: rect ? +rect.getAttribute('y') : null,
69
+ rectHeight: rect ? +rect.getAttribute('height') : null,
70
+ rectTinted: rect ? rect.getAttribute('data-legend-row-tinted') : null,
71
+ swatchCy: swatch ? +swatch.getAttribute('cy') : null,
72
+ fullscreenClasses: fullscreen ? fullscreen.className.toString() : null,
73
+ };
74
+ });
75
+ await browser.close();
76
+
77
+ const rectCenter = (probe.rectY != null && probe.rectHeight != null) ? probe.rectY + probe.rectHeight / 2 : null;
78
+ const centerMatchesSwatch = (rectCenter != null && probe.swatchCy != null) && rectCenter === probe.swatchCy;
79
+
80
+ const results = {
81
+ rect_y_is_21: probe.rectY === 21,
82
+ rect_height_22: probe.rectHeight === 22,
83
+ rect_tinted_on_hover: probe.rectTinted === 'hover',
84
+ swatch_cy_32: probe.swatchCy === 32,
85
+ rect_center_matches_swatch: centerMatchesSwatch,
86
+ r270_fullscreen_inactive_cyan: (probe.fullscreenClasses || '').includes('hover:bg-cyan-500/5'),
87
+ };
88
+ const ok = Object.values(results).every(Boolean);
89
+ console.log(`${ok ? '✅' : '❌'} legend hitbox swatch center:`, JSON.stringify(results),
90
+ '\n rect y/height:', probe.rectY, probe.rectHeight, '(center', rectCenter, ')',
91
+ '\n swatch cy:', probe.swatchCy,
92
+ '\n rect tinted:', probe.rectTinted);
93
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,113 @@
1
+ /* Round 55 verification: hovering a legend status row dims nodes whose
2
+ * status doesn't match.
3
+ *
4
+ * Sessions (mix of statuses):
5
+ * wkr — working (status=working)
6
+ * idl1 — idle online (status=idle)
7
+ * idl2 — idle online (status=idle)
8
+ * off — offline (status=offline, last_seen old)
9
+ *
10
+ * Hover the "working node" row → wkr stays bright (~1.0), the rest dim
11
+ * (~0.28). Hover "online idle" → idl1/idl2 stay, wkr+off dim. Hover
12
+ * "offline / no SSE" → off stays, the three online dim. Release →
13
+ * all back to base.
14
+ */
15
+ import { chromium } from 'playwright';
16
+ import { readFileSync } from 'node:fs';
17
+
18
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
19
+ const browser = await chromium.launch({ headless: true });
20
+
21
+ // Dashboard chrome pushes the SVG ~830 px down at width 1400, so the
22
+ // legend's offline row sits at screen y≈900 — below a 900-px viewport.
23
+ // 1500 px tall gives a comfortable margin for all three rows.
24
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
25
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
26
+ await ctx.addInitScript(() => {
27
+ try {
28
+ localStorage.setItem('anet-theme', 'cyber');
29
+ localStorage.setItem('anet-topo-layout', 'ring');
30
+ sessionStorage.setItem('anet_v3_auth', '1');
31
+ } catch {}
32
+ });
33
+
34
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
35
+ const stale = new Date(Date.now() - 30 * 60 * 1000).toISOString();
36
+ await ctx.route('**/api/hub/status*', async (route) => {
37
+ const r = await route.fetch();
38
+ const b = await r.json();
39
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
40
+ const sessions = [
41
+ { alias: 'wkr', status: 'working', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
42
+ { alias: 'idl1', status: 'idle', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
43
+ { alias: 'idl2', status: 'idle', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
44
+ { alias: 'off', status: 'offline', network_id: nid, project_dir: null, created_at: stale, updated_at: stale, last_seen_at: stale },
45
+ ];
46
+ await route.fulfill({ response: r, json: { ...b, sessions } });
47
+ });
48
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
49
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
50
+ const page = await ctx.newPage();
51
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
52
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
53
+ await page.waitForTimeout(500);
54
+
55
+ const opacities = () => page.evaluate(() => {
56
+ const o = {};
57
+ for (const a of ['wkr', 'idl1', 'idl2', 'off']) {
58
+ const g = document.querySelector(`g[data-node="${a}"]`);
59
+ o[a] = g ? +(g.style.opacity || '1') : null;
60
+ }
61
+ return o;
62
+ });
63
+
64
+ const before = await opacities();
65
+
66
+ async function hoverRow(key) {
67
+ const target = await page.evaluate((k) => {
68
+ const g = document.querySelector(`g[data-legend-status="${k}"]`);
69
+ if (!g) return null;
70
+ const r = g.getBoundingClientRect();
71
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
72
+ }, key);
73
+ if (!target) throw new Error(`legend row ${key} not found`);
74
+ // jiggle from a neutral spot, then to the target
75
+ await page.mouse.move(10, 10);
76
+ await page.mouse.move(target.x, target.y);
77
+ await page.waitForTimeout(300);
78
+ return await opacities();
79
+ }
80
+
81
+ const onWorking = await hoverRow('working');
82
+ const onIdle = await hoverRow('idle');
83
+ const onOffline = await hoverRow('offline');
84
+ await page.mouse.move(10, 10);
85
+ await page.waitForTimeout(300);
86
+ const released = await opacities();
87
+
88
+ await browser.close();
89
+
90
+ const dim = (v) => v != null && v < 0.4;
91
+ const bright = (v) => v != null && v >= 0.55;
92
+ const results = {
93
+ before_baseline: bright(before.wkr) && bright(before.idl1) && bright(before.idl2),
94
+ working_keepsWorking: bright(onWorking.wkr),
95
+ working_dimsIdle: dim(onWorking.idl1) && dim(onWorking.idl2),
96
+ working_dimsOffline: dim(onWorking.off),
97
+ idle_keepsIdle: bright(onIdle.idl1) && bright(onIdle.idl2),
98
+ idle_dimsWorking: dim(onIdle.wkr),
99
+ idle_dimsOffline: dim(onIdle.off),
100
+ // Offline base opacity is 0.6 (R17), so "kept" means >= 0.55 which the
101
+ // bright() helper already covers — anything in [0.55, 1] counts.
102
+ offline_keepsOffline: bright(onOffline.off),
103
+ offline_dimsOnline: dim(onOffline.wkr) && dim(onOffline.idl1) && dim(onOffline.idl2),
104
+ released_restoresAll: bright(released.wkr) && bright(released.idl1) && bright(released.idl2),
105
+ };
106
+ const ok = Object.values(results).every(Boolean);
107
+ console.log(`${ok ? '✅' : '❌'} legend status hover:`, JSON.stringify(results),
108
+ `\n before=`, before,
109
+ `\n onWorking=`, onWorking,
110
+ `\n onIdle=`, onIdle,
111
+ `\n onOffline=`, onOffline,
112
+ `\n released=`, released);
113
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,84 @@
1
+ /* Round 269 verification: legend offline-row label " / " → " · "
2
+ * delimiter unification — closes R138 sweep.
3
+ *
4
+ * Pre-R269 the legend's offline row read "offline / no SSE" (ASCII
5
+ * forward-slash separator). R138 already swept the recent-signal
6
+ * row separators to " · " (typographic middot) along with filter
7
+ * pills, node tooltips, edge badges, and active-links tooltip. The
8
+ * legend offline label was the last hardcoded " / " in TopoGraph.
9
+ *
10
+ * R269 changes the label string from "offline / no SSE" to
11
+ * "offline · no SSE" — same monospace cell width (no layout shift),
12
+ * matches the dashboard-wide typographic delimiter convention.
13
+ *
14
+ * Test scope:
15
+ * 1. Legend offline row label text contains the typographic middot.
16
+ * 2. Label DOES NOT contain " / " (the ASCII separator).
17
+ * 3. Working + idle row labels unchanged (regression: they never
18
+ * used " / ").
19
+ * 4. R257 legend header symmetric inset intact (legend count x === 211).
20
+ */
21
+ import { chromium } from 'playwright';
22
+ import { readFileSync } from 'node:fs';
23
+
24
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
25
+ const browser = await chromium.launch({ headless: true });
26
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
27
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
28
+ await ctx.addInitScript(() => {
29
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
30
+ });
31
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
32
+ await ctx.route('**/api/hub/status*', async (route) => {
33
+ const r = await route.fetch();
34
+ const b = await r.json();
35
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
36
+ const mk = (alias, status = 'working') => ({
37
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
38
+ network_id: nid, project_dir: null,
39
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
40
+ });
41
+ await route.fulfill({ response: r, json: { ...b, sessions: [
42
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
43
+ ] } });
44
+ });
45
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
46
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
47
+
48
+ const page = await ctx.newPage();
49
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
50
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
51
+ await page.waitForSelector('[data-legend-row-label="offline"]', { timeout: 10000 });
52
+ await page.waitForSelector('[data-legend-row-label="working"]', { timeout: 10000 });
53
+ await page.waitForSelector('[data-legend-row-label="idle"]', { timeout: 10000 });
54
+ await page.waitForSelector('[data-legend-panel-count]', { timeout: 10000 });
55
+ await page.waitForTimeout(300);
56
+
57
+ const probe = await page.evaluate(() => {
58
+ const offline = document.querySelector('[data-legend-row-label="offline"]');
59
+ const working = document.querySelector('[data-legend-row-label="working"]');
60
+ const idle = document.querySelector('[data-legend-row-label="idle"]');
61
+ const count = document.querySelector('[data-legend-panel-count]');
62
+ return {
63
+ offlineText: offline ? offline.textContent : null,
64
+ workingText: working ? working.textContent : null,
65
+ idleText: idle ? idle.textContent : null,
66
+ countX: count ? +count.getAttribute('x') : null,
67
+ };
68
+ });
69
+ await browser.close();
70
+
71
+ const results = {
72
+ offline_uses_middot: probe.offlineText === 'offline · no SSE',
73
+ offline_no_ascii_slash: probe.offlineText != null && !probe.offlineText.includes(' / '),
74
+ working_text_unchanged: probe.workingText === 'working node',
75
+ idle_text_unchanged: probe.idleText === 'online idle',
76
+ r257_legend_count_x_211: probe.countX === 211,
77
+ };
78
+ const ok = Object.values(results).every(Boolean);
79
+ console.log(`${ok ? '✅' : '❌'} legend offline delimiter:`, JSON.stringify(results),
80
+ '\n offline:', JSON.stringify(probe.offlineText),
81
+ '\n working:', JSON.stringify(probe.workingText),
82
+ '\n idle: ', JSON.stringify(probe.idleText),
83
+ '\n count x:', probe.countX);
84
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,86 @@
1
+ /* Round 307 verification: legend offline row label drops the
2
+ * '· no SSE' qualifier. 'offline · no SSE' (16 chars) → 'offline'
3
+ * (7 chars). Visual already communicates the disconnected state
4
+ * via status ring dashed pattern + gray fill + gray swatch.
5
+ *
6
+ * Contract:
7
+ * - [data-legend-row-label='offline'] text content === 'offline'.
8
+ * - Does NOT contain 'no SSE' substring.
9
+ * - Working row label still 'working node'.
10
+ * - Idle row label still 'online idle'.
11
+ * - R306 focus-ring-1 + R305 alias chat-target attr + R304/R302/
12
+ * R301/R294 regressions intact.
13
+ */
14
+ import { chromium } from 'playwright';
15
+ import { readFileSync } from 'node:fs';
16
+
17
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
18
+ const browser = await chromium.launch({ headless: true });
19
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
20
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
21
+ await ctx.addInitScript(() => {
22
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
23
+ });
24
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
25
+ await ctx.route('**/api/hub/status*', async (route) => {
26
+ const r = await route.fetch();
27
+ const b = await r.json();
28
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
29
+ const mk = (alias, model, status) => ({
30
+ alias, status, model, runtime: 'claude-code-cli',
31
+ network_id: nid, project_dir: null,
32
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
33
+ });
34
+ await route.fulfill({ response: r, json: { ...b, sessions: [
35
+ mk('alpha', 'claude-opus-4', 'working'),
36
+ mk('beta', 'gpt-4o', 'idle'),
37
+ mk('gamma', 'claude-sonnet-4', 'offline'),
38
+ ] } });
39
+ });
40
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
41
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
42
+
43
+ const page = await ctx.newPage();
44
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
45
+ await page.waitForSelector('[data-legend-row-label="offline"]', { timeout: 15000 });
46
+ await page.waitForTimeout(300);
47
+
48
+ const probe = await page.evaluate(() => {
49
+ const sel = (s) => document.querySelector(s);
50
+ const txt = (el) => (el?.textContent || '').trim();
51
+ const offlineLabel = sel('[data-legend-row-label="offline"]');
52
+ const workingLabel = sel('[data-legend-row-label="working"]');
53
+ const idleLabel = sel('[data-legend-row-label="idle"]');
54
+ const layoutRing = sel('[data-topo-chrome-layout="ring"]');
55
+ const alias = sel('[data-node-alias-text]');
56
+ const subhint = sel('[data-recent-signal-empty-hint]');
57
+ return {
58
+ offlineText: txt(offlineLabel),
59
+ workingText: txt(workingLabel),
60
+ idleText: txt(idleLabel),
61
+ layoutRingCls: layoutRing?.className ?? '',
62
+ aliasChatTargetAttr: alias?.hasAttribute('data-node-alias-chat-target') ?? false,
63
+ subhintLs: subhint?.getAttribute('letter-spacing') ?? null,
64
+ pulseCount: document.querySelectorAll('[data-pulse-wrapper]').length,
65
+ };
66
+ });
67
+ await browser.close();
68
+
69
+ const results = {
70
+ offline_label_simplified: probe.offlineText === 'offline',
71
+ offline_label_no_sse_dropped: !/no SSE/i.test(probe.offlineText),
72
+ // R308 superseded the working/idle assertions — both labels were
73
+ // simplified further (working node → working, online idle → idle).
74
+ // Keep this test focused on R307's offline simplification.
75
+ r306_layout_ring_1_kept: probe.layoutRingCls.includes('focus-visible:ring-1'),
76
+ r306_layout_no_ring_2: !probe.layoutRingCls.includes('focus-visible:ring-2'),
77
+ r305_alias_chat_target_attr: probe.aliasChatTargetAttr,
78
+ r304_subhint_ls_0_15: probe.subhintLs === '0.15',
79
+ r294_pulse_absent: probe.pulseCount === 0,
80
+ };
81
+ const ok = Object.values(results).every(Boolean);
82
+ console.log(`${ok ? '✅' : '❌'} legend offline label simplify:`, JSON.stringify(results),
83
+ '\n offline:', JSON.stringify(probe.offlineText),
84
+ '\n working:', JSON.stringify(probe.workingText),
85
+ '\n idle:', JSON.stringify(probe.idleText));
86
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,89 @@
1
+ /* Round 310 verification: legend panel-header count 'N nodes' gains
2
+ * fontWeight=600 for parity with R309 per-row count weight. Same
3
+ * "digit semibold > label regular" hierarchy applied to the panel-
4
+ * summary scope.
5
+ *
6
+ * Contract:
7
+ * - [data-legend-panel-count] attr font-weight === '600'.
8
+ * - Text content matches '{N} node[s]' pattern.
9
+ * - R266 fill transition + R292 tabular-nums preserved.
10
+ * - R309 per-row counts still font-weight='600' (regression).
11
+ * - R308 row labels + R307 offline label + R306 chrome focus-ring-1
12
+ * + R304 sub-hint + R294 pulse all preserved.
13
+ */
14
+ import { chromium } from 'playwright';
15
+ import { readFileSync } from 'node:fs';
16
+
17
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
18
+ const browser = await chromium.launch({ headless: true });
19
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
20
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
21
+ await ctx.addInitScript(() => {
22
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
23
+ });
24
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
25
+ await ctx.route('**/api/hub/status*', async (route) => {
26
+ const r = await route.fetch();
27
+ const b = await r.json();
28
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
29
+ const mk = (alias, model, status) => ({
30
+ alias, status, model, runtime: 'claude-code-cli',
31
+ network_id: nid, project_dir: null,
32
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
33
+ });
34
+ await route.fulfill({ response: r, json: { ...b, sessions: [
35
+ mk('alpha', 'claude-opus-4', 'working'),
36
+ mk('beta', 'gpt-4o', 'idle'),
37
+ mk('gamma', 'claude-sonnet-4', 'offline'),
38
+ ] } });
39
+ });
40
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
41
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
42
+
43
+ const page = await ctx.newPage();
44
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
45
+ await page.waitForSelector('[data-legend-panel-count]', { timeout: 15000 });
46
+ await page.waitForTimeout(300);
47
+
48
+ const probe = await page.evaluate(() => {
49
+ const sel = (s) => document.querySelector(s);
50
+ const panelCount = sel('[data-legend-panel-count]');
51
+ const fvn = (el) => el ? getComputedStyle(el).fontVariantNumeric : null;
52
+ return {
53
+ panelCountFw: panelCount?.getAttribute('font-weight') ?? null,
54
+ panelCountText: panelCount?.textContent?.trim() ?? null,
55
+ panelCountFvn: fvn(panelCount),
56
+ rowCountWorkingFw: sel('[data-legend-count="working"]')?.getAttribute('font-weight') ?? null,
57
+ rowCountIdleFw: sel('[data-legend-count="idle"]')?.getAttribute('font-weight') ?? null,
58
+ rowCountOfflineFw: sel('[data-legend-count="offline"]')?.getAttribute('font-weight') ?? null,
59
+ workingLabel: sel('[data-legend-row-label="working"]')?.textContent?.trim() ?? null,
60
+ idleLabel: sel('[data-legend-row-label="idle"]')?.textContent?.trim() ?? null,
61
+ offlineLabel: sel('[data-legend-row-label="offline"]')?.textContent?.trim() ?? null,
62
+ layoutRingCls: sel('[data-topo-chrome-layout="ring"]')?.className ?? '',
63
+ subhintLs: sel('[data-recent-signal-empty-hint]')?.getAttribute('letter-spacing') ?? null,
64
+ pulseCount: document.querySelectorAll('[data-pulse-wrapper]').length,
65
+ };
66
+ });
67
+ await browser.close();
68
+
69
+ const hasTab = (s) => /tabular-nums/.test(s || '');
70
+
71
+ const results = {
72
+ panel_count_fw_600: probe.panelCountFw === '600',
73
+ panel_count_text_pattern: /^\d+ node/.test(probe.panelCountText || ''),
74
+ panel_count_tabular_kept: hasTab(probe.panelCountFvn),
75
+ r309_row_working_fw_600: probe.rowCountWorkingFw === '600',
76
+ r309_row_idle_fw_600: probe.rowCountIdleFw === '600',
77
+ r309_row_offline_fw_600: probe.rowCountOfflineFw === '600',
78
+ r308_working_label: probe.workingLabel === 'working',
79
+ r308_idle_label: probe.idleLabel === 'idle',
80
+ r307_offline_label: probe.offlineLabel === 'offline',
81
+ r306_focus_ring_1_kept: probe.layoutRingCls.includes('focus-visible:ring-1'),
82
+ r304_subhint_ls_0_15: probe.subhintLs === '0.15',
83
+ r294_pulse_absent: probe.pulseCount === 0,
84
+ };
85
+ const ok = Object.values(results).every(Boolean);
86
+ console.log(`${ok ? '✅' : '❌'} legend panel count weight:`, JSON.stringify(results),
87
+ '\n panel count:', probe.panelCountText, 'fw=', probe.panelCountFw,
88
+ '\n row counts fw:', probe.rowCountWorkingFw, '/', probe.rowCountIdleFw, '/', probe.rowCountOfflineFw);
89
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,83 @@
1
+ /* Round 106 verification: legend panel gains a header matching the
2
+ * recent-signal panel's "recent signal · N flows" format. Same
3
+ * font/position vocabulary so the two side panels feel paired. The
4
+ * fleet count is right-aligned at x=215 in the accent colour.
5
+ *
6
+ * Sanity-checks:
7
+ * - "legend" title text exists in the legend panel
8
+ * - "N node(s)" count matches sessions.length
9
+ * - Singular vs plural ("1 node" vs "5 nodes") correct
10
+ * - Existing rows still render (hover affordance preserved)
11
+ */
12
+ import { chromium } from 'playwright';
13
+ import { readFileSync } from 'node:fs';
14
+
15
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
16
+ const browser = await chromium.launch({ headless: true });
17
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
18
+
19
+ const probe = async (count) => {
20
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
21
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
22
+ await ctx.addInitScript(() => {
23
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
24
+ });
25
+ await ctx.route('**/api/hub/status*', async (route) => {
26
+ const r = await route.fetch();
27
+ const b = await r.json();
28
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
29
+ const sessions = [];
30
+ for (let i = 0; i < count; i++) {
31
+ sessions.push({
32
+ alias: `n${i}`, 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
+ }
37
+ await route.fulfill({ response: r, json: { ...b, sessions } });
38
+ });
39
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
40
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
41
+ const page = await ctx.newPage();
42
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
43
+ await page.waitForFunction((n) => document.querySelectorAll('g[data-node]').length === n, count, { timeout: 30000 });
44
+ await page.waitForSelector('[data-legend-panel-count]', { timeout: 10000 });
45
+ await page.waitForTimeout(400);
46
+
47
+ const data = await page.evaluate(() => {
48
+ const headerCount = document.querySelector('[data-legend-panel-count]')?.textContent;
49
+ const headerTitle = (() => {
50
+ // Find a <text> with "legend" content in the legend panel <g>
51
+ const panel = document.querySelector('[data-topo-panel-elevation="legend"]')?.parentElement;
52
+ if (!panel) return null;
53
+ for (const t of panel.querySelectorAll('text')) {
54
+ if (t.textContent === 'legend') return t.textContent;
55
+ }
56
+ return null;
57
+ })();
58
+ const legendRows = document.querySelectorAll('[data-legend-status]').length;
59
+ return { headerCount, headerTitle, legendRows };
60
+ });
61
+ await ctx.close();
62
+ return data;
63
+ };
64
+
65
+ const single = await probe(1);
66
+ const many = await probe(5);
67
+ await browser.close();
68
+
69
+ const results = {
70
+ single_hasTitle: single.headerTitle === 'legend',
71
+ single_count1Node: single.headerCount === '1 node',
72
+ many_hasTitle: many.headerTitle === 'legend',
73
+ many_count5Nodes: many.headerCount === '5 nodes',
74
+ many_rowsStillRender: many.legendRows >= 1, // depends on which states are present
75
+ // Sanity: singular vs plural
76
+ singular_correct: single.headerCount?.endsWith(' node'),
77
+ plural_correct: many.headerCount?.endsWith(' nodes'),
78
+ };
79
+ const ok = Object.values(results).every(Boolean);
80
+ console.log(`${ok ? '✅' : '❌'} legend panel header:`, JSON.stringify(results),
81
+ `\n single (1 node)=`, single,
82
+ `\n many (5 nodes)=`, many);
83
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,112 @@
1
+ /* Round 181 verification: legend row pinned-state ring (R61) is
2
+ * now always-mounted with opacity-gated visibility + 150ms
3
+ * transition — smooth fade in/out on pin / unpin.
4
+ *
5
+ * Pre-R181 the ring mounted/unmounted conditionally, snapping
6
+ * on every state change. Same vocabulary as R165/R180 smooth-
7
+ * pin-mirror family, applied at the legend-row scope.
8
+ *
9
+ * Test:
10
+ * 1. Idle: ring exists in DOM but opacity=0
11
+ * 2. Click 'working' legend row → ring for working = opacity=1
12
+ * 3. Other rings (idle / offline) stay opacity=0
13
+ * 4. Re-click 'working' → unpin → opacity returns to 0
14
+ * 5. All rings carry 'opacity 150ms ease-out' transition
15
+ */
16
+ import { chromium } from 'playwright';
17
+ import { readFileSync } from 'node:fs';
18
+
19
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
20
+ const browser = await chromium.launch({ headless: true });
21
+
22
+ 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
+
28
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
29
+ const stale = new Date(Date.now() - 10 * 60 * 1000).toISOString();
30
+ await ctx.route('**/api/hub/status*', async (route) => {
31
+ const r = await route.fetch();
32
+ const b = await r.json();
33
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
34
+ const mk = (alias, status, last) => ({
35
+ alias, status, model: 'claude-opus-4', runtime: 'cli-claude-code',
36
+ network_id: nid, project_dir: null,
37
+ created_at: last, updated_at: last, last_seen_at: last,
38
+ });
39
+ // Mix of statuses so all three legend rows render
40
+ await route.fulfill({ response: r, json: { ...b, sessions: [
41
+ mk('w1', 'working', fresh), mk('w2', 'working', fresh),
42
+ mk('i1', 'idle', fresh),
43
+ mk('o1', 'offline', stale),
44
+ ] } });
45
+ });
46
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
47
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
48
+
49
+ const page = await ctx.newPage();
50
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
51
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
52
+ await page.waitForSelector('[data-legend-pin-ring]', { timeout: 10000 });
53
+ await page.waitForTimeout(400);
54
+
55
+ const probe = () => page.evaluate(() => {
56
+ const grab = (k) => {
57
+ const r = document.querySelector(`[data-legend-pin-ring="${k}"]`);
58
+ return r ? {
59
+ opacity: parseFloat(r.getAttribute('opacity') || ''),
60
+ pinnedAttr: r.getAttribute('data-legend-pin-ring-pinned'),
61
+ transition: r.style.transition || getComputedStyle(r).transition,
62
+ } : null;
63
+ };
64
+ return {
65
+ working: grab('working'),
66
+ idle: grab('idle'),
67
+ offline: grab('offline'),
68
+ };
69
+ });
70
+
71
+ const idle = await probe();
72
+
73
+ // Pin 'working' status by clicking the legend row
74
+ await page.locator('[data-legend-status="working"]').click();
75
+ await page.waitForTimeout(250);
76
+ const afterPin = await probe();
77
+
78
+ // Re-click to unpin
79
+ await page.locator('[data-legend-status="working"]').click();
80
+ await page.waitForTimeout(250);
81
+ const afterUnpin = await probe();
82
+
83
+ await browser.close();
84
+
85
+ const hasTransition = (s) =>
86
+ (s?.transition || '').includes('opacity 150ms') ||
87
+ /opacity\s+0\.15s|opacity\s+150ms/.test(s?.transition || '');
88
+
89
+ const results = {
90
+ // Idle (no pin)
91
+ three_rings_present: idle.working !== null && idle.idle !== null && idle.offline !== null,
92
+ idle_working_opacity_0: idle.working?.opacity === 0,
93
+ idle_idle_opacity_0: idle.idle?.opacity === 0,
94
+ idle_offline_opacity_0: idle.offline?.opacity === 0,
95
+ all_have_transition: hasTransition(idle.working) && hasTransition(idle.idle) && hasTransition(idle.offline),
96
+
97
+ // After pin working
98
+ pin_working_opacity_1: afterPin.working?.opacity === 1,
99
+ pin_working_pinned_attr: afterPin.working?.pinnedAttr === 'true',
100
+ pin_idle_stays_0: afterPin.idle?.opacity === 0,
101
+ pin_offline_stays_0: afterPin.offline?.opacity === 0,
102
+
103
+ // After unpin
104
+ unpin_working_opacity_0: afterUnpin.working?.opacity === 0,
105
+ unpin_working_pinned_attr: afterUnpin.working?.pinnedAttr === 'false',
106
+ };
107
+ const ok = Object.values(results).every(Boolean);
108
+ console.log(`${ok ? '✅' : '❌'} legend pin-ring fade:`, JSON.stringify(results),
109
+ `\n idle =`, idle,
110
+ `\n pinned =`, afterPin,
111
+ `\n unpinned =`, afterUnpin);
112
+ process.exit(ok ? 0 : 1);