@sleep2agi/agent-network-dashboard 0.4.6-preview.388 → 0.4.6-preview.389

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 (153) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +4 -4
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/_not-found.html +2 -2
  14. package/.next/server/app/_not-found.rsc +2 -2
  15. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/admin.html +2 -2
  23. package/.next/server/app/admin.rsc +2 -2
  24. package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
  25. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  26. package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
  27. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  28. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  29. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  30. package/.next/server/app/index.html +2 -2
  31. package/.next/server/app/index.rsc +3 -3
  32. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  33. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  34. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  35. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  36. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  37. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  38. package/.next/server/app/login.html +2 -2
  39. package/.next/server/app/login.rsc +3 -3
  40. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  41. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  42. package/.next/server/app/login.segments/_index.segment.rsc +2 -2
  43. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  44. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  46. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  47. package/.next/server/app/logs.html +2 -2
  48. package/.next/server/app/logs.rsc +2 -2
  49. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  50. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  51. package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
  52. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  54. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  55. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  56. package/.next/server/app/messages.html +2 -2
  57. package/.next/server/app/messages.rsc +2 -2
  58. package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
  59. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  60. package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
  61. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  62. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  63. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  64. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  65. package/.next/server/app/node.html +2 -2
  66. package/.next/server/app/node.rsc +2 -2
  67. package/.next/server/app/node.segments/_full.segment.rsc +2 -2
  68. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  69. package/.next/server/app/node.segments/_index.segment.rsc +2 -2
  70. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  71. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  72. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  73. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  74. package/.next/server/app/nodes.html +2 -2
  75. package/.next/server/app/nodes.rsc +2 -2
  76. package/.next/server/app/nodes.segments/_full.segment.rsc +2 -2
  77. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  78. package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
  79. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  80. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
  81. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  82. package/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/server-logs.html +2 -2
  85. package/.next/server/app/server-logs.rsc +2 -2
  86. package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
  87. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  88. package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
  89. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  90. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
  91. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  92. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app/settings/networks.html +2 -2
  94. package/.next/server/app/settings/networks.rsc +2 -2
  95. package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
  96. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  97. package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
  98. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  99. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
  100. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  101. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  102. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  103. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/tokens.html +2 -2
  105. package/.next/server/app/settings/tokens.rsc +2 -2
  106. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
  107. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  108. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
  109. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  110. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
  111. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  112. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  113. package/.next/server/app/settings.html +2 -2
  114. package/.next/server/app/settings.rsc +3 -3
  115. package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  116. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  117. package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  118. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  119. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  120. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  121. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  122. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/tasks.html +2 -2
  124. package/.next/server/app/tasks.rsc +2 -2
  125. package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
  126. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  127. package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
  128. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  129. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  130. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  131. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  132. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  133. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  134. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  135. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  136. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  137. package/.next/server/middleware-build-manifest.js +3 -3
  138. package/.next/server/pages/404.html +2 -2
  139. package/.next/server/pages/500.html +1 -1
  140. package/.next/static/chunks/055vid~-4k52i.js +4 -0
  141. package/.next/static/chunks/0aauz~36q5n2a.css +2 -0
  142. package/.next/static/chunks/{09jizem76dje~.js → 0gjcfsp73y9va.js} +1 -1
  143. package/.next/static/chunks/{0o1ml-94t26aw.js → 0i4b7l7qm45yq.js} +1 -1
  144. package/.next/trace +2 -2
  145. package/.next/trace-build +1 -1
  146. package/app/components/TopoGraph.tsx +112 -1
  147. package/package.json +1 -1
  148. package/scripts/topo-header-gap-test.mjs +94 -0
  149. package/.next/static/chunks/0abhrjbh-~5y5.js +0 -4
  150. package/.next/static/chunks/0md.j4la~1.d7.css +0 -2
  151. /package/.next/static/{QPHZZeRjZBJ_YJLcjEvz- → m5XibQCMShLsgt3nrX6RA}/_buildManifest.js +0 -0
  152. /package/.next/static/{QPHZZeRjZBJ_YJLcjEvz- → m5XibQCMShLsgt3nrX6RA}/_clientMiddlewareManifest.js +0 -0
  153. /package/.next/static/{QPHZZeRjZBJ_YJLcjEvz- → m5XibQCMShLsgt3nrX6RA}/_ssgManifest.js +0 -0
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import useSWR from 'swr';
4
5
  import { useRouter } from 'next/navigation';
