@sleep2agi/agent-network-dashboard 0.5.1-preview.4 → 0.5.1-preview.41

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 (191) 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/03s0-hiyr832o.js +4 -0
  141. package/.next/static/chunks/057c8hh9cri2y.js +1 -0
  142. package/.next/static/chunks/0hwb_h953qm4d.css +2 -0
  143. package/.next/static/chunks/0ulf.39xk5_1v.js +1 -0
  144. package/.next/trace +2 -2
  145. package/.next/trace-build +1 -1
  146. package/app/components/TopoGraph.tsx +787 -58
  147. package/package.json +1 -1
  148. package/scripts/topo-chip-digit-fontweight-test.mjs +105 -0
  149. package/scripts/topo-chrome-segmented-radius-test.mjs +100 -0
  150. package/scripts/topo-edge-badge-fontsize-test.mjs +90 -0
  151. package/scripts/topo-edge-badge-opacity-test.mjs +80 -0
  152. package/scripts/topo-edge-badge-stroke-test.mjs +92 -0
  153. package/scripts/topo-filter-pill-hover-opacity-test.mjs +110 -0
  154. package/scripts/topo-filter-pill-x-hover-scale-test.mjs +99 -0
  155. package/scripts/topo-flow-rail-linecap-test.mjs +79 -0
  156. package/scripts/topo-freshness-chip-tabular-test.mjs +41 -0
  157. package/scripts/topo-freshness-floor-lift-test.mjs +92 -0
  158. package/scripts/topo-freshness-suffix-tabular-test.mjs +88 -0
  159. package/scripts/topo-fullscreen-icon-hover-scale-test.mjs +91 -0
  160. package/scripts/topo-group-box-stroke-test.mjs +105 -0
  161. package/scripts/topo-group-label-count-fontweight-test.mjs +108 -0
  162. package/scripts/topo-hub-digit-fontsize-test.mjs +86 -0
  163. package/scripts/topo-hub-highlight-radius-test.mjs +90 -0
  164. package/scripts/topo-hub-hover-ring-opacity-test.mjs +96 -0
  165. package/scripts/topo-layout-toggle-hover-tracking-test.mjs +109 -0
  166. package/scripts/topo-layout-toggle-radius-test.mjs +87 -0
  167. package/scripts/topo-legend-label-fontweight-test.mjs +94 -0
  168. package/scripts/topo-minimap-offline-opacity-test.mjs +90 -0
  169. package/scripts/topo-minimap-viewport-hover-test.mjs +109 -0
  170. package/scripts/topo-minimap-viewport-linejoin-test.mjs +75 -0
  171. package/scripts/topo-more-flows-fontweight-test.mjs +103 -0
  172. package/scripts/topo-more-flows-hover-letterspacing-test.mjs +98 -0
  173. package/scripts/topo-panel-count-letterspacing-test.mjs +89 -0
  174. package/scripts/topo-panel-rect-opacity-hover-test.mjs +109 -0
  175. package/scripts/topo-panel-title-hover-letterspacing-test.mjs +97 -0
  176. package/scripts/topo-pressure-bar-height-test.mjs +92 -0
  177. package/scripts/topo-pressure-kicker-fontweight-test.mjs +76 -0
  178. package/scripts/topo-recent-pip-radius-test.mjs +76 -0
  179. package/scripts/topo-recent-row-text-fontweight-test.mjs +90 -0
  180. package/scripts/topo-reset-hover-rotate-test.mjs +102 -0
  181. package/scripts/topo-vendor-glyph-fontweight-test.mjs +102 -0
  182. package/scripts/topo-vendor-letter-hover-scale-test.mjs +129 -0
  183. package/scripts/topo-zoom-icon-hover-scale-test.mjs +114 -0
  184. package/scripts/topo-zoom-level-hover-letterspacing-test.mjs +91 -0
  185. package/.next/static/chunks/04sya_8oheege.js +0 -4
  186. package/.next/static/chunks/08c-dt44c.swe.js +0 -1
  187. package/.next/static/chunks/0aauz~36q5n2a.css +0 -2
  188. package/.next/static/chunks/12e0z26h2p0xl.js +0 -1
  189. /package/.next/static/{CkuUcpLXVJ-OsmjZqi3xX → 4Mzmc_vft-1RJtomdAHL-}/_buildManifest.js +0 -0
  190. /package/.next/static/{CkuUcpLXVJ-OsmjZqi3xX → 4Mzmc_vft-1RJtomdAHL-}/_clientMiddlewareManifest.js +0 -0
  191. /package/.next/static/{CkuUcpLXVJ-OsmjZqi3xX → 4Mzmc_vft-1RJtomdAHL-}/_ssgManifest.js +0 -0
