@sleep2agi/agent-network-dashboard 0.5.2-preview.1 → 0.5.2-preview.11

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 +7 -7
  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 +2 -2
  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/0h9xo63u79l9l.js +1 -0
  132. package/.next/static/chunks/0nrxcvtcs7x71.js +1 -0
  133. package/.next/static/chunks/{0pgnr.wdna.jv.js → 0rf5564pjvl7f.js} +1 -1
  134. package/.next/static/chunks/1841977._okm5.js +4 -0
  135. package/.next/trace +2 -2
  136. package/.next/trace-build +1 -1
  137. package/app/components/TopoGraph.tsx +243 -10
  138. package/package.json +1 -1
  139. package/scripts/topo-edge-badge-hot-glow-test.mjs +101 -0
  140. package/scripts/topo-group-label-glow-test.mjs +104 -0
  141. package/scripts/topo-hub-digit-glow-test.mjs +105 -0
  142. package/scripts/topo-layout-theme-attrs-test.mjs +91 -0
  143. package/scripts/topo-legend-pin-ring-glow-test.mjs +105 -0
  144. package/scripts/topo-legend-row-text-cadence-test.mjs +93 -0
  145. package/scripts/topo-legend-row-tint-cadence-test.mjs +92 -0
  146. package/scripts/topo-recent-row-freshness-glow-test.mjs +100 -0
  147. package/scripts/topo-recent-row-text-cadence-test.mjs +99 -0
  148. package/scripts/topo-recent-row-tint-cadence-test.mjs +96 -0
  149. package/.next/static/chunks/00047pvxj~z3x.js +0 -4
  150. package/.next/static/chunks/0ubkidns7e9dp.js +0 -1
  151. package/.next/static/chunks/0zpqg7iibb7fi.js +0 -1
  152. /package/.next/static/{1nsKFD4r1lqfWBZ-F7k3- → hG7R_jh4p1dno7frJBNCy}/_buildManifest.js +0 -0
  153. /package/.next/static/{1nsKFD4r1lqfWBZ-F7k3- → hG7R_jh4p1dno7frJBNCy}/_clientMiddlewareManifest.js +0 -0
  154. /package/.next/static/{1nsKFD4r1lqfWBZ-F7k3- → hG7R_jh4p1dno7frJBNCy}/_ssgManifest.js +0 -0
package/.next/trace-build CHANGED
@@ -1 +1 @@
1
- [{"name":"run-turbopack","duration":7223957,"timestamp":176663590644,"id":14,"parentId":1,"tags":{},"startTime":1778984635837,"traceId":"2c0450a1e4686fb4"},{"name":"turbopack-build-events","duration":106,"timestamp":176663924536,"id":15,"parentId":1,"tags":{},"startTime":1778984636171,"traceId":"2c0450a1e4686fb4"},{"name":"run-typescript","duration":10352446,"timestamp":176670828246,"id":16,"parentId":1,"tags":{},"startTime":1778984643075,"traceId":"2c0450a1e4686fb4"},{"name":"static-check","duration":890218,"timestamp":176681380300,"id":19,"parentId":1,"tags":{},"startTime":1778984653627,"traceId":"2c0450a1e4686fb4"},{"name":"static-generation","duration":689987,"timestamp":176682285637,"id":109,"parentId":1,"tags":{},"startTime":1778984654532,"traceId":"2c0450a1e4686fb4"},{"name":"telemetry-flush","duration":19208,"timestamp":176683013876,"id":118,"parentId":1,"tags":{},"startTime":1778984655260,"traceId":"2c0450a1e4686fb4"},{"name":"next-build","duration":19695241,"timestamp":176663337877,"id":1,"tags":{"buildMode":"default","version":"16.2.3","bundler":"turbopack","has-custom-webpack-config":"false","use-build-worker":"true"},"startTime":1778984635584,"traceId":"2c0450a1e4686fb4"}]
1
+ [{"name":"run-turbopack","duration":5978558,"timestamp":180822290369,"id":14,"parentId":1,"tags":{},"startTime":1778988794537,"traceId":"3d0010a9212a1fa1"},{"name":"turbopack-build-events","duration":95,"timestamp":180822579535,"id":15,"parentId":1,"tags":{},"startTime":1778988794826,"traceId":"3d0010a9212a1fa1"},{"name":"run-typescript","duration":9740495,"timestamp":180828286955,"id":16,"parentId":1,"tags":{},"startTime":1778988800533,"traceId":"3d0010a9212a1fa1"},{"name":"static-check","duration":761394,"timestamp":180838220234,"id":19,"parentId":1,"tags":{},"startTime":1778988810467,"traceId":"3d0010a9212a1fa1"},{"name":"static-generation","duration":655125,"timestamp":180838997327,"id":109,"parentId":1,"tags":{},"startTime":1778988811244,"traceId":"3d0010a9212a1fa1"},{"name":"telemetry-flush","duration":34437,"timestamp":180839687385,"id":118,"parentId":1,"tags":{},"startTime":1778988811934,"traceId":"3d0010a9212a1fa1"},{"name":"next-build","duration":17661477,"timestamp":180822060373,"id":1,"tags":{"buildMode":"default","version":"16.2.3","bundler":"turbopack","has-custom-webpack-config":"false","use-build-worker":"true"},"startTime":1778988794307,"traceId":"3d0010a9212a1fa1"}]
@@ -3485,6 +3485,25 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3485
3485
  data-topo-working-count={workingCount}
