@sleep2agi/agent-network-dashboard 0.5.3-preview.4 → 0.5.3-preview.40

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 (198) 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 +3 -3
  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/0-dmo67sjhl2r.js +4 -0
  145. package/.next/static/chunks/0nxvf-786~0db.js +1 -0
  146. package/.next/static/chunks/0sht0y-y7x71m.css +2 -0
  147. package/.next/static/chunks/{03a4--7ncekmk.js → 0v4-5tng.uh.7.js} +2 -2
  148. package/.next/static/chunks/0vbgjg.1cj_y_.js +1 -0
  149. package/.next/static/chunks/{0qoc2qe1owx6n.js → 13arj242cta57.js} +1 -1
  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 +956 -48
  154. package/app/globals.css +55 -7
  155. package/package.json +4 -4
  156. package/scripts/p157-servers-copy-test.mjs +95 -0
  157. package/scripts/topo-alias-glow-test.mjs +121 -0
  158. package/scripts/topo-avatar-brightness-test.mjs +116 -0
  159. package/scripts/topo-chip-row-press-test.mjs +93 -0
  160. package/scripts/topo-chrome-press-fullstrip-test.mjs +105 -0
  161. package/scripts/topo-chrome-press-scale-test.mjs +100 -0
  162. package/scripts/topo-cluster-count-attr-test.mjs +80 -0
  163. package/scripts/topo-crescent-recede-test.mjs +111 -0
  164. package/scripts/topo-filter-pills-press-test.mjs +96 -0
  165. package/scripts/topo-fleet-density-tier-test.mjs +84 -0
  166. package/scripts/topo-freshness-chip-fade-test.mjs +105 -0
  167. package/scripts/topo-fullscreen-attr-test.mjs +73 -0
  168. package/scripts/topo-grid-content-bottom-attr-test.mjs +72 -0
  169. package/scripts/topo-hub-highlight-amplify-test.mjs +139 -0
  170. package/scripts/topo-hub-highlight-fill-transition-test.mjs +84 -0
  171. package/scripts/topo-hub-highlight-recede-test.mjs +144 -0
  172. package/scripts/topo-hub-highlight-theme-fill-test.mjs +83 -0
  173. package/scripts/topo-hub-idle-breath-test.mjs +109 -0
  174. package/scripts/topo-hub-recede-test.mjs +124 -0
  175. package/scripts/topo-layout-hover-fw-test.mjs +98 -0
  176. package/scripts/topo-legend-count-letter-spacing-test.mjs +108 -0
  177. package/scripts/topo-nodesize-hover-fw-test.mjs +99 -0
  178. package/scripts/topo-orphan-box-dash-test.mjs +89 -0
  179. package/scripts/topo-orphan-fill-opacity-test.mjs +91 -0
  180. package/scripts/topo-orphan-label-italic-test.mjs +90 -0
  181. package/scripts/topo-pinned-aspect-test.mjs +89 -0
  182. package/scripts/topo-pressure-seg-motion-test.mjs +101 -0
  183. package/scripts/topo-recent-hot-pulse-test.mjs +102 -0
  184. package/scripts/topo-recent-more-fw-test.mjs +126 -0
  185. package/scripts/topo-reduced-motion-attr-test.mjs +69 -0
  186. package/scripts/topo-reset-icon-hover-scale-test.mjs +102 -0
  187. package/scripts/topo-starfield-hue-test.mjs +109 -0
  188. package/scripts/topo-vendor-activelinks-press-test.mjs +100 -0
  189. package/scripts/topo-watermark-breath-test.mjs +100 -0
  190. package/scripts/topo-watermark-recede-test.mjs +114 -0
  191. package/scripts/topo-zoom-level-color-test.mjs +105 -0
  192. package/.next/static/chunks/05_0r-qhmggvj.css +0 -2
  193. package/.next/static/chunks/072cwlbf~3a0t.js +0 -1
  194. package/.next/static/chunks/0a~3lmgl2.3sm.js +0 -4
  195. package/.next/static/chunks/0t_.58jc2y.3r.js +0 -1
  196. /package/.next/static/{_cPig_Uyv0NsISAzU7MHR → mKNyup1vhTFSsL90GqtYT}/_buildManifest.js +0 -0
  197. /package/.next/static/{_cPig_Uyv0NsISAzU7MHR → mKNyup1vhTFSsL90GqtYT}/_clientMiddlewareManifest.js +0 -0
  198. /package/.next/static/{_cPig_Uyv0NsISAzU7MHR → mKNyup1vhTFSsL90GqtYT}/_ssgManifest.js +0 -0
