@sleep2agi/agent-network-dashboard 0.4.6-preview.234 → 0.4.6-preview.236

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 +2 -2
  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/{0klaaq0_k_dxm.js → 0dxbwid~u6awc.js} +1 -1
  144. package/.next/static/chunks/{0_emp5p873h~z.js → 0f_7j4d32pd_5.js} +1 -1
  145. package/.next/static/chunks/0r50330w~i9ea.css +2 -0
  146. package/.next/static/chunks/0yqmjy005995r.js +4 -0
  147. package/.next/trace +2 -2
  148. package/.next/trace-build +1 -1
  149. package/app/components/TopoGraph.tsx +95 -5
  150. package/package.json +1 -1
  151. package/scripts/topo-group-label-count-inherit-test.mjs +99 -0
  152. package/scripts/topo-ping-pulse-ease-test.mjs +96 -0
  153. package/.next/static/chunks/0j0jff7iipl3~.js +0 -4
  154. package/.next/static/chunks/0~k1yvhakbob6.css +0 -2
  155. /package/.next/static/{XbxeLaE3cwT2xRHuIRjrc → w5JTcbtsPlvOhgfAMLaO2}/_buildManifest.js +0 -0
  156. /package/.next/static/{XbxeLaE3cwT2xRHuIRjrc → w5JTcbtsPlvOhgfAMLaO2}/_clientMiddlewareManifest.js +0 -0
  157. /package/.next/static/{XbxeLaE3cwT2xRHuIRjrc → w5JTcbtsPlvOhgfAMLaO2}/_ssgManifest.js +0 -0
@@ -3055,7 +3055,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3055
3055
  the node↔label guard still catches if the chip ever
3056
3056
  pushes the label far enough right to clip a node.
3057
3057
  Smaller + lighter weight reads as metadata, not name. */}
3058
- <tspan dx="6" fill={pal.legendText} fontSize="11" fontWeight="400">· {box.count}</tspan>
3058
+ {/* Round 229 / Loop: member-count chip drops its explicit
3059
+ fill so it inherits from the parent <text>, which means
3060
+ R142's hover-fill transition (legendText → legend-
3061
+ Headline, 200ms ease-out) NOW carries the count chip
3062
+ with it. Pre-R229 the parent name brightened on
3063
+ hover while the count tspan stayed at legendText —
3064
+ "name lit, count dimmer than at rest" inverted the
3065
+ tonal hierarchy. Inheriting matches the name's
3066
+ transition; both rest and hover keep the SAME
3067
+ tonal relationship between name and count.
3068
+ 7th surface in the hover-deepen-own-hue family
3069
+ (legend rows, chip-row counts, status pip, recent
3070
+ row text, pressure-bar segments, group-box fill +
3071
+ this round's group-label-count chip).
3072
+
3073
+ Also picks up tabular-nums (5th surface in the
3074
+ info-density tabular-nums sweep after R224 edge
3075
+ badge / R225 hub digit / R225 panel header /
3076
+ R225 recent row count). The member count rolls
3077
+ over often (4→5→…→9→10 as a group grows) and
3078
+ lives at a fixed dx=6 offset from the name, so a
3079
+ digit-width jitter at 9→10 used to shift the
3080
+ whole count visibly. Tabular locks it. */}
3081
+ <tspan
3082
+ dx="6"
3083
+ fontSize="11"
3084
+ fontWeight="400"
3085
+ data-group-label-count={box.key}
3086
+ data-group-label-count-value={box.count}
3087
+ style={{ fontVariantNumeric: 'tabular-nums' }}
3088
+ >· {box.count}</tspan>
3059
3089
  {/* Round 58 / Loop: status mix pip strip. Compact text-
3060
3090
  based chips (e.g. "2w 1i") so the strip stays inside
3061
3091
  the same <text> bbox the overlap-test reads — keeps
@@ -3313,8 +3343,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3313
3343
  style={{ pointerEvents: 'none' }}
3314
3344
  data-arrival-ping={link.key}
3315
3345
  >
3316
- <animate attributeName="r" values="0;14;22" dur={`${duration}s`} begin={`-${(duration * 0.92).toFixed(2)}s`} repeatCount="indefinite" />
3317
- <animate attributeName="opacity" values="0;0.55;0" dur={`${duration}s`} begin={`-${(duration * 0.92).toFixed(2)}s`} repeatCount="indefinite" />
3346
+ {/* Round 228 / Loop: pulse-pop ease curves on the
3347
+ arrival ping SMIL — extends R227's keySplines
3348
+ adoption from the click ripple (one-shot, two-
3349
+ value linear) to the canvas's repeating delivery-
3350
+ confirmation surfaces. Both <animate>s use
3351
+ calcMode=spline with keyTimes='0;0.5;1' (two
3352
+ segments).
3353
+ • r grows 0→14→22 (monotonic): unified ease-out
3354
+ across both segments — the ring decelerates
3355
+ as it expands, settling at its widest.
3356
+ • opacity bumps 0→0.55→0 (pulse): ease-out on
3357
+ the rise (fast appearance), ease-in on the
3358
+ fall (slow start of fade then accelerates) —
3359
+ the canonical "pulse-pop" kinetic shape.
3360
+ Together r decelerating and opacity pulse-popping
3361
+ give the arrival ping a real-event physicality
3362
+ instead of the prior linear-velocity tick. */}
3363
+ <animate
3364
+ attributeName="r"
3365
+ values="0;14;22"
3366
+ dur={`${duration}s`}
3367
+ begin={`-${(duration * 0.92).toFixed(2)}s`}
3368
+ repeatCount="indefinite"
3369
+ calcMode="spline"
3370
+ keyTimes="0;0.5;1"
3371
+ keySplines="0.25 0.1 0.25 1;0.25 0.1 0.25 1"
3372
+ />
3373
+ <animate
3374
+ attributeName="opacity"
3375
+ values="0;0.55;0"
3376
+ dur={`${duration}s`}
3377
+ begin={`-${(duration * 0.92).toFixed(2)}s`}
3378
+ repeatCount="indefinite"
3379
+ calcMode="spline"
3380
+ keyTimes="0;0.5;1"
3381
+ keySplines="0.25 0.1 0.25 1;0.42 0 1 1"
3382
+ />
3318
3383
  </circle>
