@sleep2agi/agent-network-dashboard 0.4.6-preview.345 → 0.4.6-preview.346

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 (143) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +4 -4
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/server/app/_not-found.html +1 -1
  13. package/.next/server/app/_not-found.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  20. package/.next/server/app/admin.html +1 -1
  21. package/.next/server/app/admin.rsc +1 -1
  22. package/.next/server/app/admin.segments/_full.segment.rsc +1 -1
  23. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  24. package/.next/server/app/admin.segments/_index.segment.rsc +1 -1
  25. package/.next/server/app/admin.segments/_tree.segment.rsc +1 -1
  26. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  27. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  28. package/.next/server/app/index.html +2 -2
  29. package/.next/server/app/index.rsc +2 -2
  30. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  31. package/.next/server/app/index.segments/_full.segment.rsc +2 -2
  32. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  33. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  34. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  35. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/login.html +2 -2
  37. package/.next/server/app/login.rsc +2 -2
  38. package/.next/server/app/login.segments/_full.segment.rsc +2 -2
  39. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  40. package/.next/server/app/login.segments/_index.segment.rsc +1 -1
  41. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  42. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  43. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  44. package/.next/server/app/logs.html +1 -1
  45. package/.next/server/app/logs.rsc +1 -1
  46. package/.next/server/app/logs.segments/_full.segment.rsc +1 -1
  47. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  48. package/.next/server/app/logs.segments/_index.segment.rsc +1 -1
  49. package/.next/server/app/logs.segments/_tree.segment.rsc +1 -1
  50. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  51. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  52. package/.next/server/app/messages.html +1 -1
  53. package/.next/server/app/messages.rsc +1 -1
  54. package/.next/server/app/messages.segments/_full.segment.rsc +1 -1
  55. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  56. package/.next/server/app/messages.segments/_index.segment.rsc +1 -1
  57. package/.next/server/app/messages.segments/_tree.segment.rsc +1 -1
  58. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  59. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  60. package/.next/server/app/node.html +1 -1
  61. package/.next/server/app/node.rsc +1 -1
  62. package/.next/server/app/node.segments/_full.segment.rsc +1 -1
  63. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  64. package/.next/server/app/node.segments/_index.segment.rsc +1 -1
  65. package/.next/server/app/node.segments/_tree.segment.rsc +1 -1
  66. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  67. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  68. package/.next/server/app/nodes.html +1 -1
  69. package/.next/server/app/nodes.rsc +1 -1
  70. package/.next/server/app/nodes.segments/_full.segment.rsc +1 -1
  71. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  72. package/.next/server/app/nodes.segments/_index.segment.rsc +1 -1
  73. package/.next/server/app/nodes.segments/_tree.segment.rsc +1 -1
  74. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
  75. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  76. package/.next/server/app/page_client-reference-manifest.js +1 -1
  77. package/.next/server/app/server-logs.html +1 -1
  78. package/.next/server/app/server-logs.rsc +1 -1
  79. package/.next/server/app/server-logs.segments/_full.segment.rsc +1 -1
  80. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  81. package/.next/server/app/server-logs.segments/_index.segment.rsc +1 -1
  82. package/.next/server/app/server-logs.segments/_tree.segment.rsc +1 -1
  83. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
  84. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  85. package/.next/server/app/settings/networks.html +1 -1
  86. package/.next/server/app/settings/networks.rsc +1 -1
  87. package/.next/server/app/settings/networks.segments/_full.segment.rsc +1 -1
  88. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  89. package/.next/server/app/settings/networks.segments/_index.segment.rsc +1 -1
  90. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +1 -1
  91. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
  92. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  93. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  94. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app/settings/tokens.html +1 -1
  96. package/.next/server/app/settings/tokens.rsc +1 -1
  97. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +1 -1
  98. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  99. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +1 -1
  100. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +1 -1
  101. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
  102. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  103. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  104. package/.next/server/app/settings.html +2 -2
  105. package/.next/server/app/settings.rsc +2 -2
  106. package/.next/server/app/settings.segments/_full.segment.rsc +2 -2
  107. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  108. package/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  109. package/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  110. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  111. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  112. package/.next/server/app/tasks.html +1 -1
  113. package/.next/server/app/tasks.rsc +1 -1
  114. package/.next/server/app/tasks.segments/_full.segment.rsc +1 -1
  115. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  116. package/.next/server/app/tasks.segments/_index.segment.rsc +1 -1
  117. package/.next/server/app/tasks.segments/_tree.segment.rsc +1 -1
  118. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  119. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  120. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +1 -1
  121. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  122. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  123. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  124. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  125. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  126. package/.next/server/middleware-build-manifest.js +3 -3
  127. package/.next/server/pages/404.html +1 -1
  128. package/.next/server/pages/500.html +1 -1
  129. package/.next/static/chunks/{16bh0.ekdd5ge.js → 0d6e7f~0oulwt.js} +1 -1
  130. package/.next/static/chunks/{0ul0qqy8mexbh.js → 0hq2~4ey_gfcx.js} +1 -1
  131. package/.next/static/chunks/{0_e7lmzh87xp3.js → 0rqg8f7ygcty8.js} +1 -1
  132. package/.next/trace +2 -2
  133. package/.next/trace-build +1 -1
  134. package/app/components/TopoGraph.tsx +17 -3
  135. package/package.json +1 -1
  136. package/screenshots/r293-p0/servers-drawer-v0.10.0-full.png +0 -0
  137. package/screenshots/r293-p0/servers-drawer-v0.10.0.png +0 -0
  138. package/scripts/p0-servers-drawer-screenshot.mjs +99 -0
  139. package/scripts/topo-group-pip-fadein-test.mjs +5 -1
  140. package/scripts/topo-group-pip-singletier-drop-test.mjs +131 -0
  141. /package/.next/static/{NhfYap25UUapTiHJmca96 → MBGamBXWPewuLnqj0_Ed-}/_buildManifest.js +0 -0
  142. /package/.next/static/{NhfYap25UUapTiHJmca96 → MBGamBXWPewuLnqj0_Ed-}/_clientMiddlewareManifest.js +0 -0
  143. /package/.next/static/{NhfYap25UUapTiHJmca96 → MBGamBXWPewuLnqj0_Ed-}/_ssgManifest.js +0 -0
