@sleep2agi/agent-network-dashboard 0.4.6-preview.191 → 0.4.6-preview.193

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +4 -4
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/server/app/_global-error.html +1 -1
  7. package/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +2 -2
  15. package/.next/server/app/_not-found.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/admin.html +2 -2
  24. package/.next/server/app/admin.rsc +2 -2
  25. package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
  26. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  27. package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
  28. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  30. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  31. package/.next/server/app/index.html +2 -2
  32. package/.next/server/app/index.rsc +3 -3
  33. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  34. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  35. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  36. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  37. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  38. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  39. package/.next/server/app/login.html +2 -2
  40. package/.next/server/app/login.rsc +3 -3
  41. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  42. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  43. package/.next/server/app/login.segments/_index.segment.rsc +2 -2
  44. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  46. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  47. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/logs.html +2 -2
  49. package/.next/server/app/logs.rsc +2 -2
  50. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  51. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  52. package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
  53. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  55. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  56. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/messages.html +2 -2
  58. package/.next/server/app/messages.rsc +2 -2
  59. package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
  60. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  61. package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
  62. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  64. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  65. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/node.html +2 -2
  67. package/.next/server/app/node.rsc +2 -2
  68. package/.next/server/app/node.segments/_full.segment.rsc +2 -2
  69. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  70. package/.next/server/app/node.segments/_index.segment.rsc +2 -2
  71. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  73. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  74. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/nodes.html +2 -2
  76. package/.next/server/app/nodes.rsc +2 -2
  77. package/.next/server/app/nodes.segments/_full.segment.rsc +2 -2
  78. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  79. package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
  80. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
  82. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  83. package/.next/server/app/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs.html +2 -2
  86. package/.next/server/app/server-logs.rsc +2 -2
  87. package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
  88. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  89. package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
  90. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  91. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
  92. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  93. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/settings/networks.html +2 -2
  95. package/.next/server/app/settings/networks.rsc +2 -2
  96. package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
  97. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  98. package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
  99. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  100. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
  101. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  102. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  103. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/settings/tokens.html +2 -2
  106. package/.next/server/app/settings/tokens.rsc +2 -2
  107. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
  108. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  109. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
  110. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  111. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
  112. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  113. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  114. package/.next/server/app/settings.html +2 -2
  115. package/.next/server/app/settings.rsc +3 -3
  116. package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  117. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  118. package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  119. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  120. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  121. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  122. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/tasks.html +2 -2
  125. package/.next/server/app/tasks.rsc +2 -2
  126. package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
  127. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  128. package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
  129. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  130. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  131. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  132. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  133. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  134. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  135. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  136. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  138. package/.next/server/middleware-build-manifest.js +3 -3
  139. package/.next/server/pages/404.html +2 -2
  140. package/.next/server/pages/500.html +1 -1
  141. package/.next/server/server-reference-manifest.js +1 -1
  142. package/.next/server/server-reference-manifest.json +1 -1
  143. package/.next/static/chunks/{0h3od5x-eekc_.js → 01210q-e964v1.js} +1 -1
  144. package/.next/static/chunks/{0agj1oq3ux9.a.css → 0sawyu8-~t._z.css} +1 -1
  145. package/.next/static/chunks/{07r1dbd70cp.g.js → 0sidpnaa1gqb3.js} +1 -1
  146. package/.next/static/chunks/0y640y4-_rlak.js +4 -0
  147. package/.next/trace +2 -2
  148. package/.next/trace-build +1 -1
  149. package/app/components/TopoGraph.tsx +52 -4
  150. package/app/globals.css +18 -0
  151. package/package.json +1 -1
  152. package/scripts/topo-chrome-zoom-pop-test.mjs +130 -0
  153. package/scripts/topo-edge-badge-ripple-test.mjs +111 -0
  154. package/.next/static/chunks/0k_ko8c_wcaa2.js +0 -4
  155. /package/.next/static/{73xpyKgxUshGvnn998bi4 → k9TZj5y92rMjV8cjsul1j}/_buildManifest.js +0 -0
  156. /package/.next/static/{73xpyKgxUshGvnn998bi4 → k9TZj5y92rMjV8cjsul1j}/_clientMiddlewareManifest.js +0 -0
  157. /package/.next/static/{73xpyKgxUshGvnn998bi4 → k9TZj5y92rMjV8cjsul1j}/_ssgManifest.js +0 -0
@@ -1051,6 +1051,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1051
1051
  setResetSpinning(true);
1052
1052
  setTimeout(() => setResetSpinning(false), 460);
1053
1053
  };
