@sleep2agi/agent-network-dashboard 0.5.1-preview.98 → 0.5.1

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 (202) 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 +32 -32
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/server/app/_global-error.html +1 -1
  7. package/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +2 -2
  15. package/.next/server/app/_not-found.rsc +12 -12
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/admin.html +2 -2
  24. package/.next/server/app/admin.rsc +14 -14
  25. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  26. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  27. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  28. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  30. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  31. package/.next/server/app/index.html +2 -2
  32. package/.next/server/app/index.rsc +14 -14
  33. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  34. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  35. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  36. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  37. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  38. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  39. package/.next/server/app/login.html +2 -2
  40. package/.next/server/app/login.rsc +14 -14
  41. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  42. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  43. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  44. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  46. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  47. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/logs.html +2 -2
  49. package/.next/server/app/logs.rsc +14 -14
  50. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  51. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  52. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  53. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  55. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  56. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/messages.html +2 -2
  58. package/.next/server/app/messages.rsc +14 -14
  59. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  60. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  61. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  62. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  64. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  65. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/node.html +2 -2
  67. package/.next/server/app/node.rsc +14 -14
  68. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  69. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  70. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  71. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  73. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  74. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/nodes.html +2 -2
  76. package/.next/server/app/nodes.rsc +14 -14
  77. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  78. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  79. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  80. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  82. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  83. package/.next/server/app/page.js.nft.json +1 -1
  84. package/.next/server/app/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  86. package/.next/server/app/server-logs.html +2 -2
  87. package/.next/server/app/server-logs.rsc +14 -14
  88. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  89. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  90. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  91. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  92. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  93. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  94. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app/settings/networks.html +2 -2
  96. package/.next/server/app/settings/networks.rsc +14 -14
  97. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  98. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  99. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  100. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  101. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  102. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  103. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  104. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  106. package/.next/server/app/settings/tokens.html +2 -2
  107. package/.next/server/app/settings/tokens.rsc +14 -14
  108. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  109. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  110. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  111. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  112. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  113. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  114. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  115. package/.next/server/app/settings.html +2 -2
  116. package/.next/server/app/settings.rsc +14 -14
  117. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  118. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  119. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  120. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  121. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  122. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  123. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  125. package/.next/server/app/tasks.html +2 -2
  126. package/.next/server/app/tasks.rsc +14 -14
  127. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  128. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  129. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  130. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  131. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  132. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  133. package/.next/server/chunks/ssr/{[root-of-the-server]__03b.f76._.js → [root-of-the-server]__0sv~g.o._.js} +2 -2
  134. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -0
  135. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  136. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  138. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  139. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  140. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  141. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  142. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  143. package/.next/server/middleware-build-manifest.js +3 -3
  144. package/.next/server/pages/404.html +2 -2
  145. package/.next/server/pages/500.html +1 -1
  146. package/.next/server/server-reference-manifest.js +1 -1
  147. package/.next/server/server-reference-manifest.json +1 -1
  148. package/.next/static/chunks/{0l_~q07bhpkcx.js → 03a4--7ncekmk.js} +1 -1
  149. package/.next/static/chunks/05admfiu6qfp2.js +1 -0
  150. package/.next/static/chunks/0a4hmfvj-81x5.css +2 -0
  151. package/.next/static/chunks/0d9mlqf.rjey5.js +1 -0
  152. package/.next/static/chunks/11puuje6at2jt.js +4 -0
  153. package/.next/static/chunks/{03~~oirxz7~vc.js → 15ub0_3b099x1.js} +1 -1
  154. package/.next/trace +2 -2
  155. package/.next/trace-build +1 -1
  156. package/app/components/ServersDrawer.tsx +17 -0
  157. package/app/components/TopoGraph.tsx +778 -90
  158. package/package.json +1 -1
  159. package/screenshots/v0.10.2-disk-verify/disk-render-full.png +0 -0
  160. package/screenshots/v0.10.2-disk-verify/disk-render.png +0 -0
  161. package/screenshots/v0.11.0-147/after.png +0 -0
  162. package/scripts/p0-147-screenshot.mjs +53 -0
  163. package/scripts/p0-servers-drawer-screenshot.mjs +2 -0
  164. package/scripts/topo-any-hover-attr-test.mjs +83 -0
  165. package/scripts/topo-any-pinned-attr-test.mjs +86 -0
  166. package/scripts/topo-chrome-fullscreen-icon-sw-test.mjs +92 -0
  167. package/scripts/topo-chrome-reset-icon-sw-test.mjs +80 -0
  168. package/scripts/topo-chrome-zoom-icon-sw-test.mjs +90 -0
  169. package/scripts/topo-dashboard-version-attr-test.mjs +69 -0
  170. package/scripts/topo-dense-alias-opacity-test.mjs +68 -0
  171. package/scripts/topo-edge-particle-hover-r-test.mjs +113 -0
  172. package/scripts/topo-endpoint-ring-r-hover-test.mjs +89 -0
  173. package/scripts/topo-group-box-geom-transition-test.mjs +110 -0
  174. package/scripts/topo-group-box-rx-pin-test.mjs +103 -0
  175. package/scripts/topo-group-label-count-fw-test.mjs +100 -0
  176. package/scripts/topo-group-label-fw-pin-test.mjs +99 -0
  177. package/scripts/topo-group-label-tint-geom-test.mjs +94 -0
  178. package/scripts/topo-group-label-tint-transition-test.mjs +97 -0
  179. package/scripts/topo-group-pip-fontsize-test.mjs +106 -0
  180. package/scripts/topo-group-tint-rx-pin-test.mjs +107 -0
  181. package/scripts/topo-hub-core-fill-hover-test.mjs +85 -0
  182. package/scripts/topo-hub-halo-r-hover-test.mjs +82 -0
  183. package/scripts/topo-legend-count-active-opacity-test.mjs +102 -0
  184. package/scripts/topo-legend-count-pin-fw-test.mjs +90 -0
  185. package/scripts/topo-minimap-viewport-opacity-test.mjs +96 -0
  186. package/scripts/topo-node-halo-hover-opacity-test.mjs +104 -0
  187. package/scripts/topo-node-halo-light-offline-test.mjs +80 -0
  188. package/scripts/topo-node-sub-text-fw-test.mjs +75 -0
  189. package/scripts/topo-overlap-stale-build-guard-test.mjs +66 -0
  190. package/scripts/topo-overlap-test.mjs +42 -1
  191. package/scripts/topo-recent-row-count-pin-fw-test.mjs +106 -0
  192. package/scripts/topo-recent-row-pip-hover-r-test.mjs +104 -0
  193. package/scripts/topo-runtime-icon-hover-test.mjs +96 -0
  194. package/scripts/topo-status-ring-hover-sw-test.mjs +105 -0
  195. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +0 -1
  196. package/.next/static/chunks/017hq2-5l~_98.css +0 -2
  197. package/.next/static/chunks/0kmjjpdppowr2.js +0 -1
  198. package/.next/static/chunks/11-cqnshuwwij.js +0 -4
  199. package/.next/static/chunks/14myvippibo7y.js +0 -1
  200. /package/.next/static/{4VuFWzQ7d8yfyNm5pNIB0 → gowHgWkn_kpMCSBn8RFaa}/_buildManifest.js +0 -0
  201. /package/.next/static/{4VuFWzQ7d8yfyNm5pNIB0 → gowHgWkn_kpMCSBn8RFaa}/_clientMiddlewareManifest.js +0 -0
  202. /package/.next/static/{4VuFWzQ7d8yfyNm5pNIB0 → gowHgWkn_kpMCSBn8RFaa}/_ssgManifest.js +0 -0
