@sleep2agi/agent-network-dashboard 0.4.6-preview.31 → 0.4.6-preview.310

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 (477) 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/08kf-.bgs10ze.js +1 -0
  171. package/.next/static/chunks/0ggj5ummw2a84.js +7 -0
  172. package/.next/static/chunks/0glmrdj9112tc.js +7 -0
  173. package/.next/static/chunks/0o40uf-rn28q6.js +1 -0
  174. package/.next/static/chunks/0q3hr1~viwzkt.js +4 -0
  175. package/.next/static/chunks/0rxrez_cjn.eh.js +1 -0
  176. package/.next/static/chunks/0x8x.29hkju5a.js +1 -0
  177. package/.next/static/chunks/0~ks9b48lw8hx.css +2 -0
  178. package/.next/static/chunks/13lvf-h-j8bmc.js +1 -0
  179. package/.next/static/chunks/174re5ar~kxne.js +1 -0
  180. package/.next/trace +2 -2
  181. package/.next/trace-build +1 -1
  182. package/app/components/ChatPopover.tsx +38 -2
  183. package/app/components/CommandPalette.tsx +107 -0
  184. package/app/components/HelpOverlay.tsx +126 -0
  185. package/app/components/TopoGraph.tsx +5927 -258
  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/scripts/help-overlay-topo-test.mjs +44 -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-watermark-test.mjs +97 -0
  214. package/scripts/topo-canvas-aria-test.mjs +108 -0
  215. package/scripts/topo-chat-target-breath-test.mjs +84 -0
  216. package/scripts/topo-chat-target-ring-ease-test.mjs +82 -0
  217. package/scripts/topo-chat-target-ring-fade-test.mjs +124 -0
  218. package/scripts/topo-chip-empty-recede-test.mjs +116 -0
  219. package/scripts/topo-chip-focus-test.mjs +95 -0
  220. package/scripts/topo-chip-pin-mirror-test.mjs +113 -0
  221. package/scripts/topo-chip-row-semantic-gap-test.mjs +97 -0
  222. package/scripts/topo-chip-row-tabular-test.mjs +111 -0
  223. package/scripts/topo-chrome-a11y-test.mjs +107 -0
  224. package/scripts/topo-chrome-icon-stroke-test.mjs +95 -0
  225. package/scripts/topo-chrome-pop-extension-test.mjs +98 -0
  226. package/scripts/topo-chrome-pop-nodesize-test.mjs +88 -0
  227. package/scripts/topo-chrome-press-state-test.mjs +135 -0
  228. package/scripts/topo-chrome-strip-semantic-gap-test.mjs +99 -0
  229. package/scripts/topo-chrome-strip-theme-ease-test.mjs +99 -0
  230. package/scripts/topo-chrome-zoom-level-pop-test.mjs +122 -0
  231. package/scripts/topo-chrome-zoom-pop-test.mjs +130 -0
  232. package/scripts/topo-click-ripple-ease-test.mjs +94 -0
  233. package/scripts/topo-cmdk-edge-clear-test.mjs +109 -0
  234. package/scripts/topo-cmdk-layout-view-test.mjs +108 -0
  235. package/scripts/topo-cmdk-pin-test.mjs +123 -0
  236. package/scripts/topo-cmdk-vendor-clear-test.mjs +133 -0
  237. package/scripts/topo-cmdk-vendor-pin-test.mjs +103 -0
  238. package/scripts/topo-corner-radius-unify-test.mjs +103 -0
  239. package/scripts/topo-cwd-tooltip-test.mjs +64 -0
  240. package/scripts/topo-dblclick-reset-test.mjs +74 -0
  241. package/scripts/topo-dense-alias-fill-transition-test.mjs +133 -0
  242. package/scripts/topo-discrete-zoom-smooth-test.mjs +125 -0
  243. package/scripts/topo-edge-badge-click-pin-test.mjs +94 -0
  244. package/scripts/topo-edge-badge-crossfade-test.mjs +138 -0
  245. package/scripts/topo-edge-badge-hot-test.mjs +115 -0
  246. package/scripts/topo-edge-badge-hover-test.mjs +85 -0
  247. package/scripts/topo-edge-badge-lift-test.mjs +150 -0
  248. package/scripts/topo-edge-badge-ripple-test.mjs +111 -0
  249. package/scripts/topo-edge-badge-stroke-transition-test.mjs +118 -0
  250. package/scripts/topo-edge-badge-text-pin-test.mjs +121 -0
  251. package/scripts/topo-edge-badge-theme-ease-test.mjs +89 -0
  252. package/scripts/topo-edge-count-badge-test.mjs +86 -0
  253. package/scripts/topo-edge-endpoint-highlight-test.mjs +111 -0
  254. package/scripts/topo-edge-fadein-test.mjs +106 -0
  255. package/scripts/topo-edge-focus-test.mjs +113 -0
  256. package/scripts/topo-edge-hover-test.mjs +93 -0
  257. package/scripts/topo-edge-pill-hot-test.mjs +129 -0
  258. package/scripts/topo-edge-pill-test.mjs +105 -0
  259. package/scripts/topo-edge-ping-pulse-retired-test.mjs +119 -0
  260. package/scripts/topo-edge-stroke-ease-test.mjs +100 -0
  261. package/scripts/topo-edge-stroke-transition-test.mjs +132 -0
  262. package/scripts/topo-edge-tooltip-test.mjs +70 -0
  263. package/scripts/topo-empty-state-breath-test.mjs +150 -0
  264. package/scripts/topo-empty-state-crossfade-test.mjs +130 -0
  265. package/scripts/topo-empty-state-recenter-test.mjs +107 -0
  266. package/scripts/topo-endpoint-ring-fade-test.mjs +132 -0
  267. package/scripts/topo-endpoint-ring-test.mjs +93 -0
  268. package/scripts/topo-endpoint-ring-thicken-test.mjs +112 -0
  269. package/scripts/topo-esc-clears-pin-test.mjs +98 -0
  270. package/scripts/topo-filter-pill-aliases-test.mjs +121 -0
  271. package/scripts/topo-filter-pill-body-click-test.mjs +112 -0
  272. package/scripts/topo-filter-pill-count-test.mjs +110 -0
  273. package/scripts/topo-filter-pill-mobile-test.mjs +90 -0
  274. package/scripts/topo-fit-key-test.mjs +106 -0
  275. package/scripts/topo-freshness-chip-test.mjs +74 -0
  276. package/scripts/topo-freshness-chip-transition-test.mjs +80 -0
  277. package/scripts/topo-freshness-conditional-render-test.mjs +92 -0
  278. package/scripts/topo-fullscreen-active-test.mjs +115 -0
  279. package/scripts/topo-group-box-pinned-state-test.mjs +95 -0
  280. package/scripts/topo-group-count-test.mjs +6 -1
  281. package/scripts/topo-group-label-a11y-test.mjs +113 -0
  282. package/scripts/topo-group-label-count-inherit-test.mjs +99 -0
  283. package/scripts/topo-group-label-hover-test.mjs +119 -0
  284. package/scripts/topo-group-label-pin-spacing-test.mjs +136 -0
  285. package/scripts/topo-group-label-tint-test.mjs +99 -0
  286. package/scripts/topo-group-label-title-test.mjs +83 -0
  287. package/scripts/topo-group-pin-test.mjs +119 -0
  288. package/scripts/topo-group-pip-fadein-test.mjs +100 -0
  289. package/scripts/topo-group-pip-tabular-test.mjs +95 -0
  290. package/scripts/topo-group-status-pips-test.mjs +103 -0
  291. package/scripts/topo-groupbox-fadein-test.mjs +95 -0
  292. package/scripts/topo-groupbox-hover-lift-test.mjs +127 -0
  293. package/scripts/topo-groupbox-hubcore-ease-test.mjs +106 -0
  294. package/scripts/topo-groupbox-live-test.mjs +100 -0
  295. package/scripts/topo-groupbox-march-busyness-test.mjs +155 -0
  296. package/scripts/topo-groupbox-test.mjs +4 -2
  297. package/scripts/topo-help-overlay-r109-test.mjs +67 -0
  298. package/scripts/topo-help-overlay-r118-test.mjs +69 -0
  299. package/scripts/topo-help-overlay-r123-test.mjs +69 -0
  300. package/scripts/topo-help-overlay-r134-test.mjs +74 -0
  301. package/scripts/topo-help-overlay-r137-test.mjs +81 -0
  302. package/scripts/topo-help-overlay-r141-test.mjs +86 -0
  303. package/scripts/topo-help-overlay-r153-test.mjs +85 -0
  304. package/scripts/topo-help-overlay-r59-test.mjs +67 -0
  305. package/scripts/topo-help-overlay-r65-test.mjs +64 -0
  306. package/scripts/topo-help-overlay-r70-test.mjs +63 -0
  307. package/scripts/topo-help-overlay-r78-test.mjs +59 -0
  308. package/scripts/topo-help-overlay-r81-test.mjs +58 -0
  309. package/scripts/topo-help-overlay-r87-test.mjs +64 -0
  310. package/scripts/topo-help-overlay-r91-test.mjs +65 -0
  311. package/scripts/topo-hot-tail-crossfade-test.mjs +117 -0
  312. package/scripts/topo-html-overlay-padding-unify-test.mjs +120 -0
  313. package/scripts/topo-hub-a11y-test.mjs +112 -0
  314. package/scripts/topo-hub-busyness-test.mjs +100 -0
  315. package/scripts/topo-hub-center-crossfade-test.mjs +124 -0
  316. package/scripts/topo-hub-click-fit-test.mjs +137 -0
  317. package/scripts/topo-hub-digit-scale-test.mjs +129 -0
  318. package/scripts/topo-hub-fadein-test.mjs +88 -0
  319. package/scripts/topo-hub-hover-lift-test.mjs +101 -0
  320. package/scripts/topo-hub-hover-ring-test.mjs +82 -0
  321. package/scripts/topo-hub-pip-theme-ease-test.mjs +116 -0
  322. package/scripts/topo-hub-spoke-ease-test.mjs +120 -0
  323. package/scripts/topo-hub-title-test.mjs +93 -0
  324. package/scripts/topo-hub-working-count-test.mjs +99 -0
  325. package/scripts/topo-ingroup-edge-test.mjs +103 -0
  326. package/scripts/topo-inner-radar-ring-retired-test.mjs +89 -0
  327. package/scripts/topo-kicker-color-test.mjs +93 -0
  328. package/scripts/topo-kicker-tracking-test.mjs +101 -0
  329. package/scripts/topo-label-card-theme-ease-test.mjs +85 -0
  330. package/scripts/topo-label-lift-test.mjs +119 -0
  331. package/scripts/topo-lastseen-tooltip-test.mjs +59 -0
  332. package/scripts/topo-layout-hover-text-preview-test.mjs +101 -0
  333. package/scripts/topo-layout-key-test.mjs +91 -0
  334. package/scripts/topo-layout-switch-crossfade-test.mjs +126 -0
  335. package/scripts/topo-layout-toggle-border-unify-test.mjs +104 -0
  336. package/scripts/topo-layout-toggle-test.mjs +131 -0
  337. package/scripts/topo-legend-compress-test.mjs +107 -0
  338. package/scripts/topo-legend-count-empty-test.mjs +126 -0
  339. package/scripts/topo-legend-count-tabular-parity-test.mjs +90 -0
  340. package/scripts/topo-legend-count-tabular-test.mjs +87 -0
  341. package/scripts/topo-legend-count-test.mjs +88 -0
  342. package/scripts/topo-legend-count-tier-fill-test.mjs +121 -0
  343. package/scripts/topo-legend-header-symmetric-inset-test.mjs +112 -0
  344. package/scripts/topo-legend-hitbox-swatch-center-test.mjs +93 -0
  345. package/scripts/topo-legend-hover-test.mjs +113 -0
  346. package/scripts/topo-legend-offline-delimiter-test.mjs +84 -0
  347. package/scripts/topo-legend-panel-header-test.mjs +83 -0
  348. package/scripts/topo-legend-pin-ring-fade-test.mjs +112 -0
  349. package/scripts/topo-legend-pin-test.mjs +128 -0
  350. package/scripts/topo-legend-row-hover-lift-test.mjs +109 -0
  351. package/scripts/topo-legend-row-pin-spacing-test.mjs +124 -0
  352. package/scripts/topo-legend-row-tint-test.mjs +95 -0
  353. package/scripts/topo-legend-row-title-test.mjs +102 -0
  354. package/scripts/topo-legend-swatch-grow-test.mjs +116 -0
  355. package/scripts/topo-legend-swatch-radius-test.mjs +78 -0
  356. package/scripts/topo-minimap-a11y-test.mjs +101 -0
  357. package/scripts/topo-minimap-dot-transition-test.mjs +113 -0
  358. package/scripts/topo-minimap-rect-stroke-test.mjs +94 -0
  359. package/scripts/topo-minimap-test.mjs +68 -0
  360. package/scripts/topo-minimap-viewport-smooth-test.mjs +124 -0
  361. package/scripts/topo-mobile-chips-test.mjs +103 -0
  362. package/scripts/topo-monogram-font-test.mjs +112 -0
  363. package/scripts/topo-monogram-stroke-test.mjs +101 -0
  364. package/scripts/topo-node-a11y-test.mjs +107 -0
  365. package/scripts/topo-node-hover-lift-test.mjs +85 -0
  366. package/scripts/topo-node-label-card-elevation-test.mjs +157 -0
  367. package/scripts/topo-node-label-card-stroke-test.mjs +135 -0
  368. package/scripts/topo-node-stroke-transition-test.mjs +103 -0
  369. package/scripts/topo-node-text-fill-transition-test.mjs +119 -0
  370. package/scripts/topo-node-title-peers-test.mjs +107 -0
  371. package/scripts/topo-node-title-test.mjs +101 -0
  372. package/scripts/topo-nodesize-active-hover-test.mjs +109 -0
  373. package/scripts/topo-nodesize-crossfade-test.mjs +141 -0
  374. package/scripts/topo-online-chip-nav-test.mjs +143 -0
  375. package/scripts/topo-orbit-particles-retired-test.mjs +91 -0
  376. package/scripts/topo-overlap-test.mjs +32 -5
  377. package/scripts/topo-panel-elevation-test.mjs +76 -0
  378. package/scripts/topo-panel-fadein-test.mjs +102 -0
  379. package/scripts/topo-panel-hot-tail-fade-test.mjs +132 -0
  380. package/scripts/topo-panel-hover-elevation-test.mjs +118 -0
  381. package/scripts/topo-panel-theme-ease-test.mjs +85 -0
  382. package/scripts/topo-panel-title-fill-ease-test.mjs +91 -0
  383. package/scripts/topo-particle-stagger-test.mjs +82 -0
  384. package/scripts/topo-pin-intersection-color-ease-test.mjs +111 -0
  385. package/scripts/topo-pin-intersection-empty-test.mjs +129 -0
  386. package/scripts/topo-pin-intersection-tabular-test.mjs +96 -0
  387. package/scripts/topo-pin-intersection-test.mjs +121 -0
  388. package/scripts/topo-pin-intersection-warning-crossfade-test.mjs +107 -0
  389. package/scripts/topo-pin-mirror-transition-test.mjs +101 -0
  390. package/scripts/topo-pin-persistence-test.mjs +105 -0
  391. package/scripts/topo-popover-meta-test.mjs +78 -0
  392. package/scripts/topo-prepublish-guard-test.mjs +95 -0
  393. package/scripts/topo-pressure-bar-test.mjs +69 -0
  394. package/scripts/topo-pressure-hover-test.mjs +123 -0
  395. package/scripts/topo-pressure-pin-test.mjs +119 -0
  396. package/scripts/topo-pressure-seg-self-hover-test.mjs +126 -0
  397. package/scripts/topo-pressure-seg-tooltip-test.mjs +98 -0
  398. package/scripts/topo-pressure-seg-transition-test.mjs +107 -0
  399. package/scripts/topo-pulse-dot-crossfade-test.mjs +122 -0
  400. package/scripts/topo-pulse-dot-retired-test.mjs +86 -0
  401. package/scripts/topo-pulse-rate-test.mjs +71 -0
  402. package/scripts/topo-recent-panel-count-freshness-test.mjs +109 -0
  403. package/scripts/topo-recent-panel-count-test.mjs +72 -0
  404. package/scripts/topo-recent-panel-footer-breath-test.mjs +142 -0
  405. package/scripts/topo-recent-panel-hot-header-test.mjs +132 -0
  406. package/scripts/topo-recent-panel-more-crossfade-test.mjs +124 -0
  407. package/scripts/topo-recent-panel-more-cyan-hover-test.mjs +151 -0
  408. package/scripts/topo-recent-panel-more-nav-test.mjs +127 -0
  409. package/scripts/topo-recent-panel-more-test.mjs +106 -0
  410. package/scripts/topo-recent-panel-readability-bump-test.mjs +151 -0
  411. package/scripts/topo-recent-row-count-fill-test.mjs +129 -0
  412. package/scripts/topo-recent-row-fadein-test.mjs +107 -0
  413. package/scripts/topo-recent-row-freshness-test.mjs +102 -0
  414. package/scripts/topo-recent-row-hot-test.mjs +110 -0
  415. package/scripts/topo-recent-row-hover-lift-test.mjs +128 -0
  416. package/scripts/topo-recent-row-hover-test.mjs +122 -0
  417. package/scripts/topo-recent-row-pin-spacing-test.mjs +137 -0
  418. package/scripts/topo-recent-row-pin-test.mjs +126 -0
  419. package/scripts/topo-recent-row-tint-test.mjs +91 -0
  420. package/scripts/topo-recent-row-title-test.mjs +112 -0
  421. package/scripts/topo-recent-row-ts-alpha-test.mjs +100 -0
  422. package/scripts/topo-recent-row-ts-test.mjs +95 -0
  423. package/scripts/topo-recent-row-typography-test.mjs +119 -0
  424. package/scripts/topo-recent-signal-empty-hint-test.mjs +73 -0
  425. package/scripts/topo-recent-signal-empty-test.mjs +63 -0
  426. package/scripts/topo-reduced-motion-test.mjs +77 -0
  427. package/scripts/topo-reset-spin-test.mjs +120 -0
  428. package/scripts/topo-runtime-badge-grow-test.mjs +114 -0
  429. package/scripts/topo-spokes-retired-test.mjs +93 -0
  430. package/scripts/topo-starfield-density-test.mjs +89 -0
  431. package/scripts/topo-status-chip-hover-test.mjs +101 -0
  432. package/scripts/topo-status-chip-tooltip-test.mjs +95 -0
  433. package/scripts/topo-statusbar-chips-self-hover-test.mjs +177 -0
  434. package/scripts/topo-svg-focus-test.mjs +112 -0
  435. package/scripts/topo-sweep-busyness-test.mjs +94 -0
  436. package/scripts/topo-tabular-nums-sweep-test.mjs +93 -0
  437. package/scripts/topo-tier-ring-fadein-test.mjs +107 -0
  438. package/scripts/topo-tier-ring-occupancy-test.mjs +87 -0
  439. package/scripts/topo-tier-ring-test.mjs +69 -0
  440. package/scripts/topo-tier-ring-tint-test.mjs +110 -0
  441. package/scripts/topo-tier-stagger-test.mjs +89 -0
  442. package/scripts/topo-title-block-tight-leading-test.mjs +95 -0
  443. package/scripts/topo-title-tracking-test.mjs +98 -0
  444. package/scripts/topo-toggle-inactive-cyan-hover-test.mjs +110 -0
  445. package/scripts/topo-top-rail-theme-ease-test.mjs +89 -0
  446. package/scripts/topo-tz-ghost-test.mjs +69 -0
  447. package/scripts/topo-vendor-chip-hover-test.mjs +111 -0
  448. package/scripts/topo-vendor-chip-self-hover-test.mjs +131 -0
  449. package/scripts/topo-vendor-chip-tabular-test.mjs +92 -0
  450. package/scripts/topo-vendor-chip-threshold-test.mjs +78 -0
  451. package/scripts/topo-vendor-letter-tooltip-test.mjs +92 -0
  452. package/scripts/topo-vendor-pill-test.mjs +125 -0
  453. package/scripts/topo-vendor-pin-test.mjs +125 -0
  454. package/scripts/topo-view-smooth-test.mjs +131 -0
  455. package/scripts/topo-watermark-tracking-test.mjs +99 -0
  456. package/scripts/topo-working-chip-pin-test.mjs +154 -0
  457. package/scripts/topo-working-halo-breath-retired-test.mjs +99 -0
  458. package/scripts/topo-wrapper-flow-arrow-theme-test.mjs +97 -0
  459. package/scripts/topo-wrapper-shadow-theme-ease-test.mjs +89 -0
  460. package/.next/static/chunks/0-7iowzu4.798.js +0 -1
  461. package/.next/static/chunks/01u5zo8b7r_r3.js +0 -1
  462. package/.next/static/chunks/02t5jr4shh.hy.js +0 -1
  463. package/.next/static/chunks/09nd.t_hd.y6t.css +0 -2
  464. package/.next/static/chunks/0_qfg5s5uzs7..js +0 -7
  465. package/.next/static/chunks/0elalb9n_.~di.js +0 -1
  466. package/.next/static/chunks/0fqit.b4j_6rz.js +0 -1
  467. package/.next/static/chunks/0j_q__o8p6g83.js +0 -1
  468. package/.next/static/chunks/0jl6_w50zh4l5.js +0 -1
  469. package/.next/static/chunks/0pl7l_.2im.j0.js +0 -7
  470. package/.next/static/chunks/0qcfcgdthhos6.js +0 -1
  471. package/.next/static/chunks/0r4q73.-1bkyl.js +0 -1
  472. package/.next/static/chunks/0r_c8eccg3z2o.js +0 -1
  473. package/.next/static/chunks/10oi7l-tycgl9.js +0 -1
  474. package/.next/static/chunks/11uk1izuv3~0t.js +0 -1
  475. /package/.next/static/{mgs4E0Fu2JlZsA5DB0XUu → Kkkh4U0z1GCAKfQMsfmjt}/_buildManifest.js +0 -0
  476. /package/.next/static/{mgs4E0Fu2JlZsA5DB0XUu → Kkkh4U0z1GCAKfQMsfmjt}/_clientMiddlewareManifest.js +0 -0
  477. /package/.next/static/{mgs4E0Fu2JlZsA5DB0XUu → Kkkh4U0z1GCAKfQMsfmjt}/_ssgManifest.js +0 -0
