@sleep2agi/agent-network-dashboard 0.4.6-preview.151 → 0.4.6-preview.153

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 (157) 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/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 +2 -2
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  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 +2 -2
  25. package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
  26. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  27. package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
  28. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  30. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  31. package/.next/server/app/index.html +2 -2
  32. package/.next/server/app/index.rsc +3 -3
  33. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  34. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  35. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  36. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  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 +3 -3
  41. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  42. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  43. package/.next/server/app/login.segments/_index.segment.rsc +2 -2
  44. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  46. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  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 +2 -2
  50. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  51. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  52. package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
  53. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  55. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  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 +2 -2
  59. package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
  60. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  61. package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
  62. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  64. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  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 +2 -2
  68. package/.next/server/app/node.segments/_full.segment.rsc +2 -2
  69. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  70. package/.next/server/app/node.segments/_index.segment.rsc +2 -2
  71. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  73. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  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 +2 -2
  77. package/.next/server/app/nodes.segments/_full.segment.rsc +2 -2
  78. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  79. package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
  80. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
  82. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  83. package/.next/server/app/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs.html +2 -2
  86. package/.next/server/app/server-logs.rsc +2 -2
  87. package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
  88. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  89. package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
  90. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  91. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
  92. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  93. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/settings/networks.html +2 -2
  95. package/.next/server/app/settings/networks.rsc +2 -2
  96. package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
  97. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  98. package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
  99. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  100. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
  101. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  102. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  103. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/settings/tokens.html +2 -2
  106. package/.next/server/app/settings/tokens.rsc +2 -2
  107. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
  108. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  109. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
  110. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  111. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
  112. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  113. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  114. package/.next/server/app/settings.html +2 -2
  115. package/.next/server/app/settings.rsc +3 -3
  116. package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  117. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  118. package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  119. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  120. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  121. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  122. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/tasks.html +2 -2
  125. package/.next/server/app/tasks.rsc +2 -2
  126. package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
  127. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  128. package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
  129. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  130. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  131. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  132. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  133. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  134. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  135. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  136. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  138. package/.next/server/middleware-build-manifest.js +3 -3
  139. package/.next/server/pages/404.html +2 -2
  140. package/.next/server/pages/500.html +1 -1
  141. package/.next/server/server-reference-manifest.js +1 -1
  142. package/.next/server/server-reference-manifest.json +1 -1
  143. package/.next/static/chunks/{0d45f.svln5hm.js → 0fzk_ocmoh5w2.js} +1 -1
  144. package/.next/static/chunks/{05b69-7w4fvbn.css → 0g_sxsw.42had.css} +1 -1
  145. package/.next/static/chunks/{0f6m4wjhlakrw.js → 138cyngurs2f9.js} +1 -1
  146. package/.next/static/chunks/13o6jyrf7z4kk.js +4 -0
  147. package/.next/trace +2 -2
  148. package/.next/trace-build +1 -1
  149. package/app/components/TopoGraph.tsx +87 -34
  150. package/app/globals.css +19 -4
  151. package/package.json +1 -1
  152. package/scripts/topo-spoke-busyness-test.mjs +110 -0
  153. package/scripts/topo-sweep-busyness-test.mjs +94 -0
  154. package/.next/static/chunks/0g1ap~wkozy~6.js +0 -4
  155. /package/.next/static/{jNoJAOY9PpVhTslXxUvqQ → yEklOmrUTGrENL-pX_YPo}/_buildManifest.js +0 -0
  156. /package/.next/static/{jNoJAOY9PpVhTslXxUvqQ → yEklOmrUTGrENL-pX_YPo}/_clientMiddlewareManifest.js +0 -0
  157. /package/.next/static/{jNoJAOY9PpVhTslXxUvqQ → yEklOmrUTGrENL-pX_YPo}/_ssgManifest.js +0 -0
@@ -2106,20 +2106,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2106
2106
  scan without being noisy. Inline transform-origin on the