1054
+ // Round 186 / Loop: chrome zoom-in / zoom-out buttons get a brief
1055
+ // icon pop on click — same click-feel idiom R184 added for the
1056
+ // reset spin, but a scale pulse rather than a rotation since +/−
1057
+ // icons rotating wouldn't read semantically. Tracks which button
1058
+ // is currently popping so only that icon picks up the class.
1059
+ // Arms for 240ms (CSS animation 220ms + 20ms buffer) so a quick
1060
+ // re-click can replay cleanly.
1061
+ const [chromePopping, setChromePopping] = useState<'zoom-in' | 'zoom-out' | null>(null);
1062
+ const popChrome = (which: 'zoom-in' | 'zoom-out') => {
1063
+ setChromePopping(which);
1064
+ setTimeout(() => setChromePopping(prev => prev === which ? null : prev), 240);
1065
+ };
1054
1066
  // Issue #100: singleton chat popover. One alias at a time — clicking
1055
1067
  // another node swaps the target and the conversation switches in place.
1056
1068
  const [chatAlias, setChatAlias] = useState<string | null>(null);
@@ -3193,14 +3205,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3193
3205
  // which felt like two surfaces rather than one.
3194
3206
  onMouseEnter={() => setHoveredEdgeKey(link.key)}
3195
3207
  onMouseLeave={() => setHoveredEdgeKey(prev => prev === link.key ? null : prev)}
3208
+ // Round 185 / Loop: edge badge click fires the same
3209
+ // one-shot expanding-ring ripple R14 uses for node
3210
+ // click and R52 uses for hub click — anchored at
3211
+ // the badge midpoint with the edge's own flowEdge
3212
+ // colour. Closes the click-feel idiom across all
3213
+ // three pinnable canvas surfaces (hub / node /
3214
+ // edge badge). Reused setClickRipple state machine
3215
+ // so only one ripple at a time; ts coordinate
3216
+ // guard in the setTimeout cleanup prevents an
3217
+ // older ripple from clobbering a newer one if
3218
+ // the user clicks two badges in quick succession.
3196
3219
  onClick={(e) => {
3197
3220
  e.stopPropagation();
3198
3221
  setPinnedEdgeKey(prev => prev === link.key ? null : link.key);
3222
+ const ts = Date.now();
3223
+ setClickRipple({ ts, x: badgeX, y: badgeY, r0: 10.5, color: pal.flowEdge });
3224
+ setTimeout(() => setClickRipple(prev => prev && prev.ts === ts ? null : prev), 600);
3199
3225
  }}
3200
3226
  onKeyDown={(e) => {
3201
3227
  if (e.key === 'Enter' || e.key === ' ') {
3202
3228
  e.preventDefault();
3203
3229
  setPinnedEdgeKey(prev => prev === link.key ? null : link.key);
3230
+ const ts = Date.now();
3231
+ setClickRipple({ ts, x: badgeX, y: badgeY, r0: 10.5, color: pal.flowEdge });
3232
+ setTimeout(() => setClickRipple(prev => prev && prev.ts === ts ? null : prev), 600);
3204
3233
  }
3205
3234
  }}
3206
3235
  >
@@ -4924,14 +4953,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4924
4953
  style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder }}
4925
4954
  >
4926
4955
  <button
4927
- onClick={() => zoomByDiscrete(1 / 1.2)}
4956
+ onClick={() => { popChrome('zoom-out'); zoomByDiscrete(1 / 1.2); }}
4928
4957
  data-topo-chrome-zoom-out
4958
+ data-topo-chrome-zoom-out-popping={chromePopping === 'zoom-out' ? 'true' : 'false'}
4929
4959
  className="px-2 py-1 hover:bg-white/5 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
4930
4960
  style={{ color: pal.legendText }}
4931
4961
  aria-label="Zoom out"
4932
4962
  title="Zoom out (−)"
4933
4963
  >
4934
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" aria-hidden><path d="M5 12h14" /></svg>
4964
+ {/* R186: icon pop on click. CSS animation runs once;
4965
+ React removes the class after 240ms so a quick
4966
+ re-click can replay. */}
4967
+ <svg
4968
+ width="12" height="12" viewBox="0 0 24 24"
4969
+ fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
4970
+ aria-hidden
4971
+ className={chromePopping === 'zoom-out' ? 'anet-chrome-pop' : undefined}
4972
+ data-topo-chrome-zoom-out-icon
4973
+ ><path d="M5 12h14" /></svg>
4935
4974
  </button>
4936
4975
  <span
4937
4976
  className="px-2 py-1 tabular-nums border-x text-center"
