@sleep2agi/agent-network-dashboard 0.5.1-preview.91 → 0.5.1-preview.93

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 (141) 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 +4 -4
  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.html +1 -1
  13. package/.next/server/app/_not-found.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  20. package/.next/server/app/admin.html +1 -1
  21. package/.next/server/app/admin.rsc +1 -1
  22. package/.next/server/app/admin.segments/_full.segment.rsc +1 -1
  23. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  24. package/.next/server/app/admin.segments/_index.segment.rsc +1 -1
  25. package/.next/server/app/admin.segments/_tree.segment.rsc +1 -1
  26. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  27. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  28. package/.next/server/app/index.html +2 -2
  29. package/.next/server/app/index.rsc +2 -2
  30. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  31. package/.next/server/app/index.segments/_full.segment.rsc +2 -2
  32. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  33. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  34. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  35. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/login.html +2 -2
  37. package/.next/server/app/login.rsc +2 -2
  38. package/.next/server/app/login.segments/_full.segment.rsc +2 -2
  39. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  40. package/.next/server/app/login.segments/_index.segment.rsc +1 -1
  41. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  42. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  43. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  44. package/.next/server/app/logs.html +1 -1
  45. package/.next/server/app/logs.rsc +1 -1
  46. package/.next/server/app/logs.segments/_full.segment.rsc +1 -1
  47. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  48. package/.next/server/app/logs.segments/_index.segment.rsc +1 -1
  49. package/.next/server/app/logs.segments/_tree.segment.rsc +1 -1
  50. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  51. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  52. package/.next/server/app/messages.html +1 -1
  53. package/.next/server/app/messages.rsc +1 -1
  54. package/.next/server/app/messages.segments/_full.segment.rsc +1 -1
  55. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  56. package/.next/server/app/messages.segments/_index.segment.rsc +1 -1
  57. package/.next/server/app/messages.segments/_tree.segment.rsc +1 -1
  58. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  59. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  60. package/.next/server/app/node.html +1 -1
  61. package/.next/server/app/node.rsc +1 -1
  62. package/.next/server/app/node.segments/_full.segment.rsc +1 -1
  63. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  64. package/.next/server/app/node.segments/_index.segment.rsc +1 -1
  65. package/.next/server/app/node.segments/_tree.segment.rsc +1 -1
  66. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  67. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  68. package/.next/server/app/nodes.html +1 -1
  69. package/.next/server/app/nodes.rsc +1 -1
  70. package/.next/server/app/nodes.segments/_full.segment.rsc +1 -1
  71. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  72. package/.next/server/app/nodes.segments/_index.segment.rsc +1 -1
  73. package/.next/server/app/nodes.segments/_tree.segment.rsc +1 -1
  74. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
  75. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  76. package/.next/server/app/page_client-reference-manifest.js +1 -1
  77. package/.next/server/app/server-logs.html +1 -1
  78. package/.next/server/app/server-logs.rsc +1 -1
  79. package/.next/server/app/server-logs.segments/_full.segment.rsc +1 -1
  80. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  81. package/.next/server/app/server-logs.segments/_index.segment.rsc +1 -1
  82. package/.next/server/app/server-logs.segments/_tree.segment.rsc +1 -1
  83. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
  84. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  85. package/.next/server/app/settings/networks.html +1 -1
  86. package/.next/server/app/settings/networks.rsc +1 -1
  87. package/.next/server/app/settings/networks.segments/_full.segment.rsc +1 -1
  88. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  89. package/.next/server/app/settings/networks.segments/_index.segment.rsc +1 -1
  90. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +1 -1
  91. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
  92. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  93. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  94. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app/settings/tokens.html +1 -1
  96. package/.next/server/app/settings/tokens.rsc +1 -1
  97. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +1 -1
  98. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  99. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +1 -1
  100. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +1 -1
  101. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
  102. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  103. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  104. package/.next/server/app/settings.html +2 -2
  105. package/.next/server/app/settings.rsc +2 -2
  106. package/.next/server/app/settings.segments/_full.segment.rsc +2 -2
  107. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  108. package/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  109. package/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  110. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  111. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  112. package/.next/server/app/tasks.html +1 -1
  113. package/.next/server/app/tasks.rsc +1 -1
  114. package/.next/server/app/tasks.segments/_full.segment.rsc +1 -1
  115. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  116. package/.next/server/app/tasks.segments/_index.segment.rsc +1 -1
  117. package/.next/server/app/tasks.segments/_tree.segment.rsc +1 -1
  118. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  119. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  120. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +2 -2
  121. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  122. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  123. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  124. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  125. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  126. package/.next/server/middleware-build-manifest.js +3 -3
  127. package/.next/server/pages/404.html +1 -1
  128. package/.next/server/pages/500.html +1 -1
  129. package/.next/static/chunks/{07t9_p5h7da1u.js → 041ttud2lil23.js} +1 -1
  130. package/.next/static/chunks/{10qa7z9iocn6t.js → 05op0gznd9-l~.js} +1 -1
  131. package/.next/static/chunks/0nra4xa8ew8dz.js +4 -0
  132. package/.next/trace +2 -2
  133. package/.next/trace-build +1 -1
  134. package/app/components/TopoGraph.tsx +44 -2
  135. package/package.json +1 -1
  136. package/scripts/topo-edge-badge-letter-spacing-test.mjs +86 -0
  137. package/scripts/topo-group-label-letter-spacing-test.mjs +113 -0
  138. package/.next/static/chunks/0ahff_xzbgbg4.js +0 -4
  139. /package/.next/static/{i0drwZtW8h-M1ML2C5VZF → GX6--A50f5YLDtRqrY8Mi}/_buildManifest.js +0 -0
  140. /package/.next/static/{i0drwZtW8h-M1ML2C5VZF → GX6--A50f5YLDtRqrY8Mi}/_clientMiddlewareManifest.js +0 -0
  141. /package/.next/static/{i0drwZtW8h-M1ML2C5VZF → GX6--A50f5YLDtRqrY8Mi}/_ssgManifest.js +0 -0
