@sleep2agi/agent-network-dashboard 0.5.1-preview.7 → 0.5.1-preview.71

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 (218) 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 +6 -6
  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 +2 -2
  15. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  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 +2 -2
  24. package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
  25. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  26. package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
  27. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  28. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  29. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  30. package/.next/server/app/index.html +2 -2
  31. package/.next/server/app/index.rsc +3 -3
  32. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  33. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  34. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  35. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  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 +3 -3
  40. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  41. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  42. package/.next/server/app/login.segments/_index.segment.rsc +2 -2
  43. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  44. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  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 +2 -2
  49. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  50. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  51. package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
  52. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  54. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  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 +2 -2
  58. package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
  59. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  60. package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
  61. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  62. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  63. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  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 +2 -2
  67. package/.next/server/app/node.segments/_full.segment.rsc +2 -2
  68. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  69. package/.next/server/app/node.segments/_index.segment.rsc +2 -2
  70. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  71. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  72. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  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 +2 -2
  76. package/.next/server/app/nodes.segments/_full.segment.rsc +2 -2
  77. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  78. package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
  79. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  80. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
  81. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  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 +2 -2
  86. package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
  87. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  88. package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
  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 +1 -1
  91. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  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 +2 -2
  95. package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
  96. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  97. package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
  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 +1 -1
  100. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  101. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  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 +2 -2
  106. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
  107. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  108. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
  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 +1 -1
  111. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  112. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  113. package/.next/server/app/settings.html +2 -2
  114. package/.next/server/app/settings.rsc +3 -3
  115. package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  116. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  117. package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  118. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  119. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  120. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  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 +2 -2
  125. package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
  126. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  127. package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
  128. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  129. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  130. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  131. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  132. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  133. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  134. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  135. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  136. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  137. package/.next/server/middleware-build-manifest.js +3 -3
  138. package/.next/server/pages/404.html +2 -2
  139. package/.next/server/pages/500.html +1 -1
  140. package/.next/static/chunks/04.j~y8e~sbs4.js +1 -0
  141. package/.next/static/chunks/06llamqb4jsu..js +4 -0
  142. package/.next/static/chunks/0_p8jkzdw5x2_.css +2 -0
  143. package/.next/static/chunks/12heglqfrp1bm.js +1 -0
  144. package/.next/trace +2 -2
  145. package/.next/trace-build +1 -1
  146. package/app/components/TopoGraph.tsx +1426 -84
  147. package/package.json +1 -1
  148. package/scripts/topo-active-links-chip-hover-lift-test.mjs +93 -0
  149. package/scripts/topo-chip-digit-fontweight-test.mjs +105 -0
  150. package/scripts/topo-chip-row-hover-lift-test.mjs +95 -0
  151. package/scripts/topo-chrome-button-hover-lift-test.mjs +94 -0
  152. package/scripts/topo-chrome-segmented-radius-test.mjs +100 -0
  153. package/scripts/topo-click-ripple-opacity-test.mjs +99 -0
  154. package/scripts/topo-edge-badge-fontsize-test.mjs +90 -0
  155. package/scripts/topo-edge-badge-hover-opacity-test.mjs +94 -0
  156. package/scripts/topo-edge-badge-hover-stroke-test.mjs +92 -0
  157. package/scripts/topo-edge-badge-opacity-test.mjs +80 -0
  158. package/scripts/topo-edge-badge-pin-opacity-test.mjs +86 -0
  159. package/scripts/topo-edge-badge-stroke-test.mjs +92 -0
  160. package/scripts/topo-edge-freshness-floor-test.mjs +99 -0
  161. package/scripts/topo-edge-visible-linecap-test.mjs +89 -0
  162. package/scripts/topo-filter-pill-hover-lift-test.mjs +101 -0
  163. package/scripts/topo-filter-pill-hover-opacity-test.mjs +110 -0
  164. package/scripts/topo-filter-pill-x-hover-scale-test.mjs +99 -0
  165. package/scripts/topo-flow-rail-linecap-test.mjs +79 -0
  166. package/scripts/topo-freshness-chip-hierarchy-test.mjs +93 -0
  167. package/scripts/topo-freshness-chip-tabular-test.mjs +41 -0
  168. package/scripts/topo-freshness-floor-lift-test.mjs +92 -0
  169. package/scripts/topo-freshness-suffix-tabular-test.mjs +88 -0
  170. package/scripts/topo-fullscreen-icon-hover-scale-test.mjs +91 -0
  171. package/scripts/topo-group-box-stroke-test.mjs +105 -0
  172. package/scripts/topo-group-label-count-fontweight-test.mjs +108 -0
  173. package/scripts/topo-hover-detail-body-fw-test.mjs +101 -0
  174. package/scripts/topo-hover-detail-model-fw-test.mjs +98 -0
  175. package/scripts/topo-hover-detail-opacity-test.mjs +98 -0
  176. package/scripts/topo-hover-detail-rx-test.mjs +81 -0
  177. package/scripts/topo-hub-digit-fontsize-test.mjs +86 -0
  178. package/scripts/topo-hub-halo-light-trough-test.mjs +88 -0
  179. package/scripts/topo-hub-halo-radius-test.mjs +86 -0
  180. package/scripts/topo-hub-halo-trough-test.mjs +83 -0
  181. package/scripts/topo-hub-highlight-opacity-test.mjs +88 -0
  182. package/scripts/topo-hub-highlight-radius-test.mjs +90 -0
  183. package/scripts/topo-hub-hover-ring-opacity-test.mjs +96 -0
  184. package/scripts/topo-hub-hover-ring-stroke-test.mjs +86 -0
  185. package/scripts/topo-hub-spoke-linecap-test.mjs +80 -0
  186. package/scripts/topo-layout-toggle-hover-tracking-test.mjs +109 -0
  187. package/scripts/topo-layout-toggle-radius-test.mjs +87 -0
  188. package/scripts/topo-legend-label-fontweight-test.mjs +94 -0
  189. package/scripts/topo-legend-pin-ring-stroke-test.mjs +101 -0
  190. package/scripts/topo-minimap-offline-opacity-test.mjs +90 -0
  191. package/scripts/topo-minimap-online-opacity-test.mjs +93 -0
  192. package/scripts/topo-minimap-online-radius-test.mjs +85 -0
  193. package/scripts/topo-minimap-viewport-linejoin-test.mjs +75 -0
  194. package/scripts/topo-minimap-viewport-rx-test.mjs +85 -0
  195. package/scripts/topo-more-flows-fontweight-test.mjs +103 -0
  196. package/scripts/topo-node-halo-offline-opacity-test.mjs +87 -0
  197. package/scripts/topo-node-pulse-peak-test.mjs +89 -0
  198. package/scripts/topo-panel-count-letterspacing-test.mjs +89 -0
  199. package/scripts/topo-panel-rect-opacity-hover-test.mjs +109 -0
  200. package/scripts/topo-pressure-bar-height-test.mjs +92 -0
  201. package/scripts/topo-pressure-kicker-fontweight-test.mjs +76 -0
  202. package/scripts/topo-recent-pip-radius-2-test.mjs +72 -0
  203. package/scripts/topo-recent-pip-radius-test.mjs +76 -0
  204. package/scripts/topo-recent-row-text-fontweight-test.mjs +90 -0
  205. package/scripts/topo-reset-hover-rotate-test.mjs +102 -0
  206. package/scripts/topo-spoke-active-opacity-test.mjs +104 -0
  207. package/scripts/topo-vendor-chip-hover-lift-test.mjs +87 -0
  208. package/scripts/topo-vendor-glyph-fontweight-test.mjs +102 -0
  209. package/scripts/topo-vendor-letter-hover-scale-test.mjs +129 -0
  210. package/scripts/topo-zoom-icon-hover-scale-test.mjs +114 -0
  211. package/scripts/topo-zoom-level-hover-letterspacing-test.mjs +91 -0
  212. package/.next/static/chunks/0aauz~36q5n2a.css +0 -2
  213. package/.next/static/chunks/0bja1amnrg3li.js +0 -1
  214. package/.next/static/chunks/0k~uc0~~19hyy.js +0 -4
  215. package/.next/static/chunks/0wtq_6dnzems6.js +0 -1
  216. /package/.next/static/{x9zCCrMkHsIYlXNY791KF → gaK6yNvVjshUCmKR9qrPn}/_buildManifest.js +0 -0
  217. /package/.next/static/{x9zCCrMkHsIYlXNY791KF → gaK6yNvVjshUCmKR9qrPn}/_clientMiddlewareManifest.js +0 -0
  218. /package/.next/static/{x9zCCrMkHsIYlXNY791KF → gaK6yNvVjshUCmKR9qrPn}/_ssgManifest.js +0 -0
