@sleep2agi/agent-network-dashboard 0.5.2-preview.9 → 0.5.3-preview.0

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 (154) 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 +5 -5
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/server/app/_not-found.html +1 -1
  13. package/.next/server/app/_not-found.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  20. package/.next/server/app/admin.html +1 -1
  21. package/.next/server/app/admin.rsc +1 -1
  22. package/.next/server/app/admin.segments/_full.segment.rsc +1 -1
  23. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  24. package/.next/server/app/admin.segments/_index.segment.rsc +1 -1
  25. package/.next/server/app/admin.segments/_tree.segment.rsc +1 -1
  26. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  27. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  28. package/.next/server/app/index.html +2 -2
  29. package/.next/server/app/index.rsc +2 -2
  30. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  31. package/.next/server/app/index.segments/_full.segment.rsc +2 -2
  32. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  33. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  34. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  35. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/login.html +2 -2
  37. package/.next/server/app/login.rsc +2 -2
  38. package/.next/server/app/login.segments/_full.segment.rsc +2 -2
  39. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  40. package/.next/server/app/login.segments/_index.segment.rsc +1 -1
  41. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  42. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  43. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  44. package/.next/server/app/logs.html +1 -1
  45. package/.next/server/app/logs.rsc +1 -1
  46. package/.next/server/app/logs.segments/_full.segment.rsc +1 -1
  47. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  48. package/.next/server/app/logs.segments/_index.segment.rsc +1 -1
  49. package/.next/server/app/logs.segments/_tree.segment.rsc +1 -1
  50. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  51. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  52. package/.next/server/app/messages.html +1 -1
  53. package/.next/server/app/messages.rsc +1 -1
  54. package/.next/server/app/messages.segments/_full.segment.rsc +1 -1
  55. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  56. package/.next/server/app/messages.segments/_index.segment.rsc +1 -1
  57. package/.next/server/app/messages.segments/_tree.segment.rsc +1 -1
  58. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  59. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  60. package/.next/server/app/node.html +1 -1
  61. package/.next/server/app/node.rsc +1 -1
  62. package/.next/server/app/node.segments/_full.segment.rsc +1 -1
  63. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  64. package/.next/server/app/node.segments/_index.segment.rsc +1 -1
  65. package/.next/server/app/node.segments/_tree.segment.rsc +1 -1
  66. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  67. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  68. package/.next/server/app/nodes.html +1 -1
  69. package/.next/server/app/nodes.rsc +1 -1
  70. package/.next/server/app/nodes.segments/_full.segment.rsc +1 -1
  71. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  72. package/.next/server/app/nodes.segments/_index.segment.rsc +1 -1
  73. package/.next/server/app/nodes.segments/_tree.segment.rsc +1 -1
  74. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
  75. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  76. package/.next/server/app/page_client-reference-manifest.js +1 -1
  77. package/.next/server/app/server-logs.html +1 -1
  78. package/.next/server/app/server-logs.rsc +1 -1
  79. package/.next/server/app/server-logs.segments/_full.segment.rsc +1 -1
  80. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  81. package/.next/server/app/server-logs.segments/_index.segment.rsc +1 -1
  82. package/.next/server/app/server-logs.segments/_tree.segment.rsc +1 -1
  83. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
  84. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  85. package/.next/server/app/settings/networks.html +1 -1
  86. package/.next/server/app/settings/networks.rsc +1 -1
  87. package/.next/server/app/settings/networks.segments/_full.segment.rsc +1 -1
  88. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  89. package/.next/server/app/settings/networks.segments/_index.segment.rsc +1 -1
  90. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +1 -1
  91. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
  92. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  93. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  94. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app/settings/tokens.html +1 -1
  96. package/.next/server/app/settings/tokens.rsc +1 -1
  97. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +1 -1
  98. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  99. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +1 -1
  100. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +1 -1
  101. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
  102. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  103. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  104. package/.next/server/app/settings.html +2 -2
  105. package/.next/server/app/settings.rsc +2 -2
  106. package/.next/server/app/settings.segments/_full.segment.rsc +2 -2
  107. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  108. package/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  109. package/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  110. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  111. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  112. package/.next/server/app/tasks.html +1 -1
  113. package/.next/server/app/tasks.rsc +1 -1
  114. package/.next/server/app/tasks.segments/_full.segment.rsc +1 -1
  115. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  116. package/.next/server/app/tasks.segments/_index.segment.rsc +1 -1
  117. package/.next/server/app/tasks.segments/_tree.segment.rsc +1 -1
  118. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  119. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  120. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js +1 -1
  121. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -1
  122. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  123. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  124. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  125. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  126. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  127. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  128. package/.next/server/middleware-build-manifest.js +3 -3
  129. package/.next/server/pages/404.html +1 -1
  130. package/.next/server/pages/500.html +1 -1
  131. package/.next/static/chunks/{06zeyg88cp6h-.js → 06umjir3xxdkg.js} +1 -1
  132. package/.next/static/chunks/0lpjexuspbsws.js +4 -0
  133. package/.next/static/chunks/{167vt72829uc..js → 0udv401.-i0wx.js} +1 -1
  134. package/.next/static/chunks/{0anxuadtbfxck.js → 0xv4drghck_7d.js} +1 -1
  135. package/.next/trace +2 -2
  136. package/.next/trace-build +1 -1
  137. package/app/components/TopoGraph.tsx +266 -29
  138. package/package.json +1 -1
  139. package/screenshots/v0.10.4-150-orphans/after.png +0 -0
  140. package/scripts/p150-orphan-layout-screenshot.mjs +82 -0
  141. package/scripts/topo-edge-badge-hot-glow-test.mjs +101 -0
  142. package/scripts/topo-edge-particle-opacity-lift-test.mjs +109 -0
  143. package/scripts/topo-group-label-glow-test.mjs +104 -0
  144. package/scripts/topo-legend-panel-title-fw-test.mjs +95 -0
  145. package/scripts/topo-minimap-dot-lift-test.mjs +115 -0
  146. package/scripts/topo-minimap-zoom-glow-test.mjs +86 -0
  147. package/scripts/topo-orphan-band-test.mjs +106 -0
  148. package/scripts/topo-recent-panel-title-fw-test.mjs +106 -0
  149. package/scripts/topo-recent-row-ts-lift-test.mjs +97 -0
  150. package/scripts/topo-zoom-attr-test.mjs +74 -0
  151. package/.next/static/chunks/0nw~q-jv9.x7v.js +0 -4
  152. /package/.next/static/{BgTBIeF-pfDSGSCiCttKZ → Q7AfghHYz-a578rrDCx1S}/_buildManifest.js +0 -0
  153. /package/.next/static/{BgTBIeF-pfDSGSCiCttKZ → Q7AfghHYz-a578rrDCx1S}/_clientMiddlewareManifest.js +0 -0
  154. /package/.next/static/{BgTBIeF-pfDSGSCiCttKZ → Q7AfghHYz-a578rrDCx1S}/_ssgManifest.js +0 -0
