@sleep2agi/agent-network-dashboard 0.5.1-preview.14 → 0.5.1-preview.140

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 (283) 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 +32 -32
  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.js.nft.json +1 -1
  84. package/.next/server/app/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  86. package/.next/server/app/server-logs.html +2 -2
  87. package/.next/server/app/server-logs.rsc +14 -14
  88. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  89. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  90. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  91. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  92. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  93. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  94. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app/settings/networks.html +2 -2
  96. package/.next/server/app/settings/networks.rsc +14 -14
  97. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  98. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  99. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  100. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  101. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  102. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  103. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  104. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  106. package/.next/server/app/settings/tokens.html +2 -2
  107. package/.next/server/app/settings/tokens.rsc +14 -14
  108. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  109. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  110. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  111. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  112. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  113. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  114. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  115. package/.next/server/app/settings.html +2 -2
  116. package/.next/server/app/settings.rsc +14 -14
  117. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  118. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  119. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  120. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  121. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  122. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  123. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  125. package/.next/server/app/tasks.html +2 -2
  126. package/.next/server/app/tasks.rsc +14 -14
  127. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  128. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  129. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  130. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  131. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  132. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  133. package/.next/server/chunks/ssr/{[root-of-the-server]__03b.f76._.js → [root-of-the-server]__0sv~g.o._.js} +2 -2
  134. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -0
  135. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  136. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  138. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  139. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  140. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  141. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  142. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  143. package/.next/server/middleware-build-manifest.js +3 -3
  144. package/.next/server/pages/404.html +2 -2
  145. package/.next/server/pages/500.html +1 -1
  146. package/.next/server/server-reference-manifest.js +1 -1
  147. package/.next/server/server-reference-manifest.json +1 -1
  148. package/.next/static/chunks/{0l_~q07bhpkcx.js → 03a4--7ncekmk.js} +1 -1
  149. package/.next/static/chunks/0a4hmfvj-81x5.css +2 -0
  150. package/.next/static/chunks/0hkc0z2pgava~.js +1 -0
  151. package/.next/static/chunks/0xv-z5onm7655.js +1 -0
  152. package/.next/static/chunks/{03~~oirxz7~vc.js → 0xv9kehogd-35.js} +1 -1
  153. package/.next/static/chunks/18aaelbu9gx4w.js +4 -0
  154. package/.next/trace +2 -2
  155. package/.next/trace-build +1 -1
  156. package/app/components/ServersDrawer.tsx +17 -0
  157. package/app/components/TopoGraph.tsx +2538 -187
  158. package/package.json +1 -1
  159. package/screenshots/v0.10.2-disk-verify/disk-render-full.png +0 -0
  160. package/screenshots/v0.10.2-disk-verify/disk-render.png +0 -0
  161. package/screenshots/v0.11.0-147/after.png +0 -0
  162. package/scripts/p0-147-screenshot.mjs +53 -0
  163. package/scripts/p0-servers-drawer-screenshot.mjs +2 -0
  164. package/scripts/topo-active-links-chip-hover-lift-test.mjs +93 -0
  165. package/scripts/topo-chip-digit-fontweight-test.mjs +105 -0
  166. package/scripts/topo-chip-digit-hover-bold-test.mjs +94 -0
  167. package/scripts/topo-chip-row-group-hover-brighten-test.mjs +107 -0
  168. package/scripts/topo-chip-row-hover-lift-test.mjs +95 -0
  169. package/scripts/topo-chrome-button-hover-lift-test.mjs +94 -0
  170. package/scripts/topo-chrome-fullscreen-icon-sw-test.mjs +92 -0
  171. package/scripts/topo-chrome-reset-icon-sw-test.mjs +80 -0
  172. package/scripts/topo-chrome-segmented-radius-test.mjs +100 -0
  173. package/scripts/topo-chrome-zoom-icon-sw-test.mjs +90 -0
  174. package/scripts/topo-click-ripple-opacity-test.mjs +99 -0
  175. package/scripts/topo-dashboard-version-attr-test.mjs +69 -0
  176. package/scripts/topo-dense-alias-opacity-test.mjs +68 -0
  177. package/scripts/topo-edge-badge-digit-fw-test.mjs +103 -0
  178. package/scripts/topo-edge-badge-fontsize-test.mjs +90 -0
  179. package/scripts/topo-edge-badge-hover-opacity-test.mjs +94 -0
  180. package/scripts/topo-edge-badge-hover-stroke-test.mjs +92 -0
  181. package/scripts/topo-edge-badge-letter-spacing-test.mjs +86 -0
  182. package/scripts/topo-edge-badge-opacity-test.mjs +80 -0
  183. package/scripts/topo-edge-badge-pin-opacity-test.mjs +86 -0
  184. package/scripts/topo-edge-badge-stroke-test.mjs +92 -0
  185. package/scripts/topo-edge-endpoint-hover-sw-test.mjs +127 -0
  186. package/scripts/topo-edge-freshness-floor-test.mjs +99 -0
  187. package/scripts/topo-edge-particle-hover-r-test.mjs +113 -0
  188. package/scripts/topo-edge-particle-radius-test.mjs +76 -0
  189. package/scripts/topo-edge-visible-linecap-test.mjs +89 -0
  190. package/scripts/topo-endpoint-ring-r-hover-test.mjs +89 -0
  191. package/scripts/topo-filter-pill-hover-lift-test.mjs +101 -0
  192. package/scripts/topo-filter-pill-hover-opacity-test.mjs +110 -0
  193. package/scripts/topo-filter-pill-value-fw-test.mjs +88 -0
  194. package/scripts/topo-filter-pill-x-hover-scale-test.mjs +99 -0
  195. package/scripts/topo-flow-rail-hover-sw-test.mjs +113 -0
  196. package/scripts/topo-flow-rail-linecap-test.mjs +79 -0
  197. package/scripts/topo-freshness-chip-hierarchy-test.mjs +93 -0
  198. package/scripts/topo-freshness-chip-tabular-test.mjs +41 -0
  199. package/scripts/topo-freshness-floor-lift-test.mjs +92 -0
  200. package/scripts/topo-freshness-suffix-tabular-test.mjs +88 -0
  201. package/scripts/topo-group-box-geom-transition-test.mjs +110 -0
  202. package/scripts/topo-group-box-rx-pin-test.mjs +103 -0
  203. package/scripts/topo-group-box-stroke-test.mjs +105 -0
  204. package/scripts/topo-group-label-count-fontweight-test.mjs +108 -0
  205. package/scripts/topo-group-label-count-fw-test.mjs +100 -0
  206. package/scripts/topo-group-label-fw-pin-test.mjs +99 -0
  207. package/scripts/topo-group-label-letter-spacing-test.mjs +113 -0
  208. package/scripts/topo-group-label-tint-geom-test.mjs +94 -0
  209. package/scripts/topo-group-label-tint-transition-test.mjs +97 -0
  210. package/scripts/topo-group-pip-fontsize-test.mjs +106 -0
  211. package/scripts/topo-hover-detail-body-fw-test.mjs +101 -0
  212. package/scripts/topo-hover-detail-model-fw-test.mjs +98 -0
  213. package/scripts/topo-hover-detail-opacity-test.mjs +98 -0
  214. package/scripts/topo-hover-detail-rx-test.mjs +81 -0
  215. package/scripts/topo-hub-core-fill-hover-test.mjs +85 -0
  216. package/scripts/topo-hub-digit-fontsize-test.mjs +86 -0
  217. package/scripts/topo-hub-digit-fw-hover-test.mjs +102 -0
  218. package/scripts/topo-hub-halo-light-trough-test.mjs +88 -0
  219. package/scripts/topo-hub-halo-r-hover-test.mjs +82 -0
  220. package/scripts/topo-hub-halo-radius-test.mjs +86 -0
  221. package/scripts/topo-hub-halo-trough-test.mjs +83 -0
  222. package/scripts/topo-hub-highlight-opacity-test.mjs +88 -0
  223. package/scripts/topo-hub-highlight-radius-test.mjs +90 -0
  224. package/scripts/topo-hub-hover-ring-opacity-test.mjs +96 -0
  225. package/scripts/topo-hub-hover-ring-stroke-test.mjs +86 -0
  226. package/scripts/topo-hub-spoke-hover-opacity-test.mjs +119 -0
  227. package/scripts/topo-hub-spoke-hover-sw-test.mjs +121 -0
  228. package/scripts/topo-hub-spoke-linecap-test.mjs +80 -0
  229. package/scripts/topo-label-card-opacity-hover-test.mjs +99 -0
  230. package/scripts/topo-layout-toggle-radius-test.mjs +87 -0
  231. package/scripts/topo-legend-count-active-opacity-test.mjs +102 -0
  232. package/scripts/topo-legend-count-pin-fw-test.mjs +90 -0
  233. package/scripts/topo-legend-label-fontweight-test.mjs +94 -0
  234. package/scripts/topo-legend-pin-ring-stroke-test.mjs +101 -0
  235. package/scripts/topo-legend-row-letter-spacing-test.mjs +101 -0
  236. package/scripts/topo-minimap-offline-opacity-test.mjs +90 -0
  237. package/scripts/topo-minimap-online-hover-opacity-test.mjs +92 -0
  238. package/scripts/topo-minimap-online-opacity-test.mjs +93 -0
  239. package/scripts/topo-minimap-online-radius-test.mjs +85 -0
  240. package/scripts/topo-minimap-viewport-linejoin-test.mjs +75 -0
  241. package/scripts/topo-minimap-viewport-opacity-test.mjs +96 -0
  242. package/scripts/topo-minimap-viewport-rx-test.mjs +85 -0
  243. package/scripts/topo-more-flows-fontweight-test.mjs +103 -0
  244. package/scripts/topo-node-alias-letter-spacing-test.mjs +112 -0
  245. package/scripts/topo-node-halo-hover-opacity-test.mjs +104 -0
  246. package/scripts/topo-node-halo-light-offline-test.mjs +80 -0
  247. package/scripts/topo-node-halo-offline-opacity-test.mjs +87 -0
  248. package/scripts/topo-node-label-card-rx-test.mjs +85 -0
  249. package/scripts/topo-node-pulse-peak-test.mjs +89 -0
  250. package/scripts/topo-node-pulse-trough-test.mjs +91 -0
  251. package/scripts/topo-node-sub-text-fw-test.mjs +75 -0
  252. package/scripts/topo-node-sub-text-letter-spacing-test.mjs +115 -0
  253. package/scripts/topo-overlap-stale-build-guard-test.mjs +66 -0
  254. package/scripts/topo-overlap-test.mjs +42 -1
  255. package/scripts/topo-panel-count-fw-hover-test.mjs +105 -0
  256. package/scripts/topo-panel-stroke-hover-test.mjs +110 -0
  257. package/scripts/topo-pressure-bar-height-test.mjs +92 -0
  258. package/scripts/topo-pressure-kicker-fontweight-test.mjs +76 -0
  259. package/scripts/topo-recent-pip-radius-2-test.mjs +72 -0
  260. package/scripts/topo-recent-pip-radius-test.mjs +76 -0
  261. package/scripts/topo-recent-row-content-opacity-test.mjs +81 -0
  262. package/scripts/topo-recent-row-count-pin-fw-test.mjs +106 -0
  263. package/scripts/topo-recent-row-letter-spacing-test.mjs +109 -0
  264. package/scripts/topo-recent-row-pip-hover-r-test.mjs +104 -0
  265. package/scripts/topo-recent-row-text-fontweight-test.mjs +90 -0
  266. package/scripts/topo-runtime-icon-hover-test.mjs +96 -0
  267. package/scripts/topo-spoke-active-opacity-test.mjs +104 -0
  268. package/scripts/topo-spoke-active-stroke-test.mjs +95 -0
  269. package/scripts/topo-spoke-idle-opacity-test.mjs +91 -0
  270. package/scripts/topo-status-ring-hover-sw-test.mjs +105 -0
  271. package/scripts/topo-vendor-chip-hover-lift-test.mjs +87 -0
  272. package/scripts/topo-vendor-glyph-fontweight-test.mjs +102 -0
  273. package/scripts/topo-vendor-letter-hover-scale-test.mjs +129 -0
  274. package/scripts/topo-vendor-suffix-hover-brighten-test.mjs +87 -0
  275. package/scripts/topo-zoom-level-hover-fw-test.mjs +95 -0
  276. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +0 -1
  277. package/.next/static/chunks/03mfrl9fd9smg.js +0 -1
  278. package/.next/static/chunks/0kt7qs66ra2.2.css +0 -2
  279. package/.next/static/chunks/116zy6jdhuaa2.js +0 -1
  280. package/.next/static/chunks/12m~64tlig.y4.js +0 -4
  281. /package/.next/static/{rDAS8dVgx4ldg7AtRGdUB → nLppKA36xKrZ-Bv6UcCHa}/_buildManifest.js +0 -0
  282. /package/.next/static/{rDAS8dVgx4ldg7AtRGdUB → nLppKA36xKrZ-Bv6UcCHa}/_clientMiddlewareManifest.js +0 -0
  283. /package/.next/static/{rDAS8dVgx4ldg7AtRGdUB → nLppKA36xKrZ-Bv6UcCHa}/_ssgManifest.js +0 -0