package/.next/trace-build CHANGED
@@ -1 +1 @@
1
- [{"name":"run-turbopack","duration":7169618,"timestamp":119686219071,"id":14,"parentId":1,"tags":{},"startTime":1778927658465,"traceId":"4fd1bc6cde53e8ce"},{"name":"turbopack-build-events","duration":73,"timestamp":119686542528,"id":15,"parentId":1,"tags":{},"startTime":1778927658789,"traceId":"4fd1bc6cde53e8ce"},{"name":"run-typescript","duration":10849130,"timestamp":119693397679,"id":16,"parentId":1,"tags":{},"startTime":1778927665644,"traceId":"4fd1bc6cde53e8ce"},{"name":"static-check","duration":1124476,"timestamp":119704477253,"id":19,"parentId":1,"tags":{},"startTime":1778927676724,"traceId":"4fd1bc6cde53e8ce"},{"name":"static-generation","duration":766923,"timestamp":119705616717,"id":105,"parentId":1,"tags":{},"startTime":1778927677863,"traceId":"4fd1bc6cde53e8ce"},{"name":"telemetry-flush","duration":30882,"timestamp":119706423954,"id":114,"parentId":1,"tags":{},"startTime":1778927678670,"traceId":"4fd1bc6cde53e8ce"},{"name":"next-build","duration":20489760,"timestamp":119685965135,"id":1,"tags":{"buildMode":"default","version":"16.2.3","bundler":"turbopack","has-custom-webpack-config":"false","use-build-worker":"true"},"startTime":1778927658211,"traceId":"4fd1bc6cde53e8ce"}]
1
+ [{"name":"run-turbopack","duration":7434216,"timestamp":120214470345,"id":14,"parentId":1,"tags":{},"startTime":1778928186717,"traceId":"04efb3bd722f9121"},{"name":"turbopack-build-events","duration":77,"timestamp":120214815235,"id":15,"parentId":1,"tags":{},"startTime":1778928187062,"traceId":"04efb3bd722f9121"},{"name":"run-typescript","duration":11026777,"timestamp":120221914825,"id":16,"parentId":1,"tags":{},"startTime":1778928194161,"traceId":"04efb3bd722f9121"},{"name":"static-check","duration":1074250,"timestamp":120233175548,"id":19,"parentId":1,"tags":{},"startTime":1778928205422,"traceId":"04efb3bd722f9121"},{"name":"static-generation","duration":697719,"timestamp":120234265215,"id":105,"parentId":1,"tags":{},"startTime":1778928206511,"traceId":"04efb3bd722f9121"},{"name":"telemetry-flush","duration":33209,"timestamp":120235008650,"id":114,"parentId":1,"tags":{},"startTime":1778928207255,"traceId":"04efb3bd722f9121"},{"name":"next-build","duration":20863334,"timestamp":120214178594,"id":1,"tags":{"buildMode":"default","version":"16.2.3","bundler":"turbopack","has-custom-webpack-config":"false","use-build-worker":"true"},"startTime":1778928186425,"traceId":"04efb3bd722f9121"}]
@@ -3698,7 +3698,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3698
3698
  tier-coloured pips alongside every other