3486
3486
  data-topo-offline-count={offlineNodes.length}
3487
3487
  data-topo-flow-count={flowLinks.length}
3488
+ /* Round 471 / Loop — surface 2 remaining canvas-level mode
3489
+ attrs alongside the R462/R466/R467/R469 set. Pre-R471 the
3490
+ root svg exposed 7 attrs but tests probing "what layout
3491
+ is active" had to query DOM internals (data-topo-chrome-
3492
+ layout-active on the chrome button row) or parse the URL
3493
+ for theme. R471 puts both modes on the root for one-stop
3494
+ snapshot reads:
3495
+ data-topo-layout — 'ring' | 'grid'
3496
+ data-topo-theme — 'cyber' | 'light'
3497
+ Together with R469 the canvas root now carries 9 cross-
3498
+ cutting attrs (1 build identity + 2 inspection mode + 4
3499
+ fleet split + 2 layout/theme). Test harness can read the
3500
+ FULL canvas state with 9 getAttribute calls; no traversal
3501
+ into chrome strip / theme provider / panel rows.
3502
+ Composed from existing `layout` (R138 ring↔grid toggle
3503
+ state) + `isLight` (R12 theme palette gate) — no new
3504
+ state, zero re-render cost. */
3505
+ data-topo-layout={layout}
3506
+ data-topo-theme={isLight ? 'light' : 'cyber'}
3488
3507
  /* Round 466 / Loop — aggregate hover signal on the root SVG.
3489
3508
  Exposes a single boolean `data-topo-any-hover` that
3490
3509
  reflects whether ANY hover state in the topology is
@@ -4742,10 +4761,35 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4742
4761
  opacity={isPinned || isHovered ? 1 : 0.55}
4743
4762
  data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}
4744
4763
  data-group-label-font-weight={isPinned ? '800' : '700'}
4764
+ /* Round 479 / Loop — extend drop-shadow visual-polish
4765
+ family to a 4th anchor: group-label parent text
4766
+ on isPinned. Continues the R476/R477/R478 arc:
4767
+ R476 hub digit hover-gated emerald
4768
+ R477 legend pin-ring pin-gated row.fill
4769
+ R478 recent-row pip freshness-gated cyan
4770
+ R479 group-label text pin-gated cyan
4771
+ Hue: pal.legendAccent at 0x80 alpha (≈50%) — same
4772
+ accent family R107/R477 use for tint surfaces. 3px
4773
+ blur reads as a soft cyan halo around the locked
4774
+ cluster name. Stacks with the R432 letter-spacing
4775
+ spread + R457 fw lift + R63 fill brighten + R142
4776
+ drop-shadow on the parent rect — pin signature on
4777
+ group label scope now spans typography + chroma +
4778
+ paint + container-lift + text-glow.
4779
+ Filter is paint-only; bbox unchanged; overlap-test
4780
+ invariants hold (R51 selector gated to g[data-node]
4781
+ descendants, this label is invisible to the probe).
4782
+ transition list extends to include 'filter 200ms
4783
+ ease-out' alongside the existing fill/ls/fw/opacity
4784
+ 200ms tweens. */
4785
+ data-group-label-glow={isPinned ? 'true' : 'false'}
4745
4786
  style={{
4746
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out',
4787
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
4747
4788
  letterSpacing: isPinned ? '0.5px' :
4748
4789
  isHovered ? '0.25px' : '0px',
4790
+ filter: isPinned
4791
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`
4792
+ : undefined,
4749
4793
  }}
4750
4794
  data-group-label={box.key}
4751
4795
  data-group-label-pinned={isPinned ? 'true' : 'false'}
@@ -5771,6 +5815,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5771
5815
  via the new -opacity-active attr; the
5772
5816
  legacy -opacity-hover attr kept for R395
5773
5817
  test compatibility. */}
5818
+ {/* Round 480 / Loop — 5th anchor in the drop-shadow
5819
+ visual-polish family. Gates on isHot (link.
5820
+ count >= 10, R129 hot-lane threshold) so the
5821
+ badge gets a warm-amber halo when its edge
5822
+ crosses the high-traffic boundary.
5823
+ Drop-shadow family ledger now:
5824
+ R476 hub digit hover-gated emerald
5825
+ R477 legend pin-ring pin-gated row.fill
5826
+ R478 freshness pip freshness-gated cyan
5827
+ R479 group label pin-gated cyan
5828
+ R480 edge badge hot-lane-gated amber ← this round
5829
+ 5th gate type — traffic volume — joins hover,
5830
+ pin, freshness, pin. Each polish anchor uses
5831
+ a distinct semantic gate but the same paint
5832
+ vocabulary. Hue: hotStroke (amber-tinted
5833
+ palette member) at 0x80 alpha — picks up the
5834
+ R126/R188 hot-edge accent colour family so
5835
+ the glow reads as a chromatic extension of
5836
+ the existing hot-lane stroke. 3-px blur
5837
+ radius reads as soft heat rather than
5838
+ emergency klaxon.
5839
+ R51 sentinel safety: badge sw=2 only matters
5840
+ when the overlap probe runs on g[data-node]
5841
+ descendants, which this edge-internal badge
5842
+ is not. Filter is paint-only, bbox unchanged.
5843
+ transition list extends to include 'filter
5844
+ 200ms ease-out' so the heat halo eases on
5845
+ the count-crosses-threshold flip. */}
5774
5846
  <circle
5775
5847
  cx={badgeX} cy={badgeY}
5776
5848
  r={isHoveredEdge || isPinned ? 10.5 : 9}
@@ -5785,7 +5857,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5785
5857
  data-edge-badge-opacity-rest={isLight ? 0.95 : 0.85}
5786
5858
  data-edge-badge-opacity-hover="1"
5787
5859
  data-edge-badge-opacity-active="1"
5788
- style={{ transition: 'r 180ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, fill 200ms ease-out, opacity 200ms ease-out' }}
5860
+ data-edge-badge-glow={isHot ? 'true' : 'false'}
5861
+ style={{
5862
+ filter: isHot
5863
+ ? `drop-shadow(0 0 3px ${hotStroke}80)`
5864
+ : undefined,
5865
+ 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',
5866
+ }}
5789
5867
  />
5790
5868
  {/* Round 224 / Loop: edge badge text gains the 4th
5791
5869
  pin-signature typography. Pre-R224 the digit
@@ -6284,15 +6362,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6284
6362
  /* Round 253 / Loop: append fill 200ms to the hub
6285
6363
  digit transition list — theme toggle (cyber #ecfdf5
6286
6364
  ↔ light #d1fae5) was the last hub-area snap. */
6365
+ /* Round 476 / Loop — hub working-count digit gains a
6366
+ filter: drop-shadow glow on hub-hover. Stacks with
6367
+ the existing 4-axis hub-hover gesture stack on this
6368
+ element:
6369
+ R209 transform: scale(1.08) geometry
6370
+ R425 fontWeight 700 → 800 typography
6371
+ R253 fill ease-out chroma (theme)
6372
+ R213 opacity gate fade (count cross)
6373
+ R476 filter drop-shadow glow paint (this round)
6374
+ The glow uses the cyber emerald-400 (#34d399) /
6375
+ light emerald-500 (#10b981) hue family so the
6376
+ chroma stays inside the hub-area palette. Subtle
6377
+ 2-3 px blur radius at 0.6 opacity — visible but
6378
+ not loud, reads as "the focal digit lit up under
6379
+ attention".
6380
+ Reduced-motion users skip the filter via the
6381
+ !reducedMotion gate (R29 a11y blanket).
6382
+ Filter is a paint-only attribute — bbox stays
6383
+ the same, R51 overlap-test invariants hold.
6384
+ transition list extends to 'filter 200ms ease-out'
6385
+ so the glow eases under the same cadence as the
6386
+ scale + fw + fill axes. */
6387
+ data-topo-hub-working-count-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
6287
6388
  style={{
6288
6389
  pointerEvents: 'none',
6289
6390
  transform: !reducedMotion && hoveredHub ? 'scale(1.08)' : 'scale(1)',
6290
6391
  transformBox: 'fill-box',
6291
6392
  transformOrigin: 'center',
6393
+ filter: !reducedMotion && hoveredHub
6394
+ ? (isLight
6395
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6))'
6396
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6))')
6397
+ : undefined,
6292
6398
  /* R425: font-weight 200ms appended so the hover fw
6293
6399
  bump 700 → 800 eases under the same cadence as
6294
- R209 scale + R253 fill + R213 opacity. */
6295
- transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out',
6400
+ R209 scale + R253 fill + R213 opacity.
6401
+ R476: filter 200ms appended so the new drop-
6402
+ shadow glow eases at the same cadence. */
6403
+ transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
6296
6404
  fontVariantNumeric: 'tabular-nums',
6297
6405
  }}
6298
6406
  >
@@ -8806,6 +8914,22 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8806
8914
  the chip-row pills. R116: pinned rows tint
8807
8915
  stronger than hovered ones so locked vs preview
8808
8916
  is discriminable. */}
8917
+ {/* Round 472 / Loop — cadence-sync follow-on to the
8918
+ R459/R460/R461/R464/R465/R470 200ms uniform
8919
+ motion stack established at the cluster scope.
8920
+ This R104 recent-signal row tint rect was still
8921
+ at the legacy 150ms cadence — when a user
8922
+ hovers/pins a recent-signal row, the tint
8923
+ snapped in 50ms ahead of the rest of the row's
8924
+ state-change cascade (R143 translateY,
8925
+ R220+R434 letter-spacing, R434 fill tween).
8926
+ R472 lifts to 200ms ease-out to match. Same
8927
+ sibling idiom R459 closed at the group-label
8928
+ hitbox tier; now applied at the recent-signal
8929
+ row tier. data-recent-row-tint-transition attr
8930
+ exposes the cadence for tests.
8931
+ Geometry/paint logic unchanged — purely the
8932
+ transition timing. */}
8809
8933
  <rect
8810
8934
  x="6" y={38 + index * 16 - 10}
8811
8935
  width="218" height="14" rx="3"
@@ -8813,7 +8937,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8813
8937
  opacity={isRowPinned ? (isLight ? 0.18 : 0.22)
8814
8938
  : isRowHovered ? (isLight ? 0.10 : 0.14)
8815
8939
  : 1}
8816
- style={{ transition: 'fill 150ms ease-out, opacity 150ms ease-out' }}
8940
+ data-recent-row-tint={link.key}
8941
+ data-recent-row-tint-transition="200ms"
8942
+ style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}
8817
8943
  />
8818
8944
  {/* Round 160 / Loop: recency pip. Canvas flow edges
8819
8945
  fade by freshness (R10: full intensity ≤30s →
@@ -8915,16 +9041,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8915
9041
  to include 'r 200ms ease-out' matching the
8916
9042
  opacity cadence. data-recent-row-freshness-
8917
9043
  lifted attr exposes the gate for tests. */
9044
+ /* Round 478 / Loop — extend the R476/R477
9045
+ drop-shadow vocabulary to a third anchor:
9046
+ the recent-row freshness pip on `alpha
9047
+ > 0.7` (just-fired flow within ~30s per
9048
+ R10 freshness ramp). Gate is FRESHNESS-
9049
+ driven not pin/hover-driven, so the glow
9050
+ reads as "this signal is live" rather
9051
+ than "user is inspecting". As the alpha
9052
+ decays past 0.7 (≈45s after last fire),
9053
+ the glow eases off — natural breathing
9054
+ feel that tracks actual data freshness.
9055
+ Hue: pal.legendAccent at 0.5 alpha so
9056
+ the glow inherits the row's accent color
9057
+ family. 2.5-3px blur reads as soft
9058
+ radiance, not loud bloom.
9059
+ Drop-shadow visual-polish family now 3
9060
+ anchors:
9061
+ R476 hub digit hover-gated
9062
+ R477 legend pin-ring pin-gated
9063
+ R478 recent freshness freshness-gated
9064
+ Each anchor uses a different state gate
9065
+ but the same `filter: drop-shadow` paint
9066
+ vocabulary. Filter affects paint only —
9067
+ bbox unchanged, overlap-test invariants
9068
+ hold. Transition list extends to include
9069
+ 'filter 200ms ease-out' alongside
9070
+ R10/R447 opacity + r tweens. */
8918
9071
  fill={pal.legendAccent}
8919
9072
  opacity={alpha}
8920
9073
  data-recent-row-freshness={link.key}
8921
9074
  data-recent-row-freshness-alpha={alpha.toFixed(2)}
8922
9075
  data-recent-row-freshness-radius={(isRowHovered || isRowPinned) ? 2.5 : 2.0}
8923
9076
  data-recent-row-freshness-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}
9077
+ data-recent-row-freshness-glow={alpha > 0.7 ? 'true' : 'false'}
8924
9078
  style={{
8925
9079
  pointerEvents: 'none',
8926
9080
  r: `${(isRowHovered || isRowPinned) ? 2.5 : 2.0}px`,
8927
- transition: 'opacity 200ms ease-out, r 200ms ease-out',
9081
+ filter: alpha > 0.7
9082
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`
9083
+ : undefined,
9084
+ transition: 'opacity 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
8928
9085
  } as React.CSSProperties}