@@ -4942,14 +4981,23 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4942
4981
  {Math.round(view.zoom * 100)}%
4943
4982
  </span>
4944
4983
  <button
4945
- onClick={() => zoomByDiscrete(1.2)}
4984
+ onClick={() => { popChrome('zoom-in'); zoomByDiscrete(1.2); }}
4946
4985
  data-topo-chrome-zoom-in
4986
+ data-topo-chrome-zoom-in-popping={chromePopping === 'zoom-in' ? 'true' : 'false'}
4947
4987
  className="px-2 py-1 hover:bg-white/5 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
4948
4988
  style={{ color: pal.legendText }}
4949
4989
  aria-label="Zoom in"
4950
4990
  title="Zoom in (+)"
4951
4991
  >
4952
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" aria-hidden><path d="M12 5v14M5 12h14" /></svg>
4992
+ {/* R186: icon pop on click. Same one-shot CSS animation
4993
+ as zoom-out; React removes the class after 240ms. */}
4994
+ <svg
4995
+ width="12" height="12" viewBox="0 0 24 24"
4996
+ fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
4997
+ aria-hidden
4998
+ className={chromePopping === 'zoom-in' ? 'anet-chrome-pop' : undefined}
4999
+ data-topo-chrome-zoom-in-icon
5000
+ ><path d="M12 5v14M5 12h14" /></svg>
4953
5001
  </button>
4954
5002
  </div>
4955
5003
  <button
package/app/globals.css CHANGED
@@ -845,6 +845,24 @@ body {
845
845
  transform-box: fill-box;
846
846
  }
847
847
 
848
+ /* Round 186 — chrome zoom-in / zoom-out buttons get a brief icon pop
849
+ on click. Same click-feel idiom R184 added for the reset button —
850
+ the canvas already smoothes via R168/R169 smoothView, but the
851
+ chrome BUTTON itself needs its own confirmation. A quick scale
852
+ up-then-back (1 → 1.18 → 1 over 220ms) reads as "tapped". Same
853
+ transform-box: fill-box anchoring as R184 keeps the icon
854
+ centered in place. */
855
+ @keyframes anet-chrome-pop {
856
+ 0% { transform: scale(1); }
857
+ 50% { transform: scale(1.18); }
858
+ 100% { transform: scale(1); }
859
+ }
860
+ .anet-chrome-pop {
861
+ animation: anet-chrome-pop 0.22s ease-out;
862
+ transform-origin: center;
863
+ transform-box: fill-box;
864
+ }
865
+
848
866
  /* Round 36 — current-step ring pulse on TaskDrawer timeline. Halo gently
849
867
  breathes around the active step's dot so users can spot "task is here". */