5
6
  import { Session } from './types';
6
7
  import { aliasAvatarColors, aliasInitial } from './AliasAvatar';
@@ -8,6 +9,61 @@ import { ChatPopover } from './ChatPopover';
8
9
  import { vendorForModel, runtimeIdentity, identityLine } from '../lib/vendorIdentity';
9
10
  import { parseHubTime, relativeAgo } from '../lib/time';
10
11
 
12
+ /** v0.10.0 Hero 1+2 / §3.F server-health hook — fetches the normalized
13
+ * /api/hub/servers payload (preview.370 unblocked real-data via the
14
+ * proxy schema-normalize layer) and exposes a per-hostname health
15
+ * tier. red → worst-of-CPU/Mem/Disk ≥ 85%; amber → 60-85%; green
16
+ * → < 60%; null when telemetry hasn't shipped yet OR the host is
17
+ * offline.
18
+ *
19
+ * Shared with ServersDrawer via SWR's key-based dedup — both
20
+ * consumers point at /api/hub/servers so the cache layer fans out
21
+ * to a single fetch per refresh interval.
22
+ */
23
+ interface ServerHealthRow {
24
+ hostname: string;
25
+ cpu_load_1min: number | null;
26
+ cpu_cores: number;
27
+ mem_used_gb: number | null;
28
+ mem_total_gb: number | null;
29
+ disk_used_gb: number | null;
30
+ disk_total_gb: number | null;
31
+ status: 'online' | 'offline';
32
+ }
33
+ type ServerTier = 'red' | 'amber' | 'green';
34
+ function classifyServer(s: ServerHealthRow): ServerTier | null {
35
+ if (s.status === 'offline') return null;
36
+ const cpuPct = s.cpu_load_1min != null && s.cpu_cores > 0 ? (s.cpu_load_1min / s.cpu_cores) * 100 : null;
37
+ const memPct = s.mem_used_gb != null && s.mem_total_gb != null && s.mem_total_gb > 0 ? (s.mem_used_gb / s.mem_total_gb) * 100 : null;
38
+ const diskPct = s.disk_used_gb != null && s.disk_total_gb != null && s.disk_total_gb > 0 ? (s.disk_used_gb / s.disk_total_gb) * 100 : null;
39
+ const vals = [cpuPct, memPct, diskPct].filter((v): v is number => typeof v === 'number');
40
+ if (vals.length === 0) return null;
41
+ const worst = Math.max(...vals);
42
+ if (worst >= 85) return 'red';
43
+ if (worst >= 60) return 'amber';
44
+ return 'green';
45
+ }
46
+ const serversFetcher = async (url: string): Promise<{ servers: ServerHealthRow[] } | null> => {
47
+ const res = await fetch(url);
48
+ if (!res.ok) return null;
49
+ return res.json();
50
+ };
51
+ function useServerHealthMap(): Map<string, ServerTier> {
52
+ const { data } = useSWR<{ servers: ServerHealthRow[] } | null>(
53
+ '/api/hub/servers',
54
+ serversFetcher,
55
+ { refreshInterval: 15000, dedupingInterval: 5000 },
56
+ );
57
+ return useMemo(() => {
58
+ const m = new Map<string, ServerTier>();
59
+ for (const s of data?.servers ?? []) {
60
+ const tier = classifyServer(s);
61
+ if (tier) m.set(s.hostname, tier);
62
+ }
63
+ return m;
64
+ }, [data]);
65
+ }
66
+
11
67
  interface MessageFlow {
12
68
  from_alias: string;
13
69
  to_alias: string;
@@ -480,6 +536,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
480
536
  const pal = isLight ? LIGHT_PALETTE : DARK_PALETTE;
481
537
  const brand = useBrand();
482
538
  const isIntern = brand === 'intern';
539
+ // v0.10.0 Hero 1+2 / §3.F — per-host health tier map. Composes
540
+ // with #119 ServersDrawer (same SWR key, deduped). When a node's
541
+ // host server crosses into the red tier (CPU/Mem/Disk ≥ 85%),
542
+ // the per-node SVG render adds a faint amber outer ring to flag
543
+ // the issue without leaving the topology view.
544
+ const hostHealthMap = useServerHealthMap();
483
545
  // R133: Next.js client-router for the recent-signal panel "+N more"
484
546
  // navigation. TopoGraph hasn't needed routing before — every other
485
547
  // affordance composes back into the canvas's own state — but the
@@ -1460,7 +1522,22 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1460
1522
  frame top — small but cumulative with the R298 internal
1461
1523
  tighten, the title block reads as a *deliberate* badge
1462
1524
  rather than a casually-stacked label. */}