@@ -759,28 +759,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
759
759
  else runs.push({ key: gk, members: [s] });
760
760
  }
761
761
 
762
- // Pass 1 — assign each run to a band: a multi-member group owns its
763
- // rows (left-aligned, so its bounding box is a tidy rect); contiguous
764
- // singletons pack into shared rows (centred). Collect total row count.
765
- type Band = { members: Session[]; startRow: number; centred: boolean; isGroup: boolean };
762
+ // Pass 1 — assign each run to a band.
763
+ //
764
+ // v0.10.4 #150 (Vincent /goal 5453): "不是一起的落单的怎么散落在中间了".
765
+ // Pre-#150 algo interleaved singletons between real groups as
766
+ // centred bands, so orphan nodes appeared scattered in the middle
767
+ // between cluster boxes. Vincent screenshot called this out as
768
+ // "layout 算法一点都不好". Fix: bundle ALL singletons into ONE
769
+ // band at the bottom of the grid + render an "其他" cluster box
770
+ // around them. Multi-member prefix groups still go first in
771
+ // alias order (existing #83/#111 behaviour). Net effect:
772
+ // row 0..N-1: real prefix groups (left-aligned, own cluster box)
773
+ // row N..M: single "其他" band collecting all orphans
774
+ // (left-aligned, single cluster box at bottom)
775
+ // No orphans → no orphan band → behaviour identical to pre-#150
776
+ // for fleets where every node has a prefix-group match.
777
+ type Band = { members: Session[]; startRow: number; centred: boolean; isGroup: boolean; isOrphan?: boolean };
766
778
  const bands: Band[] = [];
767
779
  let row = 0;