3319
3384
  )}
3320
3385
  {/* Round 76 / Loop: source dispatch pulse — mirror to the
@@ -3340,8 +3405,33 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3340
3405
  style={{ pointerEvents: 'none' }}
3341
3406
  data-dispatch-pulse={link.key}
3342
3407
  >
3343
- <animate attributeName="r" values="0;12;18" dur={`${duration}s`} begin="0s" repeatCount="indefinite" />
3344
- <animate attributeName="opacity" values="0;0.45;0" dur={`${duration}s`} begin="0s" repeatCount="indefinite" />
3408
+ {/* Round 228 / Loop: same pulse-pop curves as the
3409
+ arrival ping above. r ease-out + opacity
3410
+ ease-out→ease-in. The dispatch pulse is smaller
3411
+ (0→12→18 vs arrival's 0→14→22), but the kinetic
3412
+ feel should be identical — both bookend a
3413
+ single message in flight, so they should
3414
+ physically ease the same way. */}
3415
+ <animate
3416
+ attributeName="r"
3417
+ values="0;12;18"
3418
+ dur={`${duration}s`}
3419
+ begin="0s"
3420
+ repeatCount="indefinite"
3421
+ calcMode="spline"
3422
+ keyTimes="0;0.5;1"
3423
+ keySplines="0.25 0.1 0.25 1;0.25 0.1 0.25 1"
3424
+ />
3425
+ <animate
3426
+ attributeName="opacity"
3427
+ values="0;0.45;0"
3428
+ dur={`${duration}s`}
3429
+ begin="0s"
3430
+ repeatCount="indefinite"
3431
+ calcMode="spline"
3432
+ keyTimes="0;0.5;1"
3433
+ keySplines="0.25 0.1 0.25 1;0.42 0 1 1"
3434
+ />
3345
3435
  </circle>
3346
3436
  )}