8929
9086
  />
8930
9087
  );
@@ -9009,8 +9166,25 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9009
9166
  R427/R431/R432/R433/R434. R55 fill 150ms +
9010
9167
  R220 letter-spacing 150ms transition kept
9011
9168
  (additive conditional case, no new property). */
9169
+ /* Round 474 / Loop — cadence-sync follow-on to
9170
+ R472. R472 lifted the recent-row TINT RECT
9171
+ to 200ms but the row TEXT alongside still
9172
+ ran 150ms — same panel-row scope, two
9173
+ different rates. When a user hovered/pinned
9174
+ a row the rect background brightened in
9175
+ 200ms while the text fill + letter-spacing
9176
+ finished in 150ms. R474 closes that internal
9177
+ desync by lifting the text transitions to
9178
+ match. Whole recent-row state-flip now
9179
+ eases at 200ms ease-out across rect AND
9180
+ text. data-recent-row-text-transition='200ms'
9181
+ attr exposed for tests. R434 3-tier letter-
9182
+ spacing values unchanged; R363 fw + R55 fill
9183
+ brighten unchanged — only the timing axis
9184
+ shifts. */
9185
+ data-recent-row-text-transition="200ms"
9012
9186
  style={{
9013
- transition: 'fill 150ms ease-out, letter-spacing 150ms ease-out',
9187
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
9014
9188
  letterSpacing: isRowPinned ? '0.5px' :
9015
9189
  isRowHovered ? '0.25px' : '0px',
9016
9190
  }}