768
- let i = 0;
769
- while (i < runs.length) {
770
- if (runs[i].members.length >= 2) {
771
- bands.push({ members: runs[i].members, startRow: row, centred: false, isGroup: true });
772
- row += Math.ceil(runs[i].members.length / cols);
773
- i++;
780
+ const orphanMembers: Session[] = [];
781
+ for (const run of runs) {
782
+ if (run.members.length >= 2) {
783
+ bands.push({ members: run.members, startRow: row, centred: false, isGroup: true });
784
+ row += Math.ceil(run.members.length / cols);
774
785
  } else {
775
- const singles: Session[] = [];
776
- while (i < runs.length && runs[i].members.length < 2) {
777
- singles.push(runs[i].members[0]);
778
- i++;
779
- }
780
- bands.push({ members: singles, startRow: row, centred: true, isGroup: false });
781
- row += Math.ceil(singles.length / cols);
786
+ // single-member run collect for the bottom orphan band
787
+ orphanMembers.push(...run.members);
782
788
  }
783
789
  }
790
+ if (orphanMembers.length > 0) {
791
+ bands.push({ members: orphanMembers, startRow: row, centred: false, isGroup: true, isOrphan: true });
792
+ row += Math.ceil(orphanMembers.length / cols);
793
+ }
784
794
  const totalRows = Math.max(1, row);
785
795
  // #112: the group label sits in a band ABOVE the topmost node, so the
786
796
  // band must clear the node radius — GROUP_TOP is node-relative, never
@@ -852,8 +862,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
852
862
  else if (isOn) i++;
853
863
  else o++;
854
864
  }
865
+ // v0.10.4 #150 — orphan band (singletons bundled at bottom)
866
+ // renders with a "其他" cluster box; the box-key drives the
867
+ // R63 label render + R86 hover-pin keying + #99 tooltip
868
+ // member listing, so all the existing group-box machinery
869
+ // applies uniformly to the orphan bucket too.
855
870
  return {
856
- key: band.members.length ? groupKeys[band.members[0].alias] : '',
871
+ key: band.isOrphan
872
+ ? '其他'
873
+ : band.members.length
874
+ ? groupKeys[band.members[0].alias]
875
+ : '',
857
876
  count: band.members.length,
858
877
  statuses: { working: w, idle: i, offline: o },
859
878
  x: minX - GROUP_PAD,
@@ -3504,6 +3523,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3504
3523
  state, zero re-render cost. */
3505
3524
  data-topo-layout={layout}
3506
3525
  data-topo-theme={isLight ? 'light' : 'cyber'}
3526
+ /* Round 487 / Loop — extends R469/R471 root-svg state surface
3527
+ with current zoom level (numeric attr, 2 decimals). Pre-
3528
+ R487 the canvas zoom was queryable via `data-topo-minimap-
3529
+ viewport-glow='true'` boolean (R481, gated at > 1.5) but
3530
+ the exact zoom number only lived in the chrome-strip span
3531
+ (`{Math.round(view.zoom * 100)}%`). Tests + external CSS
3532
+ that need the zoom value had to traverse to the chrome
3533
+ strip or read view state via React internals.
3534
+ R487 surfaces it at the canvas root, consistent with
3535
+ R469's fleet-count numeric pattern. Two-decimal precision
3536
+ matches the internal `view.zoom` float without losing
3537
+ info. Composed from existing state — no new state.
3538
+ Root svg attribute set now 10 attrs total:
3539
+ R462 data-dashboard-version build identity
3540
+ R466 data-topo-any-hover transient mode
3541
+ R467 data-topo-any-pinned sticky mode
3542
+ R469 data-topo-online-count fleet (4 numeric)
3543
+ R469 data-topo-working-count
3544
+ R469 data-topo-offline-count
3545
+ R469 data-topo-flow-count
3546
+ R471 data-topo-layout canvas mode
3547
+ R471 data-topo-theme canvas mode
3548
+ R487 data-topo-zoom canvas zoom (this round) */
3549
+ data-topo-zoom={view.zoom.toFixed(2)}
3507
3550
  /* Round 466 / Loop — aggregate hover signal on the root SVG.
3508
3551
  Exposes a single boolean `data-topo-any-hover` that
3509
3552
  reflects whether ANY hover state in the topology is
@@ -4761,10 +4804,35 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4761
4804
  opacity={isPinned || isHovered ? 1 : 0.55}
4762
4805
  data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}
4763
4806
  data-group-label-font-weight={isPinned ? '800' : '700'}
4807
+ /* Round 479 / Loop — extend drop-shadow visual-polish
4808
+ family to a 4th anchor: group-label parent text
4809
+ on isPinned. Continues the R476/R477/R478 arc:
4810
+ R476 hub digit hover-gated emerald
4811
+ R477 legend pin-ring pin-gated row.fill
4812
+ R478 recent-row pip freshness-gated cyan
4813
+ R479 group-label text pin-gated cyan
4814
+ Hue: pal.legendAccent at 0x80 alpha (≈50%) — same
4815
+ accent family R107/R477 use for tint surfaces. 3px
4816
+ blur reads as a soft cyan halo around the locked
4817
+ cluster name. Stacks with the R432 letter-spacing
4818
+ spread + R457 fw lift + R63 fill brighten + R142
4819
+ drop-shadow on the parent rect — pin signature on
4820
+ group label scope now spans typography + chroma +
4821
+ paint + container-lift + text-glow.
4822
+ Filter is paint-only; bbox unchanged; overlap-test
4823
+ invariants hold (R51 selector gated to g[data-node]
4824
+ descendants, this label is invisible to the probe).
4825
+ transition list extends to include 'filter 200ms
4826
+ ease-out' alongside the existing fill/ls/fw/opacity
4827
+ 200ms tweens. */
4828
+ data-group-label-glow={isPinned ? 'true' : 'false'}
4764
4829
  style={{
4765
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out',
4830
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
4766
4831
  letterSpacing: isPinned ? '0.5px' :
4767
4832
  isHovered ? '0.25px' : '0px',
4833
+ filter: isPinned
4834
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`
4835
+ : undefined,
4768
4836
  }}
4769
4837
  data-group-label={box.key}
4770
4838
  data-group-label-pinned={isPinned ? 'true' : 'false'}
@@ -5338,10 +5406,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5338
5406
  r={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}
5339
5407
  fill={pal.flowParticle}
5340
5408
  filter={isLight ? undefined : 'url(#topo-glow)'}
5341
- opacity={Math.min(1, fresh * edgeOpacityMul)}
5409
+ /* Round 485 / Loop — extends R484's "inspection
5410
+ overrides encoding" pattern to a 2nd anchor:
5411
+ edge particle opacity lifts to 1.0 on
5412
+ isHoveredEdge OR isEndpointHoveredEdge (user
5413
+ hovering the edge directly OR hovering one
5414
+ of its endpoint nodes). Pre-R485 the particle
5415
+ inherited freshness × edgeOpacityMul decay
5416
+ so a stale edge's particle painted near the
5417
+ 0.30 floor even when the operator was
5418
+ inspecting it; R485 lifts to 1.0 on attention.
5419
+ data-recent-row-ts-alpha-attribute analog —
5420
+ freshness encoding preserved on rest tier,
5421
+ opacity override engages only on inspection.
5422
+ Sibling lift family — inspection-overrides-
5423
+ encoding pattern, now 2 anchors:
5424
+ R484 recent-row timestamp freshness → 1.0
5425
+ R485 edge particle freshness → 1.0 (this)
5426
+ data-edge-particle-opacity-lifted attr exposes
5427
+ the override gate; data-edge-particle-opacity-
5428
+ rest preserves the freshness reading. */
5429
+ opacity={(isHoveredEdge || isEndpointHoveredEdge) ? 1 : Math.min(1, fresh * edgeOpacityMul)}
5342
5430
  data-edge-particle={link.key}
5343
5431
  data-edge-particle-radius={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}
5344
5432
  data-edge-particle-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5433
+ data-edge-particle-opacity-rest={Math.min(1, fresh * edgeOpacityMul).toFixed(2)}
5434
+ data-edge-particle-opacity-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5345
5435
  style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out' }}
5346
5436
  >
5347
5437
  <animateMotion
@@ -5790,6 +5880,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5790
5880
  via the new -opacity-active attr; the
5791
5881
  legacy -opacity-hover attr kept for R395
5792
5882
  test compatibility. */}
5883
+ {/* Round 480 / Loop — 5th anchor in the drop-shadow
5884
+ visual-polish family. Gates on isHot (link.
5885
+ count >= 10, R129 hot-lane threshold) so the
5886
+ badge gets a warm-amber halo when its edge
5887
+ crosses the high-traffic boundary.
5888
+ Drop-shadow family ledger now:
5889
+ R476 hub digit hover-gated emerald
5890
+ R477 legend pin-ring pin-gated row.fill
5891
+ R478 freshness pip freshness-gated cyan
5892
+ R479 group label pin-gated cyan
5893
+ R480 edge badge hot-lane-gated amber ← this round
5894
+ 5th gate type — traffic volume — joins hover,
5895
+ pin, freshness, pin. Each polish anchor uses
5896
+ a distinct semantic gate but the same paint
5897
+ vocabulary. Hue: hotStroke (amber-tinted
5898
+ palette member) at 0x80 alpha — picks up the
5899
+ R126/R188 hot-edge accent colour family so
5900
+ the glow reads as a chromatic extension of
5901
+ the existing hot-lane stroke. 3-px blur
5902
+ radius reads as soft heat rather than
5903
+ emergency klaxon.
5904
+ R51 sentinel safety: badge sw=2 only matters
5905
+ when the overlap probe runs on g[data-node]
5906
+ descendants, which this edge-internal badge
5907
+ is not. Filter is paint-only, bbox unchanged.
5908
+ transition list extends to include 'filter
5909
+ 200ms ease-out' so the heat halo eases on
5910
+ the count-crosses-threshold flip. */}
5793
5911
  <circle
5794
5912
  cx={badgeX} cy={badgeY}
5795
5913
  r={isHoveredEdge || isPinned ? 10.5 : 9}
@@ -5804,7 +5922,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5804
5922
  data-edge-badge-opacity-rest={isLight ? 0.95 : 0.85}
5805
5923
  data-edge-badge-opacity-hover="1"
5806
5924
  data-edge-badge-opacity-active="1"
5807
- style={{ transition: 'r 180ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, fill 200ms ease-out, opacity 200ms ease-out' }}
5925
+ data-edge-badge-glow={isHot ? 'true' : 'false'}
5926
+ style={{
5927
+ filter: isHot
5928
+ ? `drop-shadow(0 0 3px ${hotStroke}80)`
5929
+ : undefined,
5930
+ transition: 'r 180ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, fill 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
5931
+ }}
5808
5932
  />
5809
5933
  {/* Round 224 / Loop: edge badge text gains the 4th
5810
5934
  pin-signature typography. Pre-R224 the digit
@@ -8325,8 +8449,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8325
8449
  title scope: hovering the panel chrome spreads the
8326
8450
  title 0.1 px, signalling "this is a coherent unit
8327
8451
  you're entering". transition list extends letter-
8328
- spacing 200ms ease-out alongside existing fill 200ms. */}
8329
- <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight="700" letterSpacing={hoveredPanel === 'recent' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out' }} data-recent-panel-title>recent signal</text>
8452
+ spacing 200ms ease-out alongside existing fill 200ms.
8453
+ Round 482 / Loop add 2nd typographic axis to the
8454
+ title: fontWeight 700 → 800 on activeEdgeKey (any
8455
+ row hover OR pin propagates from hoveredEdgeKey ??
8456
+ pinnedEdgeKey). Pre-R482 the title only responded
8457
+ to panel-chrome hover via R345 ls; when a specific
8458
+ row was hovered/pinned inside the panel, the title
8459
+ stayed flat. R482 closes the gap: when ANY row is
8460
+ active inside the panel, the title tightens
8461
+ typographically alongside the row's own R143 lift +
8462
+ R472 tint + R474 text spread. data tightens under
8463
+ attention pattern extension (panel-scope variant
8464
+ following R416/R424/R425/R426/R444/R445/R446/R457
8465
+ at the chip / panel / hub / edge / count / parent-
8466
+ label tiers).
8467
+ transition list extends to include 'font-weight
8468
+ 200ms ease-out' alongside R345's ls + R55's fill
8469
+ 200ms. data-recent-panel-title-fw exposes the
8470
+ resolved weight for tests. */}
8471
+ <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight={activeEdgeKey ? '800' : '700'} letterSpacing={hoveredPanel === 'recent' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out' }} data-recent-panel-title data-recent-panel-title-fw={activeEdgeKey ? '800' : '700'} data-recent-panel-title-active={activeEdgeKey ? 'true' : 'false'}>recent signal</text>
8330
8472
  {/* R96: header count now matches what the rows show. Pre-R96
8331
8473
  this read "X msgs" off the raw messages array, but the
8332
8474
  rows below render DEDUPED flowLinks — so a fleet with 10
@@ -9242,6 +9384,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9242
9384
  {' · '}
9243
9385
  <tspan opacity="0.7" data-recent-row-content-tspan>{truncate(link.content, 8)}</tspan>
9244
9386
  </text>
9387
+ {/* Round 484 / Loop — recent-row timestamp opacity
9388
+ lifts to 1.0 when isRowHovered || isRowPinned,
9389
+ regardless of freshness alpha. R191 origin
9390
+ decays tsAlpha along with the row's freshness;
9391
+ pre-R484 hovering/pinning the row left the
9392
+ timestamp dim — user inspecting stale data
9393
+ fought the freshness encoding. R484 lifts to
9394
+ 1.0 on attention. Sibling to R472/R474 in the
9395
+ recent-row state-flip family. data-recent-row-
9396
+ ts-lifted attr exposes the gate; original
9397
+ data-recent-row-ts-alpha preserved as R191
9398
+ freshness reading. */}
9245
9399
  {lastAt ? (
9246
9400
  /* Round 321 / Loop: lastAt freshness timestamp picks
9247
9401
  up fontVariantNumeric tabular-nums. The string
@@ -9265,9 +9419,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9265
9419
  fill={pal.legendText}
9266
9420
  fontSize="8"
9267
9421
  fontFamily="monospace"
9268
- opacity={tsAlpha}
9422
+ opacity={(isRowHovered || isRowPinned) ? 1 : tsAlpha}
9269
9423
  data-recent-row-ts={link.key}
9270
9424
  data-recent-row-ts-alpha={tsAlpha.toFixed(2)}
9425
+ data-recent-row-ts-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}
9271
9426
  style={{
9272
9427
  pointerEvents: 'none',
9273
9428
  transition: 'opacity 200ms ease-out',
@@ -9542,9 +9697,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9542
9697
  surrounding chrome eased; R266 closes both at once. */}
9543
9698
  {/* R301: sibling to recent-signal panel title above —
9544
9699
  same letterSpacing 0.3 for editorial parity. */}
9700
+ {/* Round 483 / Loop — sibling to R482 (recent-signal panel
9701
+ title): legend panel title fontWeight 700 → 800 on
9702
+ pinnedStatus (any legend row pinned propagates to the
9703
+ panel title). Pre-R483 the title responded only to
9704
+ panel-chrome hover via R345 ls; the pinnedStatus row
9705
+ highlighted its own swatch + tint via R181/R477 but
9706
+ the title stayed flat — no upstream tightening to
9707
+ signal "panel context = inspecting".
9708
+ R483 closes the symmetry with R482: both panel titles
9709
+ (recent-signal + legend) now tighten typographically
9710
+ when ANY row inside them is in the active filter
9711
+ state. Same idiom, mirrored at the legend-row scope.
9712
+ data tightens family — now 10 anchors:
9713
+ R416/R424/R425/R426 chip/panel/hub/edge digits
9714
+ R444/R445/R446 group/recent/legend counts
9715
+ R457 group-label parent
9716
+ R482 recent-panel title
9717
+ R483 legend-panel title (this round)
9718
+ transition list extends to include 'font-weight 200ms
9719
+ ease-out' alongside R345's ls + R55's fill 200ms.
9720
+ data-legend-panel-title-fw + -active exposed for tests. */}
9545
9721
  {/* R345 sibling — legend panel title same hover letter-
9546
9722
  spacing tween 0.3 → 0.4 on panel hover. */}
9547
- <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight="700" letterSpacing={hoveredPanel === 'legend' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out' }} data-legend-panel-title>legend</text>
9723
+ <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight={pinnedStatus ? '800' : '700'} letterSpacing={hoveredPanel === 'legend' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out' }} data-legend-panel-title data-legend-panel-title-fw={pinnedStatus ? '800' : '700'} data-legend-panel-title-active={pinnedStatus ? 'true' : 'false'}>legend</text>
9548
9724
  {/* Round 257 / Loop: legend panel header count picks up the
9549
9725
  symmetric 13L/13R inner-padding pattern from the recent-
9550
9726
  signal panel. Pre-R257 the legend header was 13px from
@@ -10440,12 +10616,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10440
10616
  stale ones. data-topo-minimap-dot-opacity
10441
10617
  attr (R392) reflects the resolved hover-
10442
10618
  state value for tests. */
10619
+ /* Round 486 / Loop — 3rd anchor in the
10620
+ inspection-overrides-encoding pattern. Sibling
10621
+ to R484 (recent-row timestamp) + R485 (edge
10622
+ particle). When the operator hovers a node
10623
+ alias on the main canvas, the matching
10624
+ minimap dot lifts to opacity=1.0 regardless
10625
+ of the binary online/offline encoding —
10626
+ cross-reference cue between canvas focal
10627
+ and the minimap wayfinding overlay.
10628
+ Pre-R486 an offline node's minimap dot stayed
10629
+ at 0.6 even when the operator was inspecting
10630
+ it via canvas hover; R486 makes the
10631
+ inspection signal jump the minimap dot to
10632
+ full presence so the spatial reference is
10633
+ unambiguous.
10634
+ Encoding survives: data-topo-minimap-dot-
10635
+ online preserves the online/offline binary,
10636
+ data-topo-minimap-dot-opacity-rest preserves
10637
+ the would-be opacity. Only the LIVE painted
10638
+ opacity flips on inspection.
10639
+ inspection-overrides-encoding family — 3
10640
+ anchors now:
10641
+ R484 recent-row timestamp
10642
+ R485 edge particle
10643
+ R486 minimap dot ← this round
10644
+ data-topo-minimap-dot-lifted attr exposes
10645
+ the override gate. */
10443
10646
  r={isOn ? 1.9 : 1.2}
10444
10647
  fill={st.primary}
10445
- opacity={isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6}
10648
+ opacity={hoveredAlias === s.alias ? 1 : (isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6)}
10446
10649
  data-topo-minimap-dot={s.alias}
10447
10650
  data-topo-minimap-dot-online={isOn ? 'true' : 'false'}
10448
- data-topo-minimap-dot-opacity={isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6}
10651
+ data-topo-minimap-dot-opacity={hoveredAlias === s.alias ? 1 : (isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6)}
10652
+ data-topo-minimap-dot-opacity-rest={isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6}
10653
+ data-topo-minimap-dot-lifted={hoveredAlias === s.alias ? 'true' : 'false'}
10449
10654
  data-topo-minimap-dot-radius={isOn ? 1.9 : 1.2}
10450
10655
  style={{
10451
10656
  transition: 'opacity 200ms ease-out, fill 200ms ease-out, r 200ms ease-out',
@@ -10569,10 +10774,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10569
10774
  data-topo-minimap-viewport-smooth={smoothView ? 'true' : 'false'}
10570
10775
  data-topo-minimap-viewport-hover={hoveredMinimap ? 'true' : 'false'}
10571
10776
  data-topo-minimap-viewport-linejoin="round"
10777
+ /* Round 481 / Loop — 6th anchor in the drop-shadow
10778
+ visual-polish family. New gate type: ZOOM STATE.
10779
+ When current canvas zoom > 1.5x (50% above the
10780
+ default 1.0 baseline), the minimap viewport rect
10781
+ gains a soft cyan halo signaling "you're zoomed
10782
+ in beyond default". The minimap viewport already
10783
+ shrinks as you zoom in (rectW = VIEWBOX_W /
10784
+ view.zoom * sx, so at zoom=2 it halves) — the
10785
+ glow tells you the wayfinding marker is now
10786
+ scaled-down rather than at canvas-default size.
10787
+ Drop-shadow family — 6 gate types covered:
10788
+ R476 hub digit hover-gated
10789
+ R477 legend pin-ring pin-gated
10790
+ R478 freshness pip freshness-gated
10791
+ R479 group label pin-gated
10792
+ R480 edge badge hot-lane-gated
10793
+ R481 minimap zoom-gated ← this round
10794
+ 6 distinct semantic gates (user interaction
10795
+ transient/sticky × 2, data freshness, data
10796
+ volume, canvas zoom state). Each anchor uses
10797
+ hue family appropriate to its semantic context.
10798
+ Hue: pal.legendAccent at 0x80 alpha — matches
10799
+ the existing R107 tint family and R478/R479
10800
+ cyan-tone choices. 2-px blur reads as subtle
10801
+ (the minimap viewport is small, ~120×82 px).
10802
+ Filter is paint-only — bbox unchanged. transition
10803
+ list extends to include 'filter 200ms ease-out'
10804
+ so the glow eases when zoom crosses 1.5x. */
10805
+ data-topo-minimap-viewport-glow={view.zoom > 1.5 ? 'true' : 'false'}
10572
10806
  style={{
10807
+ filter: view.zoom > 1.5
10808
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}80)`
10809
+ : undefined,
10573
10810
  transition: smoothView
10574
- ? 'x 280ms ease-out, y 280ms ease-out, width 280ms ease-out, height 280ms ease-out, stroke-width 200ms ease-out, opacity 200ms ease-out'
10575
- : 'stroke-width 200ms ease-out, opacity 200ms ease-out',
10811
+ ? 'x 280ms ease-out, y 280ms ease-out, width 280ms ease-out, height 280ms ease-out, stroke-width 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out'
10812
+ : 'stroke-width 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
10576
10813
  } as React.CSSProperties}
10577
10814
  />
10578
10815
  </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.5.2-preview.9",
3
+ "version": "0.5.3-preview.0",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,82 @@
1
+ /* v0.10.4 #150 — orphan layout screenshot evidence.
2
+ *
3
+ * Fixture: 3 prefix groups (5+3+4 = 12 grouped) + 5 distinct-prefix
4
+ * singletons (orphans). Pre-#150 the singletons appeared scattered
5
+ * in centred bands between cluster boxes. Post-#150 they all bundle
6
+ * into one "其他" cluster box at the bottom. */
7
+ import { chromium } from 'playwright';
8
+ import { readFileSync } from 'node:fs';
9
+
10
+ const out = process.argv[2] || 'screenshots/v0.10.4-150-orphans/after';
11
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
12
+ const browser = await chromium.launch({ headless: true });
13
+ const ctx = await browser.newContext({ viewport: { width: 1600, height: 1500 }, deviceScaleFactor: 2 });
14
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
15
+ await ctx.addInitScript(() => {
16
+ try {
17
+ localStorage.setItem('anet-theme', 'cyber');
18
+ sessionStorage.setItem('anet_v3_auth', '1');
19
+ // Force grid layout so the orphan band is visible
20
+ localStorage.setItem('anet-topo-layout', 'grid');
21
+ } catch {}
22
+ });
23
+ const fresh = new Date(Date.now() - 60_000).toISOString();
24
+ const sessions = [];
25
+ // Group 1: ai-insight × 5
26
+ for (let i = 1; i <= 5; i++) sessions.push({
27
+ alias: `ai-insight-node-${i}`, status: i === 1 ? 'working' : 'idle',
28
+ model: 'claude-opus-4', runtime: 'claude-code-cli',
29
+ network_id: 'default', project_dir: null,
30
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
31
+ });
32
+ // Group 2: agent-network-dashboard × 3
33
+ for (let i = 1; i <= 3; i++) sessions.push({
34
+ alias: `agent-network-dashboard-${i}`, status: i === 1 ? 'working' : 'idle',
35
+ model: 'gpt-4o', runtime: 'codex-sdk',
36
+ network_id: 'default', project_dir: null,
37
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
38
+ });
39
+ // Group 3: p-station × 4
40
+ for (let i = 1; i <= 4; i++) sessions.push({
41
+ alias: `p-station-worker-${i}`, status: i === 1 ? 'working' : 'idle',
42
+ model: 'minimax/abab6', runtime: 'http-api',
43
+ network_id: 'default', project_dir: null,
44
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
45
+ });
46
+ // Orphans: 5 distinct-prefix singletons — should bundle into "其他" bottom band
47
+ ['monitor-prod', 'dispatcher-bot', 'runner-helper', 'oracle-svc', 'cron-worker'].forEach((alias, idx) => {
48
+ sessions.push({
49
+ alias, status: idx % 2 === 0 ? 'working' : 'idle',
50
+ model: 'claude-sonnet-4', runtime: 'claude-agent-sdk',
51
+ network_id: 'default', project_dir: null,
52
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
53
+ });
54
+ });
55
+ await ctx.route('**/api/hub/status*', async (route) => {
56
+ const r = await route.fetch();
57
+ const b = await r.json();
58
+ await route.fulfill({ response: r, json: { ...b, sessions } });
59
+ });
60
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
61
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
62
+ const page = await ctx.newPage();
63
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
64
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length >= 17, { timeout: 30000 });
65
+ await page.waitForTimeout(800);
66
+ const box = await page.evaluate(() => {
67
+ const chrome = document.querySelector('[data-topo-chrome]');
68
+ let card = chrome?.parentElement;
69
+ while (card && !card.querySelector(':scope > svg')) card = card?.parentElement;
70
+ if (!card) return null;
71
+ card.scrollIntoView({ block: 'start' });
72
+ return new Promise(resolve => requestAnimationFrame(() => {
73
+ const r = card.getBoundingClientRect();
74
+ resolve({ x: Math.max(0, r.x), y: Math.max(0, r.y), width: r.width, height: r.height });
75
+ }));
76
+ });
77
+ await page.waitForTimeout(400);
78
+ if (box && box.width > 100) {
79
+ await page.screenshot({ path: `${out}.png`, clip: { x: box.x, y: box.y, width: box.width, height: Math.min(box.height, 1200) } });
80
+ }
81
+ console.log(`✅ wrote ${out}.png`);
82
+ await browser.close();
@@ -0,0 +1,101 @@
1
+ /* Round 480 verification: edge badge circle gains filter: drop-
2
+ * shadow glow on isHot (link.count >= 10). 5th anchor in the
3
+ * R476/R477/R478/R479 drop-shadow family — first traffic-volume-
4
+ * gated variant.
5
+ *
6
+ * Contract:
7
+ * - cold edge (count < 10): data-edge-badge-glow='false' AND
8
+ * computed filter === 'none'
9
+ * - hot edge (count >= 10): glow='true' AND computed filter
10
+ * starts with 'drop-shadow' using hotStroke @ 0x80 alpha
11
+ * - source-file conditional wired
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 nowIso = () => new Date().toISOString();
18
+ const sessionFresh = 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: 1200 } });
22
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
23
+ await ctx.addInitScript(() => {
24
+ try {
25
+ localStorage.setItem('anet-theme', 'cyber');
26
+ localStorage.setItem('anet-topo-layout', 'ring');
27
+ sessionStorage.setItem('anet_v3_auth', '1');
28
+ } catch {}
29
+ });
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 mk = (alias, status) => ({
35
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
36
+ network_id: nid, project_dir: null,
37
+ created_at: sessionFresh, updated_at: sessionFresh, last_seen_at: sessionFresh,
38
+ });
39
+ await route.fulfill({ response: r, json: { ...b, sessions: [
40
+ mk('a·1', 'working'), mk('a·2', 'idle'), mk('b·1', 'working'),
41
+ ] } });
42
+ });
43
+ // Build messages: 12 from a·1 → a·2 (HOT, count=12 >= 10)
44
+ // 2 from b·1 → a·1 (cold, count=2 < 10)
45
+ const hotMessages = [];
46
+ for (let i = 0; i < 12; i++) {
47
+ hotMessages.push({
48
+ id: `hot-${i}`, from_alias: 'a·1', to_alias: 'a·2',
49
+ content: `m${i}`, created_at: nowIso(),
50
+ });
51
+ }
52
+ const coldMessages = [
53
+ { id: 'c1', from_alias: 'b·1', to_alias: 'a·1', content: 'c1', created_at: nowIso() },
54
+ { id: 'c2', from_alias: 'b·1', to_alias: 'a·1', content: 'c2', created_at: nowIso() },
55
+ ];
56
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: {
57
+ messages: [...hotMessages, ...coldMessages],
58
+ } }));
59
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
60
+
61
+ const page = await ctx.newPage();
62
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
63
+ await page.waitForSelector('[data-edge-badge-glow]', { timeout: 15000 });
64
+ await page.waitForTimeout(500);
65
+
66
+ const probe = await page.evaluate(() => {
67
+ const badges = [...document.querySelectorAll('[data-edge-badge-glow]')];
68
+ return badges.map(b => {
69
+ const cs = getComputedStyle(b);
70
+ return {
71
+ glow: b.getAttribute('data-edge-badge-glow'),
72
+ filter: cs.filter,
73
+ };
74
+ });
75
+ });
76
+
77
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
78
+ const sourceGlowAttr = /data-edge-badge-glow=\{isHot/.test(src);
79
+ const sourceDropShadow = /drop-shadow\(0 0 3px \$\{hotStroke\}80\)/.test(src);
80
+ const sourceFilterTween = /filter 200ms ease-out/.test(src);
81
+
82
+ await browser.close();
83
+
84
+ const hot = probe.find(b => b.glow === 'true');
85
+ const cold = probe.find(b => b.glow === 'false');
86
+
87
+ const results = {
88
+ badges_count_ge_2: probe.length >= 2,
89
+ hot_badge_found: !!hot,
90
+ hot_filter_has_drop: hot && /drop-shadow/.test(hot.filter),
91
+ cold_badge_found: !!cold,
92
+ cold_filter_none: cold?.filter === 'none',
93
+ source_glow_attr: sourceGlowAttr,
94
+ source_drop_shadow: sourceDropShadow,
95
+ source_filter_tween: sourceFilterTween,
96
+ };
97
+ const ok = Object.values(results).every(Boolean);
98
+ console.log(`${ok ? '✅' : '❌'} edge badge hot-lane drop-shadow:`, JSON.stringify(results),
99
+ '\n hot badge:', JSON.stringify(hot),
100
+ '\n cold badge:', JSON.stringify(cold));
101
+ process.exit(ok ? 0 : 1);