2107
2107
  <g> wrapper ensures Chrome / Firefox rotate around (cx,cy)
2108
2108
  instead of the SVG viewBox corner. */}
2109
- <g
2110
- style={{
2111
- transformOrigin: `${cx}px ${cy}px`,
2112
- transformBox: 'view-box',
2113
- pointerEvents: 'none',
2114
- }}
2115
- className="anet-topo-sweep"
2116
- opacity={isLight ? 0.7 : 1}
2117
- >
2118
- <path
2119
- d={`M ${cx} ${cy} L ${cx + 330} ${cy} A 330 330 0 0 0 ${cx + 330 * Math.cos(-Math.PI / 4.5)} ${cy + 330 * Math.sin(-Math.PI / 4.5)} Z`}
2120
- fill="url(#topo-sweep)"
2121
- />
2122
- </g>
2109
+ {(() => {
2110
+ // R146: radar sweep rotation buckets on workingCount, joining
2111
+ // R84 hub breath / R131 outer orbit / R132 group march /
2112
+ // R145 idle spokes as the 5th and final layer in the busyness-
2113
+ // driven motion family. 8 / 6 / 4 / 3 seconds — same 0 / 1-2 /
2114
+ // 3-5 / 6+ thresholds R84 uses. "Busier fleet = more frequent
2115
+ // scans" feels semantically right for a radar idiom. R45
2116
+ // baseline 6s sits at bucket 1 so the calm/busy spread bracks
2117
+ // around the historical default.
2118
+ const busy = workingCount === 0 ? 0
2119
+ : workingCount <= 2 ? 1
2120
+ : workingCount <= 5 ? 2
2121
+ : 3;
2122
+ const sweepDur = [8, 6, 4, 3][busy];
2123
+ return (
2124
+ <g
2125
+ style={{
2126
+ transformOrigin: `${cx}px ${cy}px`,
2127
+ transformBox: 'view-box',
2128
+ pointerEvents: 'none',
2129
+ // CSS var consumed by `.anet-topo-sweep` (line 848 of
2130
+ // globals.css). React's CSSProperties type doesn't model
2131
+ // custom properties → cast through Record<string, string>.
2132
+ ...({ ['--sweep-dur']: `${sweepDur}s` } as Record<string, string>),
2133
+ } as React.CSSProperties}
2134
+ className="anet-topo-sweep"
2135
+ opacity={isLight ? 0.7 : 1}
2136
+ data-topo-sweep-bucket={busy}
2137
+ data-topo-sweep-dur={sweepDur}
2138
+ >
2139
+ <path
2140
+ d={`M ${cx} ${cy} L ${cx + 330} ${cy} A 330 330 0 0 0 ${cx + 330 * Math.cos(-Math.PI / 4.5)} ${cy + 330 * Math.sin(-Math.PI / 4.5)} Z`}
2141
+ fill="url(#topo-sweep)"
2142
+ />
2143
+ </g>
2144
+ );
2145
+ })()}
2123
2146
 
