@sleep2agi/agent-network-dashboard 0.5.3-preview.9 → 0.5.3-preview.91

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 (244) 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/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/_not-found.html +2 -2
  14. package/.next/server/app/_not-found.rsc +12 -12
  15. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  16. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  17. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  20. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/admin.html +2 -2
  23. package/.next/server/app/admin.rsc +14 -14
  24. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  25. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  26. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  27. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  28. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  29. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  30. package/.next/server/app/index.html +2 -2
  31. package/.next/server/app/index.rsc +14 -14
  32. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  33. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  34. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  35. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  36. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  37. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  38. package/.next/server/app/login.html +2 -2
  39. package/.next/server/app/login.rsc +14 -14
  40. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  41. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  42. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  43. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  44. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  45. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  46. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  47. package/.next/server/app/logs.html +2 -2
  48. package/.next/server/app/logs.rsc +14 -14
  49. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  50. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  51. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  52. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  54. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  55. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  56. package/.next/server/app/messages.html +2 -2
  57. package/.next/server/app/messages.rsc +14 -14
  58. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  59. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  60. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  61. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  62. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  63. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  64. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  65. package/.next/server/app/node.html +2 -2
  66. package/.next/server/app/node.rsc +14 -14
  67. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  68. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  69. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  70. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  71. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  72. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  73. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  74. package/.next/server/app/nodes.html +2 -2
  75. package/.next/server/app/nodes.rsc +14 -14
  76. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  77. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  78. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  79. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  80. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  81. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  82. package/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/server-logs.html +2 -2
  85. package/.next/server/app/server-logs.rsc +14 -14
  86. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  87. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  88. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  89. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  90. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  91. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  92. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app/settings/networks.html +2 -2
  94. package/.next/server/app/settings/networks.rsc +14 -14
  95. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  96. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  97. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  98. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  99. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  100. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  101. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  102. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  103. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/tokens.html +2 -2
  105. package/.next/server/app/settings/tokens.rsc +14 -14
  106. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  107. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  108. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  109. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  110. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  111. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  112. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  113. package/.next/server/app/settings.html +2 -2
  114. package/.next/server/app/settings.rsc +14 -14
  115. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  116. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  117. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  118. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  119. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  120. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  121. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  122. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/tasks.html +2 -2
  124. package/.next/server/app/tasks.rsc +14 -14
  125. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  126. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  127. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  128. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  129. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  130. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  131. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js +1 -1
  132. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -1
  133. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +4 -4
  134. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  135. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  136. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  138. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  139. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  140. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  141. package/.next/server/middleware-build-manifest.js +3 -3
  142. package/.next/server/pages/404.html +2 -2
  143. package/.next/server/pages/500.html +1 -1
  144. package/.next/static/chunks/00spyt85k-ze4.js +1 -0
  145. package/.next/static/chunks/06rtmmx.6i6n1.js +4 -0
  146. package/.next/static/chunks/{00d~vl~k~7f75.js → 0gqcmj8vinghl.js} +1 -1
  147. package/.next/static/chunks/{03a4--7ncekmk.js → 0v4-5tng.uh.7.js} +2 -2
  148. package/.next/static/chunks/0v~hxo-3kh3jo.js +1 -0
  149. package/.next/static/chunks/13~aih56vx-cf.css +2 -0
  150. package/.next/trace +2 -2
  151. package/.next/trace-build +1 -1
  152. package/app/components/ServersDrawer.tsx +16 -3
  153. package/app/components/TopoGraph.tsx +2460 -121
  154. package/app/globals.css +81 -7
  155. package/package.json +4 -4
  156. package/scripts/p157-servers-copy-test.mjs +95 -0
  157. package/scripts/topo-active-chrome-hover-text-test.mjs +107 -0
  158. package/scripts/topo-alias-glow-test.mjs +121 -0
  159. package/scripts/topo-avatar-brightness-test.mjs +116 -0
  160. package/scripts/topo-avatar-fallback-hover-test.mjs +104 -0
  161. package/scripts/topo-brand-logo-breath-test.mjs +102 -0
  162. package/scripts/topo-brand-logo-hover-brightness-test.mjs +105 -0
  163. package/scripts/topo-brand-logo-hover-rotate-test.mjs +93 -0
  164. package/scripts/topo-brand-logo-hover-test.mjs +85 -0
  165. package/scripts/topo-chip-row-digit-ls-test.mjs +135 -0
  166. package/scripts/topo-chip-row-member-alias-lit-test.mjs +154 -0
  167. package/scripts/topo-chip-row-unit-hover-tracking-test.mjs +124 -0
  168. package/scripts/topo-cluster-count-attr-test.mjs +80 -0
  169. package/scripts/topo-crescent-breath-test.mjs +104 -0
  170. package/scripts/topo-crescent-recede-test.mjs +111 -0
  171. package/scripts/topo-edge-badge-hover-glow-test.mjs +90 -0
  172. package/scripts/topo-edge-badge-text-brightness-test.mjs +83 -0
  173. package/scripts/topo-edge-pill-glow-test.mjs +67 -0
  174. package/scripts/topo-filter-pill-glow-test.mjs +90 -0
  175. package/scripts/topo-fleet-density-tier-test.mjs +84 -0
  176. package/scripts/topo-freshness-chip-fade-test.mjs +105 -0
  177. package/scripts/topo-fullscreen-attr-test.mjs +73 -0
  178. package/scripts/topo-fullscreen-icon-rotate-test.mjs +93 -0
  179. package/scripts/topo-grid-content-bottom-attr-test.mjs +72 -0
  180. package/scripts/topo-group-label-brightness-test.mjs +84 -0
  181. package/scripts/topo-group-label-hover-glow-test.mjs +86 -0
  182. package/scripts/topo-group-label-member-alias-hover-test.mjs +125 -0
  183. package/scripts/topo-group-pill-glow-test.mjs +76 -0
  184. package/scripts/topo-hub-digit-brightness-test.mjs +79 -0
  185. package/scripts/topo-hub-digit-ls-test.mjs +119 -0
  186. package/scripts/topo-hub-halo-brightness-test.mjs +80 -0
  187. package/scripts/topo-hub-halo-glow-test.mjs +96 -0
  188. package/scripts/topo-hub-highlight-amplify-test.mjs +139 -0
  189. package/scripts/topo-hub-highlight-brightness-test.mjs +84 -0
  190. package/scripts/topo-hub-highlight-fill-transition-test.mjs +84 -0
  191. package/scripts/topo-hub-highlight-glow-test.mjs +99 -0
  192. package/scripts/topo-hub-highlight-r-test.mjs +112 -0
  193. package/scripts/topo-hub-highlight-recede-test.mjs +144 -0
  194. package/scripts/topo-hub-highlight-theme-fill-test.mjs +83 -0
  195. package/scripts/topo-hub-hover-ring-glow-test.mjs +97 -0
  196. package/scripts/topo-hub-idle-breath-test.mjs +109 -0
  197. package/scripts/topo-hub-recede-test.mjs +124 -0
  198. package/scripts/topo-hub-spoke-glow-test.mjs +112 -0
  199. package/scripts/topo-layout-hover-fw-test.mjs +98 -0
  200. package/scripts/topo-legend-count-letter-spacing-test.mjs +108 -0
  201. package/scripts/topo-legend-label-fw-test.mjs +107 -0
  202. package/scripts/topo-legend-row-label-glow-test.mjs +102 -0
  203. package/scripts/topo-legend-swatch-glow-test.mjs +109 -0
  204. package/scripts/topo-legend-swatch-member-alias-match-test.mjs +139 -0
  205. package/scripts/topo-minimap-hover-glow-test.mjs +109 -0
  206. package/scripts/topo-node-alias-brightness-test.mjs +84 -0
  207. package/scripts/topo-node-sub-text-brightness-test.mjs +88 -0
  208. package/scripts/topo-nodesize-hover-fw-test.mjs +99 -0
  209. package/scripts/topo-orphan-box-dash-test.mjs +89 -0
  210. package/scripts/topo-orphan-fill-opacity-test.mjs +91 -0
  211. package/scripts/topo-orphan-label-italic-test.mjs +90 -0
  212. package/scripts/topo-orphan-label-opacity-test.mjs +98 -0
  213. package/scripts/topo-panel-count-hover-ls-test.mjs +87 -0
  214. package/scripts/topo-panel-row-brightness-test.mjs +116 -0
  215. package/scripts/topo-panel-title-brightness-test.mjs +98 -0
  216. package/scripts/topo-panel-title-glow-test.mjs +111 -0
  217. package/scripts/topo-pill-x-rotate-test.mjs +96 -0
  218. package/scripts/topo-pinned-aspect-test.mjs +89 -0
  219. package/scripts/topo-pressure-seg-glow-test.mjs +92 -0
  220. package/scripts/topo-pressure-seg-member-alias-match-test.mjs +133 -0
  221. package/scripts/topo-pressure-seg-motion-test.mjs +101 -0
  222. package/scripts/topo-recent-hot-pulse-test.mjs +102 -0
  223. package/scripts/topo-recent-more-fw-test.mjs +126 -0
  224. package/scripts/topo-recent-row-fw-test.mjs +115 -0
  225. package/scripts/topo-recent-row-text-glow-test.mjs +86 -0
  226. package/scripts/topo-reduced-motion-attr-test.mjs +69 -0
  227. package/scripts/topo-reset-icon-hover-scale-test.mjs +102 -0
  228. package/scripts/topo-runtime-badge-glow-test.mjs +108 -0
  229. package/scripts/topo-starfield-hue-test.mjs +109 -0
  230. package/scripts/topo-titleblock-h2-hover-fw-test.mjs +109 -0
  231. package/scripts/topo-titleblock-h2-hover-tracking-test.mjs +128 -0
  232. package/scripts/topo-titleblock-kicker-hover-test.mjs +134 -0
  233. package/scripts/topo-vendor-chip-glow-test.mjs +97 -0
  234. package/scripts/topo-vendor-pill-glow-test.mjs +98 -0
  235. package/scripts/topo-watermark-breath-test.mjs +100 -0
  236. package/scripts/topo-watermark-recede-test.mjs +114 -0
  237. package/scripts/topo-zoom-level-color-test.mjs +105 -0
  238. package/.next/static/chunks/0hndl9yzpqajt.css +0 -2
  239. package/.next/static/chunks/0jn4lbsj97vfl.js +0 -1
  240. package/.next/static/chunks/0rz3-6.xs78~d.js +0 -1
  241. package/.next/static/chunks/0~~xbw42y4m3v.js +0 -4
  242. /package/.next/static/{nbpD7Lhttq59fvvIw9gTX → hCtjbS0oHsw_JU2q5cp-3}/_buildManifest.js +0 -0
  243. /package/.next/static/{nbpD7Lhttq59fvvIw9gTX → hCtjbS0oHsw_JU2q5cp-3}/_clientMiddlewareManifest.js +0 -0
  244. /package/.next/static/{nbpD7Lhttq59fvvIw9gTX → hCtjbS0oHsw_JU2q5cp-3}/_ssgManifest.js +0 -0