3347
3437
  {/* Round 100 / Loop: midpoint count badge for high-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.4.6-preview.234",
3
+ "version": "0.4.6-preview.236",
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
+ /* Round 229 verification: group-label member-count tspan drops
2
+ * its explicit fill (now inherits from parent <text>) and picks up
3
+ * fontVariantNumeric: 'tabular-nums'.
4
+ *
5
+ * Two scopes:
6
+ * 1) Hover-deepen-own-hue family — count chip color now follows
7
+ * parent name's R142 hover transition (legendText → legend-
8
+ * Headline). Tonal hierarchy (count < name brightness) is
9
+ * preserved across both rest AND hover instead of inverting
10
+ * on hover.
11
+ * 2) Info-density tabular-nums — 5th surface after R224 edge
12
+ * badge / R225 hub + panel header + recent row.
13
+ *
14
+ * Scenario: 6 prefix-shared aliases (alpha-1..alpha-3, beta-1..
15
+ * beta-3) → R106 cluster-by-prefix forms two groups, each with
16
+ * count=3. Grid layout has explicit group label rendering (#111).
17
+ *
18
+ * Verifications:
19
+ * - data-group-label-count attr present (probe selector)
20
+ * - tspan has NO explicit fill attribute (inheriting)
21
+ * - getComputedStyle().fontVariantNumeric contains 'tabular-nums'
22
+ * - tspan computed fill matches PARENT <text> computed fill
23
+ * (R142 rest-state hierarchy preserved)
24
+ * - count text matches '· 3'
25
+ * - data-group-label-count-value reflects {box.count}
26
+ */
27
+ import { chromium } from 'playwright';
28
+ import { readFileSync } from 'node:fs';
29
+
30
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
31
+ const browser = await chromium.launch({ headless: true });
32
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
33
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
34
+ await ctx.addInitScript(() => {
35
+ try {
36
+ localStorage.setItem('anet-theme', 'cyber');
37
+ sessionStorage.setItem('anet_v3_auth', '1');
38
+ // R226 grid layout — group labels render here. Storage key inferred
39
+ // from data-topo-layout-toggle persistence pattern.
40
+ localStorage.setItem('anet-topo-layout', 'grid');
41
+ } catch {}
42
+ });
43
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
44
+ await ctx.route('**/api/hub/status*', async (route) => {
45
+ const r = await route.fetch();
46
+ const b = await r.json();
47
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
48
+ const mk = (alias) => ({
49
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
50
+ network_id: nid, project_dir: null,
51
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
52
+ });
53
+ await route.fulfill({ response: r, json: { ...b, sessions: [
54
+ mk('alpha-1'), mk('alpha-2'), mk('alpha-3'),
55
+ mk('beta-1'), mk('beta-2'), mk('beta-3'),
56
+ ] } });
57
+ });
58
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: [] } }));
59
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.fulfill({ json: { tasks: [] } }));
60
+
61
+ const page = await ctx.newPage();
62
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
63
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 6, { timeout: 30000 });
64
+ await page.waitForSelector('[data-group-label-count]', { timeout: 10000, state: 'attached' });
65
+ await page.waitForTimeout(400);
66
+
67
+ const out = await page.evaluate(() => {
68
+ const counts = Array.from(document.querySelectorAll('[data-group-label-count]'));
69
+ return counts.map((tspan) => {
70
+ const parent = tspan.parentElement; // the <text data-group-label>
71
+ return {
72
+ key: tspan.getAttribute('data-group-label-count'),
73
+ countValue: tspan.getAttribute('data-group-label-count-value'),
74
+ hasFillAttr: tspan.hasAttribute('fill'),
75
+ text: tspan.textContent,
76
+ fontVarNumeric: getComputedStyle(tspan).fontVariantNumeric,
77
+ tspanFill: getComputedStyle(tspan).fill,
78
+ parentFill: parent ? getComputedStyle(parent).fill : null,
79
+ };
80
+ });
81
+ });
82
+ await browser.close();
83
+
84
+ const hasTab = (s) => /tabular-nums/.test(s || '');
85
+ const present = out.length > 0;
86
+
87
+ const results = {
88
+ groups_found: out.length === 2,
89
+ all_have_key: present && out.every(o => typeof o.key === 'string' && o.key.length > 0),
90
+ all_count_3: present && out.every(o => o.countValue === '3'),
91
+ all_text_dot_3: present && out.every(o => /·\s*3$/.test((o.text || '').trim())),
92
+ all_no_fill_attr: present && out.every(o => !o.hasFillAttr),
93
+ all_tabular_nums: present && out.every(o => hasTab(o.fontVarNumeric)),
94
+ fill_inherits_parent: present && out.every(o => o.tspanFill === o.parentFill),
95
+ };
96
+ const ok = Object.values(results).every(Boolean);
97
+ console.log(`${ok ? '✅' : '❌'} group-label count inherit + tabular:`, JSON.stringify(results),
98
+ '\n probed:', out);
99
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,96 @@
1
+ /* Round 228 verification: arrival-ping (R75) and dispatch-pulse
2
+ * (R76) SMIL <animate>s get pulse-pop ease curves via calcMode/
3
+ * keyTimes/keySplines.
4
+ *
5
+ * Coverage:
6
+ * - [data-arrival-ping] r → ease-out across both segments
7
+ * - [data-arrival-ping] opacity → ease-out rise, ease-in fall
8
+ * - [data-dispatch-pulse] r → ease-out across both segments
9
+ * - [data-dispatch-pulse] opacity → ease-out rise, ease-in fall
10
+ *
11
+ * Scenario: 4 working agents + 5 messages alpha→beta so:
12
+ * - link.count = 5 ≥ 3 (dispatch pulse fires)
13
+ * - fresh > 0.5 (both ping + pulse gate active)
14
+ * - reducedMotion off (cyber-themed cookie default)
15
+ */
16
+ import { chromium } from 'playwright';
17
+ import { readFileSync } from 'node:fs';
18
+
19
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
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
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
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) => ({
32
+ alias, status: 'working', 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'), mk('beta'), mk('gamma'), mk('delta'),
38
+ ] } });
39
+ });
40
+ const now = Date.now();
41
+ const msgs = [];
42
+ for (let i = 0; i < 5; i++) {
43
+ msgs.push({
44
+ id: `m${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
45
+ network_id: 'default', created_at: new Date(now - (1000 + i * 50)).toISOString(),
46
+ });
47
+ }
48
+ await ctx.route('**/api/hub/messages*', (r2) => r2.fulfill({ json: { messages: msgs } }));
49
+ await ctx.route('**/api/hub/tasks*', (r2) => r2.fulfill({ json: { tasks: [] } }));
50
+
51
+ const page = await ctx.newPage();
52
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
53
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
54
+ await page.waitForSelector('[data-arrival-ping]', { timeout: 10000, state: 'attached' });
55
+ await page.waitForSelector('[data-dispatch-pulse]', { timeout: 10000, state: 'attached' });
56
+ await page.waitForTimeout(400);
57
+
58
+ const out = await page.evaluate(() => {
59
+ const probe = (parentSelector) => {
60
+ const el = document.querySelector(parentSelector);
61
+ if (!el) return null;
62
+ const animates = Array.from(el.querySelectorAll('animate'));
63
+ return animates.map((a) => ({
64
+ attr: a.getAttribute('attributeName'),
65
+ calcMode: a.getAttribute('calcMode'),
66
+ keyTimes: a.getAttribute('keyTimes'),
67
+ keySplines: a.getAttribute('keySplines'),
68
+ }));
69
+ };
70
+ return {
71
+ arrival: probe('[data-arrival-ping]'),
72
+ dispatch: probe('[data-dispatch-pulse]'),
73
+ };
74
+ });
75
+ await browser.close();
76
+
77
+ const easeOutBoth = '0.25 0.1 0.25 1;0.25 0.1 0.25 1';
78
+ const pulsePop = '0.25 0.1 0.25 1;0.42 0 1 1';
79
+ const keyTimes = '0;0.5;1';
80
+
81
+ const find = (arr, attr) => arr?.find(a => a.attr === attr);
82
+ const matches = (a, splines) => a && a.calcMode === 'spline' && a.keyTimes === keyTimes && a.keySplines === splines;
83
+
84
+ const results = {
85
+ arrival_present: Array.isArray(out.arrival) && out.arrival.length === 2,
86
+ arrival_r_easeout: matches(find(out.arrival, 'r'), easeOutBoth),
87
+ arrival_op_pulse: matches(find(out.arrival, 'opacity'), pulsePop),
88
+ dispatch_present: Array.isArray(out.dispatch) && out.dispatch.length === 2,
89
+ dispatch_r_easeout: matches(find(out.dispatch, 'r'), easeOutBoth),
90
+ dispatch_op_pulse: matches(find(out.dispatch, 'opacity'), pulsePop),
91
+ };
92
+ const ok = Object.values(results).every(Boolean);
93
+ console.log(`${ok ? '✅' : '❌'} ping+pulse ease:`, JSON.stringify(results),
94
+ '\n arrival:', out.arrival,
95
+ '\n dispatch:', out.dispatch);
96
+ process.exit(ok ? 0 : 1);