@sleep2agi/agent-network-dashboard 0.4.6-preview.166 → 0.4.6-preview.168

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 (156) 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/{0hzlz5.ddwdnr.js → 07elgbvrf.m7p.js} +1 -1
  144. package/.next/static/chunks/{0m7e_hszjk30d.css → 0dpycjqkj0wpy.css} +1 -1
  145. package/.next/static/chunks/0ewq-gm-h4qfe.js +4 -0
  146. package/.next/static/chunks/{00sunkev6fj30.js → 0z38w78k3jath.js} +1 -1
  147. package/.next/trace +2 -2
  148. package/.next/trace-build +1 -1
  149. package/app/components/TopoGraph.tsx +94 -1
  150. package/package.json +1 -1
  151. package/scripts/topo-active-links-freshness-test.mjs +113 -0
  152. package/scripts/topo-recent-row-freshness-test.mjs +102 -0
  153. package/.next/static/chunks/13yvfj.yxe4h_.js +0 -4
  154. /package/.next/static/{bJoaN5J5IZGHQdMKVQy5q → DQ6ZeKdg2w37u1tb9OORl}/_buildManifest.js +0 -0
  155. /package/.next/static/{bJoaN5J5IZGHQdMKVQy5q → DQ6ZeKdg2w37u1tb9OORl}/_clientMiddlewareManifest.js +0 -0
  156. /package/.next/static/{bJoaN5J5IZGHQdMKVQy5q → DQ6ZeKdg2w37u1tb9OORl}/_ssgManifest.js +0 -0
@@ -1980,7 +1980,51 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1980
1980
  }}
1981
1981
  >
1982
1982
  {flowLinks.length} active link{flowLinks.length === 1 ? '' : 's'}
1983
- {rel ? <span className="text-gray-500"> · last {rel}</span> : null}
1983
+ {rel ? (() => {
1984
+ // Round 161 / Loop: extend R160's recency-pip
1985
+ // vocabulary up one scope — from per-flow row to
1986
+ // fleet-aggregate chip. The chip already shows
1987
+ // recency in text ("last 5s ago"); the " · "
1988
+ // separator bullet was dead gray. Color the
1989
+ // bullet by freshness using the same alpha
1990
+ // ramp R160 uses on recent-signal rows:
1991
+ // ageSec ≤ 30 → 1.0 (fully fresh)
1992
+ // 30-300s → smooth decay 1.0 → 0.25
1993
+ // > 300s → 0.25 (stale floor)
1994
+ // Cyan bullet pulse when fresh + gray text tail
1995
+ // = "is the network firing right now" readable
1996
+ // at a glance, without parsing the timestamp
1997
+ // numerals. Same vocabulary the canvas uses
1998
+ // (R10 edge fade) and the recent-signal panel
1999
+ // uses (R160 row pip) — three nested scopes
2000
+ // now share one freshness ladder.
2001
+ const ageSec = recent !== null
2002
+ ? Math.max(0, (Date.now() - recent) / 1000)
2003
+ : 999;
2004
+ const alpha = ageSec <= 30
2005
+ ? 1
2006
+ : ageSec <= 300
2007
+ ? 1 - ((ageSec - 30) / 270) * 0.75
2008
+ : 0.25;
2009
+ // Cyan dark / teal light to match palette legendAccent.
2010
+ const dotColor = isLight
2011
+ ? `rgba(13, 148, 136, ${alpha.toFixed(2)})`
2012
+ : `rgba(34, 211, 238, ${alpha.toFixed(2)})`;
2013
+ return (
2014
+ <span className="text-gray-500">
2015
+ <span
2016
+ data-active-links-freshness-dot
2017
+ data-active-links-freshness-alpha={alpha.toFixed(2)}
2018
+ style={{
2019
+ color: dotColor,
2020
+ fontWeight: alpha > 0.7 ? 700 : 400,
2021
+ transition: 'color 200ms ease-out',
2022
+ }}
2023
+ >{' · '}</span>
2024
+ last {rel}
2025
+ </span>
2026
+ );
2027
+ })() : null}
1984
2028
  </span>
1985
2029
  );
1986
2030
  })()}
@@ -3934,6 +3978,55 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3934
3978
  : 1}
3935
3979
  style={{ transition: 'fill 150ms ease-out, opacity 150ms ease-out' }}
3936
3980
  />
