@sleep2agi/agent-network-dashboard 0.5.1-preview.97 → 0.5.1-preview.99

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 +1 -1
  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/{085uyvy61dd-4.js → 0.sf46gnv4wwm.js} +1 -1
  130. package/.next/static/chunks/{0a1v54cnxx83..js → 0_igeywsok2_-.js} +1 -1
  131. package/.next/static/chunks/0igz0bww16uvc.js +4 -0
  132. package/.next/trace +2 -2
  133. package/.next/trace-build +1 -1
  134. package/app/components/TopoGraph.tsx +77 -16
  135. package/package.json +1 -1
  136. package/scripts/topo-flow-rail-hover-sw-test.mjs +113 -0
  137. package/scripts/topo-status-ring-hover-sw-test.mjs +105 -0
  138. package/.next/static/chunks/0ap~mvvqp~v-_.js +0 -4
  139. /package/.next/static/{44j_WBni902eK_p3ToCrp → 4rHxTzLwe2XC9M1rN-MpJ}/_buildManifest.js +0 -0
  140. /package/.next/static/{44j_WBni902eK_p3ToCrp → 4rHxTzLwe2XC9M1rN-MpJ}/_clientMiddlewareManifest.js +0 -0
  141. /package/.next/static/{44j_WBni902eK_p3ToCrp → 4rHxTzLwe2XC9M1rN-MpJ}/_ssgManifest.js +0 -0
@@ -4921,13 +4921,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4921
4921
  d={path}
4922
4922
  fill="none"
4923
4923
  stroke={pal.flowPath}
4924
- strokeWidth="1"
4924
+ /* Round 437 / Loop: flow-rail strokeWidth hover lift —
4925
+ 1 → 1.5 on (isHoveredEdge || isEndpointHoveredEdge).
4926
+ Pre-R437 the dashed rail sat at sw=1 always while the
4927
+ visible flow path above it lifted (R50 ×1.4 on
4928
+ isHoveredEdge / R436 ×1.15 on isEndpointHoveredEdge).
4929
+ The two edge paint layers were mismatched on hover:
4930
+ top layer thickened, underline stayed thin — so the
4931
+ hover gesture lifted only half the edge surface.
4932
+ R437 lifts the underline too so the whole edge
4933
+ reads as "raised" on hover, not just its bright
4934
+ top stripe. Same +0.5 absolute delta R435 used at
4935
+ hub-spoke scope (1→1.25 there, slightly bigger
4936
+ here because the rail's base 1 is at the kerning
4937
+ floor and needs more lift to register).
4938
+ Transition list extends to include stroke-width
4939
+ 300ms so the new lift eases under the same R166
4940
+ cadence as the visible path's stroke-width. */
4941
+ strokeWidth={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}
4925
4942
  strokeDasharray="2 12"
4926
4943
  strokeLinecap="round"
4927
4944
  opacity={Math.min(1, (isLight ? 0.4 : 0.75) * fresh * edgeOpacityMul)}
4928
4945
  data-edge-flow-rail={link.key}
4929
4946
  data-edge-flow-rail-linecap="round"
4930
- style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out' }}
4947
+ data-edge-flow-rail-stroke-width={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}
4948
+ data-edge-flow-rail-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
4949
+ style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out' }}
4931
4950
  />