@@ -0,0 +1,109 @@
1
+ /* Round 348 verification: panel rect opacity hover-state bump.
2
+ * Recent-signal + legend panels both lift their fill opacity when
3
+ * hoveredPanel === <self>. Cyber: 0.92 → 0.97. Light: 0.97 → 1.0.
4
+ * Joins the panel-hover cue stack:
5
+ * - R135 drop-shadow boost
6
+ * - R345 panel title letter-spacing tween 0.3 → 0.4
7
+ * - R266 fill theme-flip
8
+ * - R348 rect opacity bump (this round)
9
+ * Pure paint-level change — geometry-safe, bbox unchanged, so
10
+ * topo-overlap-test invariants hold. The R247 transition list already
11
+ * includes `opacity 200ms ease-out` so the value tween is automatic.
12
+ *
13
+ * Contract (cyber theme):
14
+ * - Rest: both panel rects opacity=0.92.
15
+ * - Hover recent: recent-panel rect opacity=0.97.
16
+ * - Hover legend: legend-panel rect opacity=0.97.
17
+ * - data-topo-panel-elevation selectors locate the rects (existing).
18
+ * - data-topo-panel-hovered toggles on parent <g> (existing R135).
19
+ * - Pre-R348 invariants: R331 rx=10 + R247 transition list intact.
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('alpha'), mk('beta'), mk('gamma') ] } });
42
+ });
43
+ // One message so the recent-signal panel mounts.
44
+ const now = Date.now();
45
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [
46
+ { id: 'm1', from_alias: 'alpha', to_alias: 'beta', 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-topo-panel-elevation="recent"]', { timeout: 15000 });
54
+ await page.waitForSelector('[data-topo-panel-elevation="legend"]', { timeout: 5000 });
55
+ await page.waitForTimeout(400);
56
+
57
+ // Rest probe (cyber theme).
58
+ const restProbe = await page.evaluate(() => {
59
+ const r = document.querySelector('[data-topo-panel-elevation="recent"]');
60
+ const l = document.querySelector('[data-topo-panel-elevation="legend"]');
61
+ return {
62
+ recentOpacity: r?.getAttribute('opacity') ?? null,
63
+ legendOpacity: l?.getAttribute('opacity') ?? null,
64
+ recentRx: r?.getAttribute('rx') ?? null,
65
+ legendRx: l?.getAttribute('rx') ?? null,
66
+ recentTrans: r ? getComputedStyle(r).transition : null,
67
+ legendTrans: l ? getComputedStyle(l).transition : null,
68
+ };
69
+ });
70
+
71
+ // Hover recent panel.
72
+ await page.hover('[data-topo-panel="recent"]');
73
+ await page.waitForTimeout(300);
74
+ const recentHoverProbe = await page.evaluate(() => {
75
+ const r = document.querySelector('[data-topo-panel-elevation="recent"]');
76
+ return { opacity: r?.getAttribute('opacity') ?? null };
77
+ });
78
+
79
+ // Hover legend panel.
80
+ await page.hover('[data-topo-panel="legend"]');
81
+ await page.waitForTimeout(300);
82
+ const legendHoverProbe = await page.evaluate(() => {
83
+ const l = document.querySelector('[data-topo-panel-elevation="legend"]');
84
+ return { opacity: l?.getAttribute('opacity') ?? null };
85
+ });
86
+
87
+ await browser.close();
88
+
89
+ const hasTrans = (s, prop) =>
90
+ new RegExp(`${prop}\\s+\\d*\\.?\\d*s|${prop}\\s+\\d+ms`, 'i').test(s || '');
91
+
92
+ const results = {
93
+ rest_recent_0_92: restProbe.recentOpacity === '0.92',
94
+ rest_legend_0_92: restProbe.legendOpacity === '0.92',
95
+ rest_recent_rx_10: restProbe.recentRx === '10',
96
+ rest_legend_rx_10: restProbe.legendRx === '10',
97
+ trans_recent_op: hasTrans(restProbe.recentTrans, 'opacity'),
98
+ trans_legend_op: hasTrans(restProbe.legendTrans, 'opacity'),
99
+ trans_recent_filter: hasTrans(restProbe.recentTrans, 'filter'),
100
+ trans_legend_filter: hasTrans(restProbe.legendTrans, 'filter'),
101
+ hover_recent_0_97: recentHoverProbe.opacity === '0.97',
102
+ hover_legend_0_97: legendHoverProbe.opacity === '0.97',
103
+ };
104
+ const ok = Object.values(results).every(Boolean);
105
+ console.log(`${ok ? '✅' : '❌'} panel rect opacity hover bump:`, JSON.stringify(results),
106
+ '\n rest: ', restProbe,
107
+ '\n hover recent:', recentHoverProbe,
108
+ '\n hover legend:', legendHoverProbe);
109
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,97 @@
1
+ /* Round 345 verification: panel titles ("recent signal" + "legend")
2
+ * gain letter-spacing tween 0.3 → 0.4 on panel hover. Sibling to
3
+ * R344 hover-letter-spacing applied to the +N more flows footer —
4
+ * same gesture vocabulary at panel-title scope.
5
+ *
6
+ * Contract:
7
+ * - Rest: both titles letterSpacing 0.3.
8
+ * - Hover recent panel: recent-panel-title letterSpacing 0.4.
9
+ * - Hover legend panel: legend-panel-title letterSpacing 0.4
10
+ * (separately).
11
+ * - Inline transition list contains 'letter-spacing' on both.
12
+ * - R344 footer hover regression intact (when flowLinks > 3).
13
+ * - R317/R318/R294 chrome + pulse regressions intact.
14
+ */
15
+ import { chromium } from 'playwright';
16
+ import { readFileSync } from 'node:fs';
17
+
18
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
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
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
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) => ({
31
+ alias, status: 'working', 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: [ mk('alpha'), mk('beta'), mk('gamma') ] } });
36
+ });
37
+ // One message → recent-signal panel mounts so its title is testable.
38
+ const now = Date.now();
39
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [
40
+ { id: 'm1', from_alias: 'alpha', to_alias: 'beta', content: 'ping',
41
+ network_id: 'default', created_at: new Date(now - 5000).toISOString() },
42
+ ] } }));
43
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
44
+
45
+ const page = await ctx.newPage();
46
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
47
+ await page.waitForSelector('[data-recent-panel-title]', { timeout: 15000 });
48
+ await page.waitForSelector('[data-legend-panel-title]', { timeout: 5000 });
49
+ await page.waitForTimeout(300);
50
+
51
+ // Rest probe.
52
+ const restProbe = await page.evaluate(() => {
53
+ const r = document.querySelector('[data-recent-panel-title]');
54
+ const l = document.querySelector('[data-legend-panel-title]');
55
+ return {
56
+ recentLetterSp: r?.getAttribute('letter-spacing') ?? null,
57
+ recentTrans: r ? getComputedStyle(r).transition : null,
58
+ legendLetterSp: l?.getAttribute('letter-spacing') ?? null,
59
+ legendTrans: l ? getComputedStyle(l).transition : null,
60
+ };
61
+ });
62
+
63
+ // Hover the recent panel via its data attribute.
64
+ await page.hover('[data-topo-panel="recent"]');
65
+ await page.waitForTimeout(300);
66
+ const recentHoverProbe = await page.evaluate(() => {
67
+ const r = document.querySelector('[data-recent-panel-title]');
68
+ return { letterSp: r?.getAttribute('letter-spacing') ?? null };
69
+ });
70
+
71
+ // Move to legend panel.
72
+ await page.hover('[data-topo-panel="legend"]');
73
+ await page.waitForTimeout(300);
74
+ const legendHoverProbe = await page.evaluate(() => {
75
+ const l = document.querySelector('[data-legend-panel-title]');
76
+ return { letterSp: l?.getAttribute('letter-spacing') ?? null };
77
+ });
78
+
79
+ await browser.close();
80
+
81
+ const hasLetterSpacingTrans = (s) =>
82
+ /letter-spacing\s+\d*\.?\d*s|letter-spacing\s+\d+ms/i.test(s || '');
83
+
84
+ const results = {
85
+ rest_recent_0_3: restProbe.recentLetterSp === '0.3',
86
+ rest_legend_0_3: restProbe.legendLetterSp === '0.3',
87
+ recent_transition_has_ls: hasLetterSpacingTrans(restProbe.recentTrans),
88
+ legend_transition_has_ls: hasLetterSpacingTrans(restProbe.legendTrans),
89
+ hover_recent_0_4: recentHoverProbe.letterSp === '0.4',
90
+ hover_legend_0_4: legendHoverProbe.letterSp === '0.4',
91
+ };
92
+ const ok = Object.values(results).every(Boolean);
93
+ console.log(`${ok ? '✅' : '❌'} panel title hover letter-spacing:`, JSON.stringify(results),
94
+ '\n rest: ', restProbe,
95
+ '\n hover recent:', recentHoverProbe,
96
+ '\n hover legend:', legendHoverProbe);
97
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,92 @@
1
+ /* Round 374 verification: pressure-bar height h-1.5 → h-2 (6 → 8 px).
2
+ * Sibling visual-weight bump (8th canvas/HTML anchor in the family).
3
+ * +33 % bar height gives working/idle/offline segments more visibility.
4
+ *
5
+ * Geometry-safe: items-center flex centers the bar inside the chip's
6
+ * py-1 — bar at 8 px stays comfortably inside the 10-px text-row.
7
+ *
8
+ * Contract (cyber, fixture with mix):
9
+ * - Pressure bar wrapper computed height === '8px' (h-2).
10
+ * - data-fleet-pressure-bar-height === 'h-2'.
11
+ * - Pre-R374 invariants:
12
+ * * width still w-16 (64 px)
13
+ * * inline-flex display + rounded-full + overflow-hidden
14
+ * * background rgb(75 85 99 / 0.25)
15
+ * * Parent [data-fleet-pressure] chip className unchanged
16
+ * (font-mono + chip structure)
17
+ */
18
+ import { chromium } from 'playwright';
19
+ import { readFileSync } from 'node:fs';
20
+
21
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
22
+ const browser = await chromium.launch({ headless: true });
23
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
24
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
25
+ await ctx.addInitScript(() => {
26
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
27
+ });
28
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
29
+ await ctx.route('**/api/hub/status*', async (route) => {
30
+ const r = await route.fetch();
31
+ const b = await r.json();
32
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
33
+ const mk = (alias, status = 'working') => ({
34
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
35
+ network_id: nid, project_dir: null,
36
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
37
+ });
38
+ // Mix so all 3 segments render.
39
+ await route.fulfill({ response: r, json: { ...b, sessions: [
40
+ mk('w1'), mk('w2'),
41
+ mk('i1', 'idle'), mk('i2', 'idle'),
42
+ mk('o1', 'offline'),
43
+ ] } });
44
+ });
45
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
46
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
47
+
48
+ const page = await ctx.newPage();
49
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
50
+ await page.waitForSelector('[data-fleet-pressure-bar-height]', { timeout: 15000 });
51
+ await page.waitForTimeout(300);
52
+
53
+ const probe = await page.evaluate(() => {
54
+ const bar = document.querySelector('[data-fleet-pressure-bar-height]');
55
+ const cs = bar ? getComputedStyle(bar) : null;
56
+ const parent = document.querySelector('[data-fleet-pressure]');
57
+ // Count visible segments inside the bar.
58
+ const segs = bar ? Array.from(bar.querySelectorAll('[data-pressure-seg]')) : [];
59
+ return {
60
+ height: cs?.height ?? null,
61
+ width: cs?.width ?? null,
62
+ barHeightData: bar?.getAttribute('data-fleet-pressure-bar-height') ?? null,
63
+ display: cs?.display ?? null,
64
+ borderRadius: cs?.borderRadius ?? null,
65
+ overflow: cs?.overflow ?? null,
66
+ bgColor: cs?.backgroundColor ?? null,
67
+ parentMono: /font-mono/.test(parent?.getAttribute('class') || ''),
68
+ segCount: segs.length,
69
+ };
70
+ });
71
+
72
+ await browser.close();
73
+
74
+ const results = {
75
+ height_8px: probe.height === '8px', // h-2
76
+ width_64px: probe.width === '64px', // w-16
77
+ data_height_h_2: probe.barHeightData === 'h-2',
78
+ // Flex items get display blockified: inline-flex → flex when nested
79
+ // in flex container. Accept either.
80
+ display_inline_flex: probe.display === 'inline-flex' || probe.display === 'flex',
81
+ // Tailwind rounded-full is border-radius: 9999px, which browsers
82
+ // sometimes render as 3.35544e+07px (huge approximation). Accept any
83
+ // value indicating fully-rounded.
84
+ has_rounded_full: /9999|3\.355|e\+/.test(probe.borderRadius || ''),
85
+ overflow_hidden: probe.overflow === 'hidden',
86
+ parent_font_mono: probe.parentMono,
87
+ three_segments: probe.segCount === 3,
88
+ };
89
+ const ok = Object.values(results).every(Boolean);
90
+ console.log(`${ok ? '✅' : '❌'} pressure-bar height 1.5 → 2:`, JSON.stringify(results),
91
+ '\n probe:', probe);
92
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,76 @@
1
+ /* Round 373 verification: pressure-bar kicker label fontWeight 400 →
2
+ * 500. Sibling small-text fw lift family with R363/R364/R366/R368 —
3
+ * 5th surface in the family.
4
+ *
5
+ * Family snapshot post-R373:
6
+ * recent-row alias fw 500 (R363)
7
+ * legend-row label fw 500 (R364)
8
+ * group-label count fw 500 (R366)
9
+ * +N more flows footer fw 500 (R368)
10
+ * pressure-bar kicker fw 500 (R373, this round)
11
+ *
12
+ * Contract (cyber, fixture with mix so pressure bar renders):
13
+ * - [data-fleet-pressure-kicker] computed fontWeight === '500'.
14
+ * - Pre-R373 invariants:
15
+ * * text-[10px] (fontSize 10) preserved
16
+ * * tracking-wide (letter-spacing 0.025em) preserved
17
+ * * Parent [data-fleet-pressure] still has font-mono + chip
18
+ * structure
19
+ */
20
+ import { chromium } from 'playwright';
21
+ import { readFileSync } from 'node:fs';
22
+
23
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
24
+ const browser = await chromium.launch({ headless: true });
25
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
26
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
27
+ await ctx.addInitScript(() => {
28
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
29
+ });
30
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
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) => ({
36
+ alias, status: 'working', 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: [ mk('a'), mk('b'), mk('c') ] } });
41
+ });
42
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
43
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
44
+
45
+ const page = await ctx.newPage();
46
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
47
+ await page.waitForSelector('[data-fleet-pressure-kicker]', { timeout: 15000 });
48
+ await page.waitForTimeout(300);
49
+
50
+ const probe = await page.evaluate(() => {
51
+ const kicker = document.querySelector('[data-fleet-pressure-kicker]');
52
+ const parent = document.querySelector('[data-fleet-pressure]');
53
+ const cs = kicker ? getComputedStyle(kicker) : null;
54
+ return {
55
+ fontWeight: cs?.fontWeight ?? null,
56
+ fontSize: cs?.fontSize ?? null,
57
+ letterSpacing: cs?.letterSpacing ?? null,
58
+ text: kicker?.textContent ?? null,
59
+ parentCls: parent?.getAttribute('class') ?? null,
60
+ };
61
+ });
62
+
63
+ await browser.close();
64
+
65
+ const results = {
66
+ font_weight_500: probe.fontWeight === '500',
67
+ font_size_10px: probe.fontSize === '10px',
68
+ // tracking-wide = 0.025em → at 10px font that's 0.25px computed.
69
+ letter_spacing_tracking_wide: /0\.25px|0\.025em/.test(probe.letterSpacing || ''),
70
+ text_is_pressure: probe.text === 'pressure',
71
+ parent_font_mono: /font-mono/.test(probe.parentCls || ''),
72
+ };
73
+ const ok = Object.values(results).every(Boolean);
74
+ console.log(`${ok ? '✅' : '❌'} pressure-bar kicker fw 400 → 500:`, JSON.stringify(results),
75
+ '\n probe:', probe);
76
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,76 @@
1
+ /* Round 359 verification: recency pip base radius 1.6 → 1.8. Sibling
2
+ * to R358 freshness floor lift (alpha 0.25 → 0.30) — together the
3
+ * stale pip stays distinguishable as it ages.
4
+ * pre-R358/R359: stale pip painted at r=1.6 + α=0.25 (near-invisible)
5
+ * post-R358: stale pip α=0.30
6
+ * post-R359: stale pip r=1.8 (1.8² / 1.6² ≈ 1.27, ~27% more glyph)
7
+ *
8
+ * Same visual-weight family as R287 minimap stroke 1 → 1.5 and R295
9
+ * legend swatch base radius 5.5 → 6.
10
+ *
11
+ * Contract:
12
+ * - Pip element computed r === '1.8' (or rendered SVG r attr).
13
+ * - data-recent-row-freshness-radius === '1.8'.
14
+ * - Pre-R359 invariants preserved:
15
+ * * R358 stale-alpha floor 0.30 still applies (visible on > 300s)
16
+ * * data-recent-row-freshness attr still surfaces the link key
17
+ * * Pip stays inside the 7-px left margin (cx=10 unchanged)
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 browser = await chromium.launch({ headless: true });
24
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
25
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
26
+ await ctx.addInitScript(() => {
27
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
28
+ });
29
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
30
+ await ctx.route('**/api/hub/status*', async (route) => {
31
+ const r = await route.fetch();
32
+ const b = await r.json();
33
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
34
+ const mk = (alias) => ({
35
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
36
+ network_id: nid, project_dir: null,
37
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
38
+ });
39
+ await route.fulfill({ response: r, json: { ...b, sessions: [ mk('a'), mk('b') ] } });
40
+ });
41
+ const now = Date.now();
42
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [
43
+ // Fresh message → row with pip mounts.
44
+ { id: 'm1', from_alias: 'a', to_alias: 'b', content: 'ping',
45
+ network_id: 'default', created_at: new Date(now - 5000).toISOString(),
46
+ last_at: new Date(now - 5000).toISOString() },
47
+ ] } }));
48
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
49
+
50
+ const page = await ctx.newPage();
51
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
52
+ await page.waitForSelector('[data-recent-row-freshness]', { timeout: 15000 });
53
+ await page.waitForTimeout(300);
54
+
55
+ const probe = await page.evaluate(() => {
56
+ const pip = document.querySelector('[data-recent-row-freshness]');
57
+ return {
58
+ rAttr: pip?.getAttribute('r') ?? null,
59
+ radiusAttr: pip?.getAttribute('data-recent-row-freshness-radius') ?? null,
60
+ cxAttr: pip?.getAttribute('cx') ?? null,
61
+ linkKey: pip?.getAttribute('data-recent-row-freshness') ?? null,
62
+ };
63
+ });
64
+
65
+ await browser.close();
66
+
67
+ const results = {
68
+ r_attr_1_8: probe.rAttr === '1.8',
69
+ radius_attr_1_8: probe.radiusAttr === '1.8',
70
+ cx_unchanged_10: probe.cxAttr === '10', // R160 margin invariant
71
+ link_key_present: (probe.linkKey || '').length > 0,
72
+ };
73
+ const ok = Object.values(results).every(Boolean);
74
+ console.log(`${ok ? '✅' : '❌'} recent pip radius 1.6 → 1.8:`, JSON.stringify(results),
75
+ '\n probe:', probe);
76
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,90 @@
1
+ /* Round 363 verification: recent-signal row text fontWeight 400 → 500.
2
+ * At fontSize=9 the default-weight text was thin; R363 lifts to the
3
+ * font-medium tier (500) for better legibility against the panel
4
+ * chrome. R320 count tspan fw 600/700 override stays local, so the
5
+ * count-vs-alias hierarchy holds:
6
+ * alias fw 500 (R363)
7
+ * count fw 600/700 (R320)
8
+ *
9
+ * Contract:
10
+ * - data-recent-row-text element font-weight attr === '500'.
11
+ * - data-recent-row-text-font-weight === '500'.
12
+ * - The inner count tspan still has fw 600 (cold flow, R320 default).
13
+ * - Pre-R363 invariants:
14
+ * * fontSize=9 + fontFamily=monospace
15
+ * * R220 letter-spacing pin tween still wired (style.transition
16
+ * contains letter-spacing)
17
+ * * data-recent-row-text key surfaces the link key
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 browser = await chromium.launch({ headless: true });
24
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
25
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
26
+ await ctx.addInitScript(() => {
27
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
28
+ });
29
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
30
+ await ctx.route('**/api/hub/status*', async (route) => {
31
+ const r = await route.fetch();
32
+ const b = await r.json();
33
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
34
+ const mk = (alias) => ({
35
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
36
+ network_id: nid, project_dir: null,
37
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
38
+ });
39
+ await route.fulfill({ response: r, json: { ...b, sessions: [ mk('a'), mk('b') ] } });
40
+ });
41
+ const now = Date.now();
42
+ // 3 messages on the same link → count = 3 (cold, below the 10 hot threshold).
43
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [
44
+ { id: 'm1', from_alias: 'a', to_alias: 'b', content: 'ping',
45
+ network_id: 'default', created_at: new Date(now - 5000).toISOString() },
46
+ { id: 'm2', from_alias: 'a', to_alias: 'b', content: 'pong',
47
+ network_id: 'default', created_at: new Date(now - 4000).toISOString() },
48
+ { id: 'm3', from_alias: 'a', to_alias: 'b', content: 'ping2',
49
+ network_id: 'default', created_at: new Date(now - 3000).toISOString() },
50
+ ] } }));
51
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
52
+
53
+ const page = await ctx.newPage();
54
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
55
+ await page.waitForSelector('[data-recent-row-text]', { timeout: 15000 });
56
+ await page.waitForTimeout(300);
57
+
58
+ const probe = await page.evaluate(() => {
59
+ const t = document.querySelector('[data-recent-row-text]');
60
+ const count = document.querySelector('[data-recent-row-count]');
61
+ const cs = t ? getComputedStyle(t) : null;
62
+ return {
63
+ fontWeightAttr: t?.getAttribute('font-weight') ?? null,
64
+ fontWeightData: t?.getAttribute('data-recent-row-text-font-weight') ?? null,
65
+ fontSizeAttr: t?.getAttribute('font-size') ?? null,
66
+ fontFamily: t?.getAttribute('font-family') ?? null,
67
+ linkKey: t?.getAttribute('data-recent-row-text') ?? null,
68
+ transition: cs?.transition ?? null,
69
+ countFwAttr: count?.getAttribute('font-weight') ?? null, // R320 override
70
+ };
71
+ });
72
+
73
+ await browser.close();
74
+
75
+ const hasTrans = (s, prop) =>
76
+ new RegExp(`${prop}\\s+\\d*\\.?\\d*s|${prop}\\s+\\d+ms`, 'i').test(s || '');
77
+
78
+ const results = {
79
+ font_weight_500: probe.fontWeightAttr === '500',
80
+ data_fw_500: probe.fontWeightData === '500',
81
+ font_size_9: probe.fontSizeAttr === '9',
82
+ font_family_mono: /monospace/i.test(probe.fontFamily || ''),
83
+ link_key_present: (probe.linkKey || '').length > 0,
84
+ trans_has_letterspacing: hasTrans(probe.transition, 'letter-spacing'), // R220
85
+ count_fw_600: probe.countFwAttr === '600', // R320 cold default
86
+ };
87
+ const ok = Object.values(results).every(Boolean);
88
+ console.log(`${ok ? '✅' : '❌'} recent-row text fw 400 → 500:`, JSON.stringify(results),
89
+ '\n probe:', probe);
90
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,102 @@
1
+ /* Round 350 verification: chrome reset-button icon gains a hover-
2
+ * rotate preview of the R184 click-spin. Pre-R350 hovering the reset
3
+ * button only changed the button bg (white/5); the icon stayed still.
4
+ * R350 nudges the icon -8° on hover — tactile hint that this button
5
+ * rotates the icon on click. Gated on !resetSpinning so anet-reset-
6
+ * spin keyframe still owns transform during its 450 ms run.
7
+ * 350th-round milestone polish.
8
+ *
9
+ * Contract:
10
+ * - Rest: icon transform 'rotate(0deg)' (no rotation), transformOrigin
11
+ * 'center'.
12
+ * - Hover: icon transform 'rotate(-8deg)'.
13
+ * - Inline transition contains 'transform'.
14
+ * - data-topo-chrome-reset-hover toggles on container; data-topo-
15
+ * chrome-reset-icon-hover mirrors on the icon (only when !spinning).
16
+ * - Pre-R350 invariants: R288 strokeWidth=2.5 + R184 anet-reset-spin
17
+ * className gate intact (no spin running at rest).
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 browser = await chromium.launch({ headless: true });
24
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
25
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
26
+ await ctx.addInitScript(() => {
27
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
28
+ });
29
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
30
+ await ctx.route('**/api/hub/status*', async (route) => {
31
+ const r = await route.fetch();
32
+ const b = await r.json();
33
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
34
+ const mk = (alias) => ({
35
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
36
+ network_id: nid, project_dir: null,
37
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
38
+ });
39
+ await route.fulfill({ response: r, json: { ...b, sessions: [ mk('a'), mk('b'), mk('c') ] } });
40
+ });
41
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
42
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
43
+
44
+ const page = await ctx.newPage();
45
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
46
+ await page.waitForSelector('[data-topo-chrome-reset]', { timeout: 15000 });
47
+ await page.waitForTimeout(300);
48
+
49
+ // Rest probe.
50
+ const restProbe = await page.evaluate(() => {
51
+ const btn = document.querySelector('[data-topo-chrome-reset]');
52
+ const ico = document.querySelector('[data-topo-chrome-reset-icon]');
53
+ const cs = ico ? getComputedStyle(ico) : null;
54
+ return {
55
+ btnHover: btn?.getAttribute('data-topo-chrome-reset-hover') ?? null,
56
+ icoHover: ico?.getAttribute('data-topo-chrome-reset-icon-hover') ?? null,
57
+ icoTransform: cs?.transform ?? null,
58
+ icoTransition: cs?.transition ?? null,
59
+ icoStrokeW: ico?.getAttribute('stroke-width') ?? null,
60
+ spinClass: ico?.getAttribute('class') ?? null,
61
+ };
62
+ });
63
+
64
+ // Hover the reset button.
65
+ await page.hover('[data-topo-chrome-reset]');
66
+ await page.waitForTimeout(300);
67
+ const hoverProbe = await page.evaluate(() => {
68
+ const btn = document.querySelector('[data-topo-chrome-reset]');
69
+ const ico = document.querySelector('[data-topo-chrome-reset-icon]');
70
+ const cs = ico ? getComputedStyle(ico) : null;
71
+ return {
72
+ btnHover: btn?.getAttribute('data-topo-chrome-reset-hover') ?? null,
73
+ icoHover: ico?.getAttribute('data-topo-chrome-reset-icon-hover') ?? null,
74
+ icoTransform: cs?.transform ?? null,
75
+ };
76
+ });
77
+
78
+ await browser.close();
79
+
80
+ const hasTrans = (s) => /transform\s+\d*\.?\d*s|transform\s+\d+ms/i.test(s || '');
81
+ // computed transform 'none' or a matrix near identity at rest.
82
+ const isRestTransform = (t) => t === 'none' || /matrix\(1, 0, 0, 1, 0, 0\)/.test(t || '') ||
83
+ /matrix\(0\.999/i.test(t || '') /* tiny rounding tolerance */;
84
+ // computed transform for -8deg ≈ matrix(0.990, -0.139, 0.139, 0.990, 0, 0).
85
+ const isHoverTransform = (t) => /matrix\(0\.99/i.test(t || '') && /-0\.13/i.test(t || '');
86
+
87
+ const results = {
88
+ rest_btn_false: restProbe.btnHover === 'false',
89
+ rest_ico_false: restProbe.icoHover === 'false',
90
+ rest_no_rotation: isRestTransform(restProbe.icoTransform),
91
+ trans_has_transform: hasTrans(restProbe.icoTransition),
92
+ rest_strokew_2_5: restProbe.icoStrokeW === '2.5', // R288
93
+ rest_no_spin_class: !/anet-reset-spin/.test(restProbe.spinClass || ''),
94
+ hover_btn_true: hoverProbe.btnHover === 'true',
95
+ hover_ico_true: hoverProbe.icoHover === 'true',
96
+ hover_rotated: isHoverTransform(hoverProbe.icoTransform),
97
+ };
98
+ const ok = Object.values(results).every(Boolean);
99
+ console.log(`${ok ? '✅' : '❌'} reset icon hover-rotate:`, JSON.stringify(results),
100
+ '\n rest: ', restProbe,
101
+ '\n hover:', hoverProbe);
102
+ process.exit(ok ? 0 : 1);