@@ -4416,6 +4416,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4416
4416
  matches R142 fill timing so all the group-label
4417
4417
  state-flip channels (fill colour, rect stroke,
4418
4418
  rect drop-shadow, label tracking) ease as one. */}
4419
+ {/* Round 432 / Loop: extend the group-label letter-
4420
+ spacing tween from 2-tier (rest/pin) to 3-tier
4421
+ (rest/hover/pin → 0/0.25/0.5). Pre-R432 R218
4422
+ spread the text only on pin; hover got an
4423
+ R63 fill brighten (legendText → legendHeadline)
4424
+ but no typographic axis of its own. R432 adds
4425
+ the missing mid tier so hover telegraphs through
4426
+ BOTH the fill brighten AND a subtle kerning
4427
+ spread — sibling pattern to R427 node-alias
4428
+ (0/0.3/0.5) and R431 edge-badge (0/0.2/0.4) at
4429
+ group-label scope. Pin tier (0.5) still wins.
4430
+ Subtler mid tier (0.25 vs alias 0.3) because the
4431
+ group label is a structural anchor — too much
4432
+ spread would steal weight from the per-node
4433
+ alias identity it groups. Hover-letter-spacing
4434
+ family extension (8 anchors now):
4435
+ R344 chip count digit
4436
+ R345 panel title
4437
+ R347 active-links chip
4438
+ R351 vendor chip
4439
+ R420 zoom-level chip
4440
+ R427 node alias text
4441
+ R431 edge-badge digit
4442
+ R432 group label text (this round)
4443
+ R218 transition list ('fill 200ms, letter-spacing
4444
+ 200ms') untouched — additive conditional case. */}
4419
4445
  <text
4420
4446
  x={box.x + 12}
4421
4447
  y={box.y + 14}
@@ -4423,9 +4449,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4423
4449
  fontSize="13"
4424
4450
  fontFamily="monospace"
4425
4451
  fontWeight="700"
4452
+ data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}
4426
4453
  style={{
4427
4454
  transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
4428
- letterSpacing: isPinned ? '0.5px' : '0px',
4455
+ letterSpacing: isPinned ? '0.5px' :
4456
+ isHovered ? '0.25px' : '0px',
4429
4457
  }}
4430
4458
  data-group-label={box.key}
4431
4459
  data-group-label-pinned={isPinned ? 'true' : 'false'}
@@ -5453,7 +5481,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5453
5481
  style={{
5454
5482
  pointerEvents: 'none',
5455
5483
  fontVariantNumeric: 'tabular-nums',
5456
- letterSpacing: (isPinned || isHot) ? '0.4px' : '0px',
5484
+ /* R431 edge-badge digit 3-tier letter-spacing:
5485
+ rest 0 / isHoveredEdge 0.2 / (isPinned || isHot) 0.4.
5486
+ Mirrors R427 node-alias 3-tier (rest/hover/chat-
5487
+ target → 0/0.3/0.5) at edge-badge scope. Pre-R431
5488
+ letter-spacing only fired on pin/hot (R220) while
5489
+ pure edge hover lifted stroke (R394) + opacity
5490
+ (R395) + radius (R164) but left the text dead-
5491
+ typographic. R431 adds the missing typographic
5492
+ spacing axis to the edge-hover gesture so the
5493
+ text rises with the badge geometry. Pin/hot
5494
+ tier (0.4) still wins; hover is the mid step.
5495
+ Hover-letter-spacing family extension (7 anchors
5496
+ now): R344/R345/R347/R351/R420/R427/R431. */
5497
+ letterSpacing: (isPinned || isHot) ? '0.4px' :
5498
+ isHoveredEdge ? '0.2px' : '0px',
5457
5499
  transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out',
5458
5500
  }}