@@ -9666,6 +9840,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9666
9840
  now ~1.25px below hitbox center (vs ~2.25px pre).
9667
9841
  No height change, no test ripple (other than this
9668
9842
  one), no R260/R268/R270 chrome regressions. */}
9843
+ {/* Round 473 / Loop — final cadence-sync follow-on,
9844
+ closing the legacy 150ms transition at the
9845
+ LEGEND-ROW tint scope. R459 (group-label hitbox)
9846
+ + R472 (recent-row hitbox) already lifted the
9847
+ two sibling panel-row hitboxes to 200ms; the
9848
+ legend-row was the last per-row tint still
9849
+ snapping at 150ms.
9850
+ After R473 the 200ms ease-out vocabulary is
9851
+ uniform across ALL three panel-row scopes —
9852
+ group-label, recent-signal, and legend — so
9853
+ hover/pin state-change cascades read coherently
9854
+ at every panel-tier surface. data-legend-row-
9855
+ tint-transition='200ms' attr exposed for tests.
9856
+ Geometry/paint unchanged. */}
9669
9857
  <rect
9670
9858
  x="6" y={row.y0 - 11}
9671
9859
  width="170" height="22" rx="3"
@@ -9674,7 +9862,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9674
9862
  : hoveredStatus === row.key ? (isLight ? 0.08 : 0.12)