3981
+ {/* Round 160 / Loop: recency pip. Canvas flow edges
3982
+ fade by freshness (R10: full intensity ≤30s →
3983
+ ~35% over 5min). The recent-signal panel rows
3984
+ duplicate that data (alias→alias · N · 5s)
3985
+ but encode freshness purely in text — no
3986
+ at-a-glance visual cue for "which row is
3987
+ actively firing right now". A 1.6-px cyan dot
3988
+ at x=10 (in the 7-px margin between rect-
3989
+ start x=6 and text-start x=13) brightens
3990
+ fresh rows and dims stale ones — same
3991
+ vocabulary the canvas uses, brought to the
3992
+ panel side.
3993
+
3994
+ Three encodings now coexist on each row,
3995
+ none competing:
3996
+ rect fill = hover/pin state (R104/R116)
3997
+ count tspan = magnitude (R127 amber when ≥10)
3998
+ pip = recency (this round)
3999
+
4000
+ Geometry: cy = row_y - 3 (mid-row vertical
4001
+ centre, where text baseline at y=row_y sits
4002
+ slightly below). r=1.6 fits cleanly in the
4003
+ 7-px left margin. pointerEvents:none so the
4004
+ row's button-role hit area is unchanged.
4005
+ No overlap-test impact (entirely within the
4006
+ existing rect bbox). */}
4007
+ {(() => {
4008
+ if (!link.last_at) return null;
4009
+ const ageSec = Math.max(0, (Date.now() - Date.parse(link.last_at)) / 1000);
4010
+ // 0-30s: fully fresh (1.0). 30-300s: smooth
4011
+ // decay 1→0.25. >300s: stale floor (0.25).
4012
+ const alpha = ageSec <= 30
4013
+ ? 1
4014
+ : ageSec <= 300
4015
+ ? 1 - ((ageSec - 30) / 270) * 0.75
4016
+ : 0.25;
4017
+ return (
4018
+ <circle
4019
+ cx={10}
4020
+ cy={38 + index * 16 - 3}
4021
+ r={1.6}
4022
+ fill={pal.legendAccent}
4023
+ opacity={alpha}
4024
+ data-recent-row-freshness={link.key}
4025
+ data-recent-row-freshness-alpha={alpha.toFixed(2)}
4026
+ style={{ pointerEvents: 'none', transition: 'opacity 200ms ease-out' }}
4027
+ />
4028
+ );
4029
+ })()}
3937
4030
  <text
3938
4031
  x="13" y={38 + index * 16}
3939
4032
  fill={isRowActive ? pal.legendHeadline : pal.legendText}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.4.6-preview.166",