2124
2147
  {/* radar rings — pure decoration at fixed radii, independent of
2125
2148
  node positions so the radar aesthetic is preserved across tier
@@ -2264,27 +2287,57 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2264
2287
  {/* hub links — round 46: idle spokes now have animated
2265
2288
  stroke-dashoffset so dashes flow outward from the hub
2266
2289
  ("command relay" feel). Active spokes carrying live message
2267
- flow stay as solid bright strokes. */}
2268
- {layout === 'ring' && onlineNodes.map((session, idx) => {
2269
- const pos = nodePositions[session.alias];
2270
- if (!pos) return null;
2271
- const path = curvePath({ x: cx, y: cy }, pos, 0);
2272
- const isActiveSpoke = activeAliases.has(session.alias);
2290
+ flow stay as solid bright strokes.
2291
+ R145 / Loop: idle-spoke animation cadence buckets on
2292
+ workingCount, mirroring R84 hub-breath / R131 outer-
2293
+ ring orbit / R132 groupbox-march coordination. Same
2294
+ 0 / 1-2 / 3-5 / 6+ thresholds. Idle ↔ idle network
2295
+ has a slow "command relay" feel; as work accumulates
2296
+ the dashes accelerate outward, completing the 4th
2297
+ motion layer in the busyness-driven family:
2298
+ R84 hub breath (centre)
2299
+ R131 outer ring orbit (periphery)
2300
+ R132 groupbox march (per-team, grid layout)
2301
+ R145 idle-spoke flow (ring layout, hub→nodes)
2302
+ 4 surfaces, 1 signal. Same cadence ladder 2.8/2.4/2.0/
2303
+ 1.6s — 1.75× range from calm to busy, capped so the
2304
+ network never feels frantic. */}
2305
+ {layout === 'ring' && (() => {
2306
+ const busy = workingCount === 0 ? 0
2307
+ : workingCount <= 2 ? 1
2308
+ : workingCount <= 5 ? 2
2309
+ : 3;
2310
+ const spokeDur = [2.8, 2.4, 2.0, 1.6][busy];
2311
+ return onlineNodes.map((session, idx) => {
2312
+ const pos = nodePositions[session.alias];
2313
+ if (!pos) return null;
2314
+ const path = curvePath({ x: cx, y: cy }, pos, 0);
2315
+ const isActiveSpoke = activeAliases.has(session.alias);
2273
2316
 
2274
- return (
2275
- <path
2276
- key={`hub-${session.alias}`}
2277
- d={path}
2278
- fill="none"
2279
- stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}
2280
- strokeWidth={isActiveSpoke ? 2 : 1}
2281
- strokeDasharray={isActiveSpoke ? 'none' : '6 14'}
2282
- opacity={isActiveSpoke ? 0.7 : 0.45}
2283
- className={isActiveSpoke ? undefined : 'anet-topo-spoke-flow'}
2284
- style={isActiveSpoke ? undefined : { animationDelay: `${-(idx * 0.25)}s` }}
2285
- />
2286
- );
2287
- })}
2317
+ return (
2318
+ <path
2319
+ key={`hub-${session.alias}`}
2320
+ d={path}
2321
+ fill="none"
2322
+ stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}
2323
+ strokeWidth={isActiveSpoke ? 2 : 1}
2324
+ strokeDasharray={isActiveSpoke ? 'none' : '6 14'}
2325
+ opacity={isActiveSpoke ? 0.7 : 0.45}
2326
+ className={isActiveSpoke ? undefined : 'anet-topo-spoke-flow'}
2327
+ data-topo-spoke-bucket={isActiveSpoke ? undefined : busy}
2328
+ data-topo-spoke-dur={isActiveSpoke ? undefined : spokeDur}
2329
+ style={isActiveSpoke ? undefined : ({
2330
+ animationDelay: `${-(idx * 0.25)}s`,
2331
+ // CSS var consumed by `.anet-topo-spoke-flow`
2332
+ // (line 859 of globals.css). React's CSSProperties
2333
+ // type doesn't model custom properties → cast
2334
+ // through Record<string, string>.
2335
+ ...({ ['--spoke-dur']: `${spokeDur}s` } as Record<string, string>),
2336
+ } as React.CSSProperties)}
2337
+ />
2338
+ );
2339
+ });
2340
+ })()}
2288
2341
 