9675
9863
  : 1}
9676
9864
  data-legend-row-tinted={isPinned ? 'pinned' : hoveredStatus === row.key ? 'hover' : 'none'}
9677
- style={{ transition: 'fill 150ms ease-out, opacity 150ms ease-out' }}
9865
+ data-legend-row-tint-transition="200ms"
9866
+ style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}
9678
9867
  />
9679
9868
  {/* Round 197 / Loop: swatch dot scales r 5.5 → 7 when its
9680
9869
  row is hovered or pinned. Pre-R197 the swatch was a
@@ -9759,6 +9948,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9759
9948
  + pointerEvents:none all preserved. data-legend-
9760
9949
  pin-ring-stroke-width attr exposes the value for
9761
9950
  tests. */}
9951
+ {/* Round 477 / Loop — legend pin-ring gains a filter:
9952
+ drop-shadow glow on isPinned. Extends R476's
9953
+ drop-shadow idiom from hub-digit (focal scope)
9954
+ to the legend-row pin-ring (sibling pin-state
9955
+ surface). When a status row is pinned, the
9956
+ concentric ring around the swatch now lights
9957
+ up with a colour-matched halo using row.fill,
9958
+ reinforcing "this filter is locked" via a
9959
+ glow layer above the R402 sw bump + R181
9960
+ opacity fade-in.
9961
+ Hue: row.fill at 0.55 alpha — picks up each
9962
+ status tier's signature colour (working green /
9963
+ idle teal / offline slate). 3px blur stays
9964
+ subtle but unmistakable when the row is locked.
9965
+ Reduced-motion users skip the filter via R29
9966
+ a11y blanket (transition-duration → 0.001ms
9967
+ so the glow appears/disappears instantly with
9968
+ pin toggle).
9969
+ Filter is paint-only — bbox unchanged, R51
9970
+ overlap-test gated to g[data-node] descendants
9971
+ so this legend-internal ring is invisible to
9972
+ the probe anyway. Transition list extends to
9973
+ include 'filter 200ms ease-out' so the glow
9974
+ eases under the same cadence as opacity. */}
9762
9975
  <circle