1463
- <div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between mb-4 px-1">
1525
+ {/* Round 334 / Loop: header outer wrapper mobile gap-3 → gap-2.5
1526
+ (12 px → 10 px). The wrapper is `flex flex-col` on narrow
1527
+ viewports (title-block above, chip-row below) — at mobile
1528
+ its vertical gap was 12 px while the title-block internal
1529
+ (R298) and chip-row internal (R328) both rhythm at 10 px.
1530
+ R334 unifies the OUTER vertical gap to 10 px so mobile
1531
+ stacked layout matches the established gap-rhythm tier
1532
+ (title-block 10 / chip-row 10 / chrome 8 — R298/R328/R326).
1533
+ Desktop `sm:flex-row` is unaffected: in row mode the gap-3
1534
+ would have applied horizontally but the wrapper relies on
1535
+ `sm:justify-between` for left/right anchoring (gap is then
1536
+ decorative only between the two flex-grow groups). Net
1537
+ mobile bump: 2 px tighter vertical breathing between
1538
+ title-block + chip-row. Geometry-safe — topo-overlap-test
1539
+ reads SVG-internal bbox, not header layout. */}
1540
+ <div className="flex flex-col gap-2.5 sm:flex-row sm:items-end sm:justify-between mb-4 px-1">
1464
1541
  {/* Round 267 / Loop: title block adopts leading-tight on both
1465
1542
  kicker and h2 for a tighter editorial-style rhythm. Pre-
1466
1543
  R267 the kicker used Tailwind's compound `text-xs` (line-
@@ -5451,6 +5528,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5451
5528
  transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',
5452
5529
  }}
5453
5530
  />
5531
+ {/* v0.10.0 Hero 1+2 / §3.F server-health node-ring tint.
5532
+ When the host server this agent runs on is in the
5533
+ `red` tier (CPU/Mem/Disk worst-of ≥ 85% per
5534
+ classifyServer threshold), draw a faint amber outer
5535
+ halo at radius+8. strokeWidth=2.5 stays clear of the
5536
+ R51 overlap-test sentinels (1.5 = offline status ring,
5537
+ 3 = online status ring). pointerEvents:none so the
5538
+ halo can't intercept node clicks. Falls back silent
5539
+ when host telemetry hasn't shipped yet (hostHealthMap
5540
+ is empty until commhub returns telemetry). Composes
5541
+ with R209 hover-ring (which sits BETWEEN the avatar
5542
+ and this halo on hover — different radii). */
5543
+ }
5544
+ {(() => {
5545
+ const tier = hostHealthMap.get(session.server);
5546
+ if (tier !== 'red') return null;
5547
+ return (
5548
+ <circle
5549
+ cx={pos.x}
5550
+ cy={pos.y}
5551
+ r={radius + 8}
5552
+ fill="none"
5553
+ stroke={isLight ? '#d97706' : '#fbbf24'}
5554
+ strokeWidth="2.5"
5555
+ opacity="0.6"
5556
+ data-node-server-health="red"
5557
+ data-node-server-host={session.server}
5558
+ style={{
5559
+ pointerEvents: 'none',
5560
+ transition: 'stroke 200ms ease-out, opacity 200ms ease-out',
5561
+ }}
5562
+ />
5563
+ );
5564
+ })()}
5454
5565
  {/* Issue #96: node "avatar" is now driven by the model
5455
5566
  vendor. Decision order:
5456
5567
  1. ?brand=intern flag, or an intern-aliased node with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.4.6-preview.388",
3
+ "version": "0.4.6-preview.389",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,94 @@
1
+ /* Round 334 verification: header section outer wrapper mobile
2
+ * gap-3 → gap-2.5 (12 → 10 px). Unifies mobile-stacked rhythm
3
+ * with the established R298/R328 gap-rhythm tier (title-block
4
+ * 10 px, chip-row 10 px).
5
+ *
6
+ * The wrapper is the parent of [data-topo-section-kicker] (kicker
7
+ * lives inside the title-block, which is the wrapper's first
8
+ * flex child). We find the wrapper by walking up to the element
9
+ * carrying `mb-4 px-1` + `flex-col` markers.
10
+ *
11
+ * Contract:
12
+ * - Header wrapper className contains 'gap-2.5' (not 'gap-3').
13
+ * - Computed flex-col rowGap at narrow viewport === '10px'.
14
+ * - R332 minimap rounded-lg + R330 wrapper rounded-xl regressions.
15
+ * - R317 / R318 / R294 chrome + pulse regressions intact.
16
+ *
17
+ * Uses a 500-px-wide viewport to force mobile flex-col layout so
18
+ * the `gap-2.5` (vertical) applies as row-gap.
19
+ */
20
+ import { chromium } from 'playwright';
21
+ import { readFileSync } from 'node:fs';
22
+
23
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
24
+ const browser = await chromium.launch({ headless: true });
25
+ // Standard desktop viewport — the TopoGraph requires sm:+ width
26
+ // to render. R334 is a className-level polish (gap-2.5 applied in
27
+ // both flex-col mobile + sm:flex-row desktop), so we verify
28
+ // className presence regardless of layout direction.
29
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
30
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
31
+ await ctx.addInitScript(() => {
32
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
33
+ });
34
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
35
+ await ctx.route('**/api/hub/status*', async (route) => {
36
+ const r = await route.fetch();
37
+ const b = await r.json();
38
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
39
+ const mk = (alias) => ({
40
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'claude-code-cli',
41
+ network_id: nid, project_dir: null,
42
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
43
+ });
44
+ await route.fulfill({ response: r, json: { ...b, sessions: [ mk('alpha'), mk('beta') ] } });
45
+ });
46
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
47
+ await ctx.route('**/api/hub/tasks*', (r) => r.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.waitForSelector('[data-topo-section-kicker]', { timeout: 15000, state: 'attached' });
52
+ await page.waitForTimeout(300);
53
+
54
+ const probe = await page.evaluate(() => {
55
+ const kicker = document.querySelector('[data-topo-section-kicker]');
56
+ // Walk up to the header wrapper (flex-col, mb-4, px-1).
57
+ let wrapper = kicker?.parentElement || null;
58
+ while (wrapper && !wrapper.className.includes('flex-col')) {
59
+ wrapper = wrapper.parentElement;
60
+ }
61
+ const cs = wrapper ? getComputedStyle(wrapper) : null;
62
+ const canvas = document.querySelector('[data-topo-wrapper]');
63
+ return {
64
+ wrapperClass: wrapper?.className ?? '',
65
+ wrapperFlexDir: cs?.flexDirection ?? null,
66
+ wrapperRowGap: cs?.rowGap ?? null,
67
+ canvasClass: canvas?.className ?? '',
68
+ layoutInactiveCls: document.querySelector('[data-topo-chrome-layout="grid"]')?.className ?? '',
69
+ layoutActiveCls: document.querySelector('[data-topo-chrome-layout="ring"]')?.className ?? '',
70
+ pulseCount: document.querySelectorAll('[data-pulse-wrapper]').length,
71
+ };
72
+ });
73
+ await browser.close();
74
+
75
+ const results = {
76
+ wrapper_has_gap_2_5: probe.wrapperClass.includes('gap-2.5'),
77
+ wrapper_no_gap_3: !probe.wrapperClass.match(/(^|\s)gap-3(\s|$)/),
78
+ // At desktop viewport the wrapper is sm:flex-row, so flex-direction
79
+ // !== 'column'. The gap-2.5 utility applies as column-gap in row
80
+ // mode and row-gap in column mode — check className presence
81
+ // (gap-2.5 affects both axes generically).
82
+ wrapper_is_flex: probe.wrapperFlexDir === 'row' || probe.wrapperFlexDir === 'column',
83
+ // R330 regression — canvas wrapper rounded-xl.
84
+ r330_canvas_rounded_xl: probe.canvasClass.includes('rounded-xl'),
85
+ // R317 / R318 chrome regression.
86
+ r317_inactive_gray_400: probe.layoutInactiveCls.includes('text-gray-400'),
87
+ r318_active_font_medium: probe.layoutActiveCls.includes('font-medium'),
88
+ r294_pulse_absent: probe.pulseCount === 0,
89
+ };
90
+ const ok = Object.values(results).every(Boolean);
91
+ console.log(`${ok ? '✅' : '❌'} header gap-2.5 (mobile):`, JSON.stringify(results),
92
+ '\n wrapper flex-direction:', probe.wrapperFlexDir,
93
+ '\n wrapper row-gap:', probe.wrapperRowGap);
94
+ process.exit(ok ? 0 : 1);