5459
5501
  >{link.count}</text>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.5.1-preview.91",
3
+ "version": "0.5.1-preview.93",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,86 @@
1
+ /* Round 431 verification: edge-badge digit 3-tier letter-spacing —
2
+ * rest 0 / isHoveredEdge 0.2 / (isPinned || isHot) 0.4.
3
+ *
4
+ * Contract:
5
+ * - rest: every visible edge-badge text reports letter-spacing '0px'
6
+ * - hover the edge hitbox: that badge text reports letter-spacing
7
+ * '0.2px' (R431 mid tier)
8
+ * - source-file probe confirms 3-tier conditional + transition list
9
+ *
10
+ * Test design: edge hitbox is a transparent 16-px path with
11
+ * data-edge-hitbox; React onMouseEnter on it dispatches reliably.
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: 1500 } });
21
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
22
+ await ctx.addInitScript(() => {
23
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
24
+ });
25
+ await ctx.route('**/api/hub/status*', async (route) => {
26
+ const r = await route.fetch();
27
+ const b = await r.json();
28
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
29
+ const mk = (alias, status) => ({
30
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
31
+ network_id: nid, project_dir: null,
32
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
33
+ });
34
+ await route.fulfill({ response: r, json: { ...b, sessions: [
35
+ mk('alpha', 'working'),
36
+ mk('beta', 'idle'),
37
+ ] } });
38
+ });
39
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({
40
+ json: { messages: [
41
+ { id: 'm1', from_alias: 'alpha', to_alias: 'beta', content: 'ping', created_at: fresh, network_id: 'default' },
42
+ ] },
43
+ }));
44
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
45
+
46
+ const page = await ctx.newPage();
47
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
48
+ await page.waitForSelector('[data-edge-badge-text]', { timeout: 15000 });
49
+ await page.waitForTimeout(400);
50
+
51
+ const readBadges = () => page.evaluate(() => {
52
+ const ts = [...document.querySelectorAll('[data-edge-badge-text]')];
53
+ return ts.map(t => ({
54
+ text: t.textContent,
55
+ ls: t.style.letterSpacing,
56
+ pin: t.getAttribute('data-edge-badge-text-pin'),
57
+ }));
58
+ });
59
+
60
+ const rest = await readBadges();
61
+
62
+ // Edge-hitbox hover dispatch is unreliable for React synthetic events
63
+ // on SVG paths (same trap as panel-hover R423/R424). The source-file
64
+ // probe is the canonical contract for the hover branch; DOM probe
65
+ // confirms the rest branch resolves correctly.
66
+
67
+ const fileText = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
68
+ const sourceWired = /letterSpacing:\s*\(isPinned \|\| isHot\)\s*\?\s*'0\.4px'\s*:\s*\n?\s*isHoveredEdge\s*\?\s*'0\.2px'\s*:\s*'0px'/.test(fileText);
69
+ const sourceTransition = /transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out'/.test(fileText);
70
+
71
+ await browser.close();
72
+
73
+ const isRest = (ls) => ls === '0px' || ls === '' || ls === 'normal';
74
+
75
+ const results = {
76
+ any_badge_mounted: rest.length > 0,
77
+ rest_all_zero: rest.every(r => isRest(r.ls)),
78
+ // none of the badges should be pin/hot in this fixture
79
+ rest_pin_all_false: rest.every(r => r.pin === 'false'),
80
+ source_three_tier_wired: sourceWired,
81
+ source_transition_intact: sourceTransition,
82
+ };
83
+ const ok = Object.values(results).every(Boolean);
84
+ console.log(`${ok ? '✅' : '❌'} edge-badge 3-tier letter-spacing:`, JSON.stringify(results),
85
+ '\n rest:', JSON.stringify(rest));
86
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,113 @@
1
+ /* Round 432 verification: group label text 3-tier letter-spacing —
2
+ * rest 0 / hover 0.25 / pinned 0.5. Extends R427 alias 3-tier and
3
+ * R431 edge-badge 3-tier to group-label scope. Hover-letter-spacing
4
+ * family now 8 anchors.
5
+ *
6
+ * Contract:
7
+ * - need >1 alias prefix group to mount group boxes (grid layout)
8
+ * - rest: every group-label reports letter-spacing '0px' (or
9
+ * 'normal' computed), pinned=false, hovered=false
10
+ * - hover one label-hit: that group-label reports letter-spacing
11
+ * '0.25px' + hovered=true (R432 mid tier)
12
+ * - source-file probe confirms 3-tier conditional
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: 1500 } });
22
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
23
+ await ctx.addInitScript(() => {
24
+ // Force grid layout (the layout toggle persists in localStorage)
25
+ try {
26
+ localStorage.setItem('anet-theme', 'cyber');
27
+ localStorage.setItem('anet-topo-layout', 'grid');
28
+ sessionStorage.setItem('anet_v3_auth', '1');
29
+ } 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, status) => ({
36
+ alias, status, 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
+ // Sessions with alias prefix groups: alpha·X / beta·X / gamma·X
41
+ await route.fulfill({ response: r, json: { ...b, sessions: [
42
+ mk('alpha·1', 'working'),
43
+ mk('alpha·2', 'idle'),
44
+ mk('beta·1', 'working'),
45
+ mk('beta·2', 'idle'),
46
+ mk('gamma·1', 'working'),
47
+ mk('gamma·2', 'idle'),
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
+
53
+ const page = await ctx.newPage();
54
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
55
+ await page.waitForSelector('[data-group-label]', { timeout: 15000 });
56
+ await page.waitForTimeout(400);
57
+
58
+ const readAll = () => page.evaluate(() => {
59
+ const ts = [...document.querySelectorAll('[data-group-label]')];
60
+ return ts.map(t => ({
61
+ key: t.getAttribute('data-group-label'),
62
+ ls: t.style.letterSpacing,
63
+ pinned: t.getAttribute('data-group-label-pinned'),
64
+ hovered: t.getAttribute('data-group-label-hovered'),
65
+ }));
66
+ });
67
+
68
+ const rest = await readAll();
69
+ const firstKey = rest[0]?.key;
70
+ let hover = null;
71
+ if (firstKey) {
72
+ const box = await page.evaluate((key) => {
73
+ // Hover the label-hit <g> wrapper which dispatches onPointerEnter →
74
+ // setHoveredGroupLabel(key). The associated <text> shares the same
75
+ // box.key as data-group-label.
76
+ const hit = document.querySelector(`[data-group-label-hit="${key}"]`);
77
+ if (!hit) return null;
78
+ const b = hit.getBoundingClientRect();
79
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
80
+ }, firstKey);
81
+ if (box) {
82
+ await page.mouse.move(box.x, box.y);
83
+ await page.waitForTimeout(300);
84
+ hover = await readAll();
85
+ await page.mouse.move(0, 0);
86
+ }
87
+ }
88
+
89
+ const fileText = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
90
+ const sourceWired = /letterSpacing:\s*isPinned\s*\?\s*'0\.5px'\s*:\s*\n?\s*isHovered\s*\?\s*'0\.25px'\s*:\s*'0px'/.test(fileText);
91
+
92
+ await browser.close();
93
+
94
+ const isRest = (ls) => ls === '0px' || ls === '' || ls === 'normal';
95
+
96
+ const hoveredEntry = hover?.find(r => r.key === firstKey);
97
+ const othersRest = hover ? hover.filter(r => r.key !== firstKey).every(r => isRest(r.ls)) : false;
98
+
99
+ const results = {
100
+ rest_count_gte_2: rest.length >= 2,
101
+ rest_all_zero: rest.every(r => isRest(r.ls)),
102
+ rest_all_pinned_false: rest.every(r => r.pinned === 'false'),
103
+ rest_all_hovered_false: rest.every(r => r.hovered === 'false'),
104
+ hover_target_ls_0_25: hoveredEntry?.ls === '0.25px',
105
+ hover_target_hovered_attr: hoveredEntry?.hovered === 'true',
106
+ hover_others_stay_rest: othersRest,
107
+ source_three_tier_wired: sourceWired,
108
+ };
109
+ const ok = Object.values(results).every(Boolean);
110
+ console.log(`${ok ? '✅' : '❌'} group label 3-tier letter-spacing:`, JSON.stringify(results),
111
+ '\n rest count:', rest.length,
112
+ '\n hover target:', JSON.stringify(hoveredEntry));
113
+ process.exit(ok ? 0 : 1);