2289
2342
  {/* #111: prefix-group boundary boxes (Vincent 4722). Grid layout
2290
2343
  only — groupBoxes is empty in ring mode. Rendered behind the
package/app/globals.css CHANGED
@@ -840,24 +840,39 @@ body {
840
840
 
841
841
  /* Round 45 — TopoGraph rotating radar sweep wedge. 6s slow rotation
842
842
  gives the topology canvas a real "radar scan" feel without distracting
843
- from the agent nodes. */
843
+ from the agent nodes.
844
+ R146: split shorthand into longhand so per-render inline style can
845
+ override animation-duration via --sweep-dur, joining the R145
846
+ --spoke-dur / R132 --march-dur pattern. Default 6s preserves R45's
847
+ original cadence for any caller that forgets the var. */
844
848
  @keyframes anet-topo-sweep-kf {
845
849
  0% { transform: rotate(0deg); }
846
850
  100% { transform: rotate(360deg); }
847
851
  }
848
852
  .anet-topo-sweep {
849
- animation: anet-topo-sweep-kf 6s linear infinite;
853
+ animation-name: anet-topo-sweep-kf;
854
+ animation-duration: var(--sweep-dur, 6s);
855
+ animation-timing-function: linear;
856
+ animation-iteration-count: infinite;
850
857
  }
851
858
 
852
859
  /* Round 46 — animated dashed spokes flowing from hub outward. Each spoke
853
860
  gets a small animationDelay (set inline) so the dashes don't all flow
854
- in sync — reads as "command relay" pulse rather than a metronome. */
861
+ in sync — reads as "command relay" pulse rather than a metronome.
862
+ R145: split shorthand into longhand so per-render inline style can
863
+ override animation-duration via the --spoke-dur custom property —
864
+ same idiom R132 uses for --march-dur on group boxes. Default 2.4s
865
+ when --spoke-dur isn't set, so any future caller that forgets the
866
+ var keeps R46's original cadence. */
855
867
  @keyframes anet-topo-spoke-flow-kf {
856
868
  from { stroke-dashoffset: 0; }
857
869
  to { stroke-dashoffset: -40; }
858
870
  }
859
871
  .anet-topo-spoke-flow {
860
- animation: anet-topo-spoke-flow-kf 2.4s linear infinite;
872
+ animation-name: anet-topo-spoke-flow-kf;
873
+ animation-duration: var(--spoke-dur, 2.4s);
874
+ animation-timing-function: linear;
875
+ animation-iteration-count: infinite;
861
876
  }
862
877
 
863
878
  /* Round 85 — group-box "marching ants" perimeter drift.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.4.6-preview.151",
3
+ "version": "0.4.6-preview.153",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,110 @@
1
+ /* Round 145 verification: R46 idle-spoke flow rate buckets on
2
+ * workingCount, completing the 4-layer busyness-driven motion
3
+ * family:
4
+ * R84 hub breath (centre)
5
+ * R131 outer-ring orbit (periphery)
6
+ * R132 groupbox march (per-team, grid layout)
7
+ * R145 idle-spoke flow (ring layout, hub→nodes) — new
8
+ *
9
+ * Bucket ladder shared across the four layers (same R84
10
+ * thresholds):
11
+ * workingCount === 0 → bucket 0
12
+ * workingCount in [1, 2] → bucket 1
13
+ * workingCount in [3, 5] → bucket 2
14
+ * workingCount >= 6 → bucket 3
15
+ *
16
+ * Idle-spoke durations per bucket: 2.8 / 2.4 / 2.0 / 1.6 seconds
17
+ * (R46 baseline 2.4s is bucket 1).
18
+ *
19
+ * Spokes only render in ring layout. Active spokes (carrying flow
20
+ * traffic) don't get the animation — they're solid bright strokes.
21
+ * Only idle spokes carry the .anet-topo-spoke-flow class and the
22
+ * --spoke-dur var. The test seeds 0 messages so all spokes are idle.
23
+ */
24
+ import { chromium } from 'playwright';
25
+ import { readFileSync } from 'node:fs';
26
+
27
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
28
+
29
+ async function probe(workingCount, totalCount) {
30
+ const browser = await chromium.launch({ headless: true });
31
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
32
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
33
+ await ctx.addInitScript(() => {
34
+ try {
35
+ localStorage.setItem('anet-theme', 'cyber');
36
+ sessionStorage.setItem('anet_v3_auth', '1');
37
+ // ring layout is R145's target — it's also the default, but
38
+ // pinning explicitly defends against future default flips.
39
+ localStorage.setItem('anet-topo-layout', 'ring');
40
+ } catch {}
41
+ });
42
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
43
+ await ctx.route('**/api/hub/status*', async (route) => {
44
+ const r = await route.fetch();
45
+ const b = await r.json();
46
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
47
+ const sessions = [];
48
+ for (let i = 0; i < totalCount; i++) {
49
+ sessions.push({
50
+ alias: `node${i}`,
51
+ status: i < workingCount ? 'working' : 'idle',
52
+ model: 'claude-opus-4', runtime: 'cli-claude-code',
53
+ network_id: nid, project_dir: null,
54
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
55
+ });
56
+ }
57
+ await route.fulfill({ response: r, json: { ...b, sessions } });
58
+ });
59
+ // 0 messages → no active spokes → every spoke is idle (.anet-topo-spoke-flow)
60
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
61
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
62
+
63
+ const page = await ctx.newPage();
64
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
65
+ await page.waitForFunction((n) => document.querySelectorAll('g[data-node]').length === n, totalCount, { timeout: 30000 });
66
+ await page.waitForTimeout(400);
67
+
68
+ const out = await page.evaluate(() => {
69
+ const spokes = [...document.querySelectorAll('path.anet-topo-spoke-flow')];
70
+ return spokes.map(p => ({
71
+ bucket: p.getAttribute('data-topo-spoke-bucket'),
72
+ dur: p.getAttribute('data-topo-spoke-dur'),
73
+ // Resolved animation-duration from computed style — confirms
74
+ // the CSS var bridge through to the cascade.
75
+ cssDur: getComputedStyle(p).getPropertyValue('animation-duration').trim(),
76
+ }));
77
+ });
78
+ await browser.close();
79
+ return out;
80
+ }
81
+
82
+ // One probe per bucket
83
+ const b0 = await probe(0, 4); // 0 working / 4 idle → bucket 0 (2.8s)
84
+ const b1 = await probe(2, 4); // 2 working / 2 idle → bucket 1 (2.4s)
85
+ const b2 = await probe(4, 5); // 4 working / 1 idle → bucket 2 (2.0s)
86
+ const b3 = await probe(7, 8); // 7 working / 1 idle → bucket 3 (1.6s)
87
+
88
+ const consistent = (spokes, bucket, dur) =>
89
+ spokes.length > 0 &&
90
+ spokes.every(s => s.bucket === String(bucket) && s.dur === String(dur) && s.cssDur === `${dur}s`);
91
+
92
+ const results = {
93
+ b0_someSpokes: b0.length > 0,
94
+ b0_bucket0_2_8s: consistent(b0, 0, 2.8),
95
+ b1_bucket1_2_4s: consistent(b1, 1, 2.4),
96
+ b2_bucket2_2_0s: consistent(b2, 2, 2.0),
97
+ b3_bucket3_1_6s: consistent(b3, 3, 1.6),
98
+ // dur strictly decreases as busy climbs
99
+ dur_monotonic:
100
+ Number(b0[0]?.dur) > Number(b1[0]?.dur) &&
101
+ Number(b1[0]?.dur) > Number(b2[0]?.dur) &&
102
+ Number(b2[0]?.dur) > Number(b3[0]?.dur),
103
+ };
104
+ const ok = Object.values(results).every(Boolean);
105
+ console.log(`${ok ? '✅' : '❌'} spoke busyness:`, JSON.stringify(results),
106
+ `\n b0(0w/4)=`, b0.slice(0,1),
107
+ `\n b1(2w/4)=`, b1.slice(0,1),
108
+ `\n b2(4w/5)=`, b2.slice(0,1),
109
+ `\n b3(7w/8)=`, b3.slice(0,1));
110
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,94 @@
1
+ /* Round 146 verification: R45 radar sweep rotation rate buckets on
2
+ * workingCount, completing the 5-layer busyness-driven motion family:
3
+ *
4
+ * R84 hub grounding-halo breath — 4 / 3.2 / 2.7 / 2.4 s
5
+ * R131 outer-ring orbit period — 16 / 14 / 12 / 10 s
6
+ * R132 groupbox marching ants — 14 / 12 / 10 / 8 s (grid only)
7
+ * R145 idle-spoke flow rate — 2.8 / 2.4 / 2.0 / 1.6 s (ring only)
8
+ * R146 radar sweep rotation — 8 / 6 / 4 / 3 s (both layouts)
9
+ *
10
+ * Shared R84 thresholds (0 / 1-2 / 3-5 / 6+) drive every layer. The
11
+ * sweep is the only layer visible in BOTH layouts and ties the family
12
+ * together at the canvas-wide scope.
13
+ *
14
+ * R45 baseline 6s sits at bucket 1 — preserved when workingCount in
15
+ * [1, 2] so existing demos and screenshots stay identical.
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
+
22
+ async function probe(workingCount, totalCount) {
23
+ const browser = await chromium.launch({ headless: true });
24
+ const ctx = await browser.newContext({ viewport: { width: 1400, 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 sessions = [];
35
+ for (let i = 0; i < totalCount; i++) {
36
+ sessions.push({
37
+ alias: `node${i}`,
38
+ status: i < workingCount ? 'working' : 'idle',
39
+ model: 'claude-opus-4', runtime: 'cli-claude-code',
40
+ network_id: nid, project_dir: null,
41
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
42
+ });
43
+ }
44
+ await route.fulfill({ response: r, json: { ...b, sessions } });
45
+ });
46
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
47
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
48
+
49
+ const page = await ctx.newPage();
50
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
51
+ await page.waitForFunction((n) => document.querySelectorAll('g[data-node]').length === n, totalCount, { timeout: 30000 });
52
+ await page.waitForSelector('.anet-topo-sweep', { timeout: 10000 });
53
+ await page.waitForTimeout(400);
54
+
55
+ const out = await page.evaluate(() => {
56
+ const g = document.querySelector('.anet-topo-sweep');
57
+ return {
58
+ bucket: g?.getAttribute('data-topo-sweep-bucket'),
59
+ dur: g?.getAttribute('data-topo-sweep-dur'),
60
+ cssDur: g ? getComputedStyle(g).getPropertyValue('animation-duration').trim() : null,
61
+ };
62
+ });
63
+ await browser.close();
64
+ return out;
65
+ }
66
+
67
+ const b0 = await probe(0, 4); // 0 working → bucket 0 (8s)
68
+ const b1 = await probe(2, 4); // 2 working → bucket 1 (6s, R45 baseline)
69
+ const b2 = await probe(4, 5); // 4 working → bucket 2 (4s)
70
+ const b3 = await probe(7, 8); // 7 working → bucket 3 (3s)
71
+
72
+ const consistent = (probe, bucket, dur) =>
73
+ probe.bucket === String(bucket) && probe.dur === String(dur) && probe.cssDur === `${dur}s`;
74
+
75
+ const results = {
76
+ b0_bucket0_8s: consistent(b0, 0, 8),
77
+ b1_bucket1_6s: consistent(b1, 1, 6),
78
+ b2_bucket2_4s: consistent(b2, 2, 4),
79
+ b3_bucket3_3s: consistent(b3, 3, 3),
80
+ // dur strictly decreases as busy climbs
81
+ dur_monotonic:
82
+ Number(b0.dur) > Number(b1.dur) &&
83
+ Number(b1.dur) > Number(b2.dur) &&
84
+ Number(b2.dur) > Number(b3.dur),
85
+ // R45 baseline preservation: bucket 1 still 6s
86
+ baseline_preserved: b1.dur === '6',
87
+ };
88
+ const ok = Object.values(results).every(Boolean);
89
+ console.log(`${ok ? '✅' : '❌'} sweep busyness:`, JSON.stringify(results),
90
+ `\n b0(0w/4)=`, b0,
91
+ `\n b1(2w/4)=`, b1,
92
+ `\n b2(4w/5)=`, b2,
93
+ `\n b3(7w/8)=`, b3);
94
+ process.exit(ok ? 0 : 1);