@@ -0,0 +1,124 @@
1
+ /* Round 507 verification: hub-center workingCount digit fades to
2
+ * opacity 0.85 when ANY non-hub canvas surface is hovered. Lifts back
3
+ * to 1.0 on un-hover or when hovering the hub itself.
4
+ *
5
+ * Test phases:
6
+ * 1. rest (no hover) — opacity 1, data-topo-hub-recede='false'
7
+ * 2. node hover (synthetic pointerenter on g[data-node]) —
8
+ * opacity 0.85, data-topo-hub-recede='true'
9
+ * 3. release hover — back to opacity 1, recede='false'
10
+ *
11
+ * Source-side regex confirms gate + style + attr wired.
12
+ */
13
+ import { chromium } from 'playwright';
14
+ import { readFileSync } from 'node:fs';
15
+
16
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
17
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
18
+
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(() => {
23
+ try {
24
+ localStorage.setItem('anet-theme', 'cyber');
25
+ localStorage.setItem('anet-topo-layout', 'ring');
26
+ sessionStorage.setItem('anet_v3_auth', '1');
27
+ } 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, 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: [
39
+ mk('alpha·a1', 'working'),
40
+ mk('alpha·a2', 'idle'),
41
+ ] } });
42
+ });
43
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
44
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
45
+ const page = await ctx.newPage();
46
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
47
+ await page.waitForSelector('[data-topo-hub-working-count-glow]', { timeout: 15000 });
48
+ await page.waitForTimeout(1500);
49
+
50
+ // Phase 1: rest state
51
+ const rest = await page.evaluate(() => {
52
+ const el = document.querySelector('[data-topo-hub-working-count-glow]');
53
+ if (!el) return null;
54
+ return {
55
+ recede_attr: el.getAttribute('data-topo-hub-recede'),
56
+ opacity: window.getComputedStyle(el).opacity,
57
+ };
58
+ });
59
+
60
+ // Phase 2: hover a node via R488 banked synthetic dispatch
61
+ await page.evaluate(() => {
62
+ const g = document.querySelector('g[data-node]');
63
+ if (!g) return;
64
+ const target = g.querySelector('circle, image, rect') || g;
65
+ ['pointerenter', 'pointerover', 'mouseenter', 'mouseover'].forEach((t) => {
66
+ target.dispatchEvent(new Event(t, { bubbles: true, cancelable: true }));
67
+ });
68
+ });
69
+ await page.waitForTimeout(500);
70
+ const hover = await page.evaluate(() => {
71
+ const el = document.querySelector('[data-topo-hub-working-count-glow]');
72
+ if (!el) return null;
73
+ return {
74
+ recede_attr: el.getAttribute('data-topo-hub-recede'),
75
+ opacity: window.getComputedStyle(el).opacity,
76
+ };
77
+ });
78
+
79
+ // Phase 3: release hover
80
+ await page.evaluate(() => {
81
+ const g = document.querySelector('g[data-node]');
82
+ if (!g) return;
83
+ const target = g.querySelector('circle, image, rect') || g;
84
+ ['pointerleave', 'pointerout', 'mouseleave', 'mouseout'].forEach((t) => {
85
+ target.dispatchEvent(new Event(t, { bubbles: true, cancelable: true }));
86
+ });
87
+ });
88
+ await page.waitForTimeout(500);
89
+ const release = await page.evaluate(() => {
90
+ const el = document.querySelector('[data-topo-hub-working-count-glow]');
91
+ if (!el) return null;
92
+ return {
93
+ recede_attr: el.getAttribute('data-topo-hub-recede'),
94
+ opacity: window.getComputedStyle(el).opacity,
95
+ };
96
+ });
97
+
98
+ await browser.close();
99
+
100
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
101
+ const sourceGateWired = /\(hoveredAlias \|\| hoveredEdgeKey \|\| hoveredGroupLabel \|\|\s*hoveredStatus \|\| hoveredVendor\) && !hoveredHub/.test(src);
102
+ const sourceOpacity = /opacity: \(hoveredAlias \|\| hoveredEdgeKey \|\| hoveredGroupLabel/.test(src);
103
+ const sourceAttr = /data-topo-hub-recede=\{/.test(src);
104
+
105
+ // computed opacity comes back as a number-string ('1', '0.85')
106
+ const approxEq = (a, b) => Math.abs(parseFloat(a) - b) < 0.01;
107
+
108
+ const results = {
109
+ rest_recede_false: rest && rest.recede_attr === 'false',
110
+ rest_opacity_1: rest && approxEq(rest.opacity, 1),
111
+ hover_recede_true: hover && hover.recede_attr === 'true',
112
+ hover_opacity_85: hover && approxEq(hover.opacity, 0.85),
113
+ release_recede_false: release && release.recede_attr === 'false',
114
+ release_opacity_1: release && approxEq(release.opacity, 1),
115
+ source_gate_wired: sourceGateWired,
116
+ source_opacity_wired: sourceOpacity,
117
+ source_attr_wired: sourceAttr,
118
+ };
119
+ const ok = Object.values(results).every(Boolean);
120
+ console.log(`${ok ? '✅' : '❌'} R507 hub-recede:`, JSON.stringify(results),
121
+ '\n rest:', JSON.stringify(rest),
122
+ '\n hover:', JSON.stringify(hover),
123
+ '\n release:', JSON.stringify(release));
124
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,98 @@
1
+ /* Round 522 verification: chrome Ring/Grid layout toggle's inactive
2
+ * variant gains `hover:font-medium` typography preview. Closes chrome
3
+ * toggle group typography hover-fw family at the last remaining toggle.
4
+ *
5
+ * Test phases:
6
+ * 1. default (layout=ring): Ring active fw=500, Grid inactive fw=400
7
+ * + Grid data-attr = '500' (preview target)
8
+ * + Ring data-attr = null
9
+ * 2. hover Grid (inactive): fontWeight → 500 (preview lands)
10
+ * 3. source-side regex confirms hover:font-medium + transition wired
11
+ * for both buttons
12
+ */
13
+ import { chromium } from 'playwright';
14
+ import { readFileSync } from 'node:fs';
15
+
16
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
17
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
18
+
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(() => {
23
+ try {
24
+ localStorage.setItem('anet-theme', 'cyber');
25
+ localStorage.setItem('anet-topo-layout', 'ring');
26
+ sessionStorage.setItem('anet_v3_auth', '1');
27
+ } 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('a·1')] } });
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('[data-topo-chrome-layout]', { timeout: 15000 });
45
+ await page.waitForTimeout(800);
46
+
47
+ const buttons = await page.evaluate(() => {
48
+ const els = Array.from(document.querySelectorAll('[data-topo-chrome-layout]'));
49
+ return els.map((el) => ({
50
+ label: el.getAttribute('data-topo-chrome-layout'),
51
+ active: el.getAttribute('data-topo-chrome-layout-active'),
52
+ previewAttr: el.getAttribute('data-topo-chrome-layout-hover-preview-fw'),
53
+ fw: getComputedStyle(el).fontWeight,
54
+ }));
55
+ });
56
+
57
+ // Phase 2: hover the inactive (Grid since layout=ring default)
58
+ await page.hover('[data-topo-chrome-layout="grid"]');
59
+ await page.waitForTimeout(350);
60
+ const gridHoverFw = await page.evaluate(() =>
61
+ getComputedStyle(document.querySelector('[data-topo-chrome-layout="grid"]')).fontWeight
62
+ );
63
+
64
+ await browser.close();
65
+
66
+ // Source regex
67
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
68
+ const sourceWiredClassRing = /'ring' \? '.*font-medium.*' : '.*hover:font-medium'.*\$\{chromePopping === 'layout-ring'/.test(src);
69
+ const sourceWiredClassGrid = /'grid' \? '.*font-medium.*' : '.*hover:font-medium'.*\$\{chromePopping === 'layout-grid'/.test(src);
70
+ const sourceWiredAttrRing = /data-topo-chrome-layout-hover-preview-fw=\{layout === 'ring' \? null : '500'\}/.test(src);
71
+ const sourceWiredAttrGrid = /data-topo-chrome-layout-hover-preview-fw=\{layout === 'grid' \? null : '500'\}/.test(src);
72
+ const sourceWiredTransRing = /transition: '.*font-weight 150ms ease'.*\n.*Ring/.test(src) ||
73
+ /font-weight 150ms ease'\s*\}\}\s*>\s*\n\s*Ring/.test(src);
74
+ const sourceWiredTransGrid = /borderColor:.*containerBorder.*font-weight 150ms ease/.test(src);
75
+
76
+ const ring = buttons.find(b => b.label === 'ring');
77
+ const grid = buttons.find(b => b.label === 'grid');
78
+
79
+ const results = {
80
+ ring_active_true: ring?.active === 'true',
81
+ ring_active_fw_500: ring?.fw === '500',
82
+ ring_no_preview_attr: ring?.previewAttr === null,
83
+ grid_active_false: grid?.active === 'false',
84
+ grid_inactive_fw_400: grid?.fw === '400',
85
+ grid_preview_attr_500: grid?.previewAttr === '500',
86
+ grid_hover_fw_500: gridHoverFw === '500',
87
+ source_class_ring: sourceWiredClassRing,
88
+ source_class_grid: sourceWiredClassGrid,
89
+ source_attr_ring: sourceWiredAttrRing,
90
+ source_attr_grid: sourceWiredAttrGrid,
91
+ source_trans_grid: sourceWiredTransGrid,
92
+ };
93
+ const ok = Object.values(results).every(Boolean);
94
+ console.log(`${ok ? '✅' : '❌'} R522 layout hover-fw preview:`,
95
+ JSON.stringify(results, null, 2),
96
+ '\n buttons:', JSON.stringify(buttons),
97
+ '\n grid hover fw:', gridHoverFw);
98
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,108 @@
1
+ /* Round 518 verification: legend-row COUNT digit gains the same 3-tier
2
+ * hover-letter-spacing tween R433 added to the LABEL — symmetric row
3
+ * hover gesture. Also lifts transition list 150 → 200ms closing R475
4
+ * panel-row cadence at the count surface.
5
+ *
6
+ * Test phases:
7
+ * 1. rest: letter-spacing 0px, attr='0px', transition includes '200ms'
8
+ * 2. hover: letter-spacing 0.25px, attr='0.25px'
9
+ * 3. source-side regex confirms 3-tier wiring + 200ms cadence
10
+ *
11
+ * Pin tier (0.5px) requires real click + filter-pin localStorage —
12
+ * skip and rely on source-regex for the pinned tier.
13
+ */
14
+ import { chromium } from 'playwright';
15
+ import { readFileSync } from 'node:fs';
16
+
17
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
18
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
19
+
20
+ const browser = await chromium.launch({ headless: true });
21
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
22
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
23
+ await ctx.addInitScript(() => {
24
+ try {
25
+ localStorage.setItem('anet-theme', 'cyber');
26
+ localStorage.setItem('anet-topo-layout', 'ring');
27
+ sessionStorage.setItem('anet_v3_auth', '1');
28
+ } catch {}
29
+ });
30
+ await ctx.route('**/api/hub/status*', async (route) => {
31
+ const r = await route.fetch();
32
+ const b = await r.json();
33
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
34
+ const mk = (alias, status) => ({
35
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
36
+ network_id: nid, project_dir: null,
37
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
38
+ });
39
+ await route.fulfill({ response: r, json: { ...b, sessions: [
40
+ mk('a·1', 'idle'),
41
+ mk('a·2', 'idle'),
42
+ mk('b·1', 'working'),
43
+ ] } });
44
+ });
45
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
46
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
47
+ const page = await ctx.newPage();
48
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
49
+ await page.waitForSelector('[data-legend-count]', { timeout: 15000 });
50
+ await page.waitForTimeout(800);
51
+
52
+ // Pick the 'online' row's count (it'll have count > 0 so we see typography)
53
+ const targetSel = '[data-legend-count="idle"]';
54
+
55
+ // Phase 1: rest
56
+ const rest = await page.evaluate((sel) => {
57
+ const el = document.querySelector(sel);
58
+ if (!el) return null;
59
+ const cs = getComputedStyle(el);
60
+ return {
61
+ attrLS: el.getAttribute('data-legend-count-letter-spacing'),
62
+ attrTrans: el.getAttribute('data-legend-count-transition'),
63
+ letterSpacing: cs.letterSpacing,
64
+ transition: cs.transition,
65
+ };
66
+ }, targetSel);
67
+
68
+ // Phase 2: hover the row's hitbox (the parent g — but row hover is keyed
69
+ // on hoveredStatus which is set by mouseEnter on the row-hitbox rect.
70
+ // Easier: hover the LABEL text (also part of the row) and observe count.
71
+ await page.hover('[data-legend-row-label="idle"]');
72
+ await page.waitForTimeout(350);
73
+ const hover = await page.evaluate((sel) => {
74
+ const el = document.querySelector(sel);
75
+ if (!el) return null;
76
+ const cs = getComputedStyle(el);
77
+ return {
78
+ attrLS: el.getAttribute('data-legend-count-letter-spacing'),
79
+ letterSpacing: cs.letterSpacing,
80
+ };
81
+ }, targetSel);
82
+
83
+ await browser.close();
84
+
85
+ // Phase 3: source regex — verifies all 3 tiers + 200ms cadence
86
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
87
+ const sourceWiredLS =
88
+ /letterSpacing: isPinned \? '0\.5px' :\s*hoveredStatus === row\.key \? '0\.25px' : '0px',/.test(src);
89
+ const sourceWiredAttr =
90
+ /data-legend-count-letter-spacing=\{isPinned \? '0\.5px' : hoveredStatus === row\.key \? '0\.25px' : '0px'\}/.test(src);
91
+ const sourceWiredCadence =
92
+ /transition: 'opacity 200ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, letter-spacing 200ms ease-out',/.test(src);
93
+
94
+ const results = {
95
+ rest_attr_0: rest?.attrLS === '0px',
96
+ rest_attr_transition_200: rest?.attrTrans === '200ms',
97
+ rest_letter_spacing_0: rest?.letterSpacing === '0px' || rest?.letterSpacing === 'normal',
98
+ rest_transition_200ms: /(200ms|0\.2s)/.test(rest?.transition || ''),
99
+ hover_attr_025: hover?.attrLS === '0.25px',
100
+ hover_letter_spacing_025: hover?.letterSpacing === '0.25px',
101
+ source_3_tier_ls: sourceWiredLS,
102
+ source_attr_3_tier: sourceWiredAttr,
103
+ source_200ms_cadence: sourceWiredCadence,
104
+ };
105
+ const ok = Object.values(results).every(Boolean);
106
+ console.log(`${ok ? '✅' : '❌'} R518 legend-count 3-tier letter-spacing + 200ms:`, JSON.stringify(results, null, 2),
107
+ '\n rest:', JSON.stringify(rest), '\n hover:', JSON.stringify(hover));
108
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,99 @@
1
+ /* Round 521 verification: chrome nodeSize S/M/L inactive variant gains
2
+ * hover-fw preview — fw 400 (default) → 500 (font-medium, matches active
3
+ * variant). Extends R270 hover-preview idiom to the typography axis.
4
+ *
5
+ * Test phases:
6
+ * 1. inactive rest: fontWeight = 400, data-attr = '500' (preview target)
7
+ * 2. inactive hover: fontWeight = 500 (preview lands at active fw)
8
+ * 3. active rest: fontWeight = 500 (existing font-medium class)
9
+ * 4. source-side regex confirms hover:font-medium class + attr wiring
10
+ */
11
+ import { chromium } from 'playwright';
12
+ import { readFileSync } from 'node:fs';
13
+
14
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
15
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
16
+
17
+ const browser = await chromium.launch({ headless: true });
18
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
19
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
20
+ await ctx.addInitScript(() => {
21
+ try {
22
+ localStorage.setItem('anet-theme', 'cyber');
23
+ localStorage.setItem('anet-topo-layout', 'ring');
24
+ sessionStorage.setItem('anet_v3_auth', '1');
25
+ } catch {}
26
+ });
27
+ await ctx.route('**/api/hub/status*', async (route) => {
28
+ const r = await route.fetch();
29
+ const b = await r.json();
30
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
31
+ const mk = (alias) => ({
32
+ alias, status: 'idle', model: 'claude-opus-4', runtime: '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: [mk('a·1')] } });
37
+ });
38
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
39
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
40
+ const page = await ctx.newPage();
41
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
42
+ await page.waitForSelector('[data-topo-chrome-nodesize]', { timeout: 15000 });
43
+ await page.waitForTimeout(800);
44
+
45
+ // Find the inactive + active buttons via attrs
46
+ const buttons = await page.evaluate(() => {
47
+ const els = Array.from(document.querySelectorAll('[data-topo-chrome-nodesize]'));
48
+ return els.map((el) => ({
49
+ label: el.getAttribute('data-topo-chrome-nodesize'),
50
+ active: el.getAttribute('data-topo-chrome-nodesize-active'),
51
+ previewAttr: el.getAttribute('data-topo-chrome-nodesize-hover-preview-fw'),
52
+ fw: getComputedStyle(el).fontWeight,
53
+ }));
54
+ });
55
+
56
+ // Find an inactive one. Default nodeScale is M (1) so S + L should be inactive.
57
+ const inactiveLabel = buttons.find(b => b.active === 'false')?.label;
58
+ if (!inactiveLabel) {
59
+ console.log('❌ R521 — no inactive nodeSize button found');
60
+ process.exit(1);
61
+ }
62
+ const inactiveSel = `[data-topo-chrome-nodesize="${inactiveLabel}"]`;
63
+
64
+ // Phase 2: hover the inactive button
65
+ await page.hover(inactiveSel);
66
+ await page.waitForTimeout(350);
67
+ const hoverFw = await page.evaluate((sel) => getComputedStyle(document.querySelector(sel)).fontWeight, inactiveSel);
68
+
69
+ // Phase 3: find active button
70
+ const active = buttons.find(b => b.active === 'true');
71
+
72
+ await browser.close();
73
+
74
+ // Phase 4: source regex
75
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
76
+ const sourceWiredClass = /hover:bg-cyan-500\/5 active:bg-cyan-500\/15 hover:font-medium/.test(src);
77
+ const sourceWiredAttr =
78
+ /data-topo-chrome-nodesize-hover-preview-fw=\{nodeScale === v \? null : '500'\}/.test(src);
79
+ const sourceWiredTransition = /transition-\[font-weight\]/.test(src);
80
+
81
+ const inactiveRest = buttons.find(b => b.label === inactiveLabel);
82
+
83
+ const results = {
84
+ inactive_rest_fw_400: inactiveRest?.fw === '400',
85
+ inactive_rest_attr_500: inactiveRest?.previewAttr === '500',
86
+ inactive_active_attr_false: inactiveRest?.active === 'false',
87
+ inactive_hover_fw_500: hoverFw === '500',
88
+ active_rest_fw_500: active?.fw === '500',
89
+ active_no_preview_attr: active?.previewAttr === null || active?.previewAttr === undefined,
90
+ source_class_wired: sourceWiredClass,
91
+ source_attr_wired: sourceWiredAttr,
92
+ source_transition_wired: sourceWiredTransition,
93
+ };
94
+ const ok = Object.values(results).every(Boolean);
95
+ console.log(`${ok ? '✅' : '❌'} R521 nodeSize hover-fw preview:`,
96
+ JSON.stringify(results, null, 2),
97
+ '\n buttons:', JSON.stringify(buttons),
98
+ '\n inactive(', inactiveLabel, ')hover fw:', hoverFw);
99
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,89 @@
1
+ /* Round 503 verification: orphan band rect (data-group-box-orphan="true")
2
+ * uses strokeDasharray='3 6' at rest, while prefix-group bands keep
3
+ * '6 6'. Pinned/hovered orphan still gets 'none' (solid stroke) — the
4
+ * differentiation lives ONLY in the rest state.
5
+ *
6
+ * Category-differentiation family 2nd anchor:
7
+ * R499 orphan label fontStyle: italic (1st anchor, R499)
8
+ * R503 orphan rect '3 6' dash pattern (2nd anchor, this round)
9
+ *
10
+ * Fixture: 2 prefix groups (alpha×3, beta×2) + 3 orphans (zeta/omega/lonely)
11
+ * — same shape as R499 test. Drives #150 orphan-band creation with
12
+ * prefix groups for differential assertion.
13
+ */
14
+ import { chromium } from 'playwright';
15
+ import { readFileSync } from 'node:fs';
16
+
17
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
18
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
19
+
20
+ const browser = await chromium.launch({ headless: true });
21
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
22
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
23
+ await ctx.addInitScript(() => {
24
+ try {
25
+ localStorage.setItem('anet-theme', 'cyber');
26
+ localStorage.setItem('anet-topo-layout', 'grid');
27
+ sessionStorage.setItem('anet_v3_auth', '1');
28
+ } catch {}
29
+ });
30
+ await ctx.route('**/api/hub/status*', async (route) => {
31
+ const r = await route.fetch();
32
+ const b = await r.json();
33
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
34
+ const mk = (alias, status) => ({
35
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
36
+ network_id: nid, project_dir: null,
37
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
38
+ });
39
+ await route.fulfill({ response: r, json: { ...b, sessions: [
40
+ mk('alpha·1', 'working'), mk('alpha·2', 'idle'), mk('alpha·3', 'idle'),
41
+ mk('beta·1', 'working'), mk('beta·2', 'idle'),
42
+ mk('zeta', 'idle'),
43
+ mk('omega', 'idle'),
44
+ mk('lonely', 'idle'),
45
+ ] } });
46
+ });
47
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
48
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
49
+ const page = await ctx.newPage();
50
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
51
+ await page.waitForSelector('[data-group-box-orphan]', { timeout: 15000 });
52
+ await page.waitForTimeout(1500);
53
+
54
+ const rects = await page.evaluate(() => {
55
+ const els = Array.from(document.querySelectorAll('[data-group-box-orphan]'));
56
+ return els.map((el) => {
57
+ const cs = window.getComputedStyle(el);
58
+ return {
59
+ orphan_attr: el.getAttribute('data-group-box-orphan'),
60
+ dasharray: el.getAttribute('stroke-dasharray') || cs.strokeDasharray || '',
61
+ };
62
+ });
63
+ });
64
+
65
+ await browser.close();
66
+
67
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
68
+ const sourceTernary = /\(isPinned \|\| isHovered\) \? 'none' :\s*box\.isOrphan \? '3 6' : '6 6'/.test(src);
69
+ const sourceAttr = /data-group-box-orphan=\{box\.isOrphan \? 'true' : 'false'\}/.test(src);
70
+
71
+ const orphanRect = rects.find((r) => r.orphan_attr === 'true');
72
+ const prefixRects = rects.filter((r) => r.orphan_attr === 'false');
73
+
74
+ // Computed strokeDasharray comes back as 'px-suffixed' or raw — normalize
75
+ const normalize = (s) => (s || '').replace(/px/g, '').replace(/,\s*/g, ' ').trim();
76
+
77
+ const results = {
78
+ rects_found: rects.length >= 2,
79
+ orphan_rect_present: !!orphanRect,
80
+ orphan_dash_3_6: orphanRect && (normalize(orphanRect.dasharray) === '3 6' || normalize(orphanRect.dasharray) === '3 6'),
81
+ prefix_rects_present: prefixRects.length >= 1,
82
+ prefix_dash_6_6: prefixRects.length > 0 && prefixRects.every((r) => normalize(r.dasharray) === '6 6'),
83
+ source_ternary_wired: sourceTernary,
84
+ source_attr_wired: sourceAttr,
85
+ };
86
+ const ok = Object.values(results).every(Boolean);
87
+ console.log(`${ok ? '✅' : '❌'} R503 orphan box dash:`, JSON.stringify(results),
88
+ '\n rects:', JSON.stringify(rects));
89
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,91 @@
1
+ /* Round 506 verification: orphan band rect rest-state fillOpacity
2
+ * drops slightly below prefix-group rest (0.025/0.045 → 0.015/0.028).
3
+ * Category-differentiation family 3rd anchor.
4
+ *
5
+ * R499 orphan label fontStyle italic (1st anchor)
6
+ * R503 orphan rect '3 6' dasharray (2nd anchor)
7
+ * R506 orphan rect lower fillOpacity (3rd anchor)
8
+ *
9
+ * Pin and hover branches UNCHANGED (full inspection affordance).
10
+ * Differentiation lives ONLY in the rest state.
11
+ *
12
+ * Fixture: 2 prefix groups (alpha×3 + beta×2) + 3 orphans (zeta/omega/
13
+ * lonely) — same shape as R499/R503 tests for cross-round consistency.
14
+ * Cyber theme drives the 0.045 (prefix) vs 0.028 (orphan) comparison.
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', 'grid');
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) => ({
37
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
38
+ network_id: nid, project_dir: null,
39
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
40
+ });
41
+ await route.fulfill({ response: r, json: { ...b, sessions: [
42
+ mk('alpha·1', 'working'), mk('alpha·2', 'idle'), mk('alpha·3', 'idle'),
43
+ mk('beta·1', 'working'), mk('beta·2', 'idle'),
44
+ mk('zeta', 'idle'),
45
+ mk('omega', 'idle'),
46
+ mk('lonely', 'idle'),
47
+ ] } });
48
+ });
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-group-box-orphan]', { timeout: 15000 });
54
+ await page.waitForTimeout(1500);
55
+
56
+ const rects = await page.evaluate(() => {
57
+ const els = Array.from(document.querySelectorAll('[data-group-box-orphan]'));
58
+ return els.map((el) => ({
59
+ orphan_attr: el.getAttribute('data-group-box-orphan'),
60
+ fill_opacity_attr: el.getAttribute('data-group-box-fill-opacity'),
61
+ fill_opacity_dom: el.getAttribute('fill-opacity'),
62
+ }));
63
+ });
64
+
65
+ await browser.close();
66
+
67
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
68
+ const sourceTernary = /box\.isOrphan \? \(isLight \? 0\.015 : 0\.028\)\s*:\s*\(isLight \? 0\.025 : 0\.045\)/.test(src);
69
+ const sourceAttr = /data-group-box-fill-opacity=\{[\s\S]*?box\.isOrphan \? \(isLight \? 0\.015 : 0\.028\)/.test(src);
70
+
71
+ const orphanRect = rects.find((r) => r.orphan_attr === 'true');
72
+ const prefixRects = rects.filter((r) => r.orphan_attr === 'false');
73
+
74
+ // Cyber theme: prefix = 0.045, orphan = 0.028
75
+ const approxEq = (a, b) => Math.abs(parseFloat(a) - b) < 1e-9;
76
+
77
+ const results = {
78
+ rects_found: rects.length >= 2,
79
+ orphan_rect_present: !!orphanRect,
80
+ orphan_fill_028: orphanRect && approxEq(orphanRect.fill_opacity_attr, 0.028),
81
+ orphan_dom_fill_028: orphanRect && approxEq(orphanRect.fill_opacity_dom, 0.028),
82
+ prefix_rects_present: prefixRects.length >= 1,
83
+ prefix_fill_045: prefixRects.length > 0 && prefixRects.every((r) => approxEq(r.fill_opacity_attr, 0.045)),
84
+ prefix_dom_fill_045: prefixRects.length > 0 && prefixRects.every((r) => approxEq(r.fill_opacity_dom, 0.045)),
85
+ source_ternary_wired: sourceTernary,
86
+ source_attr_wired: sourceAttr,
87
+ };
88
+ const ok = Object.values(results).every(Boolean);
89
+ console.log(`${ok ? '✅' : '❌'} R506 orphan fill-opacity:`, JSON.stringify(results),
90
+ '\n rects:', JSON.stringify(rects));
91
+ process.exit(ok ? 0 : 1);