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

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/{0hduheo6~ui~q.js → 085uyvy61dd-4.js} +1 -1
  130. package/.next/static/chunks/{0d~gzizx7atsl.js → 0a1v54cnxx83..js} +1 -1
  131. package/.next/static/chunks/0ap~mvvqp~v-_.js +4 -0
  132. package/.next/trace +2 -2
  133. package/.next/trace-build +1 -1
  134. package/app/components/TopoGraph.tsx +45 -2
  135. package/package.json +1 -1
  136. package/scripts/topo-edge-endpoint-hover-sw-test.mjs +127 -0
  137. package/scripts/topo-hub-spoke-hover-sw-test.mjs +121 -0
  138. package/.next/static/chunks/0wecjbif70gvd.js +0 -4
  139. /package/.next/static/{G-QBT67axaIRmw0dasJKn → 44j_WBni902eK_p3ToCrp}/_buildManifest.js +0 -0
  140. /package/.next/static/{G-QBT67axaIRmw0dasJKn → 44j_WBni902eK_p3ToCrp}/_clientMiddlewareManifest.js +0 -0
  141. /package/.next/static/{G-QBT67axaIRmw0dasJKn → 44j_WBni902eK_p3ToCrp}/_ssgManifest.js +0 -0
@@ -4123,13 +4123,33 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4123
4123
  const spokeOpacity = isActiveSpoke
4124
4124
  ? (isHoveredSpoke ? 0.95 : 0.80)
4125
4125
  : (isHoveredSpoke ? 0.70 : 0.50);
4126
+ /* Round 435 / Loop: hub-spoke stroke-width hover lift —
4127
+ sibling to R430 opacity hover at the same surface. When
4128
+ hoveredAlias matches, BOTH opacity AND stroke-width
4129
+ lift on the matched spoke so the eye registers a
4130
+ 2-axis "this node's spoke" gesture (paint + geometry).
4131
+ idle 1.00 → 1.25 (Δ +0.25, +25%)
4132
+ active 2.25 → 2.50 (Δ +0.25, +11%)
4133
+ Same +0.25 absolute delta keeps the idle/active visual
4134
+ progression consistent — at rest sw ratio 2.25:1 = 2.25,
4135
+ on hover 2.50:1.25 = 2.0; both still clearly two-tier.
4136
+ R241 transition list already covers stroke-width 250ms
4137
+ so the lift eases for free.
4138
+ R51 sentinel-safe: spoke is canvas <path>, not
4139
+ data-node <circle> (the sentinel selector is gated to
4140
+ g[data-node] descendants). 1.25 and 2.5 are not in the
4141
+ reserved {1.5, 3} set so the overlap-test sentinel
4142
+ attribute selector wouldn't match either way. */
4143
+ const spokeStrokeWidth = isActiveSpoke
4144
+ ? (isHoveredSpoke ? 2.5 : 2.25)
4145
+ : (isHoveredSpoke ? 1.25 : 1);
4126
4146
  return (
4127
4147
  <path
4128
4148
  key={`hub-${session.alias}`}
4129
4149
  d={path}
4130
4150
  fill="none"
4131
4151
  stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}
4132
- strokeWidth={isActiveSpoke ? 2.25 : 1}
4152
+ strokeWidth={spokeStrokeWidth}
4133
4153
  strokeDasharray={isActiveSpoke ? 'none' : '6 14'}
4134
4154
  strokeLinecap="round"
4135
4155
  opacity={spokeOpacity}
@@ -4139,6 +4159,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4139
4159
  data-topo-hub-spoke-active={isActiveSpoke ? 'true' : 'false'}
4140
4160
  data-topo-hub-spoke-hovered={isHoveredSpoke ? 'true' : 'false'}
4141
4161
  data-topo-hub-spoke-opacity={spokeOpacity}
4162
+ data-topo-hub-spoke-stroke-width={spokeStrokeWidth}
4142
4163
  data-topo-hub-spoke-stroke-width-active="2.25"
4143
4164
  data-topo-hub-spoke-linecap="round"