3699
3699
  theme-driven element. R230's tabular-nums
3700
3700
  stays. */}
3701
- {box.statuses.working > 0 && (
3701
+ {/* Round 319 / Loop: drop a tier pip when its count
3702
+ equals box.count — i.e. single-tier groups (all
3703
+ working, all idle, all offline). Pre-R319 a 4-all-
3704
+ idle group rendered as `P站 · 4 4i` with the "4"
3705
+ visually doubled; Vincent telegram 5304 flagged
3706
+ this as 比较难看 in a real-data screenshot
3707
+ (ai-insight · 6 6i, blueleap · 3 3i, P站 · 4 4i).
3708
+ The dropped pip's information is already conveyed
3709
+ by the group-box stroke colour (R68 isPinned/
3710
+ hover accent uses the dominant-tier hue) plus
3711
+ the SVG <title> tooltip listing the status
3712
+ breakdown. Multi-tier groups (e.g. `alpha · 3
3713
+ 2w 1i`) render unchanged — those pips genuinely
3714
+ add breakdown info that the total doesn't carry. */}
3715
+ {box.statuses.working > 0 && box.statuses.working !== box.count && (
3702
3716
  <tspan
3703
3717
  dx="8"
3704
3718
  fill={isLight ? '#059669' : '#22c55e'}
@@ -3709,7 +3723,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3709
3723
  style={{ fontVariantNumeric: 'tabular-nums', transition: 'fill 200ms ease-out' }}
3710
3724
  >{box.statuses.working}w</tspan>
3711
3725
  )}
3712
- {box.statuses.idle > 0 && (
3726
+ {box.statuses.idle > 0 && box.statuses.idle !== box.count && (
3713
3727
  <tspan
3714
3728
  dx="4"
3715
3729
  fill={isLight ? '#0d9488' : '#2dd4bf'}
@@ -3720,7 +3734,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3720
3734
  style={{ fontVariantNumeric: 'tabular-nums', transition: 'fill 200ms ease-out' }}
3721
3735
  >{box.statuses.idle}i</tspan>
3722
3736
  )}
3723
- {box.statuses.offline > 0 && (
3737
+ {box.statuses.offline > 0 && box.statuses.offline !== box.count && (
3724
3738
  <tspan
3725
3739
  dx="4"
3726
3740
  fill={isLight ? '#94a3b8' : '#6b7280'}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.4.6-preview.345",
3
+ "version": "0.4.6-preview.346",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,99 @@
1
+ /* v0.10.0 Hero 1+2 — ServersDrawer extension screenshot evidence for #140.
2
+ * Mocks the /api/hub/servers response with disk + history + agents
3
+ * to surface the new expandable detail view. */
4
+ import { chromium } from 'playwright';
5
+ import { readFileSync } from 'node:fs';
6
+
7
+ const out = process.argv[2] || 'screenshots/r293-p0/servers-drawer-expanded';
8
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
9
+ const browser = await chromium.launch({ headless: true });
10
+ const ctx = await browser.newContext({ viewport: { width: 1600, height: 1000 }, deviceScaleFactor: 2 });
11
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
12
+ await ctx.addInitScript(() => {
13
+ try {
14
+ localStorage.setItem('anet-theme', 'cyber');
15
+ sessionStorage.setItem('anet_v3_auth', '1');
16
+ // Force drawer open + expand the first card by hostname guess.
17
+ localStorage.setItem('anet-servers-drawer', '1');
18
+ localStorage.setItem('anet-servers-drawer-expanded', JSON.stringify(['mock-1', 'mock-2']));
19
+ } catch {}
20
+ });
21
+
22
+ // Mock servers payload with full v0.10.0 schema.
23
+ await ctx.route('**/api/hub/servers*', (route) => {
24
+ route.fulfill({
25
+ json: {
26
+ servers: [
27
+ {
28
+ hostname: 'mock-1',
29
+ ip: '10.0.0.10',
30
+ cpu_load_1min: 2.4,
31
+ cpu_cores: 8,
32
+ mem_used_gb: 18.2,
33
+ mem_total_gb: 32,
34
+ disk_used_gb: 220,
35
+ disk_total_gb: 500,
36
+ cpu_history: [25, 28, 32, 30, 35, 38, 40, 36, 33, 30],
37
+ mem_history: [50, 52, 55, 56, 58, 57, 59, 60, 58, 57],
38
+ agents: [
39
+ { alias: 'claude-1', runtime: 'claude-code-cli', status: 'working', progress: 0.42 },
40
+ { alias: 'codex-1', runtime: 'codex-sdk', status: 'working', progress: 0.18 },
41
+ { alias: 'intern-1', runtime: 'claude-agent-sdk', status: 'idle' },
42
+ ],
43
+ agent_count: 3,
44
+ status: 'online',
45
+ },
46
+ {
47
+ hostname: 'mock-2',
48
+ ip: '10.0.0.11',
49
+ cpu_load_1min: 7.1,
50
+ cpu_cores: 4,
51
+ mem_used_gb: 14.5,
52
+ mem_total_gb: 16,
53
+ disk_used_gb: 460,
54
+ disk_total_gb: 500,
55
+ cpu_history: [80, 88, 92, 95, 96, 94, 93, 91, 90, 89],
56
+ mem_history: [85, 87, 89, 91, 90, 92, 93, 91, 90, 91],
57
+ agents: [
58
+ { alias: 'minimax-busy', runtime: 'claude-code-cli', status: 'working', progress: 0.78 },
59
+ ],
60
+ agent_count: 1,
61
+ status: 'online',
62
+ },
63
+ {
64
+ hostname: 'mock-offline',
65
+ ip: '10.0.0.20',
66
+ cpu_load_1min: null,
67
+ cpu_cores: 0,
68
+ mem_used_gb: null,
69
+ mem_total_gb: null,
70
+ agent_count: 2,
71
+ status: 'offline',
72
+ note: 'last seen 12m ago',
73
+ },
74
+ ],
75
+ },
76
+ });
77
+ });
78
+ await ctx.route('**/api/hub/status*', (r) => r.fulfill({ json: { sessions: [], hub: { networks: [] } } }));
79
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
80
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
81
+
82
+ const page = await ctx.newPage();
83
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
84
+ await page.waitForSelector('[data-server-card]', { timeout: 15000 });
85
+ await page.waitForTimeout(800);
86
+
87
+ // Crop to the drawer region only.
88
+ const box = await page.evaluate(() => {
89
+ const aside = document.querySelector('[aria-label="Servers panel"]');
90
+ if (!aside) return null;
91
+ const r = aside.getBoundingClientRect();
92
+ return { x: r.x - 8, y: r.y - 8, width: r.width + 16, height: Math.min(r.height + 16, 980) };
93
+ });
94
+ if (box && box.width > 100) {
95
+ await page.screenshot({ path: `${out}.png`, clip: box });
96
+ }
97
+ await page.screenshot({ path: `${out}-full.png`, fullPage: false });
98
+ console.log(`✅ wrote ${out}.png + ${out}-full.png`);
99
+ await browser.close();
@@ -44,10 +44,14 @@ await ctx.route('**/api/hub/status*', async (route) => {
44
44
  created_at: fresh, updated_at: fresh, last_seen_at: fresh,
45
45
  });
46
46
  await route.fulfill({ response: r, json: { ...b, sessions: [
47
+ // R319 fixture: both groups must be multi-tier so all tier pips
48
+ // render. Pre-R319 beta was 2 working (single-tier) and its pip
49
+ // rendered fine; post-R319 a tier pip is dropped when its count
50
+ // equals box.count, so beta needs idle to remain multi-tier here.
47
51
  mk('alpha-1', 'working'),
48
52
  mk('alpha-2', 'idle'),
49
53
  mk('beta-1', 'working'),
50
- mk('beta-2', 'working'),
54
+ mk('beta-2', 'idle'),
51
55
  ] } });
52
56
  });
53
57
  await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: [] } }));
@@ -0,0 +1,131 @@
1
+ /* Round 319 verification: prefix-group label drops the redundant
2
+ * tier pip when its count equals box.count (single-tier groups).
3
+ *
4
+ * Vincent telegram 5304 (2026-05-16) flagged real-data screenshot
5
+ * with `ai-insight · 6 6i`, `blueleap · 3 3i`, `P站 · 4 4i` as
6
+ * 比较难看 — the same number was visually doubled (total chip "·N"
7
+ * + tier pip "Ni") whenever a group was single-tier.
8
+ *
9
+ * R319: tier pip is now conditionally rendered with both
10
+ * `box.statuses.{tier} > 0` AND `box.statuses.{tier} !== box.count`
11
+ * so single-tier groups collapse to `· N` (no duplicated digit).
12
+ * Multi-tier groups keep all pips — the breakdown is genuinely
13
+ * additional info there.
14
+ *
15
+ * Fixture:
16
+ * - alpha-1..alpha-3: all working (single-tier, count=3, working=3)
17
+ * - beta-1, beta-2: working; beta-3, beta-4: idle (multi-tier)
18
+ *
19
+ * Contract:
20
+ * - alpha group: no [data-group-pip="working"] tspan (dropped because
21
+ * working === count)
22
+ * - alpha group: still has [data-group-label-count] with value=3
23
+ * (R229 total chip untouched)
24
+ * - beta group: has [data-group-pip="working"] AND [data-group-pip="idle"]
25
+ * (multi-tier — both pips render because neither equals count)
26
+ * - beta group: [data-group-label-count-value]=4 still rendered
27
+ * - R317 inactive Layout toggle gray-400 + R318 active font-medium
28
+ * + R294 pulse absent all preserved.
29
+ */
30
+ import { chromium } from 'playwright';
31
+ import { readFileSync } from 'node:fs';
32
+
33
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
34
+ const browser = await chromium.launch({ headless: true });
35
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
36
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
37
+ await ctx.addInitScript(() => {
38
+ try {
39
+ localStorage.setItem('anet-theme', 'cyber');
40
+ sessionStorage.setItem('anet_v3_auth', '1');
41
+ localStorage.setItem('anet-topo-layout', 'grid');
42
+ } catch {}
43
+ });
44
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
45
+ await ctx.route('**/api/hub/status*', async (route) => {
46
+ const r = await route.fetch();
47
+ const b = await r.json();
48
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
49
+ const mk = (alias, status) => ({
50
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
51
+ network_id: nid, project_dir: null,
52
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
53
+ });
54
+ await route.fulfill({ response: r, json: { ...b, sessions: [
55
+ // alpha — single-tier (all working, count=3)
56
+ mk('alpha-1', 'working'),
57
+ mk('alpha-2', 'working'),
58
+ mk('alpha-3', 'working'),
59
+ // beta — multi-tier (2 working + 2 idle, count=4)
60
+ mk('beta-1', 'working'),
61
+ mk('beta-2', 'working'),
62
+ mk('beta-3', 'idle'),
63
+ mk('beta-4', 'idle'),
64
+ ] } });
65
+ });
66
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
67
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
68
+
69
+ const page = await ctx.newPage();
70
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
71
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 7, { timeout: 30000 });
72
+ await page.waitForSelector('[data-group-label-count]', { timeout: 10000, state: 'attached' });
73
+ await page.waitForTimeout(500);
74
+
75
+ const probe = await page.evaluate(() => {
76
+ // Find each group's <text data-group-label> wrapper and its pip
77
+ // children + count tspan.
78
+ const labels = Array.from(document.querySelectorAll('[data-group-label]'));
79
+ const byGroup = {};
80
+ for (const label of labels) {
81
+ const key = label.getAttribute('data-group-label');
82
+ const countTspan = label.querySelector('[data-group-label-count]');
83
+ const pips = Array.from(label.querySelectorAll('[data-group-pip]'));
84
+ byGroup[key] = {
85
+ hasCountTspan: !!countTspan,
86
+ countValue: countTspan?.getAttribute('data-group-label-count-value') ?? null,
87
+ pipTiers: pips.map(p => p.getAttribute('data-group-pip')),
88
+ pipTexts: pips.map(p => p.textContent),
89
+ };
90
+ }
91
+ return {
92
+ groups: byGroup,
93
+ r317InactiveLayoutCls:
94
+ document.querySelector('[data-topo-chrome-layout="grid"]')?.className ?? '',
95
+ r317ActiveLayoutCls:
96
+ document.querySelector('[data-topo-chrome-layout="ring"]')?.className ?? '',
97
+ pulseCount: document.querySelectorAll('[data-pulse-wrapper]').length,
98
+ };
99
+ });
100
+ await browser.close();
101
+
102
+ // R106 cluster algorithm preserves the trailing separator in the
103
+ // group key (e.g. alpha-1/2/3 → key 'alpha-'); match by prefix.
104
+ const alpha = probe.groups['alpha-'] || probe.groups['alpha'] || null;
105
+ const beta = probe.groups['beta-'] || probe.groups['beta'] || null;
106
+
107
+ const results = {
108
+ alpha_group_found: alpha !== null,
109
+ alpha_has_count_tspan: alpha?.hasCountTspan === true,
110
+ alpha_count_value_3: alpha?.countValue === '3',
111
+ alpha_no_working_pip: !alpha?.pipTiers.includes('working'),
112
+ alpha_no_pips_at_all: alpha?.pipTiers.length === 0,
113
+ beta_group_found: beta !== null,
114
+ beta_has_count_tspan: beta?.hasCountTspan === true,
115
+ beta_count_value_4: beta?.countValue === '4',
116
+ beta_has_working_pip: beta?.pipTiers.includes('working'),
117
+ beta_has_idle_pip: beta?.pipTiers.includes('idle'),
118
+ beta_no_offline_pip: !beta?.pipTiers.includes('offline'),
119
+ // R317 regression — Layout toggle inactive Grid is the layout-active
120
+ // here (we forced grid via initScript), so check that Ring is the
121
+ // INACTIVE one with text-gray-400. After R318, the ACTIVE button
122
+ // (grid) has font-medium.
123
+ r317_ring_inactive_gray_400: probe.r317ActiveLayoutCls.includes('text-gray-400'),
124
+ r318_grid_active_font_medium: probe.r317InactiveLayoutCls.includes('font-medium'),
125
+ r294_pulse_absent: probe.pulseCount === 0,
126
+ };
127
+ const ok = Object.values(results).every(Boolean);
128
+ console.log(`${ok ? '✅' : '❌'} group-pip single-tier drop:`, JSON.stringify(results),
129
+ '\n alpha:', alpha,
130
+ '\n beta:', beta);
131
+ process.exit(ok ? 0 : 1);