4932
4951
  {!reducedMotion && (
4933
4952
  /* Round 103 / Loop: phase-stagger the particles so
@@ -6709,20 +6728,62 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6709
6728
  stroke-width="3"/"1.5" still works against the
6710
6729
  DOM attribute value (React-rendered, not
6711
6730
  interpolated). */}
6712
- <circle
6713
- cx={pos.x}
6714
- cy={pos.y}
6715
- r={radius}
6716
- fill={isOnline ? pal.nodeFill.online : pal.nodeFill.offline}
6717
- stroke={status.primary}
6718
- strokeWidth={isOnline ? 3 : 1.5}
6719
- strokeDasharray={isOnline ? 'none' : '5 5'}
6720
- filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
6721
- data-node-status-ring={status.label}
6722
- style={{
6723
- transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',
6724
- }}
6725
- />
6731
+ {(() => {
6732
+ /* Round 438 / Loop: status ring strokeWidth hover lift —
6733
+ when hoveredAlias matches, the node's status ring
6734
+ thickens by +0.5: online 3 → 3.5, offline 1.5 → 2.
6735
+ Same absolute delta as R435 hub-spoke (idle 1→1.25
6736
+ used Δ +0.25 because rest base was thinner; status
6737
+ ring's heavier rest values 1.5/3 need a bigger
6738
+ +0.5 to register as visible thickening).
6739
+ Status-ring axis joins the node-hover cue stack
6740
+ (now 9 layers including link surfaces):
6741
+ R26 group translateY -2px per-node geometry
6742
+ R217 stroke tint legendAccent per-node card
6743
+ R142 drop-shadow boost per-node card
6744
+ R427 alias letter-spacing per-node text
6745
+ R428 sub-text letter-spacing per-node text
6746
+ R429 body opacity 0.94 → 1.0 per-node card
6747
+ R430 hub-spoke α+ link to hub (paint)
6748
+ R435 hub-spoke sw+ link to hub (geo)
6749
+ R94 edge α 1.7× inter-node link (paint)
6750
+ R436 edge sw 1.15× inter-node link (geo)
6751
+ R437 flow-rail sw 1 → 1.5 edge paint-layer
6752
+ R438 status-ring sw +0.5 ring geometry ← this round
6753
+ R51 sentinel safety: rest values 3 / 1.5 unchanged
6754
+ so the overlap-test selector `circle[stroke-width=
6755
+ "3"]` / `circle[stroke-width="1.5"]` inside
6756
+ g[data-node] still matches at rest. Hover values
6757
+ 3.5 / 2 are not in the reserved {1.5, 3} set so
6758
+ the selector wouldn't match them anyway; but the
6759
+ test runs WITHOUT hover so this never matters
6760
+ in practice. R167 stroke-width 300ms transition
6761
+ already in the style list eases the lift for
6762
+ free. data-node-status-ring-hovered exposes the
6763
+ gate for tests. */
6764
+ const isRingHovered = !reducedMotion && hoveredAlias === session.alias;
6765
+ const ringStrokeWidth = isOnline
6766
+ ? (isRingHovered ? 3.5 : 3)
6767
+ : (isRingHovered ? 2 : 1.5);
6768
+ return (
6769
+ <circle
6770
+ cx={pos.x}
6771
+ cy={pos.y}
6772
+ r={radius}
6773
+ fill={isOnline ? pal.nodeFill.online : pal.nodeFill.offline}
6774
+ stroke={status.primary}
6775
+ strokeWidth={ringStrokeWidth}
6776
+ strokeDasharray={isOnline ? 'none' : '5 5'}
6777
+ filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
6778
+ data-node-status-ring={status.label}
6779
+ data-node-status-ring-hovered={isRingHovered ? 'true' : 'false'}
6780
+ data-node-status-ring-stroke-width={ringStrokeWidth}
6781
+ style={{
6782
+ transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',
6783
+ }}
6784
+ />
6785
+ );
6786
+ })()}
6726
6787
  {/* v0.10.0 Hero 1+2 / §3.F server-health node-ring tint.
6727
6788
  When the host server this agent runs on is in the
6728
6789
  `red` tier (CPU/Mem/Disk worst-of ≥ 85% per
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.5.1-preview.97",
3
+ "version": "0.5.1-preview.99",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,113 @@
1
+ /* Round 437 verification: flow-rail (dashed underline) strokeWidth
2
+ * hover lift — 1 → 1.5 on (isHoveredEdge || isEndpointHoveredEdge).
3
+ * Pairs with R50 visible-path width lift + R436 endpoint width lift
4
+ * so both edge paint layers thicken together on hover.
5
+ *
6
+ * Contract:
7
+ * - rest: every flow-rail reports stroke-width '1' + lifted='false'
8
+ * - hover one node alias: rails on edges incident to that node
9
+ * report stroke-width '1.5' + lifted='true'; non-incident rails
10
+ * stay rest
11
+ * - source-file probe confirms conditional + transition extension
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
+ mk('gamma', 'idle'),
38
+ ] } });
39
+ });
40
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({
41
+ json: { messages: [
42
+ { id: 'm1', from_alias: 'alpha', to_alias: 'beta', content: 'ping', created_at: fresh, network_id: 'default' },
43
+ { id: 'm2', from_alias: 'alpha', to_alias: 'gamma', content: 'pong', created_at: fresh, network_id: 'default' },
44
+ { id: 'm3', from_alias: 'gamma', to_alias: 'beta', content: 'pang', created_at: fresh, network_id: 'default' },
45
+ ] },
46
+ }));
47
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
48
+
49
+ const page = await ctx.newPage();
50
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
51
+ await page.waitForSelector('[data-edge-flow-rail]', { timeout: 15000 });
52
+ await page.waitForTimeout(400);
53
+
54
+ const readAll = () => page.evaluate(() => {
55
+ const ps = [...document.querySelectorAll('[data-edge-flow-rail]')];
56
+ return ps.map(p => ({
57
+ key: p.getAttribute('data-edge-flow-rail'),
58
+ sw: parseFloat(p.getAttribute('data-edge-flow-rail-stroke-width') || '0'),
59
+ sw_a: p.getAttribute('stroke-width'),
60
+ lifted: p.getAttribute('data-edge-flow-rail-lifted'),
61
+ }));
62
+ });
63
+
64
+ const rest = await readAll();
65
+
66
+ let hover = null;
67
+ const box = await page.evaluate(() => {
68
+ const t = document.querySelector('[data-node-alias-text="alpha"]');
69
+ if (!t) return null;
70
+ const node = t.closest('[data-node]');
71
+ const target = node || t;
72
+ const b = target.getBoundingClientRect();
73
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
74
+ });
75
+ if (box) {
76
+ await page.mouse.move(box.x, box.y);
77
+ await page.waitForTimeout(300);
78
+ hover = await readAll();
79
+ await page.mouse.move(0, 0);
80
+ }
81
+
82
+ const fileText = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
83
+ const sourceWired = /strokeWidth=\{\(isHoveredEdge \|\| isEndpointHoveredEdge\) \? 1\.5 : 1\}/.test(fileText);
84
+ const sourceTransition = /transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out'/.test(fileText);
85
+
86
+ await browser.close();
87
+
88
+ const restAllSw1 = rest.every(r => r.sw === 1);
89
+ const restNoneLifted = rest.every(r => r.lifted === 'false');
90
+
91
+ const isAlphaKey = (k) => /alpha/i.test(k);
92
+ const hoverByKey = new Map((hover || []).map(h => [h.key, h]));
93
+ const alphaEntries = [...hoverByKey.entries()].filter(([k]) => isAlphaKey(k));
94
+ const nonAlphaEntries = [...hoverByKey.entries()].filter(([k]) => !isAlphaKey(k));
95
+
96
+ const alphaAllLifted = alphaEntries.length > 0 && alphaEntries.every(([, h]) => h.sw === 1.5 && h.lifted === 'true');
97
+ const nonAlphaRestSw = nonAlphaEntries.every(([, h]) => h.sw === 1 && h.lifted === 'false');
98
+
99
+ const results = {
100
+ rest_count_ge_2: rest.length >= 2,
101
+ rest_all_sw_1: restAllSw1,
102
+ rest_none_lifted: restNoneLifted,
103
+ hover_alpha_count_ge_2: alphaEntries.length >= 2,
104
+ hover_alpha_all_lifted: alphaAllLifted,
105
+ hover_non_alpha_unaffected: nonAlphaRestSw,
106
+ source_strokeWidth_wired: sourceWired,
107
+ source_transition_wired: sourceTransition,
108
+ };
109
+ const ok = Object.values(results).every(Boolean);
110
+ console.log(`${ok ? '✅' : '❌'} flow-rail hover sw lift:`, JSON.stringify(results),
111
+ '\n rest:', JSON.stringify(rest),
112
+ '\n hover:', JSON.stringify(hover));
113
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,105 @@
1
+ /* Round 438 verification: status ring strokeWidth hover lift —
2
+ * online 3 → 3.5, offline 1.5 → 2 on hoveredAlias match.
3
+ *
4
+ * Contract:
5
+ * - rest: every online ring reports stroke-width '3', every
6
+ * offline ring '1.5' (R51 sentinels preserved at rest)
7
+ * - hover one node: that ring's stroke-width lifts to '3.5' (or '2'
8
+ * if offline) and data-node-status-ring-hovered flips to 'true'
9
+ * - siblings stay rest
10
+ * - R51 sentinel ATTRS unchanged on rest (overlap probe still works)
11
+ */
12
+ import { chromium } from 'playwright';
13
+ import { readFileSync } from 'node:fs';
14
+
15
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
16
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
17
+
18
+ const browser = await chromium.launch({ headless: true });
19
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
20
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
21
+ await ctx.addInitScript(() => {
22
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
23
+ });
24
+ await ctx.route('**/api/hub/status*', async (route) => {
25
+ const r = await route.fetch();
26
+ const b = await r.json();
27
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
28
+ const mk = (alias, status) => ({
29
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
30
+ network_id: nid, project_dir: null,
31
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
32
+ });
33
+ await route.fulfill({ response: r, json: { ...b, sessions: [
34
+ mk('alpha', 'working'), // online → sw 3
35
+ mk('beta', 'idle'), // online → sw 3
36
+ mk('gamma', 'working'), // online → sw 3
37
+ ] } });
38
+ });
39
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
40
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
41
+
42
+ const page = await ctx.newPage();
43
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
44
+ await page.waitForSelector('[data-node-status-ring]', { timeout: 15000 });
45
+ await page.waitForTimeout(400);
46
+
47
+ const readAll = () => page.evaluate(() => {
48
+ const cs = [...document.querySelectorAll('[data-node-status-ring]')];
49
+ return cs.map((c, i) => {
50
+ const node = c.closest('[data-node]');
51
+ return {
52
+ idx: i,
53
+ alias: node?.getAttribute('data-node') || null,
54
+ ring: c.getAttribute('data-node-status-ring'),
55
+ sw: c.getAttribute('stroke-width'),
56
+ hovered: c.getAttribute('data-node-status-ring-hovered'),
57
+ };
58
+ });
59
+ });
60
+
61
+ const rest = await readAll();
62
+
63
+ const firstAlias = rest[0]?.alias || 'alpha';
64
+ let hover = null;
65
+ const box = await page.evaluate((alias) => {
66
+ const node = document.querySelector(`[data-node="${alias}"]`);
67
+ if (!node) return null;
68
+ const b = node.getBoundingClientRect();
69
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
70
+ }, firstAlias);
71
+ if (box) {
72
+ await page.mouse.move(box.x, box.y);
73
+ await page.waitForTimeout(300);
74
+ hover = await readAll();
75
+ await page.mouse.move(0, 0);
76
+ }
77
+
78
+ const fileText = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
79
+ const sourceWired = /const ringStrokeWidth = isOnline\s*\n\s*\?\s*\(isRingHovered \? 3\.5 : 3\)\s*\n\s*:\s*\(isRingHovered \? 2 : 1\.5\)/.test(fileText);
80
+ const sourceIsHovered = /const isRingHovered = !reducedMotion && hoveredAlias === session\.alias/.test(fileText);
81
+
82
+ await browser.close();
83
+
84
+ const restAllSw3 = rest.every(r => r.sw === '3'); // working+idle both online
85
+ const restNoHover = rest.every(r => r.hovered === 'false');
86
+ const hoveredEntry = hover?.find(r => r.alias === firstAlias);
87
+ const hoverSw_3_5 = hoveredEntry?.sw === '3.5';
88
+ const hoverHoveredTrue = hoveredEntry?.hovered === 'true';
89
+ const othersStillSw3 = hover ? hover.filter(r => r.alias !== firstAlias).every(r => r.sw === '3' && r.hovered === 'false') : false;
90
+
91
+ const results = {
92
+ rest_ring_count_ge_3: rest.length >= 3,
93
+ rest_all_online_sw_3: restAllSw3,
94
+ rest_no_hover: restNoHover,
95
+ hover_target_sw_3_5: hoverSw_3_5,
96
+ hover_target_attr_true: hoverHoveredTrue,
97
+ hover_others_stay_sw_3: othersStillSw3,
98
+ source_strokeWidth_wired: sourceWired,
99
+ source_isRingHovered_def: sourceIsHovered,
100
+ };
101
+ const ok = Object.values(results).every(Boolean);
102
+ console.log(`${ok ? '✅' : '❌'} status-ring hover sw:`, JSON.stringify(results),
103
+ '\n rest sample:', JSON.stringify(rest[0]),
104
+ '\n hover target:', JSON.stringify(hoveredEntry));
105
+ process.exit(ok ? 0 : 1);