@sleep2agi/agent-network-dashboard 0.5.3-preview.9 → 0.5.3
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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +32 -32
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +12 -12
- package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +2 -2
- package/.next/server/app/admin.rsc +14 -14
- package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
- package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
- package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +14 -14
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +7 -7
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +2 -2
- package/.next/server/app/login.rsc +14 -14
- package/.next/server/app/login.segments/_full.segment.rsc +14 -14
- package/.next/server/app/login.segments/_head.segment.rsc +4 -4
- package/.next/server/app/login.segments/_index.segment.rsc +7 -7
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/login.segments/login.segment.rsc +3 -3
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +2 -2
- package/.next/server/app/logs.rsc +14 -14
- package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
- package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +2 -2
- package/.next/server/app/messages.rsc +14 -14
- package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
- package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
- package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +2 -2
- package/.next/server/app/node.rsc +14 -14
- package/.next/server/app/node.segments/_full.segment.rsc +14 -14
- package/.next/server/app/node.segments/_head.segment.rsc +4 -4
- package/.next/server/app/node.segments/_index.segment.rsc +7 -7
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/node.segments/node.segment.rsc +3 -3
- package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +2 -2
- package/.next/server/app/nodes.rsc +14 -14
- package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
- package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +2 -2
- package/.next/server/app/server-logs.rsc +14 -14
- package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
- package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +2 -2
- package/.next/server/app/settings/networks.rsc +14 -14
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +2 -2
- package/.next/server/app/settings/tokens.rsc +14 -14
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +14 -14
- package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks.html +2 -2
- package/.next/server/app/tasks.rsc +14 -14
- package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
- package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
- package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/0fuba20p57-zo.js +1 -0
- package/.next/static/chunks/{0hndl9yzpqajt.css → 0m.1mvl~t.avc.css} +1 -1
- package/.next/static/chunks/{00d~vl~k~7f75.js → 0ph1in3421~o-.js} +1 -1
- package/.next/static/chunks/{03a4--7ncekmk.js → 0v4-5tng.uh.7.js} +2 -2
- package/.next/static/chunks/10cj-86disers.js +1 -0
- package/.next/static/chunks/158h2g7f-nxbt.js +4 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/ServersDrawer.tsx +16 -3
- package/app/components/TopoGraph.tsx +442 -21
- package/app/globals.css +23 -0
- package/package.json +4 -4
- package/scripts/p157-servers-copy-test.mjs +95 -0
- package/scripts/topo-alias-glow-test.mjs +121 -0
- package/scripts/topo-avatar-brightness-test.mjs +116 -0
- package/scripts/topo-fleet-density-tier-test.mjs +84 -0
- package/scripts/topo-freshness-chip-fade-test.mjs +105 -0
- package/scripts/topo-hub-highlight-amplify-test.mjs +139 -0
- package/scripts/topo-hub-highlight-fill-transition-test.mjs +84 -0
- package/scripts/topo-hub-highlight-recede-test.mjs +144 -0
- package/scripts/topo-hub-highlight-theme-fill-test.mjs +83 -0
- package/scripts/topo-hub-idle-breath-test.mjs +109 -0
- package/scripts/topo-hub-recede-test.mjs +124 -0
- package/scripts/topo-orphan-box-dash-test.mjs +89 -0
- package/scripts/topo-orphan-fill-opacity-test.mjs +91 -0
- package/scripts/topo-orphan-label-italic-test.mjs +90 -0
- package/scripts/topo-pinned-aspect-test.mjs +89 -0
- package/scripts/topo-recent-hot-pulse-test.mjs +102 -0
- package/.next/static/chunks/0jn4lbsj97vfl.js +0 -1
- package/.next/static/chunks/0rz3-6.xs78~d.js +0 -1
- package/.next/static/chunks/0~~xbw42y4m3v.js +0 -4
- /package/.next/static/{nbpD7Lhttq59fvvIw9gTX → tQ1EtcEiS7ikgAoAyZWm7}/_buildManifest.js +0 -0
- /package/.next/static/{nbpD7Lhttq59fvvIw9gTX → tQ1EtcEiS7ikgAoAyZWm7}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{nbpD7Lhttq59fvvIw9gTX → tQ1EtcEiS7ikgAoAyZWm7}/_ssgManifest.js +0 -0
package/app/globals.css
CHANGED
|
@@ -863,6 +863,29 @@ body {
|
|
|
863
863
|
transform-box: fill-box;
|
|
864
864
|
}
|
|
865
865
|
|
|
866
|
+
/* Round 498 — recent-signal row hot-count subtle pulse (信息密度 +
|
|
867
|
+
呼吸感 themes). When a row's edge count crosses the hot threshold
|
|
868
|
+
(≥ 10), the existing R127/R320/R445 amber-fill + fw-700 typography
|
|
869
|
+
already calls attention; R498 adds a slow 3s opacity breath (0.85 ↔
|
|
870
|
+
1.0) on the digit so the hot tspans gently pulse — at-a-glance scan
|
|
871
|
+
of the panel reads "high-traffic lane right here" with motion in
|
|
872
|
+
addition to color+weight. 3s cycle is deliberate: faster (1-2s)
|
|
873
|
+
would compete with the working-status halo cadence; slower (5s+)
|
|
874
|
+
would lose the "alive" signal. Amplitude 15% (matches R497 hub-
|
|
875
|
+
highlight breath idiom).
|
|
876
|
+
prefers-reduced-motion handled by the R29 blanket override
|
|
877
|
+
(animation-duration: 0.001ms !important) — no per-class guard
|
|
878
|
+
needed. The component-side gate (`!reducedMotion && isHot`)
|
|
879
|
+
ensures the className is only applied when both conditions hold,
|
|
880
|
+
so even without the blanket the no-motion preference is respected. */
|
|
881
|
+
@keyframes anet-recent-hot-pulse-kf {
|
|
882
|
+
0%, 100% { opacity: 0.85; }
|
|
883
|
+
50% { opacity: 1; }
|
|
884
|
+
}
|
|
885
|
+
.anet-recent-hot-pulse {
|
|
886
|
+
animation: anet-recent-hot-pulse-kf 3s ease-in-out infinite;
|
|
887
|
+
}
|
|
888
|
+
|
|
866
889
|
/* Round 36 — current-step ring pulse on TaskDrawer timeline. Halo gently
|
|
867
890
|
breathes around the active step's dot so users can spot "task is here". */
|
|
868
891
|
@keyframes anet-current-step-pulse-kf {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/agent-network-dashboard",
|
|
3
|
-
"version": "0.5.3
|
|
4
|
-
"description": "Agent Network Dashboard
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "Agent Network Dashboard \u2014 Web UI for managing AI Agent networks",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "next dev",
|
|
7
7
|
"build": "next build",
|
|
8
8
|
"start": "next start",
|
|
9
9
|
"lint": "eslint",
|
|
10
|
-
"prepublishOnly": "[ -f .next/BUILD_ID ] || (echo 'prepublishOnly: .next/BUILD_ID missing
|
|
10
|
+
"prepublishOnly": "[ -f .next/BUILD_ID ] || (echo 'prepublishOnly: .next/BUILD_ID missing \u2014 run npm run build first (see commit 05c1ebf body for R224 chunk-500 root cause)' >&2 && exit 1)"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
13
|
"agent-network-dashboard": "./bin/start.js"
|
|
@@ -44,4 +44,4 @@
|
|
|
44
44
|
"tailwindcss": "^4",
|
|
45
45
|
"typescript": "^5"
|
|
46
46
|
}
|
|
47
|
-
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* #157 fix verification (v0.10.8 lean ship — Fix #1 only per 通信龙 5573).
|
|
2
|
+
*
|
|
3
|
+
* Pre-fix the Servers panel showed:
|
|
4
|
+
* "agent rollup pending hub ≥ 0.8.2-preview"
|
|
5
|
+
* "disk metric pending hub ≥ 0.8.2-preview"
|
|
6
|
+
* commhub-server@0.8.2 is LIVE on prod but still doesn't ship `agents[]`
|
|
7
|
+
* or `disk_*` — the version-pinned text was misleading.
|
|
8
|
+
*
|
|
9
|
+
* Post-fix:
|
|
10
|
+
* "agent rollup not reported by hub"
|
|
11
|
+
* "disk metric not reported by hub"
|
|
12
|
+
* + data-server-agents-missing + data-server-disk-missing test attrs.
|
|
13
|
+
*
|
|
14
|
+
* Test:
|
|
15
|
+
* 1. Open dashboard, expand Servers drawer (localStorage flag)
|
|
16
|
+
* 2. Expand first server card (localStorage flag)
|
|
17
|
+
* 3. Assert visible copy lacks "0.8.2-preview"
|
|
18
|
+
* 4. Assert test-surface attrs present
|
|
19
|
+
* 5. Source-side regex confirms new copy + attrs wired
|
|
20
|
+
*/
|
|
21
|
+
import { chromium } from 'playwright';
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
|
|
24
|
+
const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
|
|
25
|
+
|
|
26
|
+
const browser = await chromium.launch({ headless: true });
|
|
27
|
+
const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
|
|
28
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
29
|
+
await ctx.addInitScript(() => {
|
|
30
|
+
try {
|
|
31
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
32
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
33
|
+
// Force servers drawer open
|
|
34
|
+
localStorage.setItem('anet-servers-drawer', '1');
|
|
35
|
+
} catch {}
|
|
36
|
+
});
|
|
37
|
+
const page = await ctx.newPage();
|
|
38
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
|
|
39
|
+
await page.waitForTimeout(3500); // wait for SWR fetch + servers to render
|
|
40
|
+
|
|
41
|
+
// Expand first server card by clicking it
|
|
42
|
+
const firstCard = await page.$('[data-server-host], [data-server-card], button:has-text("iZ")');
|
|
43
|
+
// Simpler: just probe the drawer body text after expansion attempt
|
|
44
|
+
await page.evaluate(() => {
|
|
45
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
46
|
+
// Find server card expand toggles by their host text
|
|
47
|
+
const candidates = buttons.filter(b => /iZ|elaine/.test(b.textContent || ''));
|
|
48
|
+
if (candidates[0]) candidates[0].click();
|
|
49
|
+
});
|
|
50
|
+
await page.waitForTimeout(800);
|
|
51
|
+
|
|
52
|
+
const probe = await page.evaluate(() => {
|
|
53
|
+
// Probe drawer body for the placeholder text
|
|
54
|
+
const drawer = document.querySelector('[data-servers-body]') || document.body;
|
|
55
|
+
const text = drawer.textContent || '';
|
|
56
|
+
const agentsMissing = document.querySelectorAll('[data-server-agents-missing="true"]');
|
|
57
|
+
const diskMissing = document.querySelectorAll('[data-server-disk-missing="true"]');
|
|
58
|
+
return {
|
|
59
|
+
body_text_excerpt: text.slice(0, 1500),
|
|
60
|
+
has_stale_copy: /0\.8\.2-preview/.test(text),
|
|
61
|
+
has_new_agents_copy: /agent rollup not reported by hub/.test(text),
|
|
62
|
+
has_new_disk_copy: /disk metric not reported by hub/.test(text),
|
|
63
|
+
agents_missing_count: agentsMissing.length,
|
|
64
|
+
disk_missing_count: diskMissing.length,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await browser.close();
|
|
69
|
+
|
|
70
|
+
const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/ServersDrawer.tsx', 'utf8');
|
|
71
|
+
// The OLD strings should be gone from rendered text. The comment block keeps the
|
|
72
|
+
// historical "0.8.2-preview" reference for audit — that's OK because comments
|
|
73
|
+
// don't render. So source check: look for the NEW visible strings AND the test
|
|
74
|
+
// attrs, not for the absence of "0.8.2-preview".
|
|
75
|
+
const sourceNewAgentsCopy = /agent rollup not reported by hub/.test(src);
|
|
76
|
+
const sourceNewDiskCopy = /disk metric not reported by hub/.test(src);
|
|
77
|
+
const sourceAgentsAttr = /data-server-agents-missing="true"/.test(src);
|
|
78
|
+
const sourceDiskAttr = /data-server-disk-missing="true"/.test(src);
|
|
79
|
+
|
|
80
|
+
const results = {
|
|
81
|
+
dom_no_stale_copy: !probe.has_stale_copy,
|
|
82
|
+
dom_new_agents_copy: probe.has_new_agents_copy,
|
|
83
|
+
dom_new_disk_copy: probe.has_new_disk_copy,
|
|
84
|
+
agents_attr_present: probe.agents_missing_count > 0,
|
|
85
|
+
disk_attr_present: probe.disk_missing_count > 0,
|
|
86
|
+
source_new_agents: sourceNewAgentsCopy,
|
|
87
|
+
source_new_disk: sourceNewDiskCopy,
|
|
88
|
+
source_agents_attr: sourceAgentsAttr,
|
|
89
|
+
source_disk_attr: sourceDiskAttr,
|
|
90
|
+
};
|
|
91
|
+
const ok = Object.values(results).every(Boolean);
|
|
92
|
+
console.log(`${ok ? '✅' : '❌'} #157 servers copy fix:`, JSON.stringify(results),
|
|
93
|
+
'\n agents_missing_count:', probe.agents_missing_count, ' disk_missing_count:', probe.disk_missing_count,
|
|
94
|
+
'\n body_excerpt:', probe.body_text_excerpt.replace(/\s+/g, ' ').slice(0, 400));
|
|
95
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* Round 500 verification: node alias text gains a status-coloured
|
|
2
|
+
* drop-shadow on hover (extends R476-R481 drop-shadow visual-polish
|
|
3
|
+
* family to 7th anchor). Pre-R500 hover triggered card-lift + alias
|
|
4
|
+
* letter-spacing; post-R500 the alias glyph itself glows.
|
|
5
|
+
*
|
|
6
|
+
* Test scenarios:
|
|
7
|
+
* 1. Rest state — no hover: data-node-alias-glow='false', no filter
|
|
8
|
+
* 2. Hover state — after synthetic pointerenter dispatch on g[data-node]
|
|
9
|
+
* descendant: glow attr 'true', computed filter contains drop-shadow
|
|
10
|
+
* 3. reducedMotion: glow attr 'false' regardless of hover (a11y)
|
|
11
|
+
* 4. Source-side regex confirms wiring
|
|
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 fresh = new Date(Date.now() - 60 * 1000).toISOString();
|
|
18
|
+
|
|
19
|
+
async function probe({ reducedMotion }) {
|
|
20
|
+
const browser = await chromium.launch({ headless: true });
|
|
21
|
+
const ctx = await browser.newContext({
|
|
22
|
+
viewport: { width: 1500, height: 1200 },
|
|
23
|
+
reducedMotion: reducedMotion ? 'reduce' : 'no-preference',
|
|
24
|
+
});
|
|
25
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
26
|
+
await ctx.addInitScript(() => {
|
|
27
|
+
try {
|
|
28
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
29
|
+
localStorage.setItem('anet-topo-layout', 'ring');
|
|
30
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
31
|
+
} catch {}
|
|
32
|
+
});
|
|
33
|
+
await ctx.route('**/api/hub/status*', async (route) => {
|
|
34
|
+
const r = await route.fetch();
|
|
35
|
+
const b = await r.json();
|
|
36
|
+
const nid = (b.sessions || [])[0]?.network_id || 'default';
|
|
37
|
+
const mk = (alias, status) => ({
|
|
38
|
+
alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
|
|
39
|
+
network_id: nid, project_dir: null,
|
|
40
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh,
|
|
41
|
+
});
|
|
42
|
+
await route.fulfill({ response: r, json: { ...b, sessions: [
|
|
43
|
+
mk('alpha·a1', 'working'),
|
|
44
|
+
mk('alpha·a2', 'idle'),
|
|
45
|
+
] } });
|
|
46
|
+
});
|
|
47
|
+
await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
|
|
48
|
+
await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
|
|
49
|
+
const page = await ctx.newPage();
|
|
50
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
|
|
51
|
+
await page.waitForSelector('g[data-node]', { timeout: 15000 });
|
|
52
|
+
await page.waitForTimeout(1500);
|
|
53
|
+
|
|
54
|
+
// Phase 1: rest state
|
|
55
|
+
const rest = await page.evaluate(() => {
|
|
56
|
+
const text = document.querySelector('[data-node-alias-text]');
|
|
57
|
+
if (!text) return null;
|
|
58
|
+
return {
|
|
59
|
+
glow_attr: text.getAttribute('data-node-alias-glow'),
|
|
60
|
+
computed_filter: window.getComputedStyle(text).filter,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Phase 2: hover state — synthetic pointerenter (R488 banked recipe)
|
|
65
|
+
const aliasAfterHover = await page.evaluate(() => {
|
|
66
|
+
const g = document.querySelector('g[data-node]');
|
|
67
|
+
if (!g) return null;
|
|
68
|
+
const alias = g.getAttribute('data-node');
|
|
69
|
+
const target = g.querySelector('circle, image, rect') || g;
|
|
70
|
+
['pointerenter', 'pointerover', 'mouseenter', 'mouseover'].forEach((t) => {
|
|
71
|
+
target.dispatchEvent(new Event(t, { bubbles: true, cancelable: true }));
|
|
72
|
+
});
|
|
73
|
+
return alias;
|
|
74
|
+
});
|
|
75
|
+
await page.waitForTimeout(400);
|
|
76
|
+
const hover = await page.evaluate(() => {
|
|
77
|
+
const text = document.querySelector('[data-node-alias-text]');
|
|
78
|
+
if (!text) return null;
|
|
79
|
+
return {
|
|
80
|
+
glow_attr: text.getAttribute('data-node-alias-glow'),
|
|
81
|
+
computed_filter: window.getComputedStyle(text).filter,
|
|
82
|
+
hovered_attr: text.getAttribute('data-node-alias-hovered'),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await browser.close();
|
|
87
|
+
return { rest, hover, alias: aliasAfterHover };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const motion = await probe({ reducedMotion: false });
|
|
91
|
+
const a11y = await probe({ reducedMotion: true });
|
|
92
|
+
|
|
93
|
+
const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
|
|
94
|
+
const sourceFilter = /filter: !reducedMotion && hoveredAlias === session\.alias\s*\?\s*`drop-shadow\(0 0 2px \$\{status\.text\}80\)`\s*:\s*undefined/.test(src);
|
|
95
|
+
const sourceAttr = /data-node-alias-glow=\{!reducedMotion && hoveredAlias === session\.alias \? 'true' : 'false'\}/.test(src);
|
|
96
|
+
const sourceTransition = /transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out, filter 200ms ease-out'/.test(src);
|
|
97
|
+
|
|
98
|
+
const results = {
|
|
99
|
+
// Motion fixture, rest: attr='false', filter='none'
|
|
100
|
+
motion_rest_attr_false: motion.rest && motion.rest.glow_attr === 'false',
|
|
101
|
+
motion_rest_filter_none: motion.rest && motion.rest.computed_filter === 'none',
|
|
102
|
+
// Motion fixture, hover: attr='true', filter contains drop-shadow
|
|
103
|
+
motion_alias_resolved: !!motion.alias,
|
|
104
|
+
motion_hover_attr_true: motion.hover && motion.hover.glow_attr === 'true',
|
|
105
|
+
motion_hover_filter_drop:motion.hover && /drop-shadow/.test(motion.hover.computed_filter || ''),
|
|
106
|
+
// a11y fixture: attr='false' even after hover (component-side gate)
|
|
107
|
+
a11y_rest_attr_false: a11y.rest && a11y.rest.glow_attr === 'false',
|
|
108
|
+
a11y_hover_attr_false: a11y.hover && a11y.hover.glow_attr === 'false',
|
|
109
|
+
a11y_hover_filter_none: a11y.hover && a11y.hover.computed_filter === 'none',
|
|
110
|
+
// Source
|
|
111
|
+
source_filter_wired: sourceFilter,
|
|
112
|
+
source_attr_wired: sourceAttr,
|
|
113
|
+
source_transition_wired: sourceTransition,
|
|
114
|
+
};
|
|
115
|
+
const ok = Object.values(results).every(Boolean);
|
|
116
|
+
console.log(`${ok ? '✅' : '❌'} R500 node alias glow:`, JSON.stringify(results),
|
|
117
|
+
'\n motion rest :', JSON.stringify(motion.rest),
|
|
118
|
+
'\n motion hover:', JSON.stringify(motion.hover),
|
|
119
|
+
'\n a11y rest :', JSON.stringify(a11y.rest),
|
|
120
|
+
'\n a11y hover:', JSON.stringify(a11y.hover));
|
|
121
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/* Round 501 verification: vendor avatar <image> inside node circles
|
|
2
|
+
* gains a hover-gated `filter: brightness(1.15)`. Closes the per-node
|
|
3
|
+
* hover-affordance arc — every per-node element type now has a hover
|
|
4
|
+
* treatment.
|
|
5
|
+
*
|
|
6
|
+
* Test fixture: claude-code-cli runtime → vendor.logo path renders the
|
|
7
|
+
* <image> branch (vs monogram fallback). Verifies:
|
|
8
|
+
* 1. rest: data-node-avatar-hovered='false', no brightness filter
|
|
9
|
+
* 2. hover (synthetic pointerenter): attr='true', computed filter
|
|
10
|
+
* contains brightness(1.15)
|
|
11
|
+
* 3. a11y: attr='false' even after hover (component gate)
|
|
12
|
+
* 4. source-side regex confirms wiring
|
|
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
|
+
async function probe({ reducedMotion }) {
|
|
21
|
+
const browser = await chromium.launch({ headless: true });
|
|
22
|
+
const ctx = await browser.newContext({
|
|
23
|
+
viewport: { width: 1500, height: 1200 },
|
|
24
|
+
reducedMotion: reducedMotion ? 'reduce' : 'no-preference',
|
|
25
|
+
});
|
|
26
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
27
|
+
await ctx.addInitScript(() => {
|
|
28
|
+
try {
|
|
29
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
30
|
+
localStorage.setItem('anet-topo-layout', 'ring');
|
|
31
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
32
|
+
} catch {}
|
|
33
|
+
});
|
|
34
|
+
await ctx.route('**/api/hub/status*', async (route) => {
|
|
35
|
+
const r = await route.fetch();
|
|
36
|
+
const b = await r.json();
|
|
37
|
+
const nid = (b.sessions || [])[0]?.network_id || 'default';
|
|
38
|
+
const mk = (alias, status) => ({
|
|
39
|
+
// claude-code-cli renders the avatar <image> branch
|
|
40
|
+
alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
|
|
41
|
+
network_id: nid, project_dir: null,
|
|
42
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh,
|
|
43
|
+
});
|
|
44
|
+
await route.fulfill({ response: r, json: { ...b, sessions: [
|
|
45
|
+
mk('alpha·a1', 'working'),
|
|
46
|
+
mk('alpha·a2', 'idle'),
|
|
47
|
+
] } });
|
|
48
|
+
});
|
|
49
|
+
await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
|
|
50
|
+
await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
|
|
51
|
+
const page = await ctx.newPage();
|
|
52
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
|
|
53
|
+
await page.waitForSelector('g[data-node]', { timeout: 15000 });
|
|
54
|
+
await page.waitForTimeout(1500);
|
|
55
|
+
|
|
56
|
+
const rest = await page.evaluate(() => {
|
|
57
|
+
const img = document.querySelector('[data-node-avatar]');
|
|
58
|
+
if (!img) return { found: false };
|
|
59
|
+
return {
|
|
60
|
+
found: true,
|
|
61
|
+
hovered_attr: img.getAttribute('data-node-avatar-hovered'),
|
|
62
|
+
computed_filter: window.getComputedStyle(img).filter,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Synthetic hover via R488 banked recipe
|
|
67
|
+
await page.evaluate(() => {
|
|
68
|
+
const g = document.querySelector('g[data-node]');
|
|
69
|
+
if (!g) return;
|
|
70
|
+
const target = g.querySelector('circle, image, rect') || g;
|
|
71
|
+
['pointerenter', 'pointerover', 'mouseenter', 'mouseover'].forEach((t) => {
|
|
72
|
+
target.dispatchEvent(new Event(t, { bubbles: true, cancelable: true }));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
await page.waitForTimeout(400);
|
|
76
|
+
|
|
77
|
+
const hover = await page.evaluate(() => {
|
|
78
|
+
const img = document.querySelector('[data-node-avatar]');
|
|
79
|
+
if (!img) return { found: false };
|
|
80
|
+
return {
|
|
81
|
+
found: true,
|
|
82
|
+
hovered_attr: img.getAttribute('data-node-avatar-hovered'),
|
|
83
|
+
computed_filter: window.getComputedStyle(img).filter,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await browser.close();
|
|
88
|
+
return { rest, hover };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const motion = await probe({ reducedMotion: false });
|
|
92
|
+
const a11y = await probe({ reducedMotion: true });
|
|
93
|
+
|
|
94
|
+
const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
|
|
95
|
+
const sourceGate = /const isAvatarHovered = !reducedMotion && hoveredAlias === session\.alias;/.test(src);
|
|
96
|
+
const sourceFilter = /filter: isAvatarHovered \? 'brightness\(1\.15\)' : undefined/.test(src);
|
|
97
|
+
const sourceAttr = /data-node-avatar-hovered=\{isAvatarHovered \? 'true' : 'false'\}/.test(src);
|
|
98
|
+
|
|
99
|
+
const results = {
|
|
100
|
+
motion_rest_found: motion.rest.found,
|
|
101
|
+
motion_rest_attr_false: motion.rest.found && motion.rest.hovered_attr === 'false',
|
|
102
|
+
motion_rest_filter_none: motion.rest.found && motion.rest.computed_filter === 'none',
|
|
103
|
+
motion_hover_attr_true: motion.hover.found && motion.hover.hovered_attr === 'true',
|
|
104
|
+
motion_hover_filter_bright: motion.hover.found && /brightness\(1\.15\)/.test(motion.hover.computed_filter || ''),
|
|
105
|
+
a11y_rest_attr_false: a11y.rest.found && a11y.rest.hovered_attr === 'false',
|
|
106
|
+
a11y_hover_attr_false: a11y.hover.found && a11y.hover.hovered_attr === 'false',
|
|
107
|
+
a11y_hover_filter_none: a11y.hover.found && a11y.hover.computed_filter === 'none',
|
|
108
|
+
source_gate_wired: sourceGate,
|
|
109
|
+
source_filter_wired: sourceFilter,
|
|
110
|
+
source_attr_wired: sourceAttr,
|
|
111
|
+
};
|
|
112
|
+
const ok = Object.values(results).every(Boolean);
|
|
113
|
+
console.log(`${ok ? '✅' : '❌'} R501 avatar brightness:`, JSON.stringify(results),
|
|
114
|
+
'\n motion:', JSON.stringify(motion),
|
|
115
|
+
'\n a11y :', JSON.stringify(a11y));
|
|
116
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* Round 502 verification: root svg surfaces `data-topo-fleet-density-tier`
|
|
2
|
+
* categorical attribute (12th in canvas state surface set). Paired with
|
|
3
|
+
* R469 numeric counts; classifies onlineNodes.length into 5 buckets:
|
|
4
|
+
* empty (0) / sparse (1-3) / normal (4-15) / dense (16-30) / very-dense (31+)
|
|
5
|
+
*
|
|
6
|
+
* Boundaries align with R109 denseLayout = >16 gate so the tier name
|
|
7
|
+
* is semantically aligned with the canvas's existing visual-mode switch.
|
|
8
|
+
*
|
|
9
|
+
* Verification across 5 fixture scenarios + source-side regex.
|
|
10
|
+
*/
|
|
11
|
+
import { chromium } from 'playwright';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
|
|
15
|
+
const fresh = new Date(Date.now() - 60 * 1000).toISOString();
|
|
16
|
+
|
|
17
|
+
async function probe({ nodeCount, label }) {
|
|
18
|
+
const browser = await chromium.launch({ headless: true });
|
|
19
|
+
const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
|
|
20
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
21
|
+
await ctx.addInitScript(() => {
|
|
22
|
+
try {
|
|
23
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
24
|
+
localStorage.setItem('anet-topo-layout', 'ring');
|
|
25
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
26
|
+
} catch {}
|
|
27
|
+
});
|
|
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 sessions = Array.from({ length: nodeCount }, (_, i) => ({
|
|
33
|
+
alias: `a·${i}`, status: 'idle',
|
|
34
|
+
model: 'claude-opus-4', runtime: 'claude-code-cli',
|
|
35
|
+
network_id: nid, project_dir: null,
|
|
36
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh,
|
|
37
|
+
}));
|
|
38
|
+
await route.fulfill({ response: r, json: { ...b, sessions } });
|
|
39
|
+
});
|
|
40
|
+
await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
|
|
41
|
+
await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
|
|
42
|
+
const page = await ctx.newPage();
|
|
43
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
|
|
44
|
+
await page.waitForTimeout(nodeCount === 0 ? 1500 : 2500); // empty fixture has no SVG canvas; sparse+ needs render time
|
|
45
|
+
const tier = await page.evaluate(() => {
|
|
46
|
+
const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
|
|
47
|
+
return svg?.getAttribute('data-topo-fleet-density-tier');
|
|
48
|
+
});
|
|
49
|
+
const onlineCount = await page.evaluate(() => {
|
|
50
|
+
const svg = document.querySelector('svg[viewBox="0 0 1000 680"]');
|
|
51
|
+
return svg?.getAttribute('data-topo-online-count');
|
|
52
|
+
});
|
|
53
|
+
await browser.close();
|
|
54
|
+
return { label, nodeCount, tier, onlineCount };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 5 fixtures across all tier boundaries. Empty case can't probe (no SVG canvas).
|
|
58
|
+
// Use 1, 4, 16, 31 to hit each non-empty tier boundary directly.
|
|
59
|
+
const sparse = await probe({ nodeCount: 1, label: 'sparse boundary @ 1' });
|
|
60
|
+
const sparseTop = await probe({ nodeCount: 3, label: 'sparse boundary @ 3' });
|
|
61
|
+
const normal = await probe({ nodeCount: 4, label: 'normal boundary @ 4' });
|
|
62
|
+
const normalTop = await probe({ nodeCount: 15, label: 'normal boundary @ 15' });
|
|
63
|
+
const dense = await probe({ nodeCount: 16, label: 'dense boundary @ 16' });
|
|
64
|
+
// Skip dense=30 + very-dense=31 to keep test fast — boundary coverage already there
|
|
65
|
+
|
|
66
|
+
const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
|
|
67
|
+
const sourceWired = /data-topo-fleet-density-tier=\{[\s\S]*?onlineNodes\.length === 0 \? 'empty' :[\s\S]*?onlineNodes\.length <= 3 \? 'sparse' :[\s\S]*?onlineNodes\.length <= 15 \? 'normal' :[\s\S]*?onlineNodes\.length <= 30 \? 'dense' :[\s\S]*?'very-dense'/.test(src);
|
|
68
|
+
|
|
69
|
+
const results = {
|
|
70
|
+
sparse_1: sparse.tier === 'sparse',
|
|
71
|
+
sparse_top_3: sparseTop.tier === 'sparse',
|
|
72
|
+
normal_4: normal.tier === 'normal',
|
|
73
|
+
normal_top_15: normalTop.tier === 'normal',
|
|
74
|
+
dense_16: dense.tier === 'dense',
|
|
75
|
+
source_wired: sourceWired,
|
|
76
|
+
};
|
|
77
|
+
const ok = Object.values(results).every(Boolean);
|
|
78
|
+
console.log(`${ok ? '✅' : '❌'} R502 fleet-density-tier:`, JSON.stringify(results),
|
|
79
|
+
'\n ', sparse,
|
|
80
|
+
'\n ', sparseTop,
|
|
81
|
+
'\n ', normal,
|
|
82
|
+
'\n ', normalTop,
|
|
83
|
+
'\n ', dense);
|
|
84
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* Round 505 verification: FreshnessChip mount picks up `anet-fade-in`
|
|
2
|
+
* so the stale-warning amber pill eases in (opacity 0→1 over 150ms)
|
|
3
|
+
* instead of popping into the chip-row. The chip ONLY renders when
|
|
4
|
+
* stale (R275 conditional), so the fade plays exactly at the moment
|
|
5
|
+
* the stale signal first arrives — perfectly aligned with semantic.
|
|
6
|
+
*
|
|
7
|
+
* Fixture: stale data is triggered when SWR sec > 10. The chip fetches
|
|
8
|
+
* from /api/hub/status; we can route-mock with stale timestamps to
|
|
9
|
+
* force the stale gate, but the simpler path is to assert the chip
|
|
10
|
+
* IS NOT visible at rest (fresh data fixture) AND when it appears
|
|
11
|
+
* post-stale, it carries the anet-fade-in class.
|
|
12
|
+
*
|
|
13
|
+
* Strategy:
|
|
14
|
+
* 1. Source-side: regex confirms `${baseClass} ${colorClass} anet-
|
|
15
|
+
* fade-in` template literal + data-freshness-chip-mount-fade attr
|
|
16
|
+
* wired.
|
|
17
|
+
* 2. DOM-side: probe page with no stale fixture; chip not visible.
|
|
18
|
+
* Then route-mock with stale lag (timestamps > 10s old) to force
|
|
19
|
+
* stale render; chip appears with anet-fade-in class + attr.
|
|
20
|
+
*/
|
|
21
|
+
import { chromium } from 'playwright';
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
|
|
24
|
+
const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
|
|
25
|
+
const fresh = new Date(Date.now() - 60 * 1000).toISOString();
|
|
26
|
+
|
|
27
|
+
const browser = await chromium.launch({ headless: true });
|
|
28
|
+
const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
|
|
29
|
+
await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
|
|
30
|
+
await ctx.addInitScript(() => {
|
|
31
|
+
try {
|
|
32
|
+
localStorage.setItem('anet-theme', 'cyber');
|
|
33
|
+
sessionStorage.setItem('anet_v3_auth', '1');
|
|
34
|
+
} catch {}
|
|
35
|
+
});
|
|
36
|
+
await ctx.route('**/api/hub/status*', async (route) => {
|
|
37
|
+
const r = await route.fetch();
|
|
38
|
+
const b = await r.json();
|
|
39
|
+
const nid = (b.sessions || [])[0]?.network_id || 'default';
|
|
40
|
+
// Stale fixture: served_at is FAR in past so client-side `sec` computes > 10
|
|
41
|
+
// The FreshnessChip reads `sec` from SWR data freshness — but the chip
|
|
42
|
+
// also computes from `served_at` (timestamp on the response). Let's mock
|
|
43
|
+
// by holding the response so SWR's internal lastFetch timestamp ages.
|
|
44
|
+
await route.fulfill({ response: r, json: { ...b, sessions: [
|
|
45
|
+
{ alias: 'a·1', status: 'idle', model: 'claude-opus-4', runtime: 'claude-code-cli',
|
|
46
|
+
network_id: nid, project_dir: null,
|
|
47
|
+
created_at: fresh, updated_at: fresh, last_seen_at: fresh },
|
|
48
|
+
] } });
|
|
49
|
+
});
|
|
50
|
+
await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
|
|
51
|
+
await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
|
|
52
|
+
const page = await ctx.newPage();
|
|
53
|
+
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
|
|
54
|
+
await page.waitForTimeout(2000);
|
|
55
|
+
|
|
56
|
+
// Force `sec` to compute as stale by mocking a stale state. Most reliable:
|
|
57
|
+
// look for the freshness chip element after waiting > 11s. But that's slow.
|
|
58
|
+
// Faster: just verify the FreshnessChip CAN be conditionally rendered with
|
|
59
|
+
// the right class — probe a synthetic case via direct DOM injection.
|
|
60
|
+
//
|
|
61
|
+
// Best simple test: source-side regex (the canonical proof) + assert that
|
|
62
|
+
// when the chip element EXISTS in the DOM (it would after stale onset),
|
|
63
|
+
// it carries the class. We'll trigger it by waiting + polling.
|
|
64
|
+
|
|
65
|
+
// Step 1: confirm chip is NOT visible at fresh-data start
|
|
66
|
+
const initialChip = await page.evaluate(() =>
|
|
67
|
+
document.querySelector('[data-freshness-chip]')
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Step 2: Wait for stale threshold (sec > 10). FreshnessChip recalcs `sec`
|
|
71
|
+
// from `data.served_at` or SWR's last fetch timestamp every render — SWR
|
|
72
|
+
// refreshes every 5s, so the chip needs > 10s wall-clock since first fetch.
|
|
73
|
+
// Wait 13s.
|
|
74
|
+
await page.waitForTimeout(13000);
|
|
75
|
+
const chipInfo = await page.evaluate(() => {
|
|
76
|
+
const chip = document.querySelector('[data-freshness-chip]');
|
|
77
|
+
if (!chip) return null;
|
|
78
|
+
return {
|
|
79
|
+
present: true,
|
|
80
|
+
class_has_fade: /anet-fade-in/.test(chip.getAttribute('class') || ''),
|
|
81
|
+
mount_fade_attr: chip.getAttribute('data-freshness-chip-mount-fade'),
|
|
82
|
+
stale_attr: chip.getAttribute('data-freshness-chip-stale'),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await browser.close();
|
|
87
|
+
|
|
88
|
+
const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
|
|
89
|
+
const sourceClassWired = /className=\{`\$\{baseClass\} \$\{colorClass\} anet-fade-in`\}/.test(src);
|
|
90
|
+
const sourceAttrWired = /data-freshness-chip-mount-fade="true"/.test(src);
|
|
91
|
+
|
|
92
|
+
const results = {
|
|
93
|
+
initial_chip_absent: initialChip === null,
|
|
94
|
+
source_class_wired: sourceClassWired,
|
|
95
|
+
source_attr_wired: sourceAttrWired,
|
|
96
|
+
// Post-stale assertions (vacuous-or-strict pattern banked from R495)
|
|
97
|
+
post_stale_strict_or_vacuous:
|
|
98
|
+
chipInfo === null ||
|
|
99
|
+
(chipInfo.class_has_fade && chipInfo.mount_fade_attr === 'true' && chipInfo.stale_attr === 'true'),
|
|
100
|
+
post_stale_chip_seen: !!chipInfo,
|
|
101
|
+
};
|
|
102
|
+
const ok = Object.values(results).every(Boolean);
|
|
103
|
+
console.log(`${ok ? '✅' : '❌'} R505 FreshnessChip mount fade:`, JSON.stringify(results),
|
|
104
|
+
'\n initial:', initialChip, '\n post-stale:', JSON.stringify(chipInfo));
|
|
105
|
+
process.exit(ok ? 0 : 1);
|