@@ -0,0 +1,92 @@
1
+ /* Round 358 verification: freshness-ramp stale floor lifted 0.25 →
2
+ * 0.30 across 3 coordinated scopes:
3
+ * - active-links chip dot (line ~3055)
4
+ * - recent-panel header count (line ~6524)
5
+ * - recent-row freshness pip (line ~7021)
6
+ *
7
+ * The ramp shape stays the same:
8
+ * 0-30s → 1.0 (fully fresh)
9
+ * 30-300s → smooth decay 1.0 → 0.30
10
+ * > 300s → 0.30 (stale floor)
11
+ *
12
+ * R358 polish: stale state becomes ~20 % more readable (0.30 / 0.25
13
+ * = 1.2x) while the fresh-vs-stale ratio stays strong (1.0 / 0.30 ≈
14
+ * 3.3× vs prior 4×). Joins the R317 subordinate-text-lift family
15
+ * at the freshness scope.
16
+ *
17
+ * Test approach: feed a STALE message (~ 600s ago, well past the 300s
18
+ * floor boundary) to the route mock, then read all 3 freshness-alpha
19
+ * data attrs. Each should be '0.30'.
20
+ *
21
+ * Contract:
22
+ * - Active-links chip dot data-active-links-freshness-alpha === '0.30'.
23
+ * - Recent-panel header count data-recent-panel-count-freshness-alpha
24
+ * === '0.30'.
25
+ * - Recent-row pip data-recent-row-freshness-alpha === '0.30'.
26
+ * - SVG fill colors include alpha 0.3 (rgba form).
27
+ */
28
+ import { chromium } from 'playwright';
29
+ import { readFileSync } from 'node:fs';
30
+
31
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
32
+ const browser = await chromium.launch({ headless: true });
33
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
34
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
35
+ await ctx.addInitScript(() => {
36
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
37
+ });
38
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
39
+ await ctx.route('**/api/hub/status*', async (route) => {
40
+ const r = await route.fetch();
41
+ const b = await r.json();
42
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
43
+ const mk = (alias) => ({
44
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
45
+ network_id: nid, project_dir: null,
46
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
47
+ });
48
+ await route.fulfill({ response: r, json: { ...b, sessions: [ mk('a'), mk('b') ] } });
49
+ });
50
+
51
+ // STALE message: created_at ~ 600s ago + last_at ~ 600s ago.
52
+ const now = Date.now();
53
+ const staleIso = new Date(now - 600 * 1000).toISOString();
54
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [
55
+ { id: 'm1', from_alias: 'a', to_alias: 'b', content: 'old ping',
56
+ network_id: 'default', created_at: staleIso, last_at: staleIso },
57
+ ] } }));
58
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
59
+
60
+ const page = await ctx.newPage();
61
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
62
+ await page.waitForSelector('[data-active-links-freshness-dot]', { timeout: 15000 });
63
+ await page.waitForSelector('[data-recent-panel-count-freshness-alpha]', { timeout: 5000 });
64
+ await page.waitForSelector('[data-recent-row-freshness-alpha]', { timeout: 5000 });
65
+ await page.waitForTimeout(400);
66
+
67
+ const probe = await page.evaluate(() => {
68
+ const dot = document.querySelector('[data-active-links-freshness-dot]');
69
+ const count = document.querySelector('[data-recent-panel-count-freshness-alpha]');
70
+ const pip = document.querySelector('[data-recent-row-freshness-alpha]');
71
+ return {
72
+ dotAlpha: dot?.getAttribute('data-active-links-freshness-alpha') ?? null,
73
+ countAlpha: count?.getAttribute('data-recent-panel-count-freshness-alpha') ?? null,
74
+ pipAlpha: pip?.getAttribute('data-recent-row-freshness-alpha') ?? null,
75
+ // The pip uses a static fill + a separate `opacity` SVG attr —
76
+ // probe that, not the fill colour.
77
+ pipOpacity: pip?.getAttribute('opacity') ?? null,
78
+ };
79
+ });
80
+
81
+ await browser.close();
82
+
83
+ const results = {
84
+ dot_alpha_0_30: probe.dotAlpha === '0.30',
85
+ count_alpha_0_30: probe.countAlpha === '0.30',
86
+ pip_alpha_0_30: probe.pipAlpha === '0.30',
87
+ pip_opacity_0_3: /^0\.3$|^0\.30$/.test(probe.pipOpacity || ''),
88
+ };
89
+ const ok = Object.values(results).every(Boolean);
90
+ console.log(`${ok ? '✅' : '❌'} freshness floor lift 0.25 → 0.30:`, JSON.stringify(results),
91
+ '\n probe:', probe);
92
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,88 @@
1
+ /* Round 357 verification: active-links chip freshness suffix wrapper
2
+ * picks up `tabular-nums` for digit width-lock. Pre-R357 the literal
3
+ * "last {rel}" text (e.g. "last 5s ago", "last 10s ago") had
4
+ * natural-figure digits — the ticker updates every second, so the
5
+ * 9→10 boundary on seconds and 59s→1m flip both jittered ~1-2 px of
6
+ * glyph width, propagating through the inline-flex chip row and
7
+ * nudging the freshness DOT + chip's left edge. Tabular-nums on the
8
+ * wrapper applies to all descendant digits (letters render at
9
+ * natural widths). Joins the R224-R232 info-density tabular-nums
10
+ * sweep at the chip-row freshness scope.
11
+ *
12
+ * Contract (cyber, fresh messages so the suffix renders):
13
+ * - Freshness wrapper element computed font-variant-numeric
14
+ * resolves to 'tabular-nums'.
15
+ * - data-active-links-freshness-wrapper attr present on the span.
16
+ * - The wrapper still carries text-gray-400 (R342) — invariant.
17
+ * - The freshness dot still has its color transition (R342) —
18
+ * invariant.
19
+ * - The active-links chip still mounts with the suffix child.
20
+ */
21
+ import { chromium } from 'playwright';
22
+ import { readFileSync } from 'node:fs';
23
+
24
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
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
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
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) => ({
37
+ alias, status: 'working', 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: [ mk('a'), mk('b'), mk('c') ] } });
42
+ });
43
+ // A fresh message so the active-links chip surfaces a suffix.
44
+ const now = Date.now();
45
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [
46
+ { id: 'm1', from_alias: 'a', to_alias: 'b', content: 'ping',
47
+ network_id: 'default', created_at: new Date(now - 5000).toISOString() },
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-active-links-freshness-wrapper]', { timeout: 15000 });
54
+ await page.waitForTimeout(300);
55
+
56
+ const probe = await page.evaluate(() => {
57
+ const wrap = document.querySelector('[data-active-links-freshness-wrapper]');
58
+ const dot = wrap?.querySelector('[data-active-links-freshness-dot]');
59
+ const cs = wrap ? getComputedStyle(wrap) : null;
60
+ const dotCs = dot ? getComputedStyle(dot) : null;
61
+ return {
62
+ wrapperFontVariant: cs?.fontVariantNumeric ?? null,
63
+ wrapperColor: cs?.color ?? null,
64
+ wrapperCls: wrap?.getAttribute('class') ?? null,
65
+ wrapperText: wrap?.textContent ?? null,
66
+ dotPresent: !!dot,
67
+ dotTransition: dotCs?.transition ?? null,
68
+ dotColor: dotCs?.color ?? null,
69
+ };
70
+ });
71
+
72
+ await browser.close();
73
+
74
+ const hasTrans = (s, prop) =>
75
+ new RegExp(`${prop}\\s+\\d*\\.?\\d*s|${prop}\\s+\\d+ms`, 'i').test(s || '');
76
+
77
+ const results = {
78
+ tabular_nums: /tabular-nums/.test(probe.wrapperFontVariant || ''),
79
+ cls_has_text_gray: /text-gray-400/.test(probe.wrapperCls || ''), // R342
80
+ cls_has_tabular: /tabular-nums/.test(probe.wrapperCls || ''),
81
+ text_has_last: /last/.test(probe.wrapperText || ''),
82
+ dot_present: probe.dotPresent,
83
+ dot_has_color_trans: hasTrans(probe.dotTransition, 'color'), // R342
84
+ };
85
+ const ok = Object.values(results).every(Boolean);
86
+ console.log(`${ok ? '✅' : '❌'} freshness suffix tabular-nums:`, JSON.stringify(results),
87
+ '\n probe:', probe);
88
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,91 @@
1
+ /* Round 353 verification: fullscreen icon (enter + exit variants)
2
+ * picks up group-hover:scale-110 — sibling to R352 zoom icons + R350
3
+ * reset hover-rotate. Closes the chrome-strip per-icon hover-
4
+ * affordance arc:
5
+ * zoom-out R352 scale 1 → 1.1
6
+ * zoom-in R352 scale 1 → 1.1
7
+ * reset R350 rotate 0 → -8°
8
+ * fullscreen R353 scale 1 → 1.1 (this round, both enter+exit svgs)
9
+ *
10
+ * Tailwind 4 compiles scale-110 to the modern CSS `scale` property
11
+ * (verified during R352 — getComputedStyle(el).scale carries the
12
+ * value, not transform).
13
+ *
14
+ * Contract (cyber, non-fullscreen):
15
+ * - Rest: enter-icon computed scale 'none' (no scale applied).
16
+ * - Hover button: enter-icon scale='1.1'.
17
+ * - Inline transition contains scale (or transform — Tailwind 4
18
+ * emits both transition-property entries on transition-transform).
19
+ * - Pre-R353 invariants: R288 strokeWidth=2.5 preserved, anet-
20
+ * chrome-pop class absent at rest, data-topo-chrome-fullscreen-
21
+ * icon="enter" attr intact.
22
+ */
23
+ import { chromium } from 'playwright';
24
+ import { readFileSync } from 'node:fs';
25
+
26
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
27
+ const browser = await chromium.launch({ headless: true });
28
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
29
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
30
+ await ctx.addInitScript(() => {
31
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
32
+ });
33
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
34
+ await ctx.route('**/api/hub/status*', async (route) => {
35
+ const r = await route.fetch();
36
+ const b = await r.json();
37
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
38
+ const mk = (alias) => ({
39
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
40
+ network_id: nid, project_dir: null,
41
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
42
+ });
43
+ await route.fulfill({ response: r, json: { ...b, sessions: [ mk('a'), mk('b'), mk('c') ] } });
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
+
48
+ const page = await ctx.newPage();
49
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
50
+ await page.waitForSelector('[data-topo-chrome-fullscreen-icon="enter"]', { timeout: 15000 });
51
+ await page.waitForTimeout(300);
52
+
53
+ const restProbe = await page.evaluate(() => {
54
+ const ico = document.querySelector('[data-topo-chrome-fullscreen-icon="enter"]');
55
+ const cs = ico ? getComputedStyle(ico) : null;
56
+ return {
57
+ scale: cs?.scale ?? null,
58
+ transition: cs?.transition ?? null,
59
+ strokeW: ico?.getAttribute('stroke-width') ?? null,
60
+ className: ico?.getAttribute('class') ?? null,
61
+ };
62
+ });
63
+
64
+ // Hover the fullscreen button.
65
+ await page.hover('[data-topo-chrome-fullscreen]');
66
+ await page.waitForTimeout(300);
67
+ const hoverProbe = await page.evaluate(() => {
68
+ const ico = document.querySelector('[data-topo-chrome-fullscreen-icon="enter"]');
69
+ return { scale: ico ? getComputedStyle(ico).scale : null };
70
+ });
71
+
72
+ await browser.close();
73
+
74
+ const isRestScale = (s) => s === 'none' || s === '1' || s === '1 1' || s === null;
75
+ const isHoverScale110 = (s) => s === '1.1' || s === '1.1 1.1';
76
+ const hasTrans = (s, prop) =>
77
+ new RegExp(`${prop}\\s+\\d*\\.?\\d*s|${prop}\\s+\\d+ms`, 'i').test(s || '');
78
+
79
+ const results = {
80
+ rest_scale_1: isRestScale(restProbe.scale),
81
+ trans_has_scale: hasTrans(restProbe.transition, 'scale') || hasTrans(restProbe.transition, 'transform'),
82
+ strokew_2_5: restProbe.strokeW === '2.5', // R288
83
+ no_chrome_pop_rest: !/anet-chrome-pop/.test(restProbe.className || ''),
84
+ has_group_hover_cls: /group-hover:scale-110/.test(restProbe.className || ''),
85
+ hover_scale_110: isHoverScale110(hoverProbe.scale),
86
+ };
87
+ const ok = Object.values(results).every(Boolean);
88
+ console.log(`${ok ? '✅' : '❌'} fullscreen icon hover-scale:`, JSON.stringify(results),
89
+ '\n rest: ', restProbe,
90
+ '\n hover:', hoverProbe);
91
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,105 @@
1
+ /* Round 380 verification: cluster box stroke gets round linecap +
2
+ * round linejoin. Sibling SVG stroke-softening polish to R378 flow-
3
+ * rail linecap + R379 minimap viewport linejoin — extends the family
4
+ * to the group cluster boundary box (grid layout only).
5
+ *
6
+ * Family snapshot post-R380:
7
+ * R288 chrome icons strokeLinecap='round'
8
+ * R378 flow-rail dashes strokeLinecap='round'
9
+ * R380 group box dashes strokeLinecap='round' (this round)
10
+ * R379 viewport rect strokeLinejoin='round'
11
+ * R380 group box corners strokeLinejoin='round' (this round)
12
+ *
13
+ * Contract (cyber, grid layout):
14
+ * - [data-group-box-pinned] elements have stroke-linecap='round'
15
+ * + stroke-linejoin='round'.
16
+ * - data-group-box-linecap + data-group-box-linejoin === 'round'.
17
+ * - Pre-R380 invariants:
18
+ * * strokeWidth=1.5 at rest (R68 ladder)
19
+ * * strokeDasharray='6 6' at rest (R85 marching ants)
20
+ * * data-group-box-pinned 'false' at rest
21
+ * * data-group-box-lifted 'false' at rest
22
+ */
23
+ import { chromium } from 'playwright';
24
+ import { readFileSync } from 'node:fs';
25
+
26
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
27
+ const browser = await chromium.launch({ headless: true });
28
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
29
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
30
+ await ctx.addInitScript(() => {
31
+ try {
32
+ localStorage.setItem('anet-theme', 'cyber');
33
+ sessionStorage.setItem('anet_v3_auth', '1');
34
+ localStorage.setItem('anet-layout', JSON.stringify('grid'));
35
+ } catch {}
36
+ });
37
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
38
+ await ctx.route('**/api/hub/status*', async (route) => {
39
+ const r = await route.fetch();
40
+ const b = await r.json();
41
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
42
+ const mk = (alias) => ({
43
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
44
+ network_id: nid, project_dir: null,
45
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
46
+ });
47
+ // Cluster groups via prefix alias → group boxes render.
48
+ await route.fulfill({ response: r, json: { ...b, sessions: [
49
+ mk('alpha-1'), mk('alpha-2'), mk('alpha-3'),
50
+ mk('beta-1'), mk('beta-2'),
51
+ ] } });
52
+ });
53
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
54
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
55
+
56
+ const page = await ctx.newPage();
57
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
58
+ await page.waitForSelector('[data-topo-chrome-layout="grid"]', { timeout: 15000 });
59
+ const layoutActive = await page.getAttribute('[data-topo-chrome-layout="grid"]', 'data-topo-chrome-layout-active');
60
+ if (layoutActive !== 'true') await page.click('[data-topo-chrome-layout="grid"]');
61
+ await page.waitForSelector('[data-group-box-pinned]', { timeout: 10000 });
62
+ await page.waitForTimeout(400);
63
+
64
+ const probe = await page.evaluate(() => {
65
+ const boxes = Array.from(document.querySelectorAll('[data-group-box-pinned]'));
66
+ return boxes.map(el => ({
67
+ linecapAttr: el.getAttribute('stroke-linecap'),
68
+ linejoinAttr: el.getAttribute('stroke-linejoin'),
69
+ linecapData: el.getAttribute('data-group-box-linecap'),
70
+ linejoinData: el.getAttribute('data-group-box-linejoin'),
71
+ strokeWidth: el.getAttribute('stroke-width'),
72
+ strokeDasharray: el.getAttribute('stroke-dasharray'),
73
+ pinnedAttr: el.getAttribute('data-group-box-pinned'),
74
+ liftedAttr: el.getAttribute('data-group-box-lifted'),
75
+ }));
76
+ });
77
+
78
+ await browser.close();
79
+
80
+ const allOk = probe.length >= 1 && probe.every(b =>
81
+ b.linecapAttr === 'round'
82
+ && b.linejoinAttr === 'round'
83
+ && b.linecapData === 'round'
84
+ && b.linejoinData === 'round'
85
+ && b.strokeWidth === '1.5' // R68 rest
86
+ && b.strokeDasharray === '6 6' // R85 rest
87
+ && b.pinnedAttr === 'false'
88
+ && b.liftedAttr === 'false'
89
+ );
90
+
91
+ const results = {
92
+ boxes_rendered: probe.length >= 1,
93
+ all_linecap_round: probe.every(b => b.linecapAttr === 'round'),
94
+ all_linejoin_round: probe.every(b => b.linejoinAttr === 'round'),
95
+ all_data_linecap: probe.every(b => b.linecapData === 'round'),
96
+ all_data_linejoin: probe.every(b => b.linejoinData === 'round'),
97
+ all_strokew_1_5_rest: probe.every(b => b.strokeWidth === '1.5'),
98
+ all_dash_6_6_rest: probe.every(b => b.strokeDasharray === '6 6'),
99
+ all_pinned_false: probe.every(b => b.pinnedAttr === 'false'),
100
+ all_lifted_false: probe.every(b => b.liftedAttr === 'false'),
101
+ };
102
+ const ok = allOk && Object.values(results).every(Boolean);
103
+ console.log(`${ok ? '✅' : '❌'} group box strokeLinecap + Linejoin = round:`, JSON.stringify(results),
104
+ '\n boxes:', probe.map(b => ({ linecap: b.linecapAttr, linejoin: b.linejoinAttr, sw: b.strokeWidth, dash: b.strokeDasharray })));
105
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,108 @@
1
+ /* Round 366 verification: group label member-count tspan fontWeight
2
+ * 400 → 500. Sibling polish to R363 recent-row alias text fw 400 →
3
+ * 500 + R364 legend-row label fw 400 → 500. Closes the per-row
4
+ * 'count is fw 500 against label-tier fw 700' pattern at the
5
+ * group-label scope (grid layout cluster mark).
6
+ *
7
+ * Hierarchy snapshot post-R366 across 3 row surfaces:
8
+ * recent count(hot/cold) fw 700/600 (R320)
9
+ * recent alias fw 500 (R363)
10
+ * legend count fw 600 (R309)
11
+ * legend label fw 500 (R364)
12
+ * group name fw 700 (legacy)
13
+ * group count fw 500 (R366, this round)
14
+ *
15
+ * Contract (cyber, grid layout — group label only appears in grid):
16
+ * - Each rendered [data-group-label-count] tspan has
17
+ * font-weight === '500'.
18
+ * - data-group-label-count-font-weight === '500'.
19
+ * - Pre-R366 invariants:
20
+ * * dx="6" + fontSize="11" + tabular-nums
21
+ * * R229 fill-inherit (no explicit fill on tspan)
22
+ * * data-group-label-count-value still surfaces the count
23
+ */
24
+ import { chromium } from 'playwright';
25
+ import { readFileSync } from 'node:fs';
26
+
27
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
28
+ const browser = await chromium.launch({ headless: true });
29
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
30
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
31
+ await ctx.addInitScript(() => {
32
+ try {
33
+ localStorage.setItem('anet-theme', 'cyber');
34
+ sessionStorage.setItem('anet_v3_auth', '1');
35
+ // Pre-select grid layout (R106 cluster algorithm + group labels).
36
+ localStorage.setItem('anet-layout', JSON.stringify('grid'));
37
+ } catch {}
38
+ });
39
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
40
+ await ctx.route('**/api/hub/status*', async (route) => {
41
+ const r = await route.fetch();
42
+ const b = await r.json();
43
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
44
+ const mk = (alias) => ({
45
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
46
+ network_id: nid, project_dir: null,
47
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
48
+ });
49
+ // Multiple alpha/beta nodes → cluster groups render with member counts.
50
+ await route.fulfill({ response: r, json: { ...b, sessions: [
51
+ mk('alpha-1'), mk('alpha-2'), mk('alpha-3'),
52
+ mk('beta-1'), mk('beta-2'),
53
+ ] } });
54
+ });
55
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
56
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
57
+
58
+ const page = await ctx.newPage();
59
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
60
+ // In case grid layout didn't take from localStorage, click the grid toggle.
61
+ await page.waitForSelector('[data-topo-chrome-layout="grid"]', { timeout: 15000 });
62
+ const layoutActive = await page.getAttribute('[data-topo-chrome-layout="grid"]', 'data-topo-chrome-layout-active');
63
+ if (layoutActive !== 'true') {
64
+ await page.click('[data-topo-chrome-layout="grid"]');
65
+ }
66
+ await page.waitForSelector('[data-group-label-count]', { timeout: 10000 });
67
+ await page.waitForTimeout(400);
68
+
69
+ const probe = await page.evaluate(() => {
70
+ const tspans = Array.from(document.querySelectorAll('[data-group-label-count]'));
71
+ return tspans.map(el => {
72
+ const cs = getComputedStyle(el);
73
+ return {
74
+ key: el.getAttribute('data-group-label-count'),
75
+ fontWeightAttr: el.getAttribute('font-weight'),
76
+ fontWeightData: el.getAttribute('data-group-label-count-font-weight'),
77
+ fontSizeAttr: el.getAttribute('font-size'),
78
+ countValue: el.getAttribute('data-group-label-count-value'),
79
+ fontVariant: cs.fontVariantNumeric,
80
+ explicitFill: el.getAttribute('fill'), // R229: should be null (inherit)
81
+ };
82
+ });
83
+ });
84
+
85
+ await browser.close();
86
+
87
+ const allOk = probe.length > 0 && probe.every(r =>
88
+ r.fontWeightAttr === '500'
89
+ && r.fontWeightData === '500'
90
+ && r.fontSizeAttr === '11'
91
+ && /tabular-nums/.test(r.fontVariant || '')
92
+ && (r.countValue || '').length > 0
93
+ && r.explicitFill === null // R229 fill-inherit invariant
94
+ );
95
+
96
+ const results = {
97
+ rows_rendered: probe.length >= 1,
98
+ all_rows_fw_500: probe.every(r => r.fontWeightAttr === '500'),
99
+ all_rows_data_500: probe.every(r => r.fontWeightData === '500'),
100
+ all_rows_fontsize_11: probe.every(r => r.fontSizeAttr === '11'),
101
+ all_rows_tabular: probe.every(r => /tabular-nums/.test(r.fontVariant || '')),
102
+ all_rows_have_count: probe.every(r => (r.countValue || '').length > 0),
103
+ all_rows_fill_inherit: probe.every(r => r.explicitFill === null),
104
+ };
105
+ const ok = allOk && Object.values(results).every(Boolean);
106
+ console.log(`${ok ? '✅' : '❌'} group-label count fw 400 → 500:`, JSON.stringify(results),
107
+ '\n rows:', probe.map(r => ({ key: r.key, fw: r.fontWeightAttr, count: r.countValue })));
108
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,101 @@
1
+ /* Round 388 verification: hover-detail panel body lines fw 400 → 500.
2
+ * Small-text fw lift family (6th anchor) after R363 recent-row
3
+ * alias, R364 legend-row label, R366 group-label count tspan, R368
4
+ * +N footer, R373 pressure-bar kicker. Lifts the three fontSize=9
5
+ * body lines (runtime, host, task) in the on-hover product card to
6
+ * font-weight=500 so they read consistently bolder against the
7
+ * cyber-theme backdrop.
8
+ *
9
+ * Contract:
10
+ * - On hover, the hover-detail card mounts and:
11
+ * * Each fontSize=9 body line has font-weight attr === '500'
12
+ * * Each carries data-topo-hover-detail-body-fw === '500'
13
+ * * Tier structure preserved:
14
+ * - y=16 vendor headline at fw=700 unchanged
15
+ * - y=32 model subhead has no fw attr (preserved)
16
+ * - Task line keeps opacity 0.7 (R388 doesn't touch it)
17
+ * - The 3 body lines render at fontSize=9 (R388 doesn't touch size)
18
+ *
19
+ * Fixture: 2 idle sessions so hover-detail mounts cleanly without
20
+ * the dense-layout >16-node gate. Computed font-weight is asserted
21
+ * to be '500' as the browser-canonical resolution of the SVG attr.
22
+ */
23
+ import { chromium } from 'playwright';
24
+ import { readFileSync } from 'node:fs';
25
+
26
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
27
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
28
+
29
+ const browser = await chromium.launch({ headless: true });
30
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
31
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
32
+ await ctx.addInitScript(() => {
33
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
34
+ });
35
+ await ctx.route('**/api/hub/status*', async (route) => {
36
+ const r = await route.fetch();
37
+ const b = await r.json();
38
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
39
+ const mk = (alias) => ({
40
+ alias, status: 'idle', model: 'claude-opus-4', runtime: 'claude-code-cli',
41
+ network_id: nid, project_dir: null,
42
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
43
+ });
44
+ await route.fulfill({ response: r, json: { ...b, sessions: [ mk('alpha'), mk('beta') ] } });
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('g[data-node]', { timeout: 15000 });
52
+ await page.waitForTimeout(300);
53
+ await page.hover('g[data-node]');
54
+ await page.waitForSelector('[data-topo-hover-detail]', { timeout: 5000 });
55
+ await page.waitForTimeout(300);
56
+
57
+ const probe = await page.evaluate(() => {
58
+ const card = document.querySelector('[data-topo-hover-detail]');
59
+ if (!card) return null;
60
+ const texts = Array.from(card.querySelectorAll('text'));
61
+ const lines = texts.map((t) => ({
62
+ y: t.getAttribute('y'),
63
+ fontSize: t.getAttribute('font-size'),
64
+ fwAttr: t.getAttribute('font-weight'),
65
+ fwData: t.getAttribute('data-topo-hover-detail-body-fw'),
66
+ fwComputed: getComputedStyle(t).fontWeight,
67
+ opacityAttr: t.getAttribute('opacity'),
68
+ }));
69
+ return { lines };
70
+ });
71
+
72
+ await browser.close();
73
+
74
+ const byY = Object.fromEntries((probe?.lines || []).map((l) => [l.y, l]));
75
+
76
+ const results = {
77
+ // y=16 vendor headline unchanged at fw=700
78
+ vendor_y16_fw700: byY['16']?.fwAttr === '700',
79
+ // y=32 model subhead — no R388 attrs (size 10 differentiation)
80
+ model_y32_size_10: byY['32']?.fontSize === '10',
81
+ model_y32_no_body_fw_attr: byY['32']?.fwData === null || byY['32']?.fwData === undefined,
82
+ // y=48 runtime body — R388 lifts to fw=500
83
+ runtime_y48_size_9: byY['48']?.fontSize === '9',
84
+ runtime_y48_fw500: byY['48']?.fwAttr === '500',
85
+ runtime_y48_data_500: byY['48']?.fwData === '500',
86
+ runtime_y48_computed_500: byY['48']?.fwComputed === '500',
87
+ // y=64 host body — R388 lifts to fw=500
88
+ host_y64_size_9: byY['64']?.fontSize === '9',
89
+ host_y64_fw500: byY['64']?.fwAttr === '500',
90
+ host_y64_data_500: byY['64']?.fwData === '500',
91
+ host_y64_computed_500: byY['64']?.fwComputed === '500',
92
+ // y=80 task body — R388 lifts to fw=500, opacity 0.7 preserved
93
+ task_y80_size_9: byY['80']?.fontSize === '9',
94
+ task_y80_fw500: byY['80']?.fwAttr === '500',
95
+ task_y80_data_500: byY['80']?.fwData === '500',
96
+ task_y80_opacity_07: byY['80']?.opacityAttr === '0.7',
97
+ };
98
+ const ok = Object.values(results).every(Boolean);
99
+ console.log(`${ok ? '✅' : '❌'} hover-detail body lines fw 400 → 500:`, JSON.stringify(results),
100
+ '\n lines:', JSON.stringify(probe?.lines, null, 2));
101
+ process.exit(ok ? 0 : 1);