9763
9976
  cx="16" cy={row.y0} r="8"
9764
9977
  fill="none"
@@ -9768,9 +9981,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9768
9981
  data-legend-pin-ring={row.key}
9769
9982
  data-legend-pin-ring-pinned={isPinned ? 'true' : 'false'}
9770
9983
  data-legend-pin-ring-stroke-width="1.75"
9984
+ data-legend-pin-ring-glow={isPinned ? 'true' : 'false'}
9771
9985
  style={{
9772
9986
  pointerEvents: 'none',
9773
- transition: 'opacity 150ms ease-out',
9987
+ filter: isPinned
9988
+ ? `drop-shadow(0 0 3px ${row.fill}88)`
9989
+ : undefined,
9990
+ transition: 'opacity 150ms ease-out, filter 200ms ease-out',
9774
9991
  }}
9775
9992
  />
9776
9993
  {/* Round 219 / Loop: legend row text gains the same
@@ -9845,8 +10062,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9845
10062
  group-label R432, legend-row R433). R55 fill
9846
10063
  150ms + R219 letter-spacing 150ms transition
9847
10064
  untouched — additive conditional case. */
10065
+ /* Round 475 / Loop — final closure of the panel-row
10066
+ text scope cadence-sync. R473 lifted the legend-
10067
+ row TINT RECT to 200ms; R474 lifted the recent-
10068
+ row TEXT to 200ms; R475 closes the matching
10069
+ legend-row text desync — fill + letter-spacing
10070
+ both 150 → 200ms ease-out. After R475 the 3-tier
10071
+ panel-row cadence family is fully 200ms across
10072
+ BOTH rect and text at every panel-row scope
10073
+ (group-label / recent-row / legend-row). Hover/
10074
+ pin state-flip at any panel-row tier reads as
10075
+ one motion-coherent unit. data-legend-row-
10076
+ label-transition='200ms' attr exposed for tests.
10077
+ R433 3-tier letter-spacing values (0/0.25/0.5)
10078
+ unchanged; R55 fill brighten unchanged — only
10079
+ the timing axis shifts. */
10080
+ data-legend-row-label-transition="200ms"
9848
10081
  style={{
9849
- transition: 'fill 150ms ease-out, letter-spacing 150ms ease-out',
10082
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
9850
10083
  letterSpacing: isPinned ? '0.5px' :
9851
10084
  hoveredStatus === row.key ? '0.25px' : '0px',
9852
10085
  }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.5.2-preview.1",
3
+ "version": "0.5.2-preview.11",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -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);
@@ -0,0 +1,104 @@
1
+ /* Round 479 verification: group-label parent text gains filter:
2
+ * drop-shadow glow on isPinned. 4th anchor in the R476/R477/R478
3
+ * drop-shadow visual-polish family.
4
+ *
5
+ * Contract:
6
+ * - at rest (no group pinned): every group label has data-group-
7
+ * label-glow='false' AND computed filter === 'none'
8
+ * - click a group-label hitbox: that group's label flips to
9
+ * glow='true' + filter starts with 'drop-shadow' using
10
+ * pal.legendAccent at 0x80 alpha
11
+ * - sibling group labels stay rest (no spillover)
12
+ * - source-file conditional wired
13
+ */
14
+ import { chromium } from 'playwright';
15
+ import { readFileSync } from 'node:fs';
16
+
17
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
18
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
19
+
20
+ const browser = await chromium.launch({ headless: true });
21
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 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', 'grid');
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: fresh, updated_at: fresh, last_seen_at: fresh,
38
+ });
39
+ await route.fulfill({ response: r, json: { ...b, sessions: [
40
+ mk('alpha·1', 'working'), mk('alpha·2', 'idle'),
41
+ mk('beta·1', 'working'), mk('beta·2', 'idle'),
42
+ ] } });
43
+ });
44
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
45
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
46
+
47
+ const page = await ctx.newPage();
48
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
49
+ await page.waitForSelector('[data-group-label-glow]', { timeout: 15000 });
50
+ await page.waitForTimeout(500);
51
+
52
+ const readAll = () => page.evaluate(() => {
53
+ const labels = [...document.querySelectorAll('[data-group-label-glow]')];
54
+ return labels.map(l => {
55
+ const cs = getComputedStyle(l);
56
+ return {
57
+ key: l.getAttribute('data-group-label'),
58
+ glow: l.getAttribute('data-group-label-glow'),
59
+ filter: cs.filter,
60
+ };
61
+ });
62
+ });
63
+
64
+ const rest = await readAll();
65
+ const firstKey = rest[0]?.key;
66
+
67
+ // Click hitbox to pin the first group
68
+ let pinned = null;
69
+ if (firstKey) {
70
+ const hit = await page.$(`[data-group-label-hit="${firstKey}"]`);
71
+ if (hit) {
72
+ await hit.click();
73
+ await page.waitForTimeout(400);
74
+ pinned = await readAll();
75
+ }
76
+ }
77
+
78
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
79
+ const sourceGlowAttr = /data-group-label-glow=\{isPinned/.test(src);
80
+ const sourceDropShadow = /drop-shadow\(0 0 3px \$\{pal\.legendAccent\}80\)/.test(src);
81
+
82
+ await browser.close();
83
+
84
+ const restCount = rest.length;
85
+ const restAllFalse = rest.every(r => r.glow === 'false' && r.filter === 'none');
86
+ const pinnedTarget = pinned?.find(r => r.key === firstKey);
87
+ const pinTargetGlow = pinnedTarget?.glow === 'true';
88
+ const pinTargetHasShadow = pinnedTarget && /drop-shadow/.test(pinnedTarget.filter);
89
+ const pinSiblingsStill = pinned ? pinned.filter(r => r.key !== firstKey).every(r => r.glow === 'false') : false;
90
+
91
+ const results = {
92
+ rest_count_ge_2: restCount >= 2,
93
+ rest_all_false: restAllFalse,
94
+ pinned_target_glow: pinTargetGlow,
95
+ pinned_target_shadow: pinTargetHasShadow,
96
+ pinned_siblings_rest: pinSiblingsStill,
97
+ source_glow_attr: sourceGlowAttr,
98
+ source_drop_shadow: sourceDropShadow,
99
+ };
100
+ const ok = Object.values(results).every(Boolean);
101
+ console.log(`${ok ? '✅' : '❌'} group-label drop-shadow glow:`, JSON.stringify(results),
102
+ '\n rest:', JSON.stringify(rest),
103
+ '\n pinned target:', JSON.stringify(pinnedTarget));
104
+ process.exit(ok ? 0 : 1);