4144
4165
  style={{
@@ -4736,7 +4757,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4736
4757
  // tracks it even at low message counts (width was 3 → 4.5 on
4737
4758
  // hover). 1.4× is enough to read as "lifted" without
4738
4759
  // breaching the 16-px hitbox bound.
4739
- const renderWidth = isHoveredEdge ? Math.min(width * 1.4, 10) : width;
4760
+ // Round 436 / Loop: extend the thickening to the
4761
+ // "hovered-endpoint" case — when hoveredAlias matches one
4762
+ // of this edge's endpoints, lift width by 1.15× (capped at
4763
+ // 8 px to stay clear of the 16-px hitbox). Pre-R436 the
4764
+ // edgeOpacityMul=1.7 (line 4703) lifted the matched edge's
4765
+ // OPACITY when an endpoint was hovered but the stroke-WIDTH
4766
+ // stayed at base — so the edge faded brighter without
4767
+ // thickening, leaving the paint+geometry axes mismatched.
4768
+ // Mirror of R430/R435 hub-spoke pattern (opacity + stroke-
4769
+ // width co-lift on hoveredAlias); R436 brings the same
4770
+ // dual-axis "this node's link" gesture to the edge scope.
4771
+ // 1.15× is subtler than isHoveredEdge=1.4× because
4772
+ // endpoint-hover lifts MANY edges at once (every edge
4773
+ // incident on the hovered node) while edge-hover lifts ONE
4774
+ // — the gesture should read as "highlighted" not "loud".
4775
+ // R166 stroke-width 300ms transition already in the
4776
+ // visible-path style list so the lift eases for free.
4777
+ const isEndpointHoveredEdge = !!hoveredAlias && (link.from === hoveredAlias || link.to === hoveredAlias);
4778
+ const renderWidth = isHoveredEdge ? Math.min(width * 1.4, 10)
4779
+ : isEndpointHoveredEdge ? Math.min(width * 1.15, 8)
4780
+ : width;
4740
4781
  return (
4741
4782
  <g
4742
4783
  key={link.key}
@@ -4851,6 +4892,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4851
4892
  markerEnd={`url(#${arrowId})`}
4852
4893
  data-edge-visible={link.key}
4853
4894
  data-edge-visible-linecap="round"
4895
+ data-edge-visible-endpoint-hovered={isEndpointHoveredEdge ? 'true' : 'false'}
4896
+ data-edge-visible-stroke-width={renderWidth}
4854
4897
  style={{
4855
4898
  pointerEvents: 'none',
4856
4899
  transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.5.1-preview.95",
3
+ "version": "0.5.1-preview.97",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,127 @@
1
+ /* Round 436 verification: edge visible-path stroke-width lift on
2
+ * hoveredAlias endpoint. Mirrors R430/R435 hub-spoke pattern at edge
3
+ * scope — when hoveredAlias matches one of the edge's endpoints, the
4
+ * visible path thickens by 1.15× alongside the existing edgeOpacityMul
5
+ * 1.7× lift (now BOTH paint and geometry lift on endpoint hover).
6
+ *
7
+ * Contract:
8
+ * - rest: no edge reports data-edge-visible-endpoint-hovered='true'
9
+ * - hover one node alias: exactly the edges incident on that alias
10
+ * report endpoint-hovered='true' AND their renderWidth > rest base
11
+ * - siblings unaffected
12
+ * - source-file probe confirms the new conditional + endpoint cap
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
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
25
+ });
26
+ await ctx.route('**/api/hub/status*', async (route) => {
27
+ const r = await route.fetch();
28
+ const b = await r.json();
29
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
30
+ const mk = (alias, status) => ({
31
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
32
+ network_id: nid, project_dir: null,
33
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
34
+ });
35
+ await route.fulfill({ response: r, json: { ...b, sessions: [
36
+ mk('alpha', 'working'),
37
+ mk('beta', 'idle'),
38
+ mk('gamma', 'idle'),
39
+ ] } });
40
+ });
41
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({
42
+ json: { messages: [
43
+ // alpha is incident on both edges; gamma↔beta is the non-incident edge
44
+ { id: 'm1', from_alias: 'alpha', to_alias: 'beta', content: 'ping', created_at: fresh, network_id: 'default' },
45
+ { id: 'm2', from_alias: 'alpha', to_alias: 'gamma', content: 'pong', created_at: fresh, network_id: 'default' },
46
+ { id: 'm3', from_alias: 'gamma', to_alias: 'beta', content: 'pang', created_at: fresh, network_id: 'default' },
47
+ ] },
48
+ }));
49
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
50
+
51
+ const page = await ctx.newPage();
52
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
53
+ await page.waitForSelector('[data-edge-visible]', { timeout: 15000 });
54
+ await page.waitForTimeout(400);
55
+
56
+ const readAll = () => page.evaluate(() => {
57
+ const ps = [...document.querySelectorAll('[data-edge-visible]')];
58
+ return ps.map(p => ({
59
+ key: p.getAttribute('data-edge-visible'),
60
+ endHover: p.getAttribute('data-edge-visible-endpoint-hovered'),
61
+ sw: parseFloat(p.getAttribute('data-edge-visible-stroke-width') || '0'),
62
+ }));
63
+ });
64
+
65
+ const rest = await readAll();
66
+
67
+ // Hover alpha
68
+ let hover = null;
69
+ const box = await page.evaluate(() => {
70
+ const t = document.querySelector('[data-node-alias-text="alpha"]');
71
+ if (!t) return null;
72
+ const node = t.closest('[data-node]');
73
+ const target = node || t;
74
+ const b = target.getBoundingClientRect();
75
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
76
+ });
77
+ if (box) {
78
+ await page.mouse.move(box.x, box.y);
79
+ await page.waitForTimeout(300);
80
+ hover = await readAll();
81
+ await page.mouse.move(0, 0);
82
+ }
83
+
84
+ const fileText = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
85
+ const sourceWired = /isEndpointHoveredEdge\s*\?\s*Math\.min\(width \* 1\.15,\s*8\)\s*\n?\s*:\s*width/.test(fileText);
86
+ const sourceIsEndpoint = /const isEndpointHoveredEdge = !!hoveredAlias && \(link\.from === hoveredAlias \|\| link\.to === hoveredAlias\)/.test(fileText);
87
+
88
+ await browser.close();
89
+
90
+ // rest: no edge should report endpoint-hover=true
91
+ const restNoneEndHover = rest.every(r => r.endHover === 'false');
92
+
93
+ // hover state: edges where alpha is an endpoint should report endpoint=true.
94
+ // fixture edges: alpha↔beta, alpha↔gamma, gamma↔beta (deduped keys).
95
+ const hoverByKey = new Map((hover || []).map(h => [h.key, h]));
96
+ const restByKey = new Map((rest || []).map(r => [r.key, r]));
97
+
98
+ // flow link keys are e.g. "alpha->beta" or "alpha-beta" — we don't know the
99
+ // exact format. Detect alpha-incident edges by key substring.
100
+ const isAlphaEdge = (key) => key && /alpha/i.test(key);
101
+ const alphaEdges = [...hoverByKey.entries()].filter(([k]) => isAlphaEdge(k));
102
+ const nonAlphaEdges = [...hoverByKey.entries()].filter(([k]) => !isAlphaEdge(k));
103
+
104
+ const alphaAllEndHover = alphaEdges.length > 0 && alphaEdges.every(([, h]) => h.endHover === 'true');
105
+ const nonAlphaNoEndHover = nonAlphaEdges.every(([, h]) => h.endHover === 'false');
106
+
107
+ // width lifted for alpha edges (compare to rest sw for same key)
108
+ const alphaSwLifted = alphaEdges.every(([k, h]) => {
109
+ const r = restByKey.get(k);
110
+ return r && h.sw > r.sw;
111
+ });
112
+
113
+ const results = {
114
+ rest_edge_count_ge_2: rest.length >= 2,
115
+ rest_no_endpoint_hover: restNoneEndHover,
116
+ hover_alpha_edges_present: alphaEdges.length >= 1,
117
+ hover_alpha_all_endHover: alphaAllEndHover,
118
+ hover_alpha_sw_lifted: alphaSwLifted,
119
+ hover_non_alpha_unaffected: nonAlphaNoEndHover,
120
+ source_renderWidth_wired: sourceWired,
121
+ source_isEndpoint_wired: sourceIsEndpoint,
122
+ };
123
+ const ok = Object.values(results).every(Boolean);
124
+ console.log(`${ok ? '✅' : '❌'} edge endpoint-hover sw lift:`, JSON.stringify(results),
125
+ '\n rest:', JSON.stringify(rest),
126
+ '\n hover:', JSON.stringify(hover));
127
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,121 @@
1
+ /* Round 435 verification: hub-spoke stroke-width hover lift —
2
+ * sibling to R430 opacity hover at the same surface. 2-axis hover
3
+ * signature now: paint (opacity R430) + geometry (stroke-width R435).
4
+ *
5
+ * Rest tiers:
6
+ * idle sw 1.00 / α 0.50
7
+ * active sw 2.25 / α 0.80
8
+ * Hover tiers (hoveredAlias === session.alias):
9
+ * idle sw 1.25 / α 0.70
10
+ * active sw 2.50 / α 0.95
11
+ *
12
+ * Contract:
13
+ * - rest: all idle spokes report data-topo-hub-spoke-stroke-width '1'
14
+ * - hover one node: exactly one spoke flips to hover with sw '1.25'
15
+ * AND opacity '0.7' (both R430+R435 axes lift together)
16
+ * - siblings stay rest sw '1' / opacity '0.5'
17
+ * - source-file probe confirms wired stroke-width conditional
18
+ */
19
+ import { chromium } from 'playwright';
20
+ import { readFileSync } from 'node:fs';
21
+
22
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
23
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
24
+
25
+ const browser = await chromium.launch({ headless: true });
26
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
27
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
28
+ await ctx.addInitScript(() => {
29
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
30
+ });
31
+ await ctx.route('**/api/hub/status*', async (route) => {
32
+ const r = await route.fetch();
33
+ const b = await r.json();
34
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
35
+ const mk = (alias, 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
+ await route.fulfill({ response: r, json: { ...b, sessions: [
41
+ mk('alpha', 'working'),
42
+ mk('beta', 'idle'),
43
+ mk('gamma', 'idle'),
44
+ ] } });
45
+ });
46
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
47
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
48
+
49
+ const page = await ctx.newPage();
50
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
51
+ await page.waitForSelector('[data-topo-hub-spoke-active]', { timeout: 15000 });
52
+ await page.waitForTimeout(400);
53
+
54
+ const readAll = () => page.evaluate(() => {
55
+ const paths = [...document.querySelectorAll('[data-topo-hub-spoke-active]')];
56
+ return paths.map((p, i) => ({
57
+ idx: i,
58
+ active: p.getAttribute('data-topo-hub-spoke-active'),
59
+ hovered: p.getAttribute('data-topo-hub-spoke-hovered'),
60
+ opacity: p.getAttribute('data-topo-hub-spoke-opacity'),
61
+ sw: p.getAttribute('data-topo-hub-spoke-stroke-width'),
62
+ sw_a: p.getAttribute('stroke-width'),
63
+ }));
64
+ });
65
+
66
+ const rest = await readAll();
67
+
68
+ // Hover the first node's <g> group
69
+ const firstAlias = await page.evaluate(() => {
70
+ const t = document.querySelector('[data-node-alias-text]');
71
+ return t?.getAttribute('data-node-alias-text');
72
+ });
73
+ let hover = null;
74
+ if (firstAlias) {
75
+ const box = await page.evaluate((alias) => {
76
+ const t = document.querySelector(`[data-node-alias-text="${alias}"]`);
77
+ if (!t) return null;
78
+ const node = t.closest('[data-node]');
79
+ const target = node || t;
80
+ const b = target.getBoundingClientRect();
81
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
82
+ }, firstAlias);
83
+ if (box) {
84
+ await page.mouse.move(box.x, box.y);
85
+ await page.waitForTimeout(300);
86
+ hover = await readAll();
87
+ await page.mouse.move(0, 0);
88
+ }
89
+ }
90
+
91
+ const fileText = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
92
+ const sourceWired = /const spokeStrokeWidth = isActiveSpoke\s*\n\s*\?\s*\(isHoveredSpoke\s*\?\s*2\.5\s*:\s*2\.25\)\s*\n\s*:\s*\(isHoveredSpoke\s*\?\s*1\.25\s*:\s*1\)/.test(fileText);
93
+
94
+ await browser.close();
95
+
96
+ const restAllIdle = rest.length > 0 && rest.every(r => r.active === 'false');
97
+ const restAllSw1 = rest.every(r => r.sw === '1');
98
+ const restAllOpacity = rest.every(r => r.opacity === '0.5');
99
+ const restNoHover = rest.every(r => r.hovered === 'false');
100
+ const hoveredCount = hover ? hover.filter(s => s.hovered === 'true').length : -1;
101
+ const hoveredEntry = hover?.find(s => s.hovered === 'true');
102
+ const hoverSw_1_25 = hoveredEntry?.sw === '1.25';
103
+ const hoverOpacity_0_7 = hoveredEntry?.opacity === '0.7';
104
+ const othersRestSw = hover ? hover.filter(s => s.hovered === 'false').every(s => s.sw === '1') : false;
105
+
106
+ const results = {
107
+ rest_spoke_count_ge_3: rest.length >= 3,
108
+ rest_all_idle: restAllIdle,
109
+ rest_all_sw_1: restAllSw1,
110
+ rest_all_opacity_0_5: restAllOpacity,
111
+ rest_no_hover: restNoHover,
112
+ hover_exactly_one_match: hoveredCount === 1,
113
+ hover_target_sw_1_25: hoverSw_1_25,
114
+ hover_target_opacity_0_7: hoverOpacity_0_7, /* R430 parity */
115
+ hover_others_stay_sw_1: othersRestSw,
116
+ source_strokeWidth_3tier: sourceWired,
117
+ };
118
+ const ok = Object.values(results).every(Boolean);
119
+ console.log(`${ok ? '✅' : '❌'} hub-spoke hover sw (R435):`, JSON.stringify(results),
120
+ '\n hover target:', JSON.stringify(hoveredEntry));
121
+ process.exit(ok ? 0 : 1);