850
868
  @keyframes anet-current-step-pulse-kf {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.4.6-preview.191",
3
+ "version": "0.4.6-preview.193",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,130 @@
1
+ /* Round 186 verification: chrome zoom-in / zoom-out buttons get
2
+ * a one-shot icon "pop" on click — same click-feel idiom R184
3
+ * added for the reset spin, applied to the two zoom buttons.
4
+ *
5
+ * Test:
6
+ * 1. Idle: neither popping
7
+ * 2. Click chrome zoom-in → zoom-in icon gets 'anet-chrome-pop',
8
+ * attr 'true' within 50ms
9
+ * 3. Wait 280ms → class + attr cleared
10
+ * 4. Click chrome zoom-out → zoom-out icon gets the class, attr
11
+ * 'true'; zoom-in stays cleared
12
+ * 5. CSS keyframe 'anet-chrome-pop' exists in stylesheet and
13
+ * includes scale(1.18)
14
+ */
15
+ import { chromium } from 'playwright';
16
+ import { readFileSync } from 'node:fs';
17
+
18
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
19
+ const browser = await chromium.launch({ headless: true });
20
+
21
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
22
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
23
+ await ctx.addInitScript(() => {
24
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
25
+ });
26
+
27
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
28
+ await ctx.route('**/api/hub/status*', async (route) => {
29
+ const r = await route.fetch();
30
+ const b = await r.json();
31
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
32
+ const mk = (alias) => ({
33
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
34
+ network_id: nid, project_dir: null,
35
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
36
+ });
37
+ await route.fulfill({ response: r, json: { ...b, sessions: [mk('alpha'), mk('beta'), mk('gamma')] } });
38
+ });
39
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
40
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
41
+
42
+ const page = await ctx.newPage();
43
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
44
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 3, { timeout: 30000 });
45
+ await page.waitForSelector('[data-topo-chrome-zoom-in]', { timeout: 10000 });
46
+ await page.waitForTimeout(400);
47
+
48
+ const probe = () => page.evaluate(() => {
49
+ const inBtn = document.querySelector('[data-topo-chrome-zoom-in]');
50
+ const outBtn = document.querySelector('[data-topo-chrome-zoom-out]');
51
+ const inIcon = document.querySelector('[data-topo-chrome-zoom-in-icon]');
52
+ const outIcon = document.querySelector('[data-topo-chrome-zoom-out-icon]');
53
+ return {
54
+ inAttr: inBtn?.getAttribute('data-topo-chrome-zoom-in-popping'),
55
+ outAttr: outBtn?.getAttribute('data-topo-chrome-zoom-out-popping'),
56
+ inClass: inIcon?.getAttribute('class') || '',
57
+ outClass: outIcon?.getAttribute('class') || '',
58
+ };
59
+ });
60
+
61
+ const idle = await probe();
62
+
63
+ await page.locator('[data-topo-chrome-zoom-in]').click();
64
+ await page.waitForTimeout(50);
65
+ const duringIn = await probe();
66
+
67
+ await page.waitForTimeout(280);
68
+ const afterIn = await probe();
69
+
70
+ await page.locator('[data-topo-chrome-zoom-out]').click();
71
+ await page.waitForTimeout(50);
72
+ const duringOut = await probe();
73
+
74
+ await page.waitForTimeout(280);
75
+ const afterOut = await probe();
76
+
77
+ const cssCheck = await page.evaluate(() => {
78
+ for (const sheet of document.styleSheets) {
79
+ let rules;
80
+ try { rules = sheet.cssRules; } catch { continue; }
81
+ if (!rules) continue;
82
+ for (const rule of rules) {
83
+ if (rule.type === 7 && rule.name === 'anet-chrome-pop') {
84
+ return { found: true, cssText: rule.cssText };
85
+ }
86
+ }
87
+ }
88
+ return { found: false, cssText: '' };
89
+ });
90
+
91
+ await browser.close();
92
+
93
+ const results = {
94
+ idle_in_attr_false: idle.inAttr === 'false',
95
+ idle_out_attr_false: idle.outAttr === 'false',
96
+ idle_no_in_pop: !idle.inClass.includes('anet-chrome-pop'),
97
+ idle_no_out_pop: !idle.outClass.includes('anet-chrome-pop'),
98
+
99
+ // After zoom-in click: in popping, out static
100
+ in_attr_true: duringIn.inAttr === 'true',
101
+ in_has_pop_class: duringIn.inClass.includes('anet-chrome-pop'),
102
+ out_stays_false: duringIn.outAttr === 'false',
103
+ out_no_pop_class: !duringIn.outClass.includes('anet-chrome-pop'),
104
+
105
+ // After 280ms: in cleared
106
+ after_in_attr_false: afterIn.inAttr === 'false',
107
+ after_in_no_pop: !afterIn.inClass.includes('anet-chrome-pop'),
108
+
109
+ // After zoom-out click: out popping
110
+ out_attr_true: duringOut.outAttr === 'true',
111
+ out_has_pop_class: duringOut.outClass.includes('anet-chrome-pop'),
112
+ in_stays_false: duringOut.inAttr === 'false',
113
+ in_no_pop_after: !duringOut.inClass.includes('anet-chrome-pop'),
114
+
115
+ // After 280ms: out cleared
116
+ after_out_attr_false: afterOut.outAttr === 'false',
117
+ after_out_no_pop: !afterOut.outClass.includes('anet-chrome-pop'),
118
+
119
+ keyframe_found: cssCheck.found,
120
+ keyframe_has_scale_1p18: cssCheck.cssText.includes('scale(1.18)'),
121
+ };
122
+ const ok = Object.values(results).every(Boolean);
123
+ console.log(`${ok ? '✅' : '❌'} chrome zoom pop:`, JSON.stringify(results),
124
+ `\n idle:`, idle,
125
+ `\n duringIn:`, duringIn,
126
+ `\n afterIn:`, afterIn,
127
+ `\n duringOut:`, duringOut,
128
+ `\n afterOut:`, afterOut,
129
+ `\n keyframe found:`, cssCheck.found);
130
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,111 @@
1
+ /* Round 185 verification: edge badge click fires a one-shot
2
+ * ripple at the badge midpoint — closes the click-feel idiom
3
+ * across hub (R52) / node (R14) / edge badge (R185).
4
+ *
5
+ * Test:
6
+ * 1. Mock 4 sessions + 4-msg alpha→beta flow (R100 threshold)
7
+ * 2. Idle: no ripple <circle> visible
8
+ * 3. Click edge midpoint badge → ripple circle appears with
9
+ * stroke=pal.flowEdge (#67e8f9 cyber), positioned at
10
+ * badge coords (close to alpha-beta midpoint with R164
11
+ * perpendicular lift offset)
12
+ * 4. Wait 700ms → ripple cleared (R14 600ms + buffer)
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 browser = await chromium.launch({ headless: true });
19
+
20
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
21
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
22
+ await ctx.addInitScript(() => {
23
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
24
+ });
25
+
26
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
27
+ await ctx.route('**/api/hub/status*', async (route) => {
28
+ const r = await route.fetch();
29
+ const b = await r.json();
30
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
31
+ const mk = (alias) => ({
32
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
33
+ network_id: nid, project_dir: null,
34
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
35
+ });
36
+ // 4 sessions so alpha/beta sit 90° apart (avoid hub overlap)
37
+ await route.fulfill({ response: r, json: { ...b, sessions: [
38
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
39
+ ] } });
40
+ });
41
+
42
+ const now = Date.now();
43
+ const msgs = [];
44
+ for (let i = 0; i < 4; i++) {
45
+ msgs.push({ id: `m${i}`, from_alias: 'alpha', to_alias: 'beta', content: 'hi',
46
+ network_id: 'default', created_at: new Date(now - (10000 + i * 500)).toISOString() });
47
+ }
48
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: msgs } }));
49
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
50
+
51
+ const page = await ctx.newPage();
52
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
53
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
54
+ await page.waitForSelector('[data-edge-count-badge]', { timeout: 10000 });
55
+ await page.waitForTimeout(400);
56
+
57
+ // Idle: no click ripple anywhere. The ripple is identified by:
58
+ // <circle stroke="#67e8f9" stroke-width="2" opacity="0"> with an animate child
59
+ // Probe via "circles with stroke-width=2 and a child <animate>".
60
+ const probeRipple = () => page.evaluate(() => {
61
+ const circles = [...document.querySelectorAll('circle[stroke-width="2"]')];
62
+ const ripple = circles.find(c => {
63
+ const anim = c.querySelector('animate[attributeName="r"]');
64
+ return anim && anim.getAttribute('dur') === '0.5s';
65
+ });
66
+ if (!ripple) return null;
67
+ return {
68
+ cx: parseFloat(ripple.getAttribute('cx') || ''),
69
+ cy: parseFloat(ripple.getAttribute('cy') || ''),
70
+ stroke: ripple.getAttribute('stroke'),
71
+ };
72
+ });
73
+
74
+ const idle = await probeRipple();
75
+
76
+ // Capture badge position first
77
+ const badgeBox = await page.locator('[data-edge-count-badge]').first().boundingBox();
78
+
79
+ // Click the edge midpoint badge → ripple should appear
80
+ await page.locator('[data-edge-count-badge]').click();
81
+ await page.waitForTimeout(50);
82
+ const duringRipple = await probeRipple();
83
+
84
+ // Wait past 600ms ripple lifetime + buffer
85
+ await page.waitForTimeout(700);
86
+ const afterRipple = await probeRipple();
87
+
88
+ await browser.close();
89
+
90
+ const results = {
91
+ idle_no_ripple: idle === null,
92
+ ripple_appears: duringRipple !== null,
93
+ ripple_stroke_cyan: duringRipple?.stroke === '#67e8f9',
94
+ ripple_has_position: duringRipple !== null
95
+ && Number.isFinite(duringRipple.cx)
96
+ && Number.isFinite(duringRipple.cy),
97
+ // The badge has bbox in screen coords; the ripple is in SVG viewBox
98
+ // coords. We can't compare directly. Just verify it's a sane number
99
+ // (not 0/NaN) — that's enough to know it's placed at the badge.
100
+ ripple_cx_nonzero: duringRipple !== null && duringRipple.cx > 0,
101
+ ripple_cy_nonzero: duringRipple !== null && duringRipple.cy > 0,
102
+ ripple_cleared_after_600: afterRipple === null,
103
+ badge_was_clickable: badgeBox !== null,
104
+ };
105
+ const ok = Object.values(results).every(Boolean);
106
+ console.log(`${ok ? '✅' : '❌'} edge badge ripple:`, JSON.stringify(results),
107
+ `\n idle:`, idle,
108
+ `\n duringRipple:`, duringRipple,
109
+ `\n afterRipple:`, afterRipple,
110
+ `\n badgeBox:`, badgeBox);
111
+ process.exit(ok ? 0 : 1);