@@ -0,0 +1,154 @@
1
+ /* Round 565 (50-round milestone) verification: chip-row chips
2
+ * gain "lit" bg/border treatment when operator hovers a node
3
+ * alias matching the chip's status tier. 7th anchor in the
4
+ * inspection-overrides-encoding family.
5
+ *
6
+ * Mock: alpha·1 (working) + alpha·2 (idle) + alpha·3 (offline).
7
+ * Hover alpha·1 → working chip lit (bg-green-500/15); hover
8
+ * alpha·2 → online chip lit (bg-cyan-500/15).
9
+ *
10
+ * Test phases:
11
+ * 1. rest: both chip bg's at /10 alpha (0.1); attrs 'false'
12
+ * 2. hover alpha·1 (working) → working chip bg at /15 (0.15);
13
+ * attr 'true'; online chip stays at /10
14
+ * 3. hover alpha·2 (idle) → online chip bg at /15; attr 'true';
15
+ * working stays at /10
16
+ * 4. hover alpha·3 (offline) → neither chip lit
17
+ * 5. source-side regex confirms wiring
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: 1200 } });
27
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
28
+ await ctx.addInitScript(() => {
29
+ try {
30
+ localStorage.setItem('anet-theme', 'cyber');
31
+ localStorage.setItem('anet-topo-layout', 'ring');
32
+ sessionStorage.setItem('anet_v3_auth', '1');
33
+ } catch {}
34
+ });
35
+ await ctx.route('**/api/hub/status*', async (route) => {
36
+ const r = await route.fetch();
37
+ const b = await r.json();
38
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
39
+ const mk = (alias, status) => ({
40
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
41
+ network_id: nid, project_dir: null,
42
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
43
+ });
44
+ await route.fulfill({ response: r, json: { ...b, sessions: [
45
+ mk('alpha·1', 'working'),
46
+ mk('alpha·2', 'idle'),
47
+ mk('alpha·3', 'offline'),
48
+ ] } });
49
+ });
50
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
51
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
52
+ const page = await ctx.newPage();
53
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
54
+ await page.waitForSelector('[data-working-chip]', { timeout: 15000 });
55
+ await page.waitForTimeout(500);
56
+
57
+ const probeChips = async () => {
58
+ return page.evaluate(() => {
59
+ const probe = (sel) => {
60
+ const el = document.querySelector(sel);
61
+ if (!el) return null;
62
+ const cs = getComputedStyle(el);
63
+ // Tailwind v4 emits bg in oklab format: oklab(L A B / alpha)
64
+ // Parse the trailing alpha to detect /10 vs /15.
65
+ const m = (cs.backgroundColor || '').match(/\/\s*([0-9.]+)\s*\)/);
66
+ return {
67
+ bg: cs.backgroundColor,
68
+ bgAlpha: m ? parseFloat(m[1]) : null,
69
+ };
70
+ };
71
+ return {
72
+ working: {
73
+ ...probe('[data-working-chip]'),
74
+ lit: document.querySelector('[data-working-chip-member-alias-lit]')
75
+ ?.getAttribute('data-working-chip-member-alias-lit') === 'true',
76
+ },
77
+ online: {
78
+ ...probe('[data-online-chip]'),
79
+ lit: document.querySelector('[data-online-chip-member-alias-lit]')
80
+ ?.getAttribute('data-online-chip-member-alias-lit') === 'true',
81
+ },
82
+ };
83
+ });
84
+ };
85
+
86
+ const rest = await probeChips();
87
+
88
+ // Hover working node
89
+ await page.hover('g[data-node="alpha·1"]');
90
+ await page.waitForTimeout(400);
91
+ const hoverWorking = await probeChips();
92
+
93
+ // Move and hover idle node
94
+ await page.mouse.move(0, 0);
95
+ await page.waitForTimeout(300);
96
+ await page.hover('g[data-node="alpha·2"]');
97
+ await page.waitForTimeout(400);
98
+ const hoverIdle = await probeChips();
99
+
100
+ // Move and hover offline node
101
+ await page.mouse.move(0, 0);
102
+ await page.waitForTimeout(300);
103
+ await page.hover('g[data-node="alpha·3"]');
104
+ await page.waitForTimeout(400);
105
+ const hoverOffline = await probeChips();
106
+
107
+ await browser.close();
108
+
109
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
110
+ const sourceTierKey = /const hoveredAliasTierKey: 'working' \| 'idle' \| 'offline' \| null/.test(src);
111
+ const sourceWorkingLit = /const isWorkingChipLit = hoveredAliasTierKey === 'working';/.test(src);
112
+ const sourceOnlineLit = /const isOnlineChipLit\s+= hoveredAliasTierKey === 'idle';/.test(src);
113
+ const sourceWorkingClass = /isWorkingChipLit \? 'bg-green-500\/15 border-green-500\/30' : 'bg-green-500\/10 border-green-500\/20'/.test(src);
114
+ const sourceOnlineClass = /isOnlineChipLit \? 'bg-cyan-500\/15 border-cyan-500\/30' : 'bg-cyan-500\/10 border-cyan-500\/20'/.test(src);
115
+ const sourceWorkingAttr = /data-working-chip-member-alias-lit=/.test(src);
116
+ const sourceOnlineAttr = /data-online-chip-member-alias-lit=/.test(src);
117
+
118
+ const closeAlpha = (a, target) => a !== null && Math.abs(a - target) < 0.005;
119
+
120
+ const results = {
121
+ rest_working_alpha_10: closeAlpha(rest.working.bgAlpha, 0.1),
122
+ rest_online_alpha_10: closeAlpha(rest.online.bgAlpha, 0.1),
123
+ rest_working_lit_false: rest.working.lit === false,
124
+ rest_online_lit_false: rest.online.lit === false,
125
+ // hover working → working chip alpha lifts to 0.15
126
+ hover_working_w_alpha_15: closeAlpha(hoverWorking.working.bgAlpha, 0.15),
127
+ hover_working_w_lit_true: hoverWorking.working.lit === true,
128
+ hover_working_o_alpha_10: closeAlpha(hoverWorking.online.bgAlpha, 0.1),
129
+ hover_working_o_lit_false: hoverWorking.online.lit === false,
130
+ // hover idle → online chip alpha lifts; working stays
131
+ hover_idle_o_alpha_15: closeAlpha(hoverIdle.online.bgAlpha, 0.15),
132
+ hover_idle_o_lit_true: hoverIdle.online.lit === true,
133
+ hover_idle_w_alpha_10: closeAlpha(hoverIdle.working.bgAlpha, 0.1),
134
+ hover_idle_w_lit_false: hoverIdle.working.lit === false,
135
+ // hover offline → neither chip lit
136
+ hover_offline_w_lit_false: hoverOffline.working.lit === false,
137
+ hover_offline_o_lit_false: hoverOffline.online.lit === false,
138
+ // Source
139
+ source_tier_key: sourceTierKey,
140
+ source_working_lit: sourceWorkingLit,
141
+ source_online_lit: sourceOnlineLit,
142
+ source_working_class: sourceWorkingClass,
143
+ source_online_class: sourceOnlineClass,
144
+ source_working_attr: sourceWorkingAttr,
145
+ source_online_attr: sourceOnlineAttr,
146
+ };
147
+ const ok = Object.values(results).every(Boolean);
148
+ console.log(`${ok ? '✅' : '❌'} R565 chip-row chip member-alias-lit (7th anchor, 50-round milestone):`,
149
+ JSON.stringify(results, null, 2),
150
+ '\n rest:', JSON.stringify(rest),
151
+ '\n hover working node:', JSON.stringify(hoverWorking),
152
+ '\n hover idle node:', JSON.stringify(hoverIdle),
153
+ '\n hover offline node:', JSON.stringify(hoverOffline));
154
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,124 @@
1
+ /* Round 560 verification: chip-row chip UNIT spans + filter-pill
2
+ * prefix/count + vendor-letter count suffix gain group-hover:
3
+ * tracking-wide. Coordinated 7-occurrence replace_all swap
4
+ * extends the hover-letter-spacing family across the small
5
+ * data-label-spans-with-opacity-70 cohort.
6
+ *
7
+ * Test phases:
8
+ * 1. rest: letter-spacing ≈ 'normal' (0px) on working-chip-unit
9
+ * 2. hover the chip wrapper (group) → unit letter-spacing lifts
10
+ * to ≈ 0.025em ≈ 0.3px on a 12px font
11
+ * 3. transition-property contains BOTH 'opacity' and
12
+ * 'letter-spacing'
13
+ * 4. source-side regex confirms the new className substring
14
+ * appears 7 times (replace_all touched all sites)
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: 1200 } });
24
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
25
+ await ctx.addInitScript(() => {
26
+ try {
27
+ localStorage.setItem('anet-theme', 'cyber');
28
+ localStorage.setItem('anet-topo-layout', 'ring');
29
+ sessionStorage.setItem('anet_v3_auth', '1');
30
+ } catch {}
31
+ });
32
+ await ctx.route('**/api/hub/status*', async (route) => {
33
+ const r = await route.fetch();
34
+ const b = await r.json();
35
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
36
+ const mk = (alias, status = 'working') => ({
37
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
38
+ network_id: nid, project_dir: null,
39
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
40
+ });
41
+ // Working sessions so the working chip is clickable / hoverable.
42
+ await route.fulfill({ response: r, json: { ...b, sessions: [
43
+ mk('a·1', 'working'), mk('a·2', 'working'), mk('a·3', 'idle'),
44
+ ] } });
45
+ });
46
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
47
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
48
+ const page = await ctx.newPage();
49
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
50
+ await page.waitForSelector('[data-working-chip-unit]', { timeout: 15000 });
51
+ await page.waitForTimeout(500);
52
+
53
+ const unitSel = '[data-working-chip-unit]';
54
+ // Hover the parent chip (the <span> wrapping digit+unit) — group-hover.
55
+ // Walk up until we find the role='button' chip wrapper.
56
+ const chipWrapperHandle = await page.evaluateHandle((s) => {
57
+ let el = document.querySelector(s);
58
+ while (el && el.parentElement) {
59
+ el = el.parentElement;
60
+ if (el.getAttribute('role') === 'button' || el.getAttribute('aria-pressed') !== null) {
61
+ return el;
62
+ }
63
+ }
64
+ return el;
65
+ }, unitSel);
66
+
67
+ const parsePx = (s) => parseFloat((s || '').replace(/px$/, ''));
68
+
69
+ const rest = await page.evaluate((s) => {
70
+ const el = document.querySelector(s);
71
+ if (!el) return null;
72
+ const cs = getComputedStyle(el);
73
+ return {
74
+ letterSpacing: cs.letterSpacing,
75
+ transitionProperty: cs.transitionProperty,
76
+ transitionDuration: cs.transitionDuration,
77
+ fontSize: cs.fontSize,
78
+ opacity: cs.opacity,
79
+ };
80
+ }, unitSel);
81
+
82
+ // Hover the chip wrapper
83
+ await chipWrapperHandle.hover();
84
+ await page.waitForTimeout(400);
85
+ const hover = await page.evaluate((s) => {
86
+ const el = document.querySelector(s);
87
+ if (!el) return null;
88
+ const cs = getComputedStyle(el);
89
+ return {
90
+ letterSpacing: cs.letterSpacing,
91
+ opacity: cs.opacity,
92
+ };
93
+ }, unitSel);
94
+
95
+ await browser.close();
96
+
97
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
98
+ // Count occurrences of the new className
99
+ const occurrences = (src.match(/opacity-70 transition-\[opacity,letter-spacing\] duration-200 group-hover:opacity-100 group-hover:tracking-wide/g) || []).length;
100
+
101
+ // At text-xs (12px in chip-row context), tracking-wide = 0.025em = 0.3px
102
+ // At rest, letter-spacing = 'normal' which most browsers report as '0px'
103
+ // or 'normal'.
104
+ const restPx = parsePx(rest?.letterSpacing) || 0;
105
+ const hoverPx = parsePx(hover?.letterSpacing);
106
+
107
+ const results = {
108
+ rest_letter_spacing_zero: rest?.letterSpacing === 'normal' || Math.abs(restPx) < 0.01,
109
+ hover_letter_spacing_wide: Math.abs(hoverPx - 0.3) < 0.1, // tracking-wide @ 12px ≈ 0.3px
110
+ hover_ls_greater_than_rest: hoverPx > restPx + 0.1,
111
+ rest_opacity_0_7: Math.abs(parseFloat(rest?.opacity || '0') - 0.7) < 0.01,
112
+ hover_opacity_1: Math.abs(parseFloat(hover?.opacity || '0') - 1.0) < 0.01,
113
+ transition_has_opacity: /opacity/.test(rest?.transitionProperty || ''),
114
+ transition_has_ls: /letter-spacing/.test(rest?.transitionProperty || ''),
115
+ transition_duration: rest?.transitionDuration === '0.2s' || /^0\.2s/.test(rest?.transitionDuration || ''),
116
+ source_7_occurrences: occurrences === 7,
117
+ };
118
+ const ok = Object.values(results).every(Boolean);
119
+ console.log(`${ok ? '✅' : '❌'} R560 chip-row unit + filter-pill spans hover-tracking (7 anchors via replace_all):`,
120
+ JSON.stringify(results, null, 2),
121
+ `\n rest: ${JSON.stringify(rest)}`,
122
+ `\n hover: ${JSON.stringify(hover)}`,
123
+ `\n source replace_all occurrences: ${occurrences}`);
124
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,80 @@
1
+ /* Round 512 verification: root svg surfaces `data-topo-cluster-count`
2
+ * (14th attr in canvas state surface set). Paired with R469 fleet
3
+ * numerics + R502 categorical density.
4
+ *
5
+ * Contract:
6
+ * - grid layout: data-topo-cluster-count = groupBoxes.length (≥ 1)
7
+ * - ring layout: data-topo-cluster-count = '0' (groupBoxes is empty)
8
+ * - orphan-band fixture: cluster count includes the orphan band
9
+ *
10
+ * Tests across 2 fixtures × 2 layouts.
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
+ async function probe({ layout, sessions, label }) {
19
+ const browser = await chromium.launch({ headless: true });
20
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
21
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
22
+ await ctx.addInitScript((l) => {
23
+ try {
24
+ localStorage.setItem('anet-theme', 'cyber');
25
+ localStorage.setItem('anet-topo-layout', l);
26
+ sessionStorage.setItem('anet_v3_auth', '1');
27
+ } catch {}
28
+ }, layout);
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: '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: sessions.map(s => mk(s.alias, s.status)) } });
39
+ });
40
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
41
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
42
+ const page = await ctx.newPage();
43
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
44
+ await page.waitForSelector('svg[data-topo-cluster-count]', { timeout: 15000 });
45
+ await page.waitForTimeout(1500);
46
+ const count = await page.evaluate(() =>
47
+ document.querySelector('svg[viewBox="0 0 1000 680"]')?.getAttribute('data-topo-cluster-count')
48
+ );
49
+ await browser.close();
50
+ return { label, count };
51
+ }
52
+
53
+ // Fixture A: 2 prefix groups (alpha×3 + beta×2) + 3 orphans
54
+ const fixtureA = [
55
+ { alias: 'alpha·1', status: 'working' },
56
+ { alias: 'alpha·2', status: 'idle' },
57
+ { alias: 'alpha·3', status: 'idle' },
58
+ { alias: 'beta·1', status: 'working' },
59
+ { alias: 'beta·2', status: 'idle' },
60
+ { alias: 'zeta', status: 'idle' },
61
+ { alias: 'omega', status: 'idle' },
62
+ { alias: 'lonely', status: 'idle' },
63
+ ];
64
+
65
+ const aGrid = await probe({ layout: 'grid', sessions: fixtureA, label: 'grid+2prefix+3orph' });
66
+ const aRing = await probe({ layout: 'ring', sessions: fixtureA, label: 'ring+same fixture' });
67
+
68
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
69
+ const sourceWired = /data-topo-cluster-count=\{groupBoxes\.length\}/.test(src);
70
+
71
+ const results = {
72
+ grid_returns_3: aGrid.count === '3', // 2 prefix groups + 1 orphan band
73
+ ring_returns_0: aRing.count === '0', // no group boxes in ring layout
74
+ source_wired: sourceWired,
75
+ };
76
+ const ok = Object.values(results).every(Boolean);
77
+ console.log(`${ok ? '✅' : '❌'} R512 cluster-count attr:`, JSON.stringify(results),
78
+ '\n grid:', JSON.stringify(aGrid),
79
+ '\n ring:', JSON.stringify(aRing));
80
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,104 @@
1
+ /* Round 528 verification: brand crescent gains ambient SMIL breath
2
+ * (0.8↔1.0 fill-opacity, 7s, repeat indefinite). 呼吸感 family 4th
3
+ * anchor — symmetric to R519 watermark breath but at 7s vs 6s cadence
4
+ * so the two ambient anchors don't beat together visibly.
5
+ *
6
+ * Test phases:
7
+ * 1. motion enabled: data-topo-brand-canvas-mark-breath='true'
8
+ * + <animate> child mounted on inner <rect>
9
+ * + values='0.8;1;0.8', dur='7s', repeatCount='indefinite'
10
+ * + attributeName='fill-opacity' (NOT 'opacity')
11
+ * 2. reduced-motion: breath attr='false', NO <animate> child
12
+ * 3. wrapper recede + visibility still works (regression check)
13
+ * 4. source-side regex confirms wiring
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
+ async function probe(reducedMotion) {
22
+ const browser = await chromium.launch({ headless: true });
23
+ const ctx = await browser.newContext({
24
+ viewport: { width: 1500, height: 1200 },
25
+ reducedMotion: reducedMotion ? 'reduce' : 'no-preference',
26
+ });
27
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
28
+ await ctx.addInitScript(() => {
29
+ try {
30
+ localStorage.setItem('anet-theme', 'cyber');
31
+ localStorage.setItem('anet-topo-layout', 'ring');
32
+ sessionStorage.setItem('anet_v3_auth', '1');
33
+ } catch {}
34
+ });
35
+ await ctx.route('**/api/hub/status*', async (route) => {
36
+ const r = await route.fetch();
37
+ const b = await r.json();
38
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
39
+ const mk = (alias) => ({
40
+ alias, status: 'idle', model: 'claude-opus-4', runtime: 'claude-code-cli',
41
+ network_id: nid, project_dir: null,
42
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
43
+ });
44
+ await route.fulfill({ response: r, json: { ...b, sessions: [
45
+ mk('alpha·1'), mk('alpha·2'),
46
+ ] } });
47
+ });
48
+ // NO messages → flowLinks=0 → crescent visible
49
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
50
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
51
+ const page = await ctx.newPage();
52
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
53
+ await page.waitForSelector('[data-topo-brand-canvas-mark]', { timeout: 15000 });
54
+ await page.waitForTimeout(800);
55
+ const data = await page.evaluate(() => {
56
+ const wrap = document.querySelector('[data-topo-brand-canvas-mark]');
57
+ const rect = wrap?.querySelector('rect[mask*="canvas-corner-mask"]');
58
+ const animate = rect?.querySelector('animate');
59
+ return {
60
+ breathAttr: wrap?.getAttribute('data-topo-brand-canvas-mark-breath'),
61
+ visibleAttr: wrap?.getAttribute('data-topo-brand-canvas-mark-visible'),
62
+ hasAnimate: !!animate,
63
+ attrName: animate?.getAttribute('attributeName') || null,
64
+ values: animate?.getAttribute('values') || null,
65
+ dur: animate?.getAttribute('dur') || null,
66
+ repeatCount: animate?.getAttribute('repeatCount') || null,
67
+ };
68
+ });
69
+ await browser.close();
70
+ return data;
71
+ }
72
+
73
+ const normal = await probe(false);
74
+ const reduced = await probe(true);
75
+
76
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
77
+ const sourceAnimateGated =
78
+ /\{!reducedMotion && \(\s*<animate attributeName="fill-opacity" values="0\.8;1;0\.8" dur="7s" repeatCount="indefinite" \/>\s*\)\}/.test(src);
79
+ const sourceAttrWired =
80
+ /data-topo-brand-canvas-mark-breath=\{reducedMotion \? 'false' : 'true'\}/.test(src);
81
+
82
+ const results = {
83
+ // motion enabled
84
+ normal_breath_true: normal?.breathAttr === 'true',
85
+ normal_visible_true: normal?.visibleAttr === 'true',
86
+ normal_has_animate: normal?.hasAnimate === true,
87
+ normal_attr_fill_op: normal?.attrName === 'fill-opacity',
88
+ normal_values_correct: normal?.values === '0.8;1;0.8',
89
+ normal_dur_7s: normal?.dur === '7s',
90
+ normal_repeat_indef: normal?.repeatCount === 'indefinite',
91
+ // prefers-reduced-motion
92
+ reduced_breath_false: reduced?.breathAttr === 'false',
93
+ reduced_no_animate: reduced?.hasAnimate === false,
94
+ reduced_visible_true: reduced?.visibleAttr === 'true', // crescent still visible, just no breath
95
+ // source
96
+ source_animate_gated: sourceAnimateGated,
97
+ source_attr_wired: sourceAttrWired,
98
+ };
99
+ const ok = Object.values(results).every(Boolean);
100
+ console.log(`${ok ? '✅' : '❌'} R528 crescent ambient breath:`,
101
+ JSON.stringify(results, null, 2),
102
+ '\n normal:', JSON.stringify(normal),
103
+ '\n reduced:', JSON.stringify(reduced));
104
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,111 @@
1
+ /* Round 526 verification: brand crescent at canvas top-left gains
2
+ * focal-recede (4th anchor in family). Multiplies visible opacity by
3
+ * 0.7 when non-hub canvas surface is hovered.
4
+ *
5
+ * Test phases (flowLinks=0 state — crescent visible):
6
+ * 1. rest: opacity=0.35, recede attr='false', visible='true'
7
+ * 2. hover legend `idle` row: opacity=0.245 (0.35 × 0.7),
8
+ * recede attr='true'
9
+ * 3. mouseleave: opacity returns to 0.35, recede='false'
10
+ * 4. source-side regex confirms multiplicative opacity + attr wired
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: 1200 } });
20
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
21
+ await ctx.addInitScript(() => {
22
+ try {
23
+ localStorage.setItem('anet-theme', 'cyber');
24
+ localStorage.setItem('anet-topo-layout', 'ring');
25
+ sessionStorage.setItem('anet_v3_auth', '1');
26
+ } 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: [
38
+ mk('alpha·1'), mk('alpha·2'), mk('alpha·3'),
39
+ ] } });
40
+ });
41
+ // NO messages → flowLinks.length === 0 → crescent visible at opacity 0.35
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
+ const page = await ctx.newPage();
45
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
46
+ await page.waitForSelector('[data-topo-brand-canvas-mark]', { timeout: 15000 });
47
+ await page.waitForTimeout(800);
48
+
49
+ // Phase 1: rest
50
+ const rest = await page.evaluate(() => {
51
+ const m = document.querySelector('[data-topo-brand-canvas-mark]');
52
+ return {
53
+ opacity: parseFloat(m?.getAttribute('opacity') || '0'),
54
+ visible: m?.getAttribute('data-topo-brand-canvas-mark-visible'),
55
+ recede: m?.getAttribute('data-topo-brand-canvas-mark-recede'),
56
+ };
57
+ });
58
+
59
+ // Phase 2: hover legend `idle` row (sets hoveredStatus → recede)
60
+ await page.hover('[data-legend-row-label="idle"]');
61
+ await page.waitForTimeout(400);
62
+ const hover = await page.evaluate(() => {
63
+ const m = document.querySelector('[data-topo-brand-canvas-mark]');
64
+ return {
65
+ opacity: parseFloat(m?.getAttribute('opacity') || '0'),
66
+ visible: m?.getAttribute('data-topo-brand-canvas-mark-visible'),
67
+ recede: m?.getAttribute('data-topo-brand-canvas-mark-recede'),
68
+ };
69
+ });
70
+
71
+ // Phase 3: mouseleave
72
+ await page.mouse.move(900, 50);
73
+ await page.waitForTimeout(400);
74
+ const leave = await page.evaluate(() => {
75
+ const m = document.querySelector('[data-topo-brand-canvas-mark]');
76
+ return {
77
+ opacity: parseFloat(m?.getAttribute('opacity') || '0'),
78
+ recede: m?.getAttribute('data-topo-brand-canvas-mark-recede'),
79
+ };
80
+ });
81
+
82
+ await browser.close();
83
+
84
+ // Source regex
85
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
86
+ const sourceOpacityWired =
87
+ /opacity=\{\(flowLinks\.length === 0 \? 0\.35 : 0\) \* \(\s*\(hoveredAlias \|\| hoveredEdgeKey \|\| hoveredGroupLabel \|\|\s*hoveredStatus \|\| hoveredVendor\) && !hoveredHub \? 0\.7 : 1\s*\)\}/.test(src);
88
+ const sourceAttrWired =
89
+ /data-topo-brand-canvas-mark-recede=\{\s*\(hoveredAlias \|\| hoveredEdgeKey \|\| hoveredGroupLabel \|\|\s*hoveredStatus \|\| hoveredVendor\) && !hoveredHub \? 'true' : 'false'\s*\}/.test(src);
90
+
91
+ const approxEq = (a, b, tol = 0.001) => Math.abs(a - b) < tol;
92
+
93
+ const results = {
94
+ rest_opacity_035: approxEq(rest?.opacity, 0.35),
95
+ rest_visible_true: rest?.visible === 'true',
96
+ rest_recede_false: rest?.recede === 'false',
97
+ hover_opacity_0245: approxEq(hover?.opacity, 0.245),
98
+ hover_recede_true: hover?.recede === 'true',
99
+ hover_visible_still: hover?.visible === 'true', // crescent should still be visible
100
+ leave_opacity_035: approxEq(leave?.opacity, 0.35),
101
+ leave_recede_false: leave?.recede === 'false',
102
+ source_opacity_wired: sourceOpacityWired,
103
+ source_attr_wired: sourceAttrWired,
104
+ };
105
+ const ok = Object.values(results).every(Boolean);
106
+ console.log(`${ok ? '✅' : '❌'} R526 crescent focal-recede:`,
107
+ JSON.stringify(results, null, 2),
108
+ '\n rest:', JSON.stringify(rest),
109
+ '\n hover:', JSON.stringify(hover),
110
+ '\n leave:', JSON.stringify(leave));
111
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,90 @@
1
+ /* Round 534 verification: edge midpoint badge gains drop-shadow glow on
2
+ * hover/pin (cyan accent), with hover precedence over isHot (amber R480).
3
+ *
4
+ * Test strategy: source-side wiring is canonical because edge hover via
5
+ * Playwright is impractical (topo-panel rect intercepts at SVG root;
6
+ * R48 hitbox is the React handler target but isn't directly hoverable
7
+ * through the panel). Rest-state DOM probe + source regex covers the
8
+ * 4-axis hover-lift parity wiring.
9
+ *
10
+ * Test phases:
11
+ * 1. rest cold: glow attr='false', filter='none', lifted attr='false'
12
+ * (regression check — pre-R534 behavior unchanged at rest)
13
+ * 2. source-side regex confirms:
14
+ * - filter ternary precedence: (hovered||pinned) > isHot > undefined
15
+ * - cyan accent hue (pal.legendAccent) at 99 hex alpha
16
+ * - data-edge-badge-glow 3-value attr ('hover' | 'hot' | 'false')
17
+ * - transition list includes filter
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: 1200 } });
27
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
28
+ await ctx.addInitScript(() => {
29
+ try {
30
+ localStorage.setItem('anet-theme', 'cyber');
31
+ localStorage.setItem('anet-topo-layout', 'ring');
32
+ sessionStorage.setItem('anet_v3_auth', '1');
33
+ } catch {}
34
+ });
35
+ await ctx.route('**/api/hub/status*', async (route) => {
36
+ const r = await route.fetch();
37
+ const b = await r.json();
38
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
39
+ const mk = (alias) => ({
40
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
41
+ network_id: nid, project_dir: null,
42
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
43
+ });
44
+ await route.fulfill({ response: r, json: { ...b, sessions: [mk('a·1'), mk('a·2')] } });
45
+ });
46
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: {
47
+ messages: [{ id: 'm1', from_alias: 'a·1', to_alias: 'a·2', content: 'test', created_at: fresh }]
48
+ } }));
49
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
50
+ const page = await ctx.newPage();
51
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
52
+ await page.waitForSelector('[data-edge-badge-glow]', { timeout: 15000 });
53
+ await page.waitForTimeout(800);
54
+
55
+ const rest = await page.evaluate(() => {
56
+ const el = document.querySelector('[data-edge-badge-glow]');
57
+ if (!el) return null;
58
+ const cs = getComputedStyle(el);
59
+ return {
60
+ glowAttr: el.getAttribute('data-edge-badge-glow'),
61
+ liftedAttr: el.getAttribute('data-edge-badge-lifted'),
62
+ filter: cs.filter,
63
+ transition: cs.transition,
64
+ };
65
+ });
66
+
67
+ await browser.close();
68
+
69
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
70
+ const sourceFilterTernary =
71
+ /filter: \(isHoveredEdge \|\| isPinned\)\s+\? `drop-shadow\(0 0 3px \$\{pal\.legendAccent\}99\)`\s+: isHot\s+\? `drop-shadow\(0 0 3px \$\{hotStroke\}80\)`\s+: undefined,/.test(src);
72
+ const sourceAttrTernary =
73
+ /data-edge-badge-glow=\{\(isHoveredEdge \|\| isPinned\) \? 'hover' : isHot \? 'hot' : 'false'\}/.test(src);
74
+ const sourceTransitionFilter =
75
+ /transition: 'r 180ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, fill 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out'/.test(src);
76
+
77
+ const results = {
78
+ rest_glow_false: rest?.glowAttr === 'false',
79
+ rest_lifted_false: rest?.liftedAttr === 'false',
80
+ rest_filter_none: rest?.filter === 'none' || rest?.filter === '',
81
+ rest_transition_has_filter: /\bfilter\b/.test(rest?.transition || ''),
82
+ source_filter_ternary: sourceFilterTernary,
83
+ source_attr_ternary: sourceAttrTernary,
84
+ source_transition_filter: sourceTransitionFilter,
85
+ };
86
+ const ok = Object.values(results).every(Boolean);
87
+ console.log(`${ok ? '✅' : '❌'} R534 edge-badge hover-glow (source-canonical):`,
88
+ JSON.stringify(results, null, 2),
89
+ '\n rest:', JSON.stringify(rest));
90
+ process.exit(ok ? 0 : 1);