3
+ "version": "0.4.6-preview.168",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,113 @@
1
+ /* Round 161 verification: active-links chip bullet picks up
2
+ * the R160 freshness vocabulary.
3
+ *
4
+ * R10 fades canvas flow edges by freshness. R160 brought that
5
+ * vocabulary to the recent-signal panel rows via a 1.6-px cyan
6
+ * pip at row left edge. R161 extends it one more scope — up to
7
+ * the fleet-aggregate active-links chip in the chip-row.
8
+ *
9
+ * The chip text was already "3 active links · last 5s ago" but
10
+ * the " · " separator was dead gray. R161 colors that bullet
11
+ * by freshness using the same alpha ramp:
12
+ * ageSec ≤ 30 → 1.0 (fully fresh)
13
+ * 30-300s → 1.0 → 0.25 smooth decay
14
+ * > 300s → 0.25 stale floor
15
+ *
16
+ * Three nested scopes now share one freshness ladder:
17
+ * canvas edge fade (R10) — per-edge visual
18
+ * row pip (R160) — per-flow row in recent-signal
19
+ * chip bullet (R161) — fleet aggregate in chip-row
20
+ *
21
+ * Test scenarios (one page load each):
22
+ * A. fresh most-recent flow (5s ago) → alpha ≈ 1.0, fontWeight 700
23
+ * B. mid recency (90s ago) → alpha ≈ 0.83
24
+ * C. stale (360s ago) → alpha ≈ 0.25 (floor)
25
+ *
26
+ * Color parses out of `rgba(34, 211, 238, 1)` cyber theme.
27
+ */
28
+ import { chromium } from 'playwright';
29
+ import { readFileSync } from 'node:fs';
30
+
31
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
32
+ const browser = await chromium.launch({ headless: true });
33
+
34
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
35
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
36
+ await ctx.addInitScript(() => {
37
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
38
+ });
39
+
40
+ async function probe(msgAgeSec) {
41
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
42
+ await ctx.unroute('**/api/hub/status*').catch(() => {});
43
+ await ctx.unroute('**/api/hub/messages*').catch(() => {});
44
+ await ctx.unroute('**/api/hub/tasks*').catch(() => {});
45
+ await ctx.route('**/api/hub/status*', async (route) => {
46
+ const r = await route.fetch();
47
+ const b = await r.json();
48
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
49
+ const mk = (alias) => ({
50
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
51
+ network_id: nid, project_dir: null,
52
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
53
+ });
54
+ await route.fulfill({ response: r, json: { ...b, sessions: [mk('alpha'), mk('beta')] } });
55
+ });
56
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [
57
+ { id: 'x', from_alias: 'alpha', to_alias: 'beta', content: 'hi',
58
+ network_id: 'default', created_at: new Date(Date.now() - msgAgeSec * 1000).toISOString() },
59
+ ] } }));
60
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
61
+
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 === 2, { timeout: 30000 });
65
+ await page.waitForSelector('[data-active-links-freshness-dot]', { timeout: 10000 });
66
+ await page.waitForTimeout(400);
67
+ const out = await page.evaluate(() => {
68
+ const el = document.querySelector('[data-active-links-freshness-dot]');
69
+ if (!el) return null;
70
+ const styleColor = el.style.color || getComputedStyle(el).color;
71
+ return {
72
+ alpha: parseFloat(el.getAttribute('data-active-links-freshness-alpha') || ''),
73
+ color: styleColor,
74
+ fontWeight: el.style.fontWeight || getComputedStyle(el).fontWeight,
75
+ text: el.textContent,
76
+ };
77
+ });
78
+ await page.close();
79
+ return out;
80
+ }
81
+
82
+ const A = await probe(5); // fresh
83
+ const B = await probe(90); // mid
84
+ const C = await probe(360); // stale
85
+
86
+ await browser.close();
87
+
88
+ const cyanRgb = (str) => {
89
+ // Inline `style="color: rgba(34, 211, 238, ...)"` — alpha is part of color.
90
+ const m = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(str);
91
+ if (!m) return null;
92
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
93
+ };
94
+
95
+ const results = {
96
+ fresh_dot_found: A !== null,
97
+ mid_dot_found: B !== null,
98
+ stale_dot_found: C !== null,
99
+ fresh_alpha_1: A && Math.abs(A.alpha - 1.0) < 0.05,
100
+ mid_alpha_window: B && B.alpha > 0.7 && B.alpha < 0.95,
101
+ stale_alpha_floor: C && Math.abs(C.alpha - 0.25) < 0.05,
102
+ fresh_color_cyan: A && JSON.stringify(cyanRgb(A.color)) === '[34,211,238]',
103
+ fresh_weight_bold: A && (A.fontWeight === '700' || A.fontWeight === 'bold'),
104
+ stale_weight_norm: C && (C.fontWeight === '400' || C.fontWeight === 'normal' || C.fontWeight === ''),
105
+ ladder_descends: A && B && C && A.alpha > B.alpha && B.alpha > C.alpha,
106
+ text_is_separator: A && A.text && A.text.trim() === '·',
107
+ };
108
+ const ok = Object.values(results).every(Boolean);
109
+ console.log(`${ok ? '✅' : '❌'} active-links freshness:`, JSON.stringify(results),
110
+ `\n fresh =`, A,
111
+ `\n mid =`, B,
112
+ `\n stale =`, C);
113
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,102 @@
1
+ /* Round 160 verification: recency pip on recent-signal panel rows.
2
+ *
3
+ * Canvas flow edges fade by freshness (R10: full intensity ≤30s,
4
+ * decaying to ~35% over 5min). The recent-signal panel rows
5
+ * encoded last_at purely in text ("5s" at row right edge) — no
6
+ * at-a-glance visual cue for "which row is actively firing".
7
+ *
8
+ * R160 adds a 1.6-px cyan dot at x=10 (in the 7-px margin
9
+ * between rect-start x=6 and text-start x=13). Alpha ramp:
10
+ * ageSec ≤ 30 → 1.0 (fully fresh)
11
+ * 30 < ageSec ≤ 300 → smooth decay 1.0 → 0.25
12
+ * ageSec > 300 → 0.25 (stale floor)
13
+ *
14
+ * Three independent encodings on each row, none competing:
15
+ * rect fill = hover/pin state (R104/R116)
16
+ * count tspan = magnitude (R127 amber when ≥10)
17
+ * pip = recency (this round)
18
+ *
19
+ * Test:
20
+ * Mock 3 flows — 1 fresh (5s ago), 1 mid (90s ago), 1 stale
21
+ * (6 min ago). Each should produce one pip; alpha ladder:
22
+ * fresh > mid > stale.
23
+ */
24
+ import { chromium } from 'playwright';
25
+ import { readFileSync } from 'node:fs';
26
+
27
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
28
+ const browser = await chromium.launch({ headless: true });
29
+
30
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1500 } });
31
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
32
+ await ctx.addInitScript(() => {
33
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
34
+ });
35
+
36
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
37
+ await ctx.route('**/api/hub/status*', async (route) => {
38
+ const r = await route.fetch();
39
+ const b = await r.json();
40
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
41
+ const mk = (alias) => ({
42
+ alias, status: 'working', model: 'claude-opus-4', runtime: 'cli-claude-code',
43
+ network_id: nid, project_dir: null,
44
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
45
+ });
46
+ await route.fulfill({ response: r, json: { ...b, sessions: [
47
+ mk('alpha'), mk('beta'), mk('gamma'), mk('delta'),
48
+ ] } });
49
+ });
50
+
51
+ const now = Date.now();
52
+ // Three distinct flows with different recency:
53
+ // fresh: 5s ago → alpha ≈ 1.0
54
+ // mid: 90s ago → alpha ≈ 1 - (60/270)*0.75 ≈ 0.833
55
+ // stale: 360s ago → alpha ≈ 0.25 (floor)
56
+ const msgs = [
57
+ { id: 'f1', from_alias: 'alpha', to_alias: 'beta', content: 'hi', network_id: 'default', created_at: new Date(now - 5000).toISOString() },
58
+ { id: 'm1', from_alias: 'beta', to_alias: 'gamma', content: 'hi', network_id: 'default', created_at: new Date(now - 90000).toISOString() },
59
+ { id: 's1', from_alias: 'gamma', to_alias: 'delta', content: 'hi', network_id: 'default', created_at: new Date(now - 360000).toISOString() },
60
+ ];
61
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: msgs } }));
62
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
63
+
64
+ const page = await ctx.newPage();
65
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
66
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
67
+ await page.waitForSelector('[data-recent-row-freshness]', { timeout: 10000 });
68
+ await page.waitForTimeout(400);
69
+
70
+ const probes = await page.evaluate(() => {
71
+ const pips = [...document.querySelectorAll('[data-recent-row-freshness]')];
72
+ return pips.map(el => ({
73
+ key: el.getAttribute('data-recent-row-freshness'),
74
+ alpha: parseFloat(el.getAttribute('data-recent-row-freshness-alpha') || ''),
75
+ cx: parseFloat(el.getAttribute('cx') || ''),
76
+ r: parseFloat(el.getAttribute('r') || ''),
77
+ tag: el.tagName.toLowerCase(),
78
+ }));
79
+ });
80
+
81
+ await browser.close();
82
+
83
+ if (probes.length !== 3) {
84
+ console.log(`❌ wrong pip count: expected 3, got ${probes.length}`, probes);
85
+ process.exit(1);
86
+ }
87
+ // Rows are sorted by recency (newest first), so probes[0] is freshest.
88
+ const [pFresh, pMid, pStale] = probes;
89
+ const results = {
90
+ three_pips: probes.length === 3,
91
+ all_circles: probes.every(p => p.tag === 'circle'),
92
+ all_at_x_10: probes.every(p => Math.abs(p.cx - 10) < 0.01),
93
+ all_radius_1p6: probes.every(p => Math.abs(p.r - 1.6) < 0.01),
94
+ fresh_alpha_1: Math.abs(pFresh.alpha - 1.0) < 0.05,
95
+ mid_alpha_0p7_to_0p9: pMid.alpha > 0.7 && pMid.alpha < 0.95,
96
+ stale_alpha_floor_0p25: Math.abs(pStale.alpha - 0.25) < 0.05,
97
+ ladder_descends: pFresh.alpha > pMid.alpha && pMid.alpha > pStale.alpha,
98
+ };
99
+ const ok = Object.values(results).every(Boolean);
100
+ console.log(`${ok ? '✅' : '❌'} recent-row freshness pip:`, JSON.stringify(results),
101
+ `\n probes=`, probes);
102
+ process.exit(ok ? 0 : 1);