@@ -0,0 +1,104 @@
1
+ /* Round 391 verification: hub-spoke active opacity 0.7 → 0.8. An
2
+ * "active" spoke is one whose alias has recent message traffic
3
+ * (activeAliases is populated from buildFlowLinks(messages)). The
4
+ * lift unifies active-spoke alpha with R370 hub hover-ring opacity
5
+ * 0.7 → 0.8 cyber — paired canvas signals at matching alpha.
6
+ *
7
+ * Contract:
8
+ * - When the alias appears as flow-link from/to, its spoke has:
9
+ * * opacity attr === '0.8'
10
+ * * data-topo-hub-spoke-opacity === '0.8'
11
+ * * data-topo-hub-spoke-active === 'true'
12
+ * - Idle spokes (no flow traffic) unchanged:
13
+ * * opacity attr === '0.45'
14
+ * * data-topo-hub-spoke-active === 'false'
15
+ * - Pre-R391 invariants preserved on the active spoke:
16
+ * * R382 strokeLinecap='round'
17
+ * * R51-safe strokeWidth=2 (active)
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 fresh = new Date(Date.now() - 60 * 1000).toISOString();
24
+
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
+ await ctx.route('**/api/hub/status*', async (route) => {
32
+ const r = await route.fetch();
33
+ const b = await r.json();
34
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
35
+ const mk = (alias) => ({
36
+ alias, status: 'idle', model: 'claude-opus-4', runtime: 'claude-code-cli',
37
+ network_id: nid, project_dir: null,
38
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
39
+ });
40
+ await route.fulfill({ response: r, json: { ...b, sessions: [ mk('alpha'), mk('beta'), mk('gamma') ] } });
41
+ });
42
+ // Seed a flow link between alpha and beta so their spokes become
43
+ // active; gamma stays idle. activeAliases = { alpha, beta }.
44
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({
45
+ json: {
46
+ messages: [
47
+ { id: 'm1', from_alias: 'alpha', to_alias: 'beta', content: 'ping', created_at: fresh, network_id: 'default' },
48
+ { id: 'm2', from_alias: 'alpha', to_alias: 'beta', content: 'pong', created_at: fresh, network_id: 'default' },
49
+ ],
50
+ },
51
+ }));
52
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
53
+
54
+ const page = await ctx.newPage();
55
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
56
+ await page.waitForSelector('g[data-node]', { timeout: 15000 });
57
+ await page.waitForTimeout(300);
58
+
59
+ // Wait for the dashboard to ingest the seeded messages and recompute
60
+ // activeAliases. The flow-links derivation runs in a useMemo that
61
+ // depends on `messages`; allow a tick for SWR/state propagation.
62
+ await page.waitForTimeout(800);
63
+
64
+ const probe = await page.evaluate(() => {
65
+ const spokes = Array.from(document.querySelectorAll('[data-topo-hub-spoke-active]'));
66
+ const active = spokes.filter((s) => s.getAttribute('data-topo-hub-spoke-active') === 'true');
67
+ const idle = spokes.filter((s) => s.getAttribute('data-topo-hub-spoke-active') === 'false');
68
+ const read = (el) => el ? ({
69
+ opacityAttr: el.getAttribute('opacity'),
70
+ opacityData: el.getAttribute('data-topo-hub-spoke-opacity'),
71
+ strokeWidth: el.getAttribute('stroke-width'),
72
+ linecap: el.getAttribute('stroke-linecap'),
73
+ }) : null;
74
+ return {
75
+ total: spokes.length,
76
+ activeCount: active.length,
77
+ idleCount: idle.length,
78
+ activeSample: read(active[0]),
79
+ idleSample: read(idle[0]),
80
+ };
81
+ });
82
+
83
+ await browser.close();
84
+
85
+ const results = {
86
+ // 3 spokes total (alpha, beta, gamma)
87
+ total_3_spokes: probe.total === 3,
88
+ // alpha + beta active (flow link)
89
+ active_2_spokes: probe.activeCount === 2,
90
+ idle_1_spoke: probe.idleCount === 1,
91
+ // R391: active opacity '0.8' (was '0.7')
92
+ active_opacity_0_8: probe.activeSample?.opacityAttr === '0.8',
93
+ active_data_0_8: probe.activeSample?.opacityData === '0.8',
94
+ // Idle invariant
95
+ idle_opacity_0_45: probe.idleSample?.opacityAttr === '0.45',
96
+ idle_data_0_45: probe.idleSample?.opacityData === '0.45',
97
+ // R382 + R51-safe invariants on active spoke
98
+ active_linecap_round: probe.activeSample?.linecap === 'round',
99
+ active_strokeWidth_2: probe.activeSample?.strokeWidth === '2',
100
+ };
101
+ const ok = Object.values(results).every(Boolean);
102
+ console.log(`${ok ? '✅' : '❌'} hub-spoke active opacity 0.7 → 0.8:`, JSON.stringify(results),
103
+ '\n probe:', JSON.stringify(probe, null, 2));
104
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,95 @@
1
+ /* Round 415 verification: hub-spoke active strokeWidth 2 → 2.25.
2
+ * Visual-weight bump family 14th anchor — active-spoke stroke
3
+ * thickens to match its role as the focal connection. Pairs with
4
+ * R391 opacity 0.7 → 0.8 so the same active path lifts both
5
+ * stroke AND opacity in concert.
6
+ *
7
+ * Contract:
8
+ * - Seed a flow link so a node becomes active (R243 isActive gate)
9
+ * - The active spoke <path>:
10
+ * * stroke-width attr === '2.25'
11
+ * * data-topo-hub-spoke-stroke-width-active === '2.25'
12
+ * * data-topo-hub-spoke-opacity === '0.8' (R391 invariant)
13
+ * * stroke-linecap === 'round' (R382 invariant)
14
+ * - Idle spokes (no flow): stroke-width === '1' (R415 doesn't touch idle)
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 fresh = new Date(Date.now() - 60 * 1000).toISOString();
21
+
22
+ const browser = await chromium.launch({ headless: true });
23
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
24
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
25
+ await ctx.addInitScript(() => {
26
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
27
+ });
28
+ await ctx.route('**/api/hub/status*', async (route) => {
29
+ const r = await route.fetch();
30
+ const b = await r.json();
31
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
32
+ const mk = (alias) => ({
33
+ alias, status: 'idle', model: 'claude-opus-4', runtime: 'claude-code-cli',
34
+ network_id: nid, project_dir: null,
35
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
36
+ });
37
+ await route.fulfill({ response: r, json: { ...b, sessions: [ mk('alpha'), mk('beta'), mk('gamma') ] } });
38
+ });
39
+ // Seed flow alpha→beta so alpha + beta spokes become active
40
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({
41
+ json: { messages: [
42
+ { id: 'm1', from_alias: 'alpha', to_alias: 'beta', content: 'ping', created_at: fresh, network_id: 'default' },
43
+ ] },
44
+ }));
45
+ await ctx.route('**/api/hub/tasks*', (r) => r.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.waitForSelector('[data-topo-hub-spoke-active]', { timeout: 15000 });
50
+ await page.waitForTimeout(500);
51
+
52
+ const probe = await page.evaluate(() => {
53
+ const spokes = Array.from(document.querySelectorAll('[data-topo-hub-spoke-active]'));
54
+ const active = spokes.filter((s) => s.getAttribute('data-topo-hub-spoke-active') === 'true');
55
+ const idle = spokes.filter((s) => s.getAttribute('data-topo-hub-spoke-active') === 'false');
56
+ const read = (el) => el ? ({
57
+ strokeWidth: el.getAttribute('stroke-width'),
58
+ strokeWidthData: el.getAttribute('data-topo-hub-spoke-stroke-width-active'),
59
+ opacity: el.getAttribute('opacity'),
60
+ opacityData: el.getAttribute('data-topo-hub-spoke-opacity'),
61
+ linecap: el.getAttribute('stroke-linecap'),
62
+ }) : null;
63
+ return {
64
+ total: spokes.length,
65
+ activeCount: active.length,
66
+ idleCount: idle.length,
67
+ activeSample: read(active[0]),
68
+ idleSample: read(idle[0]),
69
+ };
70
+ });
71
+
72
+ await browser.close();
73
+
74
+ const results = {
75
+ // 3 spokes total (alpha, beta, gamma)
76
+ total_3_spokes: probe.total === 3,
77
+ // alpha + beta active (flow link)
78
+ active_2_spokes: probe.activeCount === 2,
79
+ idle_1_spoke: probe.idleCount === 1,
80
+ // R415: active strokeWidth '2.25'
81
+ active_strokeWidth_2_25: probe.activeSample?.strokeWidth === '2.25',
82
+ active_data_2_25: probe.activeSample?.strokeWidthData === '2.25',
83
+ // R391 opacity invariant
84
+ active_opacity_0_8: probe.activeSample?.opacity === '0.8',
85
+ // R382 linecap invariant
86
+ active_linecap_round: probe.activeSample?.linecap === 'round',
87
+ // Idle invariant: strokeWidth '1' (R415 doesn't touch idle)
88
+ idle_strokeWidth_1: probe.idleSample?.strokeWidth === '1',
89
+ idle_opacity_0_45: probe.idleSample?.opacity === '0.45',
90
+ };
91
+ const ok = Object.values(results).every(Boolean);
92
+ console.log(`${ok ? '✅' : '❌'} hub-spoke active strokeWidth 2 → 2.25:`, JSON.stringify(results),
93
+ '\n active:', probe.activeSample,
94
+ '\n idle: ', probe.idleSample);
95
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,91 @@
1
+ /* Round 419 verification: hub-spoke idle opacity 0.45 → 0.50.
2
+ * Stale-state legibility lift family 9th anchor — idle spokes
3
+ * read more confidently while the active/idle contrast ratio
4
+ * stays clear (0.8/0.50 = 1.6×).
5
+ *
6
+ * Contract:
7
+ * - Seed a flow alpha→beta so 2 spokes become active, 1 idle
8
+ * - Idle spoke:
9
+ * * opacity attr === '0.5'
10
+ * * data-topo-hub-spoke-opacity === '0.5'
11
+ * * data-topo-hub-spoke-active === 'false'
12
+ * * R46 className 'anet-topo-spoke-flow' preserved
13
+ * - Active spoke (R391/R415 invariants):
14
+ * * opacity '0.8'
15
+ * * strokeWidth '2.25'
16
+ */
17
+ import { chromium } from 'playwright';
18
+ import { readFileSync } from 'node:fs';
19
+
20
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
21
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
22
+
23
+ const browser = await chromium.launch({ headless: true });
24
+ const ctx = await browser.newContext({ viewport: { width: 1500, 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
+ 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) => ({
34
+ alias, status: 'idle', model: 'claude-opus-4', runtime: 'claude-code-cli',
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: [ mk('alpha'), mk('beta'), mk('gamma') ] } });
39
+ });
40
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({
41
+ json: { messages: [
42
+ { id: 'm1', from_alias: 'alpha', to_alias: 'beta', content: 'ping', created_at: fresh, network_id: 'default' },
43
+ ] },
44
+ }));
45
+ await ctx.route('**/api/hub/tasks*', (r) => r.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.waitForSelector('[data-topo-hub-spoke-active]', { timeout: 15000 });
50
+ await page.waitForTimeout(500);
51
+
52
+ const probe = await page.evaluate(() => {
53
+ const spokes = Array.from(document.querySelectorAll('[data-topo-hub-spoke-active]'));
54
+ const active = spokes.filter((s) => s.getAttribute('data-topo-hub-spoke-active') === 'true');
55
+ const idle = spokes.filter((s) => s.getAttribute('data-topo-hub-spoke-active') === 'false');
56
+ const read = (el) => el ? ({
57
+ opacity: el.getAttribute('opacity'),
58
+ opacityData: el.getAttribute('data-topo-hub-spoke-opacity'),
59
+ strokeWidth: el.getAttribute('stroke-width'),
60
+ className: el.getAttribute('class'),
61
+ }) : null;
62
+ return {
63
+ total: spokes.length,
64
+ activeCount: active.length,
65
+ idleCount: idle.length,
66
+ activeSample: read(active[0]),
67
+ idleSample: read(idle[0]),
68
+ };
69
+ });
70
+
71
+ await browser.close();
72
+
73
+ const results = {
74
+ // Setup: 2 active + 1 idle
75
+ total_3_spokes: probe.total === 3,
76
+ active_2: probe.activeCount === 2,
77
+ idle_1: probe.idleCount === 1,
78
+ // R419: idle opacity '0.5'
79
+ idle_opacity_0_5: probe.idleSample?.opacity === '0.5',
80
+ idle_data_0_5: probe.idleSample?.opacityData === '0.5',
81
+ // R46 idle className preserved
82
+ idle_has_flow_class: probe.idleSample?.className?.includes('anet-topo-spoke-flow') === true,
83
+ // R391/R415 active invariants
84
+ active_opacity_0_8: probe.activeSample?.opacity === '0.8',
85
+ active_strokeWidth_2_25: probe.activeSample?.strokeWidth === '2.25',
86
+ };
87
+ const ok = Object.values(results).every(Boolean);
88
+ console.log(`${ok ? '✅' : '❌'} hub-spoke idle opacity 0.45 → 0.50:`, JSON.stringify(results),
89
+ '\n active:', probe.activeSample,
90
+ '\n idle: ', probe.idleSample);
91
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,105 @@
1
+ /* Round 438 verification: status ring strokeWidth hover lift —
2
+ * online 3 → 3.5, offline 1.5 → 2 on hoveredAlias match.
3
+ *
4
+ * Contract:
5
+ * - rest: every online ring reports stroke-width '3', every
6
+ * offline ring '1.5' (R51 sentinels preserved at rest)
7
+ * - hover one node: that ring's stroke-width lifts to '3.5' (or '2'
8
+ * if offline) and data-node-status-ring-hovered flips to 'true'
9
+ * - siblings stay rest
10
+ * - R51 sentinel ATTRS unchanged on rest (overlap probe still works)
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 fresh = new Date(Date.now() - 60 * 1000).toISOString();
17
+
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
+ await ctx.route('**/api/hub/status*', async (route) => {
25
+ const r = await route.fetch();
26
+ const b = await r.json();
27
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
28
+ const mk = (alias, status) => ({
29
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
30
+ network_id: nid, project_dir: null,
31
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
32
+ });
33
+ await route.fulfill({ response: r, json: { ...b, sessions: [
34
+ mk('alpha', 'working'), // online → sw 3
35
+ mk('beta', 'idle'), // online → sw 3
36
+ mk('gamma', 'working'), // online → sw 3
37
+ ] } });
38
+ });
39
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
40
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
41
+
42
+ const page = await ctx.newPage();
43
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
44
+ await page.waitForSelector('[data-node-status-ring]', { timeout: 15000 });
45
+ await page.waitForTimeout(400);
46
+
47
+ const readAll = () => page.evaluate(() => {
48
+ const cs = [...document.querySelectorAll('[data-node-status-ring]')];
49
+ return cs.map((c, i) => {
50
+ const node = c.closest('[data-node]');
51
+ return {
52
+ idx: i,
53
+ alias: node?.getAttribute('data-node') || null,
54
+ ring: c.getAttribute('data-node-status-ring'),
55
+ sw: c.getAttribute('stroke-width'),
56
+ hovered: c.getAttribute('data-node-status-ring-hovered'),
57
+ };
58
+ });
59
+ });
60
+
61
+ const rest = await readAll();
62
+
63
+ const firstAlias = rest[0]?.alias || 'alpha';
64
+ let hover = null;
65
+ const box = await page.evaluate((alias) => {
66
+ const node = document.querySelector(`[data-node="${alias}"]`);
67
+ if (!node) return null;
68
+ const b = node.getBoundingClientRect();
69
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
70
+ }, firstAlias);
71
+ if (box) {
72
+ await page.mouse.move(box.x, box.y);
73
+ await page.waitForTimeout(300);
74
+ hover = await readAll();
75
+ await page.mouse.move(0, 0);
76
+ }
77
+
78
+ const fileText = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
79
+ const sourceWired = /const ringStrokeWidth = isOnline\s*\n\s*\?\s*\(isRingHovered \? 3\.5 : 3\)\s*\n\s*:\s*\(isRingHovered \? 2 : 1\.5\)/.test(fileText);
80
+ const sourceIsHovered = /const isRingHovered = !reducedMotion && hoveredAlias === session\.alias/.test(fileText);
81
+
82
+ await browser.close();
83
+
84
+ const restAllSw3 = rest.every(r => r.sw === '3'); // working+idle both online
85
+ const restNoHover = rest.every(r => r.hovered === 'false');
86
+ const hoveredEntry = hover?.find(r => r.alias === firstAlias);
87
+ const hoverSw_3_5 = hoveredEntry?.sw === '3.5';
88
+ const hoverHoveredTrue = hoveredEntry?.hovered === 'true';
89
+ const othersStillSw3 = hover ? hover.filter(r => r.alias !== firstAlias).every(r => r.sw === '3' && r.hovered === 'false') : false;
90
+
91
+ const results = {
92
+ rest_ring_count_ge_3: rest.length >= 3,
93
+ rest_all_online_sw_3: restAllSw3,
94
+ rest_no_hover: restNoHover,
95
+ hover_target_sw_3_5: hoverSw_3_5,
96
+ hover_target_attr_true: hoverHoveredTrue,
97
+ hover_others_stay_sw_3: othersStillSw3,
98
+ source_strokeWidth_wired: sourceWired,
99
+ source_isRingHovered_def: sourceIsHovered,
100
+ };
101
+ const ok = Object.values(results).every(Boolean);
102
+ console.log(`${ok ? '✅' : '❌'} status-ring hover sw:`, JSON.stringify(results),
103
+ '\n rest sample:', JSON.stringify(rest[0]),
104
+ '\n hover target:', JSON.stringify(hoveredEntry));
105
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,87 @@
1
+ /* Round 401 verification: vendor letter chips close the hover-lift
2
+ * gesture family at the last interactive HTML chip surface. Sibling
3
+ * to R397 filter pin pills + R398/R399 chip-row chips + R400 chrome
4
+ * standalone buttons.
5
+ *
6
+ * Contract:
7
+ * - Vendor letter chip mounted with at least one vendor present:
8
+ * * data-vendor-letter-hover-lift === 'true'
9
+ * * className includes 'hover:-translate-y-px'
10
+ * * className includes 'transition-transform' + 'transform-gpu'
11
+ * - Pre-R401 invariants preserved:
12
+ * * tabular-nums + font-medium + anet-topo-chip-focus
13
+ * * data-vendor-letter / -count / -pinned / -hovered attrs present
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 fresh = new Date(Date.now() - 60 * 1000).toISOString();
20
+
21
+ const browser = await chromium.launch({ headless: true });
22
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
23
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
24
+ await ctx.addInitScript(() => {
25
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
26
+ });
27
+ await ctx.route('**/api/hub/status*', async (route) => {
28
+ const r = await route.fetch();
29
+ const b = await r.json();
30
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
31
+ const mk = (alias, model) => ({
32
+ alias, status: 'idle', model, runtime: 'claude-code-cli',
33
+ network_id: nid, project_dir: null,
34
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
35
+ });
36
+ await route.fulfill({ response: r, json: { ...b, sessions: [
37
+ mk('alpha', 'claude-opus-4'),
38
+ mk('beta', 'gpt-5'),
39
+ mk('gamma', 'gemini-2.0-pro'),
40
+ ] } });
41
+ });
42
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
43
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
44
+
45
+ const page = await ctx.newPage();
46
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
47
+ await page.waitForSelector('[data-vendor-letter-hover-lift="true"]', { timeout: 15000 });
48
+ await page.waitForTimeout(400);
49
+
50
+ const probe = await page.evaluate(() => {
51
+ const chips = Array.from(document.querySelectorAll('[data-vendor-letter-hover-lift="true"]'));
52
+ if (chips.length === 0) return null;
53
+ return {
54
+ count: chips.length,
55
+ sample: {
56
+ className: chips[0].className,
57
+ letter: chips[0].getAttribute('data-vendor-letter'),
58
+ vendorCount: chips[0].getAttribute('data-vendor-letter-count'),
59
+ lift: chips[0].getAttribute('data-vendor-letter-hover-lift'),
60
+ pinned: chips[0].getAttribute('data-vendor-pinned'),
61
+ },
62
+ };
63
+ });
64
+
65
+ await browser.close();
66
+
67
+ const cls = probe?.sample?.className || '';
68
+ const results = {
69
+ // At least one vendor chip rendered (3 vendors in fixture)
70
+ chips_count_ge_1: (probe?.count || 0) >= 1,
71
+ // R401 lift attrs + className tokens
72
+ lift_true: probe?.sample?.lift === 'true',
73
+ has_hover_translate: cls.includes('hover:-translate-y-px'),
74
+ has_transition_transform: cls.includes('transition-transform'),
75
+ has_transform_gpu: cls.includes('transform-gpu'),
76
+ // Pre-R401 invariants (R314 + R232 + R266)
77
+ has_tabular_nums: cls.includes('tabular-nums'),
78
+ has_font_medium: cls.includes('font-medium'),
79
+ has_chip_focus: cls.includes('anet-topo-chip-focus'),
80
+ has_inline_flex: cls.includes('inline-flex'),
81
+ has_letter_attr: !!probe?.sample?.letter,
82
+ has_count_attr: !!probe?.sample?.vendorCount,
83
+ };
84
+ const ok = Object.values(results).every(Boolean);
85
+ console.log(`${ok ? '✅' : '❌'} vendor letter chip hover lift translateY(-1px):`, JSON.stringify(results),
86
+ '\n chips:', probe?.count, '/ sample:', JSON.stringify(probe?.sample, null, 2));
87
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,102 @@
1
+ /* Round 369 verification: vendor letter glyph fontWeight 600. Pre-
2
+ * R369 the glyph inherited fw 500 from the chip's font-medium —
3
+ * letter and `:N` count read at the same weight, contradicting the
4
+ * data-vs-label hierarchy the rest of the chip-row speaks (R362).
5
+ * R369 lifts the LETTER to fw 600 so the chip reads as the same
6
+ * two-tier pattern:
7
+ * letter (data) fw 600 (R369, this round)
8
+ * :N count fw 500 (R333 + chip-level font-medium)
9
+ *
10
+ * Extends the R333-R341/R362 chip-internal-hierarchy arc to the
11
+ * vendor-letter chip surface (9th surface family).
12
+ *
13
+ * Contract (cyber, fixture with 3 distinct vendors so the chip mounts):
14
+ * - Each rendered [data-vendor-letter-glyph] computed fontWeight === '600'.
15
+ * - data-vendor-letter-glyph-font-weight === '600'.
16
+ * - R354 transform-scale rest (matrix identity / 'none') preserved.
17
+ * - R354 transition list contains transform.
18
+ * - The sibling count suffix [data-vendor-letter-count-suffix]
19
+ * still has fontWeight inherited from chip's font-medium (500)
20
+ * — not bumped, so hierarchy is preserved.
21
+ */
22
+ import { chromium } from 'playwright';
23
+ import { readFileSync } from 'node:fs';
24
+
25
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
26
+ const browser = await chromium.launch({ headless: true });
27
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
28
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
29
+ await ctx.addInitScript(() => {
30
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
31
+ });
32
+ 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, model) => ({
38
+ alias, status: 'working', model, runtime: 'claude-code-cli',
39
+ network_id: nid, project_dir: null,
40
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
41
+ });
42
+ // R281 vendor-dist threshold > 2 — provide 3 distinct vendors.
43
+ await route.fulfill({ response: r, json: { ...b, sessions: [
44
+ mk('a1', 'claude-opus-4'),
45
+ mk('a2', 'claude-opus-4'),
46
+ mk('b1', 'gpt-4o'),
47
+ mk('b2', 'gpt-4o'),
48
+ mk('c1', 'internlm/internlm2'),
49
+ ] } });
50
+ });
51
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
52
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
53
+
54
+ const page = await ctx.newPage();
55
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
56
+ await page.waitForSelector('[data-vendor-letter-glyph]', { timeout: 15000 });
57
+ await page.waitForTimeout(300);
58
+
59
+ const probe = await page.evaluate(() => {
60
+ const glyphs = Array.from(document.querySelectorAll('[data-vendor-letter-glyph]'));
61
+ return glyphs.map(el => {
62
+ const cs = getComputedStyle(el);
63
+ const chip = el.closest('[data-vendor-letter]');
64
+ const suffix = chip?.querySelector('[data-vendor-letter-count-suffix]');
65
+ const sCs = suffix ? getComputedStyle(suffix) : null;
66
+ return {
67
+ key: el.getAttribute('data-vendor-letter-glyph'),
68
+ fontWeight: cs.fontWeight,
69
+ fontWeightData: el.getAttribute('data-vendor-letter-glyph-font-weight'),
70
+ transform: cs.transform,
71
+ transition: cs.transition,
72
+ suffixFw: sCs?.fontWeight ?? null,
73
+ };
74
+ });
75
+ });
76
+
77
+ await browser.close();
78
+
79
+ const isRestTransform = (t) => t === 'none' || /matrix\(1, 0, 0, 1, 0, 0\)/.test(t || '');
80
+ const hasTrans = (s, prop) =>
81
+ new RegExp(`${prop}\\s+\\d*\\.?\\d*s|${prop}\\s+\\d+ms`, 'i').test(s || '');
82
+
83
+ const allOk = probe.length >= 3 && probe.every(g =>
84
+ g.fontWeight === '600'
85
+ && g.fontWeightData === '600'
86
+ && isRestTransform(g.transform)
87
+ && hasTrans(g.transition, 'transform')
88
+ && g.suffixFw === '500' // chip-level font-medium inheritance
89
+ );
90
+
91
+ const results = {
92
+ glyphs_rendered_3plus: probe.length >= 3,
93
+ all_glyphs_fw_600: probe.every(g => g.fontWeight === '600'),
94
+ all_glyphs_data_600: probe.every(g => g.fontWeightData === '600'),
95
+ all_glyphs_rest_xform: probe.every(g => isRestTransform(g.transform)),
96
+ all_glyphs_trans_xform: probe.every(g => hasTrans(g.transition, 'transform')),
97
+ all_suffixes_fw_500: probe.every(g => g.suffixFw === '500'),
98
+ };
99
+ const ok = allOk && Object.values(results).every(Boolean);
100
+ console.log(`${ok ? '✅' : '❌'} vendor letter glyph fw 500 → 600:`, JSON.stringify(results),
101
+ '\n glyphs:', probe.map(g => ({ key: g.key, fw: g.fontWeight, suffixFw: g.suffixFw })));
102
+ process.exit(ok ? 0 : 1);