@@ -0,0 +1,113 @@
1
+ /* Round 439 verification: edge flow particle radius hover lift —
2
+ * r 4.5 → 5.5 on (isHoveredEdge || isEndpointHoveredEdge). Continues
3
+ * the edge paint-layer parity arc (R436 visible path / R437 flow rail
4
+ * / R439 particle).
5
+ *
6
+ * Contract:
7
+ * - rest: every particle reports data-edge-particle-radius '4.5' +
8
+ * lifted='false'
9
+ * - hover one node alias: particles on edges incident to that node
10
+ * report radius '5.5' + lifted='true'
11
+ * - siblings unaffected
12
+ * - source-file probe confirms conditional + transition extension
13
+ */
14
+ import { chromium } from 'playwright';
15
+ import { readFileSync } from 'node:fs';
16
+
17
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
18
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
19
+
20
+ const browser = await chromium.launch({ headless: true });
21
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
22
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
23
+ await ctx.addInitScript(() => {
24
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
25
+ });
26
+ await ctx.route('**/api/hub/status*', async (route) => {
27
+ const r = await route.fetch();
28
+ const b = await r.json();
29
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
30
+ const mk = (alias, status) => ({
31
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
32
+ network_id: nid, project_dir: null,
33
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
34
+ });
35
+ await route.fulfill({ response: r, json: { ...b, sessions: [
36
+ mk('alpha', 'working'),
37
+ mk('beta', 'idle'),
38
+ mk('gamma', 'idle'),
39
+ ] } });
40
+ });
41
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({
42
+ json: { messages: [
43
+ { id: 'm1', from_alias: 'alpha', to_alias: 'beta', content: 'ping', created_at: fresh, network_id: 'default' },
44
+ { id: 'm2', from_alias: 'alpha', to_alias: 'gamma', content: 'pong', created_at: fresh, network_id: 'default' },
45
+ { id: 'm3', from_alias: 'gamma', to_alias: 'beta', content: 'pang', created_at: fresh, network_id: 'default' },
46
+ ] },
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-edge-particle]', { timeout: 15000 });
53
+ await page.waitForTimeout(400);
54
+
55
+ const readAll = () => page.evaluate(() => {
56
+ const ps = [...document.querySelectorAll('[data-edge-particle]')];
57
+ return ps.map(p => ({
58
+ key: p.getAttribute('data-edge-particle'),
59
+ r_attr: p.getAttribute('r'),
60
+ r_data: parseFloat(p.getAttribute('data-edge-particle-radius') || '0'),
61
+ lifted: p.getAttribute('data-edge-particle-lifted'),
62
+ }));
63
+ });
64
+
65
+ const rest = await readAll();
66
+
67
+ let hover = null;
68
+ const box = await page.evaluate(() => {
69
+ const t = document.querySelector('[data-node-alias-text="alpha"]');
70
+ if (!t) return null;
71
+ const node = t.closest('[data-node]');
72
+ const target = node || t;
73
+ const b = target.getBoundingClientRect();
74
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
75
+ });
76
+ if (box) {
77
+ await page.mouse.move(box.x, box.y);
78
+ await page.waitForTimeout(300);
79
+ hover = await readAll();
80
+ await page.mouse.move(0, 0);
81
+ }
82
+
83
+ const fileText = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
84
+ const sourceWired = /r=\{\(isHoveredEdge \|\| isEndpointHoveredEdge\) \? 5\.5 : 4\.5\}/.test(fileText);
85
+ const sourceTransition = /transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out'/.test(fileText);
86
+
87
+ await browser.close();
88
+
89
+ const isAlphaKey = (k) => /alpha/i.test(k);
90
+ const hoverByKey = new Map((hover || []).map(h => [h.key, h]));
91
+ const alphaEntries = [...hoverByKey.entries()].filter(([k]) => isAlphaKey(k));
92
+ const nonAlphaEntries = [...hoverByKey.entries()].filter(([k]) => !isAlphaKey(k));
93
+
94
+ const restAllR_4_5 = rest.every(r => r.r_data === 4.5);
95
+ const restNoneLifted = rest.every(r => r.lifted === 'false');
96
+ const alphaAllR_5_5 = alphaEntries.length > 0 && alphaEntries.every(([, h]) => h.r_data === 5.5 && h.lifted === 'true');
97
+ const nonAlphaRestR = nonAlphaEntries.every(([, h]) => h.r_data === 4.5 && h.lifted === 'false');
98
+
99
+ const results = {
100
+ rest_particle_count_ge_2: rest.length >= 2,
101
+ rest_all_r_4_5: restAllR_4_5,
102
+ rest_none_lifted: restNoneLifted,
103
+ hover_alpha_count_ge_2: alphaEntries.length >= 2,
104
+ hover_alpha_all_lifted_5_5: alphaAllR_5_5,
105
+ hover_non_alpha_unaffected: nonAlphaRestR,
106
+ source_r_wired: sourceWired,
107
+ source_transition_wired: sourceTransition,
108
+ };
109
+ const ok = Object.values(results).every(Boolean);
110
+ console.log(`${ok ? '✅' : '❌'} edge-particle r hover lift:`, JSON.stringify(results),
111
+ '\n rest:', JSON.stringify(rest),
112
+ '\n hover:', JSON.stringify(hover));
113
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,89 @@
1
+ /* Round 442 verification: endpoint emphasis ring radius hover lift —
2
+ * r=radius+7 → radius+8 on isEndpoint. Closes 3-axis hover-elevation
3
+ * parity at endpoint ring scope (r + sw + opacity).
4
+ *
5
+ * Contract:
6
+ * - rest: every endpoint ring reports radius='27' (default node
7
+ * radius 20 + 7) + active='false'
8
+ * - source-file probe confirms conditional + transition extension
9
+ *
10
+ * Test design: edge-hover dispatch on the hitbox path is unreliable
11
+ * for React onMouseEnter (same R423/R424 trap) — source-file probe
12
+ * is the canonical contract for the isEndpoint branch; DOM probe
13
+ * confirms rest branch resolves correctly.
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 fresh = new Date(Date.now() - 60 * 1000).toISOString();
20
+
21
+ const browser = await chromium.launch({ headless: true });
22
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
23
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
24
+ await ctx.addInitScript(() => {
25
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
26
+ });
27
+ await ctx.route('**/api/hub/status*', async (route) => {
28
+ const r = await route.fetch();
29
+ const b = await r.json();
30
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
31
+ const mk = (alias, status) => ({
32
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
33
+ network_id: nid, project_dir: null,
34
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
35
+ });
36
+ await route.fulfill({ response: r, json: { ...b, sessions: [
37
+ mk('alpha', 'working'),
38
+ mk('beta', 'idle'),
39
+ ] } });
40
+ });
41
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({
42
+ json: { messages: [
43
+ { id: 'm1', from_alias: 'alpha', to_alias: 'beta', content: 'ping', created_at: fresh, network_id: 'default' },
44
+ ] },
45
+ }));
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-edge-endpoint-ring]', { timeout: 15000 });
51
+ await page.waitForTimeout(400);
52
+
53
+ const rest = await page.evaluate(() => {
54
+ const cs = [...document.querySelectorAll('[data-edge-endpoint-ring]')];
55
+ return cs.map(c => ({
56
+ r: parseFloat(c.getAttribute('data-edge-endpoint-ring-radius') || '0'),
57
+ active: c.getAttribute('data-edge-endpoint-active'),
58
+ sw: c.getAttribute('data-edge-endpoint-ring-stroke-width'),
59
+ }));
60
+ });
61
+
62
+ const fileText = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
63
+ const sourceR = /const endpointR = isEndpoint \? radius \+ 8 : radius \+ 7/.test(fileText);
64
+ const sourceTransition = /transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out'/.test(fileText);
65
+
66
+ await browser.close();
67
+
68
+ // nodeRadius default scales by nodeScale & status; in our fixture working alpha
69
+ // likely radius=20 → endpointR=27. Just check ≥2 rings mount at the rest value (radius+7).
70
+ const restAnyMounted = rest.length >= 2;
71
+ const restAllActiveFalse = rest.every(r => r.active === 'false');
72
+ const restAllSwRest = rest.every(r => r.sw === '1.6');
73
+ // Verify radius is consistent across all rings: radius+7 (any positive number, all equal)
74
+ const allSameRadius = rest.length > 0 && rest.every(r => r.r === rest[0].r);
75
+ const restRadiusPositive = (rest[0]?.r || 0) > 0;
76
+
77
+ const results = {
78
+ rest_count_ge_2: restAnyMounted,
79
+ rest_all_active_false: restAllActiveFalse,
80
+ rest_all_sw_1_6: restAllSwRest,
81
+ rest_all_radius_equal: allSameRadius,
82
+ rest_radius_positive: restRadiusPositive,
83
+ source_endpointR_wired: sourceR,
84
+ source_transition_wired: sourceTransition,
85
+ };
86
+ const ok = Object.values(results).every(Boolean);
87
+ console.log(`${ok ? '✅' : '❌'} endpoint-ring r hover lift:`, JSON.stringify(results),
88
+ '\n rest:', JSON.stringify(rest));
89
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,110 @@
1
+ /* Round 461 verification: parent group-box rect transition list
2
+ * extends to include x + y + width + height (200ms ease-out each).
3
+ * Pre-R461 the outer 200×140 px container snap-jumped on cluster
4
+ * resize while the inner R460 hitbox tint rect (160×18) slid —
5
+ * jarring two-rate motion at the same surface. R461 unifies the
6
+ * pair so the whole cluster boundary slides as one Hero D-coherent
7
+ * motion envelope at 200ms.
8
+ *
9
+ * Contract:
10
+ * - every <rect data-group-box-geom-transition="x,y,width,height">
11
+ * renders (one per cluster)
12
+ * - inline style contains all 4 geometry tweens at 200ms
13
+ * - existing R66 / R248 transition axes (stroke, stroke-width,
14
+ * fill-opacity, filter, fill) all preserved
15
+ * - source-file wired
16
+ */
17
+ import { chromium } from 'playwright';
18
+ import { readFileSync } from 'node:fs';
19
+
20
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
21
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
22
+
23
+ const browser = await chromium.launch({ headless: true });
24
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
25
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
26
+ await ctx.addInitScript(() => {
27
+ try {
28
+ localStorage.setItem('anet-theme', 'cyber');
29
+ localStorage.setItem('anet-topo-layout', 'grid');
30
+ sessionStorage.setItem('anet_v3_auth', '1');
31
+ } catch {}
32
+ });
33
+ await ctx.route('**/api/hub/status*', async (route) => {
34
+ const r = await route.fetch();
35
+ const b = await r.json();
36
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
37
+ const mk = (alias, status) => ({
38
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
39
+ network_id: nid, project_dir: null,
40
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
41
+ });
42
+ await route.fulfill({ response: r, json: { ...b, sessions: [
43
+ mk('alpha·1', 'working'),
44
+ mk('alpha·2', 'idle'),
45
+ mk('beta·1', 'working'),
46
+ mk('beta·2', 'idle'),
47
+ ] } });
48
+ });
49
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
50
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
51
+
52
+ const page = await ctx.newPage();
53
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
54
+ await page.waitForSelector('[data-group-box-geom-transition]', { timeout: 15000 });
55
+ await page.waitForTimeout(400);
56
+
57
+ const probe = await page.evaluate(() => {
58
+ const boxes = [...document.querySelectorAll('[data-group-box-geom-transition]')];
59
+ return {
60
+ count: boxes.length,
61
+ nodes: boxes.map(b => ({
62
+ geom: b.getAttribute('data-group-box-geom-transition'),
63
+ style: b.getAttribute('style') || '',
64
+ })),
65
+ };
66
+ });
67
+
68
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
69
+ const parentBoxBlockMatch = src.match(/data-group-box-pinned[\s\S]{0,5500}?\/>/);
70
+ const parentBoxBlock = parentBoxBlockMatch ? parentBoxBlockMatch[0] : '';
71
+ const sourceHasGeomAttr = /data-group-box-geom-transition="x,y,width,height"/.test(parentBoxBlock);
72
+ const sourceHasAll4Tweens = /x 200ms ease-out/.test(parentBoxBlock)
73
+ && /y 200ms ease-out/.test(parentBoxBlock)
74
+ && /width 200ms ease-out/.test(parentBoxBlock)
75
+ && /height 200ms ease-out/.test(parentBoxBlock);
76
+ const sourcePreservesR66 = /stroke 200ms ease-out/.test(parentBoxBlock)
77
+ && /stroke-width 200ms ease-out/.test(parentBoxBlock)
78
+ && /fill-opacity 200ms ease-out/.test(parentBoxBlock)
79
+ && /filter 200ms ease-out/.test(parentBoxBlock)
80
+ && /fill 200ms ease-out/.test(parentBoxBlock);
81
+
82
+ await browser.close();
83
+
84
+ const countGe2 = probe.count >= 2;
85
+ const allDataAttr = probe.nodes.every(n => n.geom === 'x,y,width,height');
86
+ const allStyle4xy = probe.nodes.every(n =>
87
+ /x 200ms ease-out/.test(n.style) &&
88
+ /y 200ms ease-out/.test(n.style) &&
89
+ /width 200ms ease-out/.test(n.style) &&
90
+ /height 200ms ease-out/.test(n.style));
91
+ const allStyleR66 = probe.nodes.every(n =>
92
+ /stroke 200ms ease-out/.test(n.style) &&
93
+ /fill-opacity 200ms ease-out/.test(n.style) &&
94
+ /filter 200ms ease-out/.test(n.style) &&
95
+ /fill 200ms ease-out/.test(n.style));
96
+
97
+ const results = {
98
+ box_count_ge_2: countGe2,
99
+ all_data_attr_xywh: allDataAttr,
100
+ all_style_has_4_geom: allStyle4xy,
101
+ all_style_preserves_r66: allStyleR66,
102
+ source_geom_attr_wired: sourceHasGeomAttr,
103
+ source_all_4_tweens: sourceHasAll4Tweens,
104
+ source_preserves_r66: sourcePreservesR66,
105
+ };
106
+ const ok = Object.values(results).every(Boolean);
107
+ console.log(`${ok ? '✅' : '❌'} group box geom transition x+y+width+height:`, JSON.stringify(results),
108
+ '\n count:', probe.count,
109
+ '\n first style (truncated):', (probe.nodes[0]?.style || '').slice(0, 220));
110
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,103 @@
1
+ /* Round 464 verification: group-box parent rect rx 14 → 16 on
2
+ * isPinned — geometric softening at the corner radius. Pin
3
+ * signature on the outer container now spans 7 axes (R63 fill +
4
+ * R142 filter + R432 ls + R444 count fw + R457 parent fw + codex
5
+ * p.125 opacity + R464 rx).
6
+ *
7
+ * Contract:
8
+ * - every <rect data-group-box-rx> renders with rx='14' at rest
9
+ * - clicking the group-label hitbox flips that group's rx to '16'
10
+ * - sibling groups stay '14'
11
+ * - transition list (R461) extends to include `rx 200ms`
12
+ */
13
+ import { chromium } from 'playwright';
14
+ import { readFileSync } from 'node:fs';
15
+
16
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
17
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
18
+
19
+ const browser = await chromium.launch({ headless: true });
20
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
21
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
22
+ await ctx.addInitScript(() => {
23
+ try {
24
+ localStorage.setItem('anet-theme', 'cyber');
25
+ localStorage.setItem('anet-topo-layout', 'grid');
26
+ sessionStorage.setItem('anet_v3_auth', '1');
27
+ } catch {}
28
+ });
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) => ({
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
+ await route.fulfill({ response: r, json: { ...b, sessions: [
39
+ mk('alpha·1', 'working'),
40
+ mk('alpha·2', 'idle'),
41
+ mk('beta·1', 'working'),
42
+ mk('beta·2', 'idle'),
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-group-box-rx]', { timeout: 15000 });
51
+ await page.waitForTimeout(400);
52
+
53
+ const readAll = () => page.evaluate(() => {
54
+ const boxes = [...document.querySelectorAll('[data-group-box-rx]')];
55
+ return boxes.map(b => {
56
+ const parentG = b.closest('g[data-group]');
57
+ return {
58
+ key: parentG ? parentG.getAttribute('data-group') : null,
59
+ rx_attr: b.getAttribute('data-group-box-rx'),
60
+ rx_live: b.getAttribute('rx'),
61
+ };
62
+ });
63
+ });
64
+
65
+ const rest = await readAll();
66
+ const firstKey = rest[0]?.key;
67
+
68
+ let pinned = null;
69
+ if (firstKey) {
70
+ // click hitbox to pin
71
+ const hit = await page.$(`[data-group-label-hit="${firstKey}"]`);
72
+ if (hit) {
73
+ await hit.click();
74
+ await page.waitForTimeout(300);
75
+ pinned = await readAll();
76
+ }
77
+ }
78
+
79
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
80
+ const sourceRxConditional = /rx=\{isPinned \? '16' : '14'\}/.test(src);
81
+ const sourceTransitionHasRx = /rx 200ms ease-out/.test(src);
82
+
83
+ await browser.close();
84
+
85
+ const restCountGe2 = rest.length >= 2;
86
+ const restAll14 = rest.every(r => r.rx_attr === '14' && r.rx_live === '14');
87
+ const pinnedTarget = pinned?.find(r => r.key === firstKey);
88
+ const pinTargetIs16 = pinnedTarget?.rx_attr === '16' && pinnedTarget?.rx_live === '16';
89
+ const pinSiblingsRest = pinned ? pinned.filter(r => r.key !== firstKey).every(r => r.rx_attr === '14') : false;
90
+
91
+ const results = {
92
+ rest_count_ge_2: restCountGe2,
93
+ rest_all_rx_14: restAll14,
94
+ pinned_target_rx_16: pinTargetIs16,
95
+ pinned_siblings_rest: pinSiblingsRest,
96
+ source_rx_conditional: sourceRxConditional,
97
+ source_transition_has_rx: sourceTransitionHasRx,
98
+ };
99
+ const ok = Object.values(results).every(Boolean);
100
+ console.log(`${ok ? '✅' : '❌'} group box rx pin 14→16:`, JSON.stringify(results),
101
+ '\n rest:', JSON.stringify(rest),
102
+ '\n pinned target:', JSON.stringify(pinnedTarget));
103
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,100 @@
1
+ /* Round 444 verification: group label count tspan fontWeight lift —
2
+ * 500 → 600 on isPinned. Mirror of R416/R424/R425/R426 "data tightens
3
+ * under attention" pattern at group-label-count scope.
4
+ *
5
+ * Contract:
6
+ * - rest: every group-label-count reports font-weight '500' +
7
+ * pinned='false'
8
+ * - click a group-label-hit: that group's count flips to '600' +
9
+ * pinned='true'
10
+ * - siblings stay rest
11
+ */
12
+ import { chromium } from 'playwright';
13
+ import { readFileSync } from 'node:fs';
14
+
15
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
16
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
17
+
18
+ const browser = await chromium.launch({ headless: true });
19
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
20
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
21
+ await ctx.addInitScript(() => {
22
+ try {
23
+ localStorage.setItem('anet-theme', 'cyber');
24
+ localStorage.setItem('anet-topo-layout', 'grid');
25
+ sessionStorage.setItem('anet_v3_auth', '1');
26
+ } catch {}
27
+ });
28
+ await ctx.route('**/api/hub/status*', async (route) => {
29
+ const r = await route.fetch();
30
+ const b = await r.json();
31
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
32
+ const mk = (alias, status) => ({
33
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
34
+ network_id: nid, project_dir: null,
35
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
36
+ });
37
+ await route.fulfill({ response: r, json: { ...b, sessions: [
38
+ mk('alpha·1', 'working'),
39
+ mk('alpha·2', 'idle'),
40
+ mk('beta·1', 'working'),
41
+ mk('beta·2', 'idle'),
42
+ ] } });
43
+ });
44
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
45
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
46
+
47
+ const page = await ctx.newPage();
48
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
49
+ await page.waitForSelector('[data-group-label-count]', { timeout: 15000 });
50
+ await page.waitForTimeout(400);
51
+
52
+ const readAll = () => page.evaluate(() => {
53
+ const ts = [...document.querySelectorAll('[data-group-label-count]')];
54
+ return ts.map(t => ({
55
+ key: t.getAttribute('data-group-label-count'),
56
+ fw: t.getAttribute('font-weight'),
57
+ pinned: t.getAttribute('data-group-label-count-pinned'),
58
+ }));
59
+ });
60
+
61
+ const rest = await readAll();
62
+ const firstKey = rest[0]?.key;
63
+
64
+ // Pin the first group by clicking its label-hit wrapper
65
+ let pinned = null;
66
+ if (firstKey) {
67
+ const hit = await page.$(`[data-group-label-hit="${firstKey}"]`);
68
+ if (hit) {
69
+ await hit.click();
70
+ await page.waitForTimeout(250);
71
+ pinned = await readAll();
72
+ }
73
+ }
74
+
75
+ const fileText = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
76
+ const sourceWired = /fontWeight=\{isPinned \? '600' : '500'\}/.test(fileText);
77
+
78
+ await browser.close();
79
+
80
+ const restAll500 = rest.every(r => r.fw === '500');
81
+ const restNoPin = rest.every(r => r.pinned === 'false');
82
+ const pinnedEntry = pinned?.find(r => r.key === firstKey);
83
+ const pinnedFw_600 = pinnedEntry?.fw === '600';
84
+ const pinnedFlag = pinnedEntry?.pinned === 'true';
85
+ const othersRest = pinned ? pinned.filter(r => r.key !== firstKey).every(r => r.fw === '500' && r.pinned === 'false') : false;
86
+
87
+ const results = {
88
+ rest_count_ge_2: rest.length >= 2,
89
+ rest_all_fw_500: restAll500,
90
+ rest_no_pin: restNoPin,
91
+ pinned_target_fw_600: pinnedFw_600,
92
+ pinned_target_flag: pinnedFlag,
93
+ pinned_others_stay: othersRest,
94
+ source_wired: sourceWired,
95
+ };
96
+ const ok = Object.values(results).every(Boolean);
97
+ console.log(`${ok ? '✅' : '❌'} group label count fw pin:`, JSON.stringify(results),
98
+ '\n rest:', JSON.stringify(rest),
99
+ '\n pinned target:', JSON.stringify(pinnedEntry));
100
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,99 @@
1
+ /* Round 457 verification: group label parent text fontWeight 700 → 800
2
+ * on isPinned. Adds typographic weight axis at parent-text scope.
3
+ *
4
+ * Contract:
5
+ * - rest: every group-label parent <text> reports font-weight '700' +
6
+ * pinned='false'
7
+ * - click a group-label-hit: that group's <text> flips to '800' +
8
+ * pinned='true'; siblings stay rest
9
+ */
10
+ import { chromium } from 'playwright';
11
+ import { readFileSync } from 'node:fs';
12
+
13
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
14
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
15
+
16
+ const browser = await chromium.launch({ headless: true });
17
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
18
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
19
+ await ctx.addInitScript(() => {
20
+ try {
21
+ localStorage.setItem('anet-theme', 'cyber');
22
+ localStorage.setItem('anet-topo-layout', 'grid');
23
+ sessionStorage.setItem('anet_v3_auth', '1');
24
+ } catch {}
25
+ });
26
+ await ctx.route('**/api/hub/status*', async (route) => {
27
+ const r = await route.fetch();
28
+ const b = await r.json();
29
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
30
+ const mk = (alias, status) => ({
31
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
32
+ network_id: nid, project_dir: null,
33
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
34
+ });
35
+ await route.fulfill({ response: r, json: { ...b, sessions: [
36
+ mk('alpha·1', 'working'),
37
+ mk('alpha·2', 'idle'),
38
+ mk('beta·1', 'working'),
39
+ mk('beta·2', 'idle'),
40
+ ] } });
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-group-label]', { timeout: 15000 });
48
+ await page.waitForTimeout(400);
49
+
50
+ const readAll = () => page.evaluate(() => {
51
+ const ts = [...document.querySelectorAll('[data-group-label]')];
52
+ return ts.map(t => ({
53
+ key: t.getAttribute('data-group-label'),
54
+ fw: t.getAttribute('font-weight'),
55
+ fw_d: t.getAttribute('data-group-label-font-weight'),
56
+ pinned: t.getAttribute('data-group-label-pinned'),
57
+ }));
58
+ });
59
+
60
+ const rest = await readAll();
61
+ const firstKey = rest[0]?.key;
62
+
63
+ // Pin first group by clicking its hit-area
64
+ let pinned = null;
65
+ if (firstKey) {
66
+ const hit = await page.$(`[data-group-label-hit="${firstKey}"]`);
67
+ if (hit) {
68
+ await hit.click();
69
+ await page.waitForTimeout(250);
70
+ pinned = await readAll();
71
+ }
72
+ }
73
+
74
+ const fileText = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
75
+ const sourceWired = /fontWeight=\{isPinned \? '800' : '700'\}/.test(fileText);
76
+
77
+ await browser.close();
78
+
79
+ const restAll700 = rest.every(r => r.fw === '700');
80
+ const restNoPin = rest.every(r => r.pinned === 'false');
81
+ const pinnedEntry = pinned?.find(r => r.key === firstKey);
82
+ const pinFw800 = pinnedEntry?.fw === '800';
83
+ const pinFlag = pinnedEntry?.pinned === 'true';
84
+ const othersRest = pinned ? pinned.filter(r => r.key !== firstKey).every(r => r.fw === '700' && r.pinned === 'false') : false;
85
+
86
+ const results = {
87
+ rest_count_ge_2: rest.length >= 2,
88
+ rest_all_fw_700: restAll700,
89
+ rest_no_pin: restNoPin,
90
+ pinned_target_fw_800: pinFw800,
91
+ pinned_target_flag: pinFlag,
92
+ pinned_others_stay: othersRest,
93
+ source_wired: sourceWired,
94
+ };
95
+ const ok = Object.values(results).every(Boolean);
96
+ console.log(`${ok ? '✅' : '❌'} group label fw pin:`, JSON.stringify(results),
97
+ '\n rest:', JSON.stringify(rest),
98
+ '\n pinned target:', JSON.stringify(pinnedEntry));
99
+ process.exit(ok ? 0 : 1);