@@ -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);
@@ -0,0 +1,128 @@
1
+ /* Round 61 verification: legend rows also pin status filter on click.
2
+ * - Hovering still works (R55, transient release).
3
+ * - Clicking a row toggles pinnedStatus same as R60 pressure segments.
4
+ * - Pinned row's swatch gets an outer ring at r=8 (concentric, row colour).
5
+ * - Click same row again → release.
6
+ * - Sticky: pin survives mouse-move-away (vs R55 hover which clears).
7
+ * - The R60 pressure-segment pin and this share `pinnedStatus`, so
8
+ * clicking the legend then clicking the pressure-segment for a
9
+ * different status replaces (not stacks) the filter — verified by
10
+ * chain: legend.working → pressure.idle → only idle stays bright.
11
+ */
12
+ import { chromium } from 'playwright';
13
+ import { readFileSync } from 'node:fs';
14
+
15
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
16
+ const browser = await chromium.launch({ headless: true });
17
+
18
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
19
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
20
+ await ctx.addInitScript(() => {
21
+ try {
22
+ localStorage.setItem('anet-theme', 'cyber');
23
+ localStorage.setItem('anet-topo-layout', 'ring');
24
+ sessionStorage.setItem('anet_v3_auth', '1');
25
+ } catch {}
26
+ });
27
+
28
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
29
+ await ctx.route('**/api/hub/status*', async (route) => {
30
+ const r = await route.fetch();
31
+ const b = await r.json();
32
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
33
+ const sessions = [
34
+ { alias: 'wkr', status: 'working', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
35
+ { alias: 'idl1', status: 'idle', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
36
+ { alias: 'idl2', status: 'idle', network_id: nid, project_dir: null, created_at: fresh, updated_at: fresh, last_seen_at: fresh },
37
+ ];
38
+ await route.fulfill({ response: r, json: { ...b, sessions } });
39
+ });
40
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
41
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
42
+ const page = await ctx.newPage();
43
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
44
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 3, { timeout: 30000 });
45
+ await page.waitForTimeout(500);
46
+
47
+ const opacities = () => page.evaluate(() => {
48
+ const o = {};
49
+ for (const a of ['wkr', 'idl1', 'idl2']) {
50
+ const g = document.querySelector(`g[data-node="${a}"]`);
51
+ o[a] = g ? +(g.style.opacity || '1') : null;
52
+ }
53
+ return o;
54
+ });
55
+
56
+ const before = await opacities();
57
+
58
+ // Click the WORKING legend row.
59
+ const workingLegend = page.locator('g[data-legend-status="working"]').first();
60
+ const workingExists = await workingLegend.count() > 0;
61
+ if (!workingExists) { console.log('❌ legend row[working] not found'); process.exit(1); }
62
+ await workingLegend.click({ force: true });
63
+ await page.waitForTimeout(250);
64
+ const afterPin = await opacities();
65
+
66
+ // Pin survives mouse move.
67
+ await page.mouse.move(10, 10);
68
+ await page.waitForTimeout(250);
69
+ const afterMouseMove = await opacities();
70
+
71
+ // Pinned row should carry an aria-pressed=true.
72
+ const pinnedAria = await page.evaluate(() => {
73
+ const g = document.querySelector('g[data-legend-status="working"]');
74
+ return g?.getAttribute('aria-pressed');
75
+ });
76
+ // And a concentric r=8 ring exists inside it.
77
+ const pinnedRingExists = await page.evaluate(() => {
78
+ const g = document.querySelector('g[data-legend-status="working"]');
79
+ if (!g) return false;
80
+ return [...g.querySelectorAll('circle')].some(c => c.getAttribute('r') === '8');
81
+ });
82
+
83
+ // Switch pin via pressure segment (idle) — should REPLACE, not stack.
84
+ const idleSeg = page.locator('[data-pressure-seg="idle"]').first();
85
+ await idleSeg.click({ force: true });
86
+ await page.waitForTimeout(250);
87
+ const afterSwitch = await opacities();
88
+
89
+ // And the legend pin should release (concentric ring gone from working).
90
+ const workingRingGone = await page.evaluate(() => {
91
+ const g = document.querySelector('g[data-legend-status="working"]');
92
+ if (!g) return true;
93
+ return ![...g.querySelectorAll('circle')].some(c => c.getAttribute('r') === '8');
94
+ });
95
+
96
+ // Click idle legend again to release pin. The click ALSO triggers a
97
+ // mouseenter that sets hoveredStatus='idle', which would keep the
98
+ // filter active (activeStatus = hoveredStatus ?? pinnedStatus). Move
99
+ // the mouse away to clear the hover so we observe the pin-only state.
100
+ await page.locator('g[data-legend-status="idle"]').first().click({ force: true });
101
+ await page.mouse.move(10, 10);
102
+ await page.waitForTimeout(250);
103
+ const afterRelease = await opacities();
104
+
105
+ await browser.close();
106
+
107
+ const bright = (v) => v != null && v >= 0.55;
108
+ const dim = (v) => v != null && v < 0.4;
109
+ const results = {
110
+ before_baseline: bright(before.wkr) && bright(before.idl1) && bright(before.idl2),
111
+ legendPin_keepsWorking: bright(afterPin.wkr),
112
+ legendPin_dimsIdle: dim(afterPin.idl1) && dim(afterPin.idl2),
113
+ legendPin_survivesMouse: bright(afterMouseMove.wkr) && dim(afterMouseMove.idl1),
114
+ legendPin_ariaPressed: pinnedAria === 'true',
115
+ legendPin_ringRendered: pinnedRingExists,
116
+ pressureSwitch_replaces: dim(afterSwitch.wkr) && bright(afterSwitch.idl1) && bright(afterSwitch.idl2),
117
+ pressureSwitch_clearsLegendRing: workingRingGone,
118
+ release_restoresAll: bright(afterRelease.wkr) && bright(afterRelease.idl1),
119
+ };
120
+ const ok = Object.values(results).every(Boolean);
121
+ console.log(`${ok ? '✅' : '❌'} legend pin:`, JSON.stringify(results),
122
+ `\n before=`, before,
123
+ `\n afterPin=`, afterPin,
124
+ `\n afterMouseMove=`, afterMouseMove,
125
+ `\n afterSwitch=`, afterSwitch,
126
+ `\n afterRelease=`, afterRelease,
127
+ `\n pinnedAria=${pinnedAria} ringRendered=${pinnedRingExists} workingRingGone=${workingRingGone}`);
128
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,109 @@
1
+ /* Round 144 verification: legend panel rows lift 1px on hover or
2
+ * pin. Symmetric extension of R143 (recent-signal rows) to the
3
+ * sibling overlay panel.
4
+ *
5
+ * Now both side panels share matching row-level affordances:
6
+ * recent-signal row (R143): hover/pin → translateY(-1px)
7
+ * legend row (R144): hover/pin → translateY(-1px)
8
+ *
9
+ * Each row composes inside its panel's hover-shadow (R135) — two
10
+ * layers of feedback simultaneously when hovering a row.
11
+ *
12
+ * Path:
13
+ * 1. idle: working/idle/offline rows all flat
14
+ * 2. hover working row → that row lifts; others stay flat
15
+ * 3. leave hover → restored
16
+ * 4. click to pin idle row → idle lifted+pinned; working flat
17
+ * 5. Esc clears pin → idle flat again
18
+ */
19
+ import { chromium } from 'playwright';
20
+ import { readFileSync } from 'node:fs';
21
+
22
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
23
+ const browser = await chromium.launch({ headless: true });
24
+
25
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
26
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
27
+ await ctx.addInitScript(() => {
28
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
29
+ });
30
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
31
+ await ctx.route('**/api/hub/status*', async (route) => {
32
+ const r = await route.fetch();
33
+ const b = await r.json();
34
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
35
+ const mk = (alias, status) => ({
36
+ alias, status, model: 'claude-opus-4', runtime: 'cli-claude-code',
37
+ network_id: nid, project_dir: null,
38
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
39
+ });
40
+ await route.fulfill({ response: r, json: { ...b, sessions: [
41
+ mk('alpha', 'working'), mk('beta', 'idle'), mk('gamma', 'working'),
42
+ ] } });
43
+ });
44
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
45
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
46
+
47
+ const page = await ctx.newPage();
48
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
49
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 3, { timeout: 30000 });
50
+ await page.waitForSelector('[data-legend-status="working"]', { timeout: 10000 });
51
+ await page.waitForTimeout(400);
52
+
53
+ const inspectAll = () => page.evaluate(() => {
54
+ const rows = [...document.querySelectorAll('[data-legend-status]')];
55
+ return rows.map(row => ({
56
+ key: row.getAttribute('data-legend-status'),
57
+ lifted: row.getAttribute('data-legend-row-lifted'),
58
+ pressed: row.getAttribute('aria-pressed'),
59
+ transform: row.getAttribute('style')?.match(/translateY\([^)]+\)/)?.[0],
60
+ }));
61
+ });
62
+
63
+ const s1 = await inspectAll();
64
+
65
+ await page.locator('[data-legend-status="working"]').hover();
66
+ await page.waitForTimeout(250);
67
+ const s2 = await inspectAll();
68
+ const s2_working = s2.find(r => r.key === 'working');
69
+ const s2_idle = s2.find(r => r.key === 'idle');
70
+
71
+ await page.mouse.move(20, 20);
72
+ await page.waitForTimeout(250);
73
+ const s3 = await inspectAll();
74
+ const s3_working = s3.find(r => r.key === 'working');
75
+
76
+ await page.locator('[data-legend-status="idle"]').click();
77
+ await page.waitForTimeout(250);
78
+ await page.mouse.move(20, 20);
79
+ await page.waitForTimeout(250);
80
+ const s4 = await inspectAll();
81
+ const s4_idle = s4.find(r => r.key === 'idle');
82
+ const s4_working = s4.find(r => r.key === 'working');
83
+
84
+ await page.keyboard.press('Escape');
85
+ await page.waitForTimeout(250);
86
+ const s5 = await inspectAll();
87
+ const s5_idle = s5.find(r => r.key === 'idle');
88
+
89
+ await browser.close();
90
+
91
+ const liftStr = 'translateY(-1px)';
92
+ const results = {
93
+ s1_threeRows: s1.length === 3,
94
+ s1_allFlat: s1.every(r => r.lifted === 'false' && !r.transform),
95
+
96
+ s2_workingLifted: s2_working?.lifted === 'true' && s2_working?.transform === liftStr,
97
+ s2_idleStillFlat: s2_idle?.lifted === 'false' && !s2_idle?.transform,
98
+
99
+ s3_workingRestored: s3_working?.lifted === 'false' && !s3_working?.transform,
100
+
101
+ s4_idlePinned: s4_idle?.pressed === 'true' && s4_idle?.lifted === 'true' && s4_idle?.transform === liftStr,
102
+ s4_workingNotLifted: s4_working?.lifted === 'false' && !s4_working?.transform,
103
+
104
+ s5_idleCleared: s5_idle?.lifted === 'false' && !s5_idle?.transform && s5_idle?.pressed === 'false',
105
+ };
106
+ const ok = Object.values(results).every(Boolean);
107
+ console.log(`${ok ? '✅' : '❌'} legend-row hover lift:`, JSON.stringify(results),
108
+ `\n s1=`, s1, `\n s2=`, s2, `\n s3=`, s3, `\n s4=`, s4, `\n s5=`, s5);
109
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,124 @@
1
+ /* Round 219 verification: legend row text gets letter-spacing pin
2
+ * signature 0px → 0.5px, mirroring R218 group label treatment.
3
+ * Pre-R219 hover and pin shared fill colour and were typographically
4
+ * identical at letter form.
5
+ *
6
+ * Test (cyber dark, 4 working sessions):
7
+ * 1. Three legend rows (working / idle / offline) render with
8
+ * data-legend-row-label.
9
+ * 2. Idle: data-legend-row-label-pinned='false' on all rows,
10
+ * computed letter-spacing ~ 0 (CSS resolves '0px' → 'normal').
11
+ * 3. Inline transition includes 'letter-spacing 150ms'.
12
+ * 4. Click the working legend row → that row becomes pinned.
13
+ * 5. Working row: pinned='true', letter-spacing ~ 0.5px.
14
+ * 6. Other rows (idle / offline) stay unpinned + 0 spacing.
15
+ * 7. Click pinned row again → unpin → back to 0 spacing.
16
+ *
17
+ * Same 'normal' vs '0px' CSS-equivalence parser as R218 (CSS
18
+ * resolves letter-spacing: 0px to keyword 'normal' in computed style).
19
+ */
20
+ import { chromium } from 'playwright';
21
+ import { readFileSync } from 'node:fs';
22
+
23
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
24
+ const browser = await chromium.launch({ headless: true });
25
+
26
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
27
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
28
+ await ctx.addInitScript(() => {
29
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
30
+ });
31
+
32
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
33
+ await ctx.route('**/api/hub/status*', async (route) => {
34
+ const r = await route.fetch();
35
+ const b = await r.json();
36
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
37
+ const mk = (alias) => ({
38
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
39
+ network_id: nid, project_dir: null,
40
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
41
+ });
42
+ await route.fulfill({ response: r, json: { ...b, sessions: [
43
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
44
+ ] } });
45
+ });
46
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: [] } }));
47
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.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-row-label]', { timeout: 10000, state: 'attached' });
53
+ await page.waitForTimeout(300);
54
+
55
+ const probe = async () => page.evaluate(() => {
56
+ return [...document.querySelectorAll('[data-legend-row-label]')].map(el => {
57
+ const cs = getComputedStyle(el);
58
+ return {
59
+ key: el.getAttribute('data-legend-row-label'),
60
+ pinned: el.getAttribute('data-legend-row-label-pinned'),
61
+ spacing: cs.letterSpacing,
62
+ transition: el.style.transition,
63
+ };
64
+ });
65
+ });
66
+
67
+ const idle = await probe();
68
+
69
+ // Click the working row to pin it. Use the row wrapper <g
70
+ // data-legend-status="working"> to ensure the click hits the hit
71
+ // area (the text itself has pointerEvents:'none' via the parent <g>).
72
+ await page.click('[data-legend-status="working"]');
73
+ await page.waitForTimeout(200);
74
+ const pinned = await probe();
75
+
76
+ await page.click('[data-legend-status="working"]');
77
+ await page.waitForTimeout(200);
78
+ const unpinned = await probe();
79
+
80
+ await browser.close();
81
+
82
+ // CSS resolves letter-spacing: 0px to 'normal' in computed style.
83
+ // Accept both forms as "zero spacing".
84
+ const near = (s, want, tol = 0.15) => {
85
+ if (want === 0 && (s === 'normal' || s === '0px' || s === '0' || !s)) return true;
86
+ const got = parseFloat(s || '0');
87
+ return Math.abs(got - want) < tol;
88
+ };
89
+ const hasLetterSpacingT = (s) => /letter-spacing\s+(?:150ms|0\.15s)/.test(s || '');
90
+
91
+ const idleWorking = idle.find(p => p.key === 'working');
92
+ const idleIdle = idle.find(p => p.key === 'idle');
93
+ const idleOffline = idle.find(p => p.key === 'offline');
94
+ const pinWorking = pinned.find(p => p.key === 'working');
95
+ const pinIdle = pinned.find(p => p.key === 'idle');
96
+ const unpinWorking = unpinned.find(p => p.key === 'working');
97
+
98
+ const results = {
99
+ three_rows_present: idle.length === 3,
100
+ has_working: !!idleWorking,
101
+ has_idle: !!idleIdle,
102
+ has_offline: !!idleOffline,
103
+
104
+ // Idle: all unpinned, spacing 0, transition present
105
+ all_idle_unpinned: idle.every(p => p.pinned === 'false'),
106
+ all_idle_spacing_zero: idle.every(p => near(p.spacing, 0)),
107
+ all_have_transition: idle.every(p => hasLetterSpacingT(p.transition)),
108
+
109
+ // After pin click: working pinned, idle unchanged
110
+ working_pinned_attr: pinWorking?.pinned === 'true',
111
+ working_spacing_half: near(pinWorking?.spacing, 0.5),
112
+ idle_stays_unpinned: pinIdle?.pinned === 'false',
113
+ idle_spacing_unchanged: near(pinIdle?.spacing, 0),
114
+
115
+ // After unpin click: working back to unpinned + 0 spacing
116
+ unpin_attr_false: unpinWorking?.pinned === 'false',
117
+ unpin_spacing_zero: near(unpinWorking?.spacing, 0),
118
+ };
119
+ const ok = Object.values(results).every(Boolean);
120
+ console.log(`${ok ? '✅' : '❌'} legend-row pin spacing:`, JSON.stringify(results),
121
+ `\n idle:`, idle,
122
+ `\n pinned:`, pinned,
123
+ `\n unpinned:`, unpinned);
124
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,95 @@
1
+ /* Round 105 verification: legend rows in the SVG panel pick up a
2
+ * row-background tint on hover and pin, mirroring R104's treatment
3
+ * on the recent-signal panel. The tint uses the ROW'S OWN swatch
4
+ * colour (working=green, idle=teal, offline=slate) so the user's
5
+ * eye associates the highlight with its swatch instead of a generic
6
+ * accent.
7
+ *
8
+ * Verifies idle / hover / pinned states via the new data-legend-
9
+ * row-tinted attribute + actual fill/opacity. Pin is stronger than
10
+ * hover; idle is transparent.
11
+ */
12
+ import { chromium } from 'playwright';
13
+ import { readFileSync } from 'node:fs';
14
+
15
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
16
+ const browser = await chromium.launch({ headless: true });
17
+
18
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
19
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
20
+ await ctx.addInitScript(() => {
21
+ try {
22
+ localStorage.setItem('anet-theme', 'cyber');
23
+ sessionStorage.setItem('anet_v3_auth', '1');
24
+ sessionStorage.removeItem('anet-topo-pinned-status');
25
+ } catch {}
26
+ });
27
+
28
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
29
+ await ctx.route('**/api/hub/status*', async (route) => {
30
+ const r = await route.fetch();
31
+ const b = await r.json();
32
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
33
+ const mk = (alias, status) => ({
34
+ alias, status, model: 'claude-opus-4', runtime: 'cli-claude-code',
35
+ network_id: nid, project_dir: null,
36
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
37
+ });
38
+ await route.fulfill({ response: r, json: { ...b, sessions: [
39
+ mk('wkr1', 'working'),
40
+ mk('idl1', 'idle'),
41
+ mk('off1', 'offline'),
42
+ ] } });
43
+ });
44
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
45
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
46
+ const page = await ctx.newPage();
47
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
48
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 3, { timeout: 30000 });
49
+ await page.waitForTimeout(400);
50
+
51
+ const readRow = (key) => page.evaluate((k) => {
52
+ const g = document.querySelector(`[data-legend-status="${k}"]`);
53
+ if (!g) return null;
54
+ const rect = g.querySelector('rect');
55
+ return {
56
+ tinted: rect?.getAttribute('data-legend-row-tinted') || '',
57
+ fill: rect?.getAttribute('fill') || '',
58
+ opacity: rect?.getAttribute('opacity') || '',
59
+ };
60
+ }, key);
61
+
62
+ const before = await Promise.all(['working', 'idle', 'offline'].map(readRow));
63
+
64
+ // Hover working.
65
+ await page.locator('[data-legend-status="working"]').hover();
66
+ await page.waitForTimeout(250);
67
+ const onHoverWorking = await Promise.all(['working', 'idle', 'offline'].map(readRow));
68
+
69
+ // Move away — clears hover.
70
+ await page.mouse.move(10, 10);
71
+ await page.waitForTimeout(250);
72
+
73
+ // Pin idle.
74
+ await page.locator('[data-legend-status="idle"]').click();
75
+ await page.waitForTimeout(250);
76
+ const onPinIdle = await Promise.all(['working', 'idle', 'offline'].map(readRow));
77
+
78
+ await browser.close();
79
+
80
+ const opacity = (s) => parseFloat(s) || 0;
81
+ const results = {
82
+ before_allTransparent: before.every(r => r?.tinted === 'none' && r?.fill === 'transparent'),
83
+ hoverWorking_workingTinted: onHoverWorking[0]?.tinted === 'hover' && onHoverWorking[0]?.fill !== 'transparent',
84
+ hoverWorking_othersUnchanged: onHoverWorking.slice(1).every(r => r?.tinted === 'none'),
85
+ hoverWorking_opacityLow: opacity(onHoverWorking[0]?.opacity || '') > 0 && opacity(onHoverWorking[0]?.opacity || '') < 0.2,
86
+ pinIdle_idlePinned: onPinIdle[1]?.tinted === 'pinned' && onPinIdle[1]?.fill !== 'transparent',
87
+ pinIdle_pinStrongerThanHover: opacity(onPinIdle[1]?.opacity || '') > opacity(onHoverWorking[0]?.opacity || ''),
88
+ pinIdle_otherRowsClean: onPinIdle.filter((_, i) => i !== 1).every(r => r?.tinted === 'none'),
89
+ };
90
+ const ok = Object.values(results).every(Boolean);
91
+ console.log(`${ok ? '✅' : '❌'} legend-row tint:`, JSON.stringify(results),
92
+ `\n before=`, before,
93
+ `\n onHoverWorking=`, onHoverWorking,
94
+ `\n onPinIdle=`, onPinIdle);
95
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,102 @@
1
+ /* Round 149 verification: legend rows gain a <title> tooltip
2
+ * symmetric with R148's recent-signal row tooltip. Closes the
3
+ * R97 idiom on the panel side — anywhere the UI shows "N" should
4
+ * hover-explain WHICH N.
5
+ *
6
+ * Both side panels now have matching row-tooltip affordances:
7
+ * recent-signal row (R148): full from/to/timestamp/content + pin hint
8
+ * legend row (R149): status label + matched-aliases + pin hint
9
+ *
10
+ * Fleet: 3 working / 2 idle / 1 offline so all three bucket rows
11
+ * show non-zero. Verify each row's title contains:
12
+ * - status label + count
13
+ * - alias list of matching nodes
14
+ * - "click to pin" hint when not pinned
15
+ *
16
+ * Then click idle to pin → title flips to "click to release pin".
17
+ */
18
+ import { chromium } from 'playwright';
19
+ import { readFileSync } from 'node:fs';
20
+
21
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
22
+ const browser = await chromium.launch({ headless: true });
23
+
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 { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
28
+ });
29
+
30
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
31
+ await ctx.route('**/api/hub/status*', async (route) => {
32
+ const r = await route.fetch();
33
+ const b = await r.json();
34
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
35
+ const mk = (alias, status) => ({
36
+ alias, status, model: 'claude-opus-4', runtime: 'cli-claude-code',
37
+ network_id: nid, project_dir: null,
38
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
39
+ });
40
+ await route.fulfill({ response: r, json: { ...b, sessions: [
41
+ mk('worker-a', 'working'), mk('worker-b', 'working'), mk('worker-c', 'working'),
42
+ mk('idle-a', 'idle'), mk('idle-b', 'idle'),
43
+ mk('off-a', 'offline'),
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
+ // 3 working + 2 idle + 1 offline = 6 sessions; offline nodes still
52
+ // render as g[data-node] (line ~2754 [...onlineNodes, ...offlineNodes]
53
+ // .map). Initial count check was off-by-one.
54
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 6, { timeout: 30000 });
55
+ await page.waitForSelector('[data-legend-status="working"]', { timeout: 10000 });
56
+ await page.waitForTimeout(400);
57
+
58
+ const readTitle = (key) => page.evaluate((k) => {
59
+ const row = document.querySelector(`[data-legend-status="${k}"]`);
60
+ return row?.querySelector('title')?.textContent || '';
61
+ }, key);
62
+
63
+ const wTitle = await readTitle('working');
64
+ const iTitle = await readTitle('idle');
65
+ const oTitle = await readTitle('offline');
66
+
67
+ // Pin idle → its title flips
68
+ await page.locator('[data-legend-status="idle"]').click();
69
+ await page.waitForTimeout(200);
70
+ const iPinnedTitle = await readTitle('idle');
71
+
72
+ await browser.close();
73
+
74
+ const results = {
75
+ // Working row: 3 working aliases
76
+ w_hasLabel: wTitle.includes('working node · 3'),
77
+ w_hasWorkerA: wTitle.includes('worker-a'),
78
+ w_hasWorkerB: wTitle.includes('worker-b'),
79
+ w_hasWorkerC: wTitle.includes('worker-c'),
80
+ w_pinHint: wTitle.includes('click to pin'),
81
+
82
+ // Idle row: 2 idle aliases
83
+ i_hasLabel: iTitle.includes('online idle · 2'),
84
+ i_hasIdleA: iTitle.includes('idle-a'),
85
+ i_hasIdleB: iTitle.includes('idle-b'),
86
+ i_pinHint: iTitle.includes('click to pin'),
87
+
88
+ // Offline row: 1 offline alias
89
+ o_hasLabel: oTitle.includes('offline / no SSE · 1'),
90
+ o_hasOffA: oTitle.includes('off-a'),
91
+
92
+ // After pin: title hint flipped
93
+ iPinned_releaseHint: iPinnedTitle.includes('click to release pin'),
94
+ iPinned_noPinHint: !iPinnedTitle.includes('click to pin · hover to preview'),
95
+ };
96
+ const ok = Object.values(results).every(Boolean);
97
+ console.log(`${ok ? '✅' : '❌'} legend-row title:`, JSON.stringify(results),
98
+ `\n workingTitle:\n${wTitle}`,
99
+ `\n idleTitle:\n${iTitle}`,
100
+ `\n offlineTitle:\n${oTitle}`,
101
+ `\n idlePinnedTitle:\n${iPinnedTitle}`);
102
+ process.exit(ok ? 0 : 1);