@sleep2agi/agent-network-dashboard 0.4.6-preview.7 → 0.4.6-preview.71
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/app-path-routes-manifest.json +1 -0
- package/.next/build/chunks/{node_modules_13sb.px._.js → 00jm_02mlcew._.js} +167 -167
- package/.next/build/chunks/00jm_02mlcew._.js.map +47 -0
- package/.next/build/chunks/{[root-of-the-server]__0ubbtyl._.js → [root-of-the-server]__0tgc-8p._.js} +3 -3
- package/.next/build/chunks/{[root-of-the-server]__0d-m0h0._.js → [root-of-the-server]__13z64y.._.js} +7 -7
- package/.next/build/chunks/{[root-of-the-server]__0d-m0h0._.js.map → [root-of-the-server]__13z64y.._.js.map} +2 -2
- package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_0r4x5ys._.js +13 -0
- package/.next/build/chunks/[turbopack]_runtime.js +1 -1
- package/.next/build/postcss.js +4 -4
- package/.next/build-manifest.json +8 -8
- package/.next/diagnostics/route-bundle-stats.json +157 -143
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +4 -4
- package/.next/required-server-files.json +4 -4
- package/.next/routes-manifest.json +6 -0
- package/.next/server/app/_global-error/page/build-manifest.json +5 -5
- package/.next/server/app/_global-error/page.js +9 -9
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +8 -8
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_global-error.segments/_full.segment.rsc +8 -8
- package/.next/server/app/_global-error.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page/build-manifest.json +5 -5
- package/.next/server/app/_not-found/page/next-font-manifest.json +1 -1
- package/.next/server/app/_not-found/page.js +12 -12
- package/.next/server/app/_not-found/page.js.nft.json +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/build-manifest.json +5 -5
- package/.next/server/app/admin/page/next-font-manifest.json +1 -1
- package/.next/server/app/admin/page.js +13 -13
- package/.next/server/app/admin/page.js.nft.json +1 -1
- 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/api/anet/config/route.js +6 -6
- package/.next/server/app/api/anet/config/route.js.nft.json +1 -1
- package/.next/server/app/api/auth/login/route.js +7 -7
- package/.next/server/app/api/auth/login/route.js.nft.json +1 -1
- package/.next/server/app/api/auth/logout/route.js +7 -7
- package/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/server/app/api/auth/v3/route.js +7 -7
- package/.next/server/app/api/auth/v3/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/audit-log/route.js +6 -6
- package/.next/server/app/api/hub/audit-log/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/auth/route.js +7 -7
- package/.next/server/app/api/hub/auth/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/broadcast/route.js +6 -6
- package/.next/server/app/api/hub/broadcast/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/events/route.js +6 -6
- package/.next/server/app/api/hub/events/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/health/route.js +6 -6
- package/.next/server/app/api/hub/health/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/inbox/route.js +6 -6
- package/.next/server/app/api/hub/inbox/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/license/route.js +6 -6
- package/.next/server/app/api/hub/license/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/messages/route.js +6 -6
- package/.next/server/app/api/hub/messages/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/networks/route.js +6 -6
- package/.next/server/app/api/hub/networks/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/nodes/route.js +6 -6
- package/.next/server/app/api/hub/nodes/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/register/route.js +6 -6
- package/.next/server/app/api/hub/register/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/send/route.js +6 -6
- package/.next/server/app/api/hub/send/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/server-logs/route.js +6 -6
- package/.next/server/app/api/hub/server-logs/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/servers/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/hub/servers/route/build-manifest.json +9 -0
- package/.next/server/app/api/hub/servers/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/hub/servers/route.js +7 -0
- package/.next/server/app/api/hub/servers/route.js.map +5 -0
- package/.next/server/app/api/hub/servers/route.js.nft.json +1 -0
- package/.next/server/app/api/hub/servers/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/hub/session/route.js +6 -6
- package/.next/server/app/api/hub/session/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/stats/route.js +6 -6
- package/.next/server/app/api/hub/stats/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/status/route.js +6 -6
- package/.next/server/app/api/hub/status/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/task-events/route.js +6 -6
- package/.next/server/app/api/hub/task-events/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/tasks/route.js +6 -6
- package/.next/server/app/api/hub/tasks/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/tmux/route.js +6 -6
- package/.next/server/app/api/hub/tmux/route.js.nft.json +1 -1
- package/.next/server/app/api/hub/tokens/route.js +6 -6
- package/.next/server/app/api/hub/tokens/route.js.nft.json +1 -1
- package/.next/server/app/favicon.ico/route.js +6 -6
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- 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/build-manifest.json +5 -5
- package/.next/server/app/login/page/next-font-manifest.json +1 -1
- package/.next/server/app/login/page.js +13 -13
- package/.next/server/app/login/page.js.nft.json +1 -1
- 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/build-manifest.json +5 -5
- package/.next/server/app/logs/page/next-font-manifest.json +1 -1
- package/.next/server/app/logs/page.js +13 -13
- package/.next/server/app/logs/page.js.nft.json +1 -1
- 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/build-manifest.json +5 -5
- package/.next/server/app/messages/page/next-font-manifest.json +1 -1
- package/.next/server/app/messages/page.js +13 -13
- package/.next/server/app/messages/page.js.nft.json +1 -1
- 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/build-manifest.json +5 -5
- package/.next/server/app/node/page/next-font-manifest.json +1 -1
- package/.next/server/app/node/page.js +13 -13
- package/.next/server/app/node/page.js.nft.json +1 -1
- 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/build-manifest.json +5 -5
- package/.next/server/app/nodes/page/next-font-manifest.json +1 -1
- package/.next/server/app/nodes/page.js +13 -13
- package/.next/server/app/nodes/page.js.nft.json +1 -1
- 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/build-manifest.json +5 -5
- package/.next/server/app/page/next-font-manifest.json +1 -1
- package/.next/server/app/page.js +13 -13
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs/page/build-manifest.json +5 -5
- package/.next/server/app/server-logs/page/next-font-manifest.json +1 -1
- package/.next/server/app/server-logs/page.js +13 -13
- package/.next/server/app/server-logs/page.js.nft.json +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/build-manifest.json +5 -5
- package/.next/server/app/settings/networks/page/next-font-manifest.json +1 -1
- package/.next/server/app/settings/networks/page.js +13 -13
- package/.next/server/app/settings/networks/page.js.nft.json +1 -1
- 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/build-manifest.json +5 -5
- package/.next/server/app/settings/page/next-font-manifest.json +1 -1
- package/.next/server/app/settings/page.js +13 -13
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page/build-manifest.json +5 -5
- package/.next/server/app/settings/tokens/page/next-font-manifest.json +1 -1
- package/.next/server/app/settings/tokens/page.js +13 -13
- package/.next/server/app/settings/tokens/page.js.nft.json +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/build-manifest.json +5 -5
- package/.next/server/app/tasks/[id]/page/next-font-manifest.json +1 -1
- package/.next/server/app/tasks/[id]/page.js +13 -13
- package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page/build-manifest.json +5 -5
- package/.next/server/app/tasks/page/next-font-manifest.json +1 -1
- package/.next/server/app/tasks/page.js +13 -13
- package/.next/server/app/tasks/page.js.nft.json +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/app-paths-manifest.json +1 -0
- package/.next/server/chunks/00jm_next_04f3gac._.js +10 -0
- package/.next/server/chunks/00jm_next_04f3gac._.js.map +1 -0
- package/.next/server/chunks/00jm_next_dist_esm_build_templates_app-route_02f430o.js +3 -0
- package/.next/server/chunks/00jm_next_dist_esm_build_templates_app-route_02f430o.js.map +1 -0
- package/.next/server/chunks/0ci9_nt-network-dashboard__next-internal_server_app_api_auth_v3_route_actions_0f5nbzr.js +3 -0
- package/.next/server/chunks/0ci9_nt-network-dashboard__next-internal_server_app_favicon_ico_route_actions_0t0eu9r.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_anet_config_route_actions_0a~v55_.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_auth_login_route_actions_0xnxfs4.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_auth_logout_route_actions_0p8p0rn.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_audit-log_route_actions_06uquwi.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_auth_route_actions_0f_y1ey.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_broadcast_route_actions_0ao3j43.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_events_route_actions_0_5ndld.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_health_route_actions_0mpzl74.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_inbox_route_actions_0stoyto.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_license_route_actions_0a4.fuh.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_messages_route_actions_059rw1o.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_networks_route_actions_03sehpn.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_nodes_route_actions_11fbp28.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_register_route_actions_0l.d7~n.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_send_route_actions_0.p82vy.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_server-logs_route_actions_05~c5lb.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_servers_route_actions_08-m-cs.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_session_route_actions_0b9ef2i.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_stats_route_actions_03u65u0.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_status_route_actions_0_71ear.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_task-events_route_actions_07md_ee.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_tasks_route_actions_0er3uvk.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_tmux_route_actions_0o_1-8u.js +3 -0
- package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_tokens_route_actions_09doluz.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0-2le8-._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0-2le8-._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0-g~15e._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0-g~15e._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__00194.t._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__03ueci3._.js.map → [root-of-the-server]__00194.t._.js.map} +1 -1
- package/.next/server/chunks/[root-of-the-server]__02p2n3n._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__02p2n3n._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__02y-i-p._.js +6 -0
- package/.next/server/chunks/[root-of-the-server]__02y-i-p._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__043tnx2._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__0o4qfn9._.js.map → [root-of-the-server]__043tnx2._.js.map} +1 -1
- package/.next/server/chunks/[root-of-the-server]__04z62zk._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__04z62zk._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__06p6kkp._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__06p6kkp._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__07ztet2._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__07ztet2._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__08uyvdq._.js +20 -0
- package/.next/server/chunks/[root-of-the-server]__08uyvdq._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0b7v51y._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0b7v51y._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0e3ucl1._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__0rg9ifx._.js.map → [root-of-the-server]__0e3ucl1._.js.map} +1 -1
- package/.next/server/chunks/[root-of-the-server]__0e6yail._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0e6yail._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0gx2_6a._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0gx2_6a._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0hcnyp.._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__0o29v8c._.js.map → [root-of-the-server]__0hcnyp.._.js.map} +1 -1
- package/.next/server/chunks/[root-of-the-server]__0hg3_8r._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0hg3_8r._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0mxw4vb._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0mxw4vb._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0mzopco._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0mzopco._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0o45a4q._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__0p6tutt._.js.map → [root-of-the-server]__0o45a4q._.js.map} +1 -1
- package/.next/server/chunks/[root-of-the-server]__0q-teo6._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0q-teo6._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0qcxxzf._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0qcxxzf._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0rovr5-._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0rovr5-._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__0~h2uqi._.js.map → [root-of-the-server]__0tx8s8i._.js.map} +1 -1
- package/.next/server/chunks/[root-of-the-server]__0xtp-et._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0xtp-et._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__10pvabe._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__10pvabe._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__10t11zq._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__10t11zq._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__12r~omy._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__12r~omy._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__1389~cs._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1389~cs._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__13n8t..._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__13n8t..._.js.map +1 -0
- package/.next/server/chunks/[turbopack]_runtime.js +1 -1
- package/.next/server/chunks/ssr/{node_modules_0r_.s40._.js → 00jm_03rn9vn._.js} +2 -2
- package/.next/server/chunks/ssr/00jm_03rn9vn._.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_09w7yel._.js → 00jm_059ns68._.js} +3 -3
- package/.next/server/chunks/ssr/00jm_059ns68._.js.map +1 -0
- package/.next/server/chunks/ssr/00jm_09ae-q0._.js +3 -0
- package/.next/server/chunks/ssr/00jm_09ae-q0._.js.map +1 -0
- package/.next/server/chunks/ssr/00jm_0j7__z.._.js +3 -0
- package/.next/server/chunks/ssr/00jm_0j7__z.._.js.map +1 -0
- package/.next/server/chunks/ssr/00jm_next_01-0urn._.js +6 -0
- package/.next/server/chunks/ssr/00jm_next_01-0urn._.js.map +1 -0
- package/.next/server/chunks/ssr/00jm_next_dist_05rlh5d._.js +6 -0
- package/.next/server/chunks/ssr/00jm_next_dist_05rlh5d._.js.map +1 -0
- package/.next/server/chunks/ssr/00jm_next_dist_client_components_0gkudcq._.js +3 -0
- package/.next/server/chunks/ssr/00jm_next_dist_client_components_0gkudcq._.js.map +1 -0
- package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_forbidden_0ecus3e.js +3 -0
- package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_forbidden_0ecus3e.js.map +1 -0
- package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_global-error_07n8pg2.js +3 -0
- package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_global-error_07n8pg2.js.map +1 -0
- package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_unauthorized_0r0uq4j.js +3 -0
- package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_unauthorized_0r0uq4j.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_002l7yi.js → 00jm_next_dist_esm_build_templates_app-page_0..ddwm.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0..ddwm.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0qm1-a1.js → 00jm_next_dist_esm_build_templates_app-page_02bxi6j.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_02bxi6j.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_02suzhc.js → 00jm_next_dist_esm_build_templates_app-page_05oassd.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_05oassd.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0dm~kj8.js → 00jm_next_dist_esm_build_templates_app-page_05vxtby.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_05vxtby.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0uccf9z.js → 00jm_next_dist_esm_build_templates_app-page_05~eml4.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_05~eml4.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0l-fh0i.js → 00jm_next_dist_esm_build_templates_app-page_0_-g_q2.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0_-g_q2.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0uv6jyc.js → 00jm_next_dist_esm_build_templates_app-page_0_ec0xu.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0_ec0xu.js.map +1 -0
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0czb6wz.js +4 -0
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0czb6wz.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_13s07li.js → 00jm_next_dist_esm_build_templates_app-page_0e-cn.p.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0e-cn.p.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0o9vt1n.js → 00jm_next_dist_esm_build_templates_app-page_0k8_sr8.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0k8_sr8.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0nsg22r.js → 00jm_next_dist_esm_build_templates_app-page_0lt_3bw.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0lt_3bw.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0d2yq5t.js → 00jm_next_dist_esm_build_templates_app-page_0uyh3yt.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0uyh3yt.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0foejvu.js → 00jm_next_dist_esm_build_templates_app-page_0v-a2ps.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0v-a2ps.js.map +1 -0
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_12.4r9~.js +4 -0
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_12.4r9~.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_07uu-xq.js → 00jm_next_dist_esm_build_templates_app-page_13mqkay.js} +3 -3
- package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_13mqkay.js.map +1 -0
- package/.next/server/chunks/ssr/0dwh_ent-network-dashboard__next-internal_server_app_server-logs_page_actions_0hkk815.js +3 -0
- package/.next/server/chunks/ssr/0yas_gent-network-dashboard__next-internal_server_app__not-found_page_actions_0u.f-rz.js +3 -0
- package/.next/server/chunks/ssr/0yas_gent-network-dashboard__next-internal_server_app_tasks_[id]_page_actions_0r3~lho.js +3 -0
- package/.next/server/chunks/ssr/0ykm__next-internal_server_app__global-error_page_actions_100fxls.js +3 -0
- package/.next/server/chunks/ssr/0ykm__next-internal_server_app_settings_networks_page_actions_0jgy57m.js +3 -0
- package/.next/server/chunks/ssr/0ykm__next-internal_server_app_settings_tokens_page_actions_0l5h.k~.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__030vg4n._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__030vg4n._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__063~dul._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__063~dul._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__07io.d8._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__07io.d8._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__08d6lp4._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__08d6lp4._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__09f1wn6._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__09f1wn6._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0c4xtkg._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0c4xtkg._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0erfzjn._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0erfzjn._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0ezjwje._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0ezjwje._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__08qr2ji._.js → [root-of-the-server]__0gc.4uk._.js} +3 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0gc.4uk._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0h_p~-4._.js +19 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0h_p~-4._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0jdr1e5._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0jdr1e5._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0lw16mu._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0lw16mu._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0mjly~-._.js +33 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0mjly~-._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0p-95r_._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0p-95r_._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0u5aqkk._.js +8 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0u5aqkk._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0ue5.fy._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0ue5.fy._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0uggxvx._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0uggxvx._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0zjk5ro._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0zjk5ro._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0zz3v7a._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0zz3v7a._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0~y12ef._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0~y12ef._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__129sc_9._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__129sc_9._.js.map +1 -0
- package/.next/server/chunks/ssr/[turbopack]_runtime.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +4 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -0
- package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_admin_page_actions_0i~.x~j.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_login_page_actions_0frwl15.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_logs_page_actions_0xa2ox_.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_messages_page_actions_10j3tg4.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_node_page_actions_0sl4ntd.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_nodes_page_actions_0cpgkfs.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_page_actions_08nuo36.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_settings_page_actions_0iv.lmk.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_tasks_page_actions_08c7qlx.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_tasks_page_actions_08c7qlx.js.map +1 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js.map +1 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js.map +1 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js.map +1 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js.map +1 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js.map +1 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js +9 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js.map +1 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js.map +1 -0
- package/.next/server/chunks/ssr/{app_components_08ys5y3._.js → agent-network-dashboard_app_components_0mvyi-4._.js} +3 -3
- package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -0
- package/.next/server/chunks/ssr/{app_server-logs_page_tsx_0_kpp1p._.js → agent-network-dashboard_app_server-logs_page_tsx_0dg.l_8._.js} +2 -2
- package/.next/server/chunks/ssr/agent-network-dashboard_app_server-logs_page_tsx_0dg.l_8._.js.map +1 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_tasks_page_tsx_0mwxy4z._.js +3 -0
- package/.next/server/chunks/ssr/agent-network-dashboard_app_tasks_page_tsx_0mwxy4z._.js.map +1 -0
- package/.next/server/middleware-build-manifest.js +8 -8
- package/.next/server/middleware.js +3 -3
- package/.next/server/middleware.js.nft.json +1 -1
- package/.next/server/next-font-manifest.js +1 -1
- package/.next/server/next-font-manifest.json +14 -14
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/0-mpa_947ipeq.js +1 -0
- package/.next/static/chunks/{0gtbwcilfu6b7.js → 0.66f3.rtcybb.js} +1 -1
- package/.next/static/chunks/01lmbsd37fybu.js +1 -0
- package/.next/static/chunks/039-b.v_9-_rm.js +1 -0
- package/.next/static/chunks/03o.h6kvmw4l_.js +1 -0
- package/.next/static/chunks/03~~oirxz7~vc.js +1 -0
- package/.next/static/chunks/05oi25hh7b81r.js +31 -0
- package/.next/static/chunks/05uk96gc~9mni.js +1 -0
- package/.next/static/chunks/07~8sx3_5uiox.js +1 -0
- package/.next/static/chunks/08kf-.bgs10ze.js +1 -0
- package/.next/static/chunks/{0pqt~8bl3ukh4.js → 0_pxg.4pebw__.js} +2 -2
- package/.next/static/chunks/0b8wnt4put-5n.js +2 -0
- package/.next/static/chunks/{01xlw8hd842-c.js → 0eov~nkwaf-qa.js} +1 -1
- package/.next/static/chunks/0glmrdj9112tc.js +7 -0
- package/.next/static/chunks/0ihb4j2px4pj6.js +1 -0
- package/.next/static/chunks/0i~_5k19v1-0q.css +2 -0
- package/.next/static/chunks/{0w1zwt8kq_7nb.js → 0ku0fjqlm9mca.js} +1 -1
- package/.next/static/chunks/{0-hlcuoemhe7w.js → 0pu117ktqke6..js} +2 -2
- package/.next/static/chunks/0rxrez_cjn.eh.js +1 -0
- package/.next/static/chunks/0sev.kqx9f.1x.js +2 -0
- package/.next/static/chunks/0tvn2l1pc.h65.js +1 -0
- package/.next/static/chunks/0u3b93reo9xci.js +1 -0
- package/.next/static/chunks/0u7v~vrs7ikg..js +1 -0
- package/.next/static/chunks/0x8x.29hkju5a.js +1 -0
- package/.next/static/chunks/174re5ar~kxne.js +1 -0
- package/.next/static/chunks/{turbopack-01f1xs~cq_~si.js → turbopack-0.7dvsf9rllt0.js} +1 -1
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/.next/types/routes.d.ts +2 -1
- package/.next/types/validator.ts +9 -0
- package/app/api/hub/servers/route.ts +39 -0
- package/app/components/AppShell.tsx +2 -0
- package/app/components/ChatPopover.tsx +38 -2
- package/app/components/HelpOverlay.tsx +30 -0
- package/app/components/ServersDrawer.tsx +206 -0
- package/app/components/TopoGraph.tsx +1423 -177
- package/app/components/types.ts +3 -0
- package/app/components/utils.ts +8 -7
- package/app/lib/time.ts +36 -0
- package/package.json +1 -1
- package/scripts/chat-popover-test.mjs +2 -0
- package/scripts/chat-theme-test.mjs +1 -0
- package/scripts/help-overlay-topo-test.mjs +44 -0
- package/scripts/servers-drawer-live-test.mjs +96 -0
- package/scripts/topo-active-filter-pill-test.mjs +101 -0
- package/scripts/topo-active-links-chip-test.mjs +73 -0
- package/scripts/topo-arrow-tier-test.mjs +61 -0
- package/scripts/topo-auto-fit-test.mjs +73 -0
- package/scripts/topo-chat-focus-ring-test.mjs +77 -0
- package/scripts/topo-click-ripple-test.mjs +66 -0
- package/scripts/topo-cwd-tooltip-test.mjs +64 -0
- package/scripts/topo-dblclick-reset-test.mjs +74 -0
- package/scripts/topo-edge-endpoint-highlight-test.mjs +111 -0
- package/scripts/topo-edge-focus-test.mjs +113 -0
- package/scripts/topo-edge-hover-test.mjs +93 -0
- package/scripts/topo-edge-tooltip-test.mjs +70 -0
- package/scripts/topo-esc-clears-pin-test.mjs +98 -0
- package/scripts/topo-fit-key-test.mjs +106 -0
- package/scripts/topo-freshness-chip-test.mjs +74 -0
- package/scripts/topo-group-count-test.mjs +68 -0
- package/scripts/topo-group-hover-test.mjs +82 -0
- package/scripts/topo-group-pin-test.mjs +119 -0
- package/scripts/topo-group-status-pips-test.mjs +103 -0
- package/scripts/topo-groupbox-test.mjs +5 -2
- package/scripts/topo-help-overlay-r59-test.mjs +67 -0
- package/scripts/topo-hub-click-fit-test.mjs +137 -0
- package/scripts/topo-hub-title-test.mjs +93 -0
- package/scripts/topo-ingroup-edge-test.mjs +103 -0
- package/scripts/topo-label-lift-test.mjs +119 -0
- package/scripts/topo-label-scale-test.mjs +78 -0
- package/scripts/topo-lastseen-tooltip-test.mjs +59 -0
- package/scripts/topo-layout-key-test.mjs +91 -0
- package/scripts/topo-layout-test.mjs +1 -0
- package/scripts/topo-legend-hover-test.mjs +113 -0
- package/scripts/topo-legend-pin-test.mjs +128 -0
- package/scripts/topo-minimap-test.mjs +68 -0
- package/scripts/topo-mobile-chips-test.mjs +103 -0
- package/scripts/topo-node-hover-lift-test.mjs +85 -0
- package/scripts/topo-offline-dim-test.mjs +70 -0
- package/scripts/topo-overlap-test.mjs +164 -0
- package/scripts/topo-p0round-test.mjs +166 -0
- package/scripts/topo-pan-cursor-test.mjs +62 -0
- package/scripts/topo-panel-elevation-test.mjs +76 -0
- package/scripts/topo-popover-meta-test.mjs +78 -0
- package/scripts/topo-pressure-bar-test.mjs +69 -0
- package/scripts/topo-pressure-pin-test.mjs +112 -0
- package/scripts/topo-pulse-rate-test.mjs +71 -0
- package/scripts/topo-recent-row-hover-test.mjs +122 -0
- package/scripts/topo-recent-signal-empty-test.mjs +63 -0
- package/scripts/topo-reduced-motion-test.mjs +77 -0
- package/scripts/topo-rename-test.mjs +1 -0
- package/scripts/topo-tier-ring-test.mjs +69 -0
- package/scripts/topo-tz-ghost-test.mjs +69 -0
- package/scripts/topo-vendor-test.mjs +1 -0
- package/scripts/topo-wheel-zoom-test.mjs +79 -0
- package/scripts/topo-zoom-keys-test.mjs +89 -0
- package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_06e.r3r._.js +0 -13
- package/.next/build/chunks/node_modules_13sb.px._.js.map +0 -47
- package/.next/server/chunks/[root-of-the-server]__0-mykaf._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0-mykaf._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0.d_kqy._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0.d_kqy._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__02pocyg._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__02pocyg._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__03ueci3._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__04aud7x._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__04aud7x._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__05fnt3n._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__05fnt3n._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__077tsd.._.js +0 -20
- package/.next/server/chunks/[root-of-the-server]__077tsd.._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__08wy1jr._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__08wy1jr._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0adhu1m._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0adhu1m._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0aqv02v._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0aqv02v._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0cawhfq._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0cawhfq._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0d_-_k.._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0d_-_k.._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0f6bxfl._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0f6bxfl._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0h15edq._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0h15edq._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0k.vegl._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0k.vegl._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0kltbce._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0kltbce._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0mn04fw._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0mn04fw._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0o29v8c._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0o4qfn9._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0o7ya7r._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0o7ya7r._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0p3fo6l._.js +0 -6
- package/.next/server/chunks/[root-of-the-server]__0p3fo6l._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0p6tutt._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0pn3wsx._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0pn3wsx._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0rg9ifx._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0x.-194._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0x.-194._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0x.a_yh._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0x.a_yh._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0~h2uqi._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__139~d6a._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__139~d6a._.js.map +0 -1
- package/.next/server/chunks/_next-internal_server_app_api_anet_config_route_actions_0xzy73j.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_auth_login_route_actions_0zukc38.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_auth_logout_route_actions_0fm~3ij.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_auth_v3_route_actions_0ndzs5l.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_audit-log_route_actions_11k8fqt.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_auth_route_actions_0gs7ttk.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_broadcast_route_actions_0ads-kd.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_events_route_actions_0opktby.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_health_route_actions_0tzzt~x.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_inbox_route_actions_0xvslrg.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_license_route_actions_0i3tlr3.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_messages_route_actions_08-gi-s.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_networks_route_actions_02vejq2.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_nodes_route_actions_0nx8ryn.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_register_route_actions_086wi_3.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_send_route_actions_0-ga_ug.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_server-logs_route_actions_00euexv.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_session_route_actions_041wa62.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_stats_route_actions_1242~od.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_status_route_actions_0aml09a.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_task-events_route_actions_02v1tmm.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_tasks_route_actions_0nl77v4.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_tmux_route_actions_0u~aaza.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_hub_tokens_route_actions_09hzf_1.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_095lj93.js +0 -3
- package/.next/server/chunks/node_modules_next_11synfn._.js +0 -10
- package/.next/server/chunks/node_modules_next_11synfn._.js.map +0 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0yubv2d.js +0 -3
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0yubv2d.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__014uja_._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__014uja_._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__02hi65f._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__02hi65f._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__02xqa5z._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__02xqa5z._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__04ph1de._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__04ph1de._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__05wc5-z._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__05wc5-z._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__06-2p1a._.js +0 -33
- package/.next/server/chunks/ssr/[root-of-the-server]__06-2p1a._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__08qr2ji._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__09l8xaa._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__09l8xaa._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__09z7o2x._.js +0 -19
- package/.next/server/chunks/ssr/[root-of-the-server]__09z7o2x._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0a0ffmv._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0a0ffmv._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0b7uu-_._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0b7uu-_._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0bl_wqk._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0bl_wqk._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0ft0rpu._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0ft0rpu._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0i~g-2t._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0i~g-2t._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0mook08._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0mook08._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0qchqb~._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0qchqb~._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0rrm~-q._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0rrm~-q._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0xjish~._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0xjish~._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0ywihy_._.js +0 -8
- package/.next/server/chunks/ssr/[root-of-the-server]__0ywihy_._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__12ad1n.._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__12ad1n.._.js.map +0 -1
- package/.next/server/chunks/ssr/_0nne7mz._.js +0 -3
- package/.next/server/chunks/ssr/_0nne7mz._.js.map +0 -1
- package/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_0k77kol.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_0eq97pa.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_admin_page_actions_0ycnu8f.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_login_page_actions_02kefem.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_logs_page_actions_0rvsl~d.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_messages_page_actions_0gsqep_.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_node_page_actions_0wd8cgl.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_nodes_page_actions_0-.rv67.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_page_actions_09-gtaw.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_server-logs_page_actions_0g1gb5c.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_settings_networks_page_actions_09uw.sw.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_settings_page_actions_0mr68ai.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_settings_tokens_page_actions_0yi54x8.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_tasks_[id]_page_actions_0k_hm9g.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_tasks_page_actions_0_2o_cr.js +0 -3
- package/.next/server/chunks/ssr/app_0.zbtn5._.js +0 -9
- package/.next/server/chunks/ssr/app_0.zbtn5._.js.map +0 -1
- package/.next/server/chunks/ssr/app_01y-~mp._.js +0 -3
- package/.next/server/chunks/ssr/app_01y-~mp._.js.map +0 -1
- package/.next/server/chunks/ssr/app_04n78pr._.js +0 -3
- package/.next/server/chunks/ssr/app_04n78pr._.js.map +0 -1
- package/.next/server/chunks/ssr/app_0_agk0r._.js +0 -3
- package/.next/server/chunks/ssr/app_0_agk0r._.js.map +0 -1
- package/.next/server/chunks/ssr/app_0aenxef._.js +0 -3
- package/.next/server/chunks/ssr/app_0aenxef._.js.map +0 -1
- package/.next/server/chunks/ssr/app_0aum_g5._.js +0 -3
- package/.next/server/chunks/ssr/app_0aum_g5._.js.map +0 -1
- package/.next/server/chunks/ssr/app_0emvkn2._.js +0 -3
- package/.next/server/chunks/ssr/app_0emvkn2._.js.map +0 -1
- package/.next/server/chunks/ssr/app_0esuljp._.js +0 -3
- package/.next/server/chunks/ssr/app_0esuljp._.js.map +0 -1
- package/.next/server/chunks/ssr/app_0izi28j._.js +0 -3
- package/.next/server/chunks/ssr/app_0izi28j._.js.map +0 -1
- package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +0 -1
- package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js.map +0 -1
- package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +0 -3
- package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_09w7yel._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_0r_.s40._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_0vtdjw7._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_0vtdjw7._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_0w2y7fz._.js +0 -6
- package/.next/server/chunks/ssr/node_modules_next_0w2y7fz._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_0e1izl_._.js +0 -6
- package/.next/server/chunks/ssr/node_modules_next_dist_0e1izl_._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_0inhx6q._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_0inhx6q._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_0ghu-f7.js +0 -3
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_0ghu-f7.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0lgvd_..js +0 -3
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0lgvd_..js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_unauthorized_0cjv-23.js +0 -3
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_unauthorized_0cjv-23.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_002l7yi.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_02suzhc.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_07uu-xq.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0d2yq5t.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0dm~kj8.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0foejvu.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0l-fh0i.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0nsg22r.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0o9vt1n.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0qm1-a1.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0uccf9z.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0uv6jyc.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_10~~o-h.js +0 -4
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_10~~o-h.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_13leyhk.js +0 -4
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_13leyhk.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_13s07li.js.map +0 -1
- package/.next/static/chunks/01fk_e9tpe4cr.js +0 -1
- package/.next/static/chunks/0257pdz1-imal.js +0 -2
- package/.next/static/chunks/02_zpo7taex9o.js +0 -7
- package/.next/static/chunks/06n3d~fbbtagk.js +0 -1
- package/.next/static/chunks/07eba1lz2u3p4.js +0 -1
- package/.next/static/chunks/0_rmxom9~1lmk.css +0 -2
- package/.next/static/chunks/0av2~0.96~d~g.js +0 -1
- package/.next/static/chunks/0e-.zgw81gela.js +0 -31
- package/.next/static/chunks/0e0c5hzwhvzje.js +0 -1
- package/.next/static/chunks/0g9aof0ob_fcj.js +0 -1
- package/.next/static/chunks/0j6t.m1a6._pk.js +0 -1
- package/.next/static/chunks/0s8jo6jypzkx5.js +0 -1
- package/.next/static/chunks/0u359llqm1_01.js +0 -1
- package/.next/static/chunks/0ubqqhhpsaez4.js +0 -1
- package/.next/static/chunks/0yz2othu-3th..js +0 -1
- package/.next/static/chunks/0~qw9kxksr1v2.js +0 -1
- package/.next/static/chunks/12gprw3l0kvvf.js +0 -1
- package/.next/static/chunks/12yuo353giomx.js +0 -1
- package/.next/static/chunks/14bsg3_-taiw4.js +0 -1
- /package/.next/build/chunks/{[root-of-the-server]__0ubbtyl._.js.map → [root-of-the-server]__0tgc-8p._.js.map} +0 -0
- /package/.next/build/chunks/{[turbopack-node]_transforms_postcss_ts_06e.r3r._.js.map → [turbopack-node]_transforms_postcss_ts_0r4x5ys._.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_anet_config_route_actions_0xzy73j.js.map → 0ci9_nt-network-dashboard__next-internal_server_app_api_auth_v3_route_actions_0f5nbzr.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_auth_login_route_actions_0zukc38.js.map → 0ci9_nt-network-dashboard__next-internal_server_app_favicon_ico_route_actions_0t0eu9r.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_auth_logout_route_actions_0fm~3ij.js.map → 0ykm__next-internal_server_app_api_anet_config_route_actions_0a~v55_.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_auth_v3_route_actions_0ndzs5l.js.map → 0ykm__next-internal_server_app_api_auth_login_route_actions_0xnxfs4.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_audit-log_route_actions_11k8fqt.js.map → 0ykm__next-internal_server_app_api_auth_logout_route_actions_0p8p0rn.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_auth_route_actions_0gs7ttk.js.map → 0ykm__next-internal_server_app_api_hub_audit-log_route_actions_06uquwi.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_broadcast_route_actions_0ads-kd.js.map → 0ykm__next-internal_server_app_api_hub_auth_route_actions_0f_y1ey.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_events_route_actions_0opktby.js.map → 0ykm__next-internal_server_app_api_hub_broadcast_route_actions_0ao3j43.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_health_route_actions_0tzzt~x.js.map → 0ykm__next-internal_server_app_api_hub_events_route_actions_0_5ndld.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_inbox_route_actions_0xvslrg.js.map → 0ykm__next-internal_server_app_api_hub_health_route_actions_0mpzl74.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_license_route_actions_0i3tlr3.js.map → 0ykm__next-internal_server_app_api_hub_inbox_route_actions_0stoyto.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_messages_route_actions_08-gi-s.js.map → 0ykm__next-internal_server_app_api_hub_license_route_actions_0a4.fuh.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_networks_route_actions_02vejq2.js.map → 0ykm__next-internal_server_app_api_hub_messages_route_actions_059rw1o.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_nodes_route_actions_0nx8ryn.js.map → 0ykm__next-internal_server_app_api_hub_networks_route_actions_03sehpn.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_register_route_actions_086wi_3.js.map → 0ykm__next-internal_server_app_api_hub_nodes_route_actions_11fbp28.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_send_route_actions_0-ga_ug.js.map → 0ykm__next-internal_server_app_api_hub_register_route_actions_0l.d7~n.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_server-logs_route_actions_00euexv.js.map → 0ykm__next-internal_server_app_api_hub_send_route_actions_0.p82vy.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_session_route_actions_041wa62.js.map → 0ykm__next-internal_server_app_api_hub_server-logs_route_actions_05~c5lb.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_stats_route_actions_1242~od.js.map → 0ykm__next-internal_server_app_api_hub_servers_route_actions_08-m-cs.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_status_route_actions_0aml09a.js.map → 0ykm__next-internal_server_app_api_hub_session_route_actions_0b9ef2i.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_task-events_route_actions_02v1tmm.js.map → 0ykm__next-internal_server_app_api_hub_stats_route_actions_03u65u0.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_tasks_route_actions_0nl77v4.js.map → 0ykm__next-internal_server_app_api_hub_status_route_actions_0_71ear.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_tmux_route_actions_0u~aaza.js.map → 0ykm__next-internal_server_app_api_hub_task-events_route_actions_07md_ee.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_api_hub_tokens_route_actions_09hzf_1.js.map → 0ykm__next-internal_server_app_api_hub_tasks_route_actions_0er3uvk.js.map} +0 -0
- /package/.next/server/chunks/{_next-internal_server_app_favicon_ico_route_actions_095lj93.js.map → 0ykm__next-internal_server_app_api_hub_tmux_route_actions_0o_1-8u.js.map} +0 -0
- /package/.next/server/chunks/{ssr/_next-internal_server_app__global-error_page_actions_0k77kol.js.map → 0ykm__next-internal_server_app_api_hub_tokens_route_actions_09doluz.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app__not-found_page_actions_0eq97pa.js.map → 0dwh_ent-network-dashboard__next-internal_server_app_server-logs_page_actions_0hkk815.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_admin_page_actions_0ycnu8f.js.map → 0yas_gent-network-dashboard__next-internal_server_app__not-found_page_actions_0u.f-rz.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_login_page_actions_02kefem.js.map → 0yas_gent-network-dashboard__next-internal_server_app_tasks_[id]_page_actions_0r3~lho.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_logs_page_actions_0rvsl~d.js.map → 0ykm__next-internal_server_app__global-error_page_actions_100fxls.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_messages_page_actions_0gsqep_.js.map → 0ykm__next-internal_server_app_settings_networks_page_actions_0jgy57m.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_node_page_actions_0wd8cgl.js.map → 0ykm__next-internal_server_app_settings_tokens_page_actions_0l5h.k~.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_nodes_page_actions_0-.rv67.js.map → agent-network-dashboard__next-internal_server_app_admin_page_actions_0i~.x~j.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_page_actions_09-gtaw.js.map → agent-network-dashboard__next-internal_server_app_login_page_actions_0frwl15.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_server-logs_page_actions_0g1gb5c.js.map → agent-network-dashboard__next-internal_server_app_logs_page_actions_0xa2ox_.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_settings_networks_page_actions_09uw.sw.js.map → agent-network-dashboard__next-internal_server_app_messages_page_actions_10j3tg4.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_settings_page_actions_0mr68ai.js.map → agent-network-dashboard__next-internal_server_app_node_page_actions_0sl4ntd.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_settings_tokens_page_actions_0yi54x8.js.map → agent-network-dashboard__next-internal_server_app_nodes_page_actions_0cpgkfs.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_tasks_[id]_page_actions_0k_hm9g.js.map → agent-network-dashboard__next-internal_server_app_page_actions_08nuo36.js.map} +0 -0
- /package/.next/server/chunks/ssr/{_next-internal_server_app_tasks_page_actions_0_2o_cr.js.map → agent-network-dashboard__next-internal_server_app_settings_page_actions_0iv.lmk.js.map} +0 -0
- /package/.next/static/{wi8v9sMITbvGOeqsOVwUq → fRqnaYLZjeQTx1-47Rbd5}/_buildManifest.js +0 -0
- /package/.next/static/{wi8v9sMITbvGOeqsOVwUq → fRqnaYLZjeQTx1-47Rbd5}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{wi8v9sMITbvGOeqsOVwUq → fRqnaYLZjeQTx1-47Rbd5}/_ssgManifest.js +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { Session } from './types';
|
|
5
5
|
import { aliasAvatarColors, aliasInitial } from './AliasAvatar';
|
|
6
6
|
import { ChatPopover } from './ChatPopover';
|
|
7
7
|
import { vendorForModel, runtimeIdentity, identityLine } from '../lib/vendorIdentity';
|
|
8
|
+
import { parseHubTime, relativeAgo } from '../lib/time';
|
|
8
9
|
|
|
9
10
|
interface MessageFlow {
|
|
10
11
|
from_alias: string;
|
|
@@ -32,6 +33,9 @@ interface FlowLink {
|
|
|
32
33
|
to: string;
|
|
33
34
|
count: number;
|
|
34
35
|
content: string;
|
|
36
|
+
/** ISO timestamp of the most recent message on this edge — drives
|
|
37
|
+
* the Round 10 freshness fade so dormant links recede visually. */
|
|
38
|
+
last_at: string;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
const cx = 500;
|
|
@@ -85,36 +89,118 @@ function truncate(value: string, max: number) {
|
|
|
85
89
|
return value.length > max ? `${value.slice(0, max - 1)}...` : value;
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/** Round 46 / Loop: SWR-freshness chip.
|
|
93
|
+
*
|
|
94
|
+
* TopoGraph's data refreshes every 5 s via SWR but the user has no
|
|
95
|
+
* visible signal that the canvas they're looking at is current. The
|
|
96
|
+
* flow particles and chips (R42 active-links, R43 hub) tell you when
|
|
97
|
+
* the FLEET last did something, not when the DATA last syncing. This
|
|
98
|
+
* chip ticks `live · Ns` against a 1-second interval so the operator
|
|
99
|
+
* can trust freshness without doing internal math.
|
|
100
|
+
*
|
|
101
|
+
* Owns its own setInterval so the parent topology doesn't re-render
|
|
102
|
+
* every second (only this small chip does). lastSyncRef is updated
|
|
103
|
+
* whenever the `sessions` prop reference changes — that's the SWR
|
|
104
|
+
* refresh signal. */
|
|
105
|
+
function FreshnessChip({ sessions }: { sessions: unknown }) {
|
|
106
|
+
const lastSyncRef = useRef<number>(Date.now());
|
|
107
|
+
const [now, setNow] = useState(() => Date.now());
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
// sessions reference changed → SWR just delivered a new payload.
|
|
110
|
+
lastSyncRef.current = Date.now();
|
|
111
|
+
}, [sessions]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
114
|
+
return () => clearInterval(id);
|
|
115
|
+
}, []);
|
|
116
|
+
const sec = Math.max(0, Math.floor((now - lastSyncRef.current) / 1000));
|
|
117
|
+
// Tint the chip warmer when the data goes stale (>10s since last sync).
|
|
118
|
+
// SWR's default refreshInterval here is 5s, so anything past ~10s is
|
|
119
|
+
// either a poll miss or a network hiccup.
|
|
120
|
+
const stale = sec > 10;
|
|
121
|
+
return (
|
|
122
|
+
<span
|
|
123
|
+
className={stale
|
|
124
|
+
? "hidden sm:inline px-2.5 py-1 rounded-md bg-amber-500/10 text-amber-300 border border-amber-500/20 font-mono"
|
|
125
|
+
: "hidden sm:inline px-2.5 py-1 rounded-md bg-gray-500/10 text-gray-400 border border-gray-500/20 font-mono"}
|
|
126
|
+
title={stale ? `Last sync ${sec}s ago — SWR refresh may be lagging` : `Live data · refreshes every 5s · last sync ${sec}s ago`}
|
|
127
|
+
data-freshness-chip
|
|
128
|
+
>
|
|
129
|
+
live · {sec}s
|
|
130
|
+
</span>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Round 36 / Loop: prefers-reduced-motion hook.
|
|
135
|
+
*
|
|
136
|
+
* Round 29's a11y sweep zeroed CSS animations via media query, but SVG
|
|
137
|
+
* SMIL `<animate>` / `<animateMotion>` elements aren't reachable from CSS
|
|
138
|
+
* — they need JS to opt out. This hook reads the media query and listens
|
|
139
|
+
* for changes so the topology's flow particles, pulses, click ripple
|
|
140
|
+
* and hub breath honour the OS-level preference. */
|
|
141
|
+
function useReducedMotion(): boolean {
|
|
142
|
+
const [reduced, setReduced] = useState(false);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (typeof window === 'undefined' || !window.matchMedia) return;
|
|
145
|
+
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
146
|
+
setReduced(mq.matches);
|
|
147
|
+
const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);
|
|
148
|
+
mq.addEventListener('change', onChange);
|
|
149
|
+
return () => mq.removeEventListener('change', onChange);
|
|
150
|
+
}, []);
|
|
151
|
+
return reduced;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Round 38 / Loop: parseHubTime + relativeAgo factored to app/lib/time.ts
|
|
155
|
+
// — the Round 35 fix lives there now alongside the Round 37 mirror so
|
|
156
|
+
// any future TZ-safe parse update happens in one place.
|
|
157
|
+
|
|
158
|
+
/** Round 12 / Loop: status trio audit.
|
|
159
|
+
*
|
|
160
|
+
* Each (status × theme) cell returns a {primary, halo, text} trio. The trio
|
|
161
|
+
* invariant — keep it when adding states or tweaking shades:
|
|
162
|
+
*
|
|
163
|
+
* light: primary = <hue>-600 halo = <hue>-100 * text = <hue>-800/900
|
|
164
|
+
* dark: primary = <hue>-400/500 halo = <hue>-900 text = <hue>-100
|
|
165
|
+
*
|
|
166
|
+
* * offline.halo light deviates intentionally to slate-200 (#e2e8f0):
|
|
167
|
+
* slate-100 (#f1f5f9) is too close to the panel bg (#f8fafc) and the
|
|
168
|
+
* halo would vanish. The other three rows keep the 100-shade.
|
|
169
|
+
*
|
|
170
|
+
* Audit caught one cross-family drift before this round: online-other halo
|
|
171
|
+
* light was #dbeafe (blue-100) while its primary (#0284c7 sky-600) and text
|
|
172
|
+
* (#0c4a6e sky-900) were sky-family — now sky-100 (#e0f2fe). Tiny visual
|
|
173
|
+
* difference; large hygiene win — every trio is now mono-hue. */
|
|
88
174
|
function nodeStatus(session: Session, isOnline: boolean, isLight: boolean) {
|
|
89
175
|
if (!isOnline) {
|
|
90
176
|
return {
|
|
91
177
|
label: 'offline',
|
|
92
|
-
primary: isLight ? '#94a3b8' : '#6b7280',
|
|
93
|
-
halo: isLight ? '#e2e8f0' : '#111827',
|
|
94
|
-
text: isLight ? '#475569' : '#9ca3af',
|
|
178
|
+
primary: isLight ? '#94a3b8' : '#6b7280', // slate-400 / gray-500
|
|
179
|
+
halo: isLight ? '#e2e8f0' : '#111827', // slate-200* / gray-900
|
|
180
|
+
text: isLight ? '#475569' : '#9ca3af', // slate-600 / gray-400
|
|
95
181
|
};
|
|
96
182
|
}
|
|
97
183
|
if (session.status === 'working') {
|
|
98
184
|
return {
|
|
99
185
|
label: 'working',
|
|
100
|
-
primary: isLight ? '#059669' : '#22c55e',
|
|
101
|
-
halo: isLight ? '#d1fae5' : '#14532d',
|
|
102
|
-
text: isLight ? '#065f46' : '#dcfce7',
|
|
186
|
+
primary: isLight ? '#059669' : '#22c55e', // emerald-600 / green-500
|
|
187
|
+
halo: isLight ? '#d1fae5' : '#14532d', // emerald-100 / green-900
|
|
188
|
+
text: isLight ? '#065f46' : '#dcfce7', // emerald-800 / green-100
|
|
103
189
|
};
|
|
104
190
|
}
|
|
105
191
|
if (session.status === 'idle') {
|
|
106
192
|
return {
|
|
107
193
|
label: 'idle',
|
|
108
|
-
primary: isLight ? '#0d9488' : '#2dd4bf',
|
|
109
|
-
halo: isLight ? '#ccfbf1' : '#134e4a',
|
|
110
|
-
text: isLight ? '#115e59' : '#ccfbf1',
|
|
194
|
+
primary: isLight ? '#0d9488' : '#2dd4bf', // teal-600 / teal-400
|
|
195
|
+
halo: isLight ? '#ccfbf1' : '#134e4a', // teal-100 / teal-900
|
|
196
|
+
text: isLight ? '#115e59' : '#ccfbf1', // teal-800 / teal-100
|
|
111
197
|
};
|
|
112
198
|
}
|
|
113
199
|
return {
|
|
114
200
|
label: session.status || 'online',
|
|
115
|
-
primary: isLight ? '#0284c7' : '#38bdf8',
|
|
116
|
-
halo: isLight ? '#
|
|
117
|
-
text: isLight ? '#0c4a6e' : '#e0f2fe',
|
|
201
|
+
primary: isLight ? '#0284c7' : '#38bdf8', // sky-600 / sky-400
|
|
202
|
+
halo: isLight ? '#e0f2fe' : '#0c4a6e', // sky-100 / sky-900 (was blue-100 — drift fixed Round 12)
|
|
203
|
+
text: isLight ? '#0c4a6e' : '#e0f2fe', // sky-900 / sky-100
|
|
118
204
|
};
|
|
119
205
|
}
|
|
120
206
|
|
|
@@ -240,22 +326,59 @@ function commonPrefix(a: string, b: string): string {
|
|
|
240
326
|
return a.slice(0, i);
|
|
241
327
|
}
|
|
242
328
|
|
|
243
|
-
|
|
244
|
-
|
|
329
|
+
/** #83 + #111: group nodes that share a ≥2-char alias prefix OR a project_dir
|
|
330
|
+
* ("either criterion → same group", Vincent 4724). Union-find over the
|
|
331
|
+
* sessions; the component label prefers the shared project_dir's basename,
|
|
332
|
+
* else the common alias prefix. Returns alias → groupKey. */
|
|
333
|
+
function computeGroups(sessions: { alias: string; project_dir?: string | null }[]): Record<string, string> {
|
|
334
|
+
const n = sessions.length;
|
|
335
|
+
const parent = sessions.map((_, i) => i);
|
|
336
|
+
const find = (i: number): number => {
|
|
337
|
+
while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; }
|
|
338
|
+
return i;
|
|
339
|
+
};
|
|
340
|
+
const union = (a: number, b: number) => {
|
|
341
|
+
const ra = find(a), rb = find(b);
|
|
342
|
+
if (ra !== rb) parent[ra] = rb;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// union by shared project_dir
|
|
346
|
+
const byDir: Record<string, number[]> = {};
|
|
347
|
+
sessions.forEach((s, i) => {
|
|
348
|
+
const d = s.project_dir?.trim();
|
|
349
|
+
if (d) (byDir[d] ||= []).push(i);
|
|
350
|
+
});
|
|
351
|
+
for (const idxs of Object.values(byDir)) {
|
|
352
|
+
for (let k = 1; k < idxs.length; k++) union(idxs[0], idxs[k]);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// union by shared ≥2-char alias prefix — sort, link adjacent pairs
|
|
356
|
+
const order = sessions.map((_, i) => i).sort((a, b) => sessions[a].alias.localeCompare(sessions[b].alias));
|
|
357
|
+
for (let k = 0; k + 1 < order.length; k++) {
|
|
358
|
+
if (commonPrefix(sessions[order[k]].alias, sessions[order[k + 1]].alias).length >= 2) {
|
|
359
|
+
union(order[k], order[k + 1]);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// label each connected component
|
|
364
|
+
const comps: Record<number, number[]> = {};
|
|
365
|
+
for (let i = 0; i < n; i++) (comps[find(i)] ||= []).push(i);
|
|
245
366
|
const keys: Record<string, string> = {};
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
367
|
+
for (const members of Object.values(comps)) {
|
|
368
|
+
let label: string;
|
|
369
|
+
if (members.length === 1) {
|
|
370
|
+
label = sessions[members[0]].alias;
|
|
371
|
+
} else {
|
|
372
|
+
const dirs = new Set(members.map(i => sessions[i].project_dir?.trim()).filter(Boolean) as string[]);
|
|
373
|
+
if (dirs.size === 1) {
|
|
374
|
+
const d = [...dirs][0];
|
|
375
|
+
label = d.split('/').filter(Boolean).pop() || d;
|
|
376
|
+
} else {
|
|
377
|
+
label = members.map(i => sessions[i].alias).reduce((a, b) => commonPrefix(a, b));
|
|
378
|
+
if (label.length < 2) label = sessions[members[0]].alias;
|
|
379
|
+
}
|
|
255
380
|
}
|
|
256
|
-
const
|
|
257
|
-
for (let k = i; k <= j; k++) keys[sorted[k]] = key;
|
|
258
|
-
i = j + 1;
|
|
381
|
+
for (const i of members) keys[sessions[i].alias] = label;
|
|
259
382
|
}
|
|
260
383
|
return keys;
|
|
261
384
|
}
|
|
@@ -275,12 +398,20 @@ function buildFlowLinks(messages: MessageFlow[], positions: Record<string, Point
|
|
|
275
398
|
const key = `${message.from_alias}->${message.to_alias}`;
|
|
276
399
|
const current = links.get(key);
|
|
277
400
|
|
|
401
|
+
// Keep the most-recent timestamp per pair so the render can fade
|
|
402
|
+
// dormant edges (Round 10 freshness fade).
|
|
403
|
+
const incoming = message.created_at || '';
|
|
404
|
+
const last_at = !current
|
|
405
|
+
? incoming
|
|
406
|
+
: (incoming > current.last_at ? incoming : current.last_at);
|
|
407
|
+
|
|
278
408
|
links.set(key, {
|
|
279
409
|
key,
|
|
280
410
|
from: message.from_alias,
|
|
281
411
|
to: message.to_alias,
|
|
282
412
|
count: (current?.count || 0) + 1,
|
|
283
413
|
content: current?.content || message.content,
|
|
414
|
+
last_at,
|
|
284
415
|
});
|
|
285
416
|
});
|
|
286
417
|
|
|
@@ -290,6 +421,7 @@ function buildFlowLinks(messages: MessageFlow[], positions: Record<string, Point
|
|
|
290
421
|
export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProps) {
|
|
291
422
|
const theme = useTheme();
|
|
292
423
|
const isLight = theme === 'light';
|
|
424
|
+
const reducedMotion = useReducedMotion();
|
|
293
425
|
const pal = isLight ? LIGHT_PALETTE : DARK_PALETTE;
|
|
294
426
|
const brand = useBrand();
|
|
295
427
|
const isIntern = brand === 'intern';
|
|
@@ -310,6 +442,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
310
442
|
try { localStorage.setItem('anet-topo-layout', next); } catch {}
|
|
311
443
|
return next;
|
|
312
444
|
});
|
|
445
|
+
// Issue #113: node size scale (Vincent 4727 — nodes too big / crowded at
|
|
446
|
+
// ~22 nodes). S/M/L → 0.7/0.84/1.0; default M (one notch down from the old
|
|
447
|
+
// fixed size). Persisted like the layout toggle.
|
|
448
|
+
const [nodeScale, setNodeScale] = useState(0.84);
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
try {
|
|
451
|
+
const saved = parseFloat(localStorage.getItem('anet-topo-nodescale') || '');
|
|
452
|
+
if (saved === 0.7 || saved === 0.84 || saved === 1) setNodeScale(saved);
|
|
453
|
+
} catch {}
|
|
454
|
+
}, []);
|
|
455
|
+
const pickNodeScale = (v: number) => {
|
|
456
|
+
setNodeScale(v);
|
|
457
|
+
try { localStorage.setItem('anet-topo-nodescale', String(v)); } catch {}
|
|
458
|
+
};
|
|
313
459
|
|
|
314
460
|
useEffect(() => {
|
|
315
461
|
const fetchMessages = async () => {
|
|
@@ -337,6 +483,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
337
483
|
activeAliases,
|
|
338
484
|
groupKeys,
|
|
339
485
|
groupBoxes,
|
|
486
|
+
gridContentBottom,
|
|
340
487
|
} = useMemo(() => {
|
|
341
488
|
const sseCount = (s: { alias: string; network_id?: string }) =>
|
|
342
489
|
(s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];
|
|
@@ -346,8 +493,28 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
346
493
|
// index, so contiguous-in-array becomes contiguous-in-ring, i.e.
|
|
347
494
|
// each team visually clusters. localeCompare keeps CJK ordering sane.
|
|
348
495
|
const byAlias = (a: Session, b: Session) => a.alias.localeCompare(b.alias);
|
|
496
|
+
// #112 umbrella: ghost age-out — an offline node not seen recently is
|
|
497
|
+
// almost certainly a deleted node the server `/api/status` still
|
|
498
|
+
// returns (#74 root cause is server-side; this is the dashboard-side
|
|
499
|
+
// fallback so stale ghosts stop cluttering the topology). A missing
|
|
500
|
+
// last_seen_at is kept (conservative — could be a legitimately new node).
|
|
501
|
+
// Round 27 / P0: the 24h threshold was too lenient — Vincent's preview.29
|
|
502
|
+
// screenshot showed B站马 nodes deleted ~4 h earlier still visible. A
|
|
503
|
+
// healthy agent heartbeats every few seconds; if it's been silent for
|
|
504
|
+
// an hour it's effectively gone. 1 h gives a fresh disconnect time to
|
|
505
|
+
// come back while removing dead nodes well within an operator session.
|
|
506
|
+
const GHOST_MS = 60 * 60 * 1000;
|
|
507
|
+
const now = Date.now();
|
|
508
|
+
const isGhost = (s: Session) => {
|
|
509
|
+
if (s.status !== 'offline' || sseCount(s) || !s.last_seen_at) return false;
|
|
510
|
+
// Round 35 / Loop: parseHubTime normalises SQL-style timestamps to UTC
|
|
511
|
+
// before parsing so non-UTC browsers don't see a phantom 8-hour skew
|
|
512
|
+
// and ghost freshly-disconnected nodes.
|
|
513
|
+
const t = parseHubTime(s.last_seen_at);
|
|
514
|
+
return t !== null && now - t > GHOST_MS;
|
|
515
|
+
};
|
|
349
516
|
const online = sessions.filter(s => s.status !== 'offline' || sseCount(s)).sort(byAlias);
|
|
350
|
-
const offline = sessions.filter(s => s.status === 'offline' && !sseCount(s)).sort(byAlias);
|
|
517
|
+
const offline = sessions.filter(s => s.status === 'offline' && !sseCount(s) && !isGhost(s)).sort(byAlias);
|
|
351
518
|
const positions: Record<string, Point> = {};
|
|
352
519
|
|
|
353
520
|
if (layout === 'grid') {
|
|
@@ -357,10 +524,16 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
357
524
|
// shared rows. Group-banded placement keeps every group's bounding box
|
|
358
525
|
// (#111) a clean rectangle — no box ever overlaps another group's.
|
|
359
526
|
const all = [...online, ...offline];
|
|
360
|
-
const groupKeys =
|
|
527
|
+
const groupKeys = computeGroups(all);
|
|
361
528
|
const cols = Math.max(1, Math.ceil(Math.sqrt(all.length)));
|
|
362
|
-
|
|
529
|
+
// #112: gy0 starts below the (now compact) recent-signal / legend
|
|
530
|
+
// overlay panels — their max bottom edge is y≈112 — so grid row 0 is
|
|
531
|
+
// never occluded; gy1 extends near the canvas bottom for breathing room.
|
|
532
|
+
const gx0 = 150, gx1 = 850, gy0 = 126, gy1 = 652;
|
|
363
533
|
const cellW = (gx1 - gx0) / cols;
|
|
534
|
+
// largest node radius at the current size scale — drives the group
|
|
535
|
+
// label band + the row-height floor so nothing ever overlaps.
|
|
536
|
+
const nodeR = Math.round(26 * nodeScale);
|
|
364
537
|
|
|
365
538
|
// ordered runs of consecutive same-group-key nodes (≥2 = real group)
|
|
366
539
|
const runs: { key: string; members: Session[] }[] = [];
|
|
@@ -394,9 +567,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
394
567
|
}
|
|
395
568
|
}
|
|
396
569
|
const totalRows = Math.max(1, row);
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
|
|
570
|
+
// #112: the group label sits in a band ABOVE the topmost node, so the
|
|
571
|
+
// band must clear the node radius — GROUP_TOP is node-relative, never
|
|
572
|
+
// cellH-derived (cellH-derived was the label↔node overlap bug Vincent
|
|
573
|
+
// hit). cellH then floors at GROUP_TOP + 30 so the label band + bottom
|
|
574
|
+
// padding always fit and stacked group boxes never touch; past the
|
|
575
|
+
// floor the grid overflows and zoom/pan handles it.
|
|
576
|
+
const GROUP_TOP = nodeR + 20;
|
|
577
|
+
// Round 27 / P0 (Vincent screenshot preview.29): dense plain-text
|
|
578
|
+
// node labels (`pos.y + radius + denseDrop`, with a 3 px containerBg
|
|
579
|
+
// stroke halo for readability) at the BOTTOM row of band N would
|
|
580
|
+
// paint OVER the start of band N+1's group label, creating the
|
|
581
|
+
// "blueleap → eleap", "agent-network-dashboard → t-network / board"
|
|
582
|
+
// visual chopping. Geometry of the collision:
|
|
583
|
+
// dense label visual bottom = node_N.y + radius + denseDrop + halo
|
|
584
|
+
// group label glyph top = boxY + 4 (text y=boxY+14, ~10px ascent)
|
|
585
|
+
// = node_N+1.y - GROUP_TOP + 4
|
|
586
|
+
// node_N+1.y - node_N.y = cellH for consecutive rows, so no-collide:
|
|
587
|
+
// cellH ≥ radius + denseDrop + halo + GROUP_TOP - 4 + buffer
|
|
588
|
+
// With halo=3 buffer=4: cellH ≥ nodeR + denseDrop + GROUP_TOP + 3.
|
|
589
|
+
const denseDrop = nodeScale < 0.8 ? 12 : 14;
|
|
590
|
+
const cellH = Math.max(
|
|
591
|
+
2 * nodeR + 22, // node + dense label within band
|
|
592
|
+
GROUP_TOP + 12, // group-label band + box padding
|
|
593
|
+
nodeR + denseDrop + GROUP_TOP + 3, // round-27 dense↔group label clearance
|
|
594
|
+
Math.min(100, (gy1 - gy0) / totalRows),
|
|
595
|
+
);
|
|
400
596
|
|
|
401
597
|
// Pass 2 — place each band's members.
|
|
402
598
|
for (const band of bands) {
|
|
@@ -416,11 +612,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
416
612
|
const active = new Set<string>();
|
|
417
613
|
links.forEach(link => { active.add(link.from); active.add(link.to); });
|
|
418
614
|
// #111: one bounding box per multi-member group (Vincent 4722). Each
|
|
419
|
-
// group owns its rows
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
const
|
|
423
|
-
const GROUP_PAD = Math.max(16, Math.min(32, cellH * 0.3)); // side/bottom
|
|
615
|
+
// group owns its rows; GROUP_PAD fills the row space left below the
|
|
616
|
+
// nodes after the label band, so stacked group boxes always have a
|
|
617
|
+
// gap between them (GROUP_TOP is defined above, with cellH).
|
|
618
|
+
const GROUP_PAD = Math.max(8, Math.min(26, cellH - GROUP_TOP - 8)); // side/bottom
|
|
424
619
|
const groupBoxes = bands
|
|
425
620
|
.filter(b => b.isGroup)
|
|
426
621
|
.map(band => {
|
|
@@ -428,14 +623,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
428
623
|
const xs = pts.map(p => p.x);
|
|
429
624
|
const ys = pts.map(p => p.y);
|
|
430
625
|
const minX = Math.min(...xs), minY = Math.min(...ys);
|
|
626
|
+
// Round 58 / Loop: per-group status mix for the label pip strip.
|
|
627
|
+
// Working = status==='working'. Idle = online but not working.
|
|
628
|
+
// Offline = !isOnline (either status==='offline' AND no SSE, or
|
|
629
|
+
// ghost-purged elsewhere — but ghosts never reach groupBoxes
|
|
630
|
+
// since they're filtered out upstream). Counts feed the label
|
|
631
|
+
// tspans directly so the strip stays inside the label's bbox,
|
|
632
|
+
// preserving the node↔label overlap-test guarantee from R19.
|
|
633
|
+
let w = 0, i = 0, o = 0;
|
|
634
|
+
for (const s of band.members) {
|
|
635
|
+
const isOn = s.status !== 'offline' || !!sseCount(s);
|
|
636
|
+
if (s.status === 'working') w++;
|
|
637
|
+
else if (isOn) i++;
|
|
638
|
+
else o++;
|
|
639
|
+
}
|
|
431
640
|
return {
|
|
432
641
|
key: band.members.length ? groupKeys[band.members[0].alias] : '',
|
|
642
|
+
count: band.members.length,
|
|
643
|
+
statuses: { working: w, idle: i, offline: o },
|
|
433
644
|
x: minX - GROUP_PAD,
|
|
434
645
|
y: minY - GROUP_TOP,
|
|
435
646
|
w: Math.max(...xs) - minX + GROUP_PAD * 2,
|
|
436
647
|
h: Math.max(...ys) - minY + GROUP_TOP + GROUP_PAD,
|
|
437
648
|
};
|
|
438
649
|
});
|
|
650
|
+
// Round 28 / Loop: surface the grid's natural content bottom so the
|
|
651
|
+
// mount effect can auto-fit zoom when the layout would overflow the
|
|
652
|
+
// viewBox. = bottom of the last node row + its label drop + a small
|
|
653
|
+
// breathing buffer. Round 27's cellH bump makes this overflow more
|
|
654
|
+
// common (30-node fleets reach ~774 px, viewBox is 680).
|
|
655
|
+
const gridContentBottom = gy0 + totalRows * cellH + 8;
|
|
439
656
|
return {
|
|
440
657
|
onlineNodes: online,
|
|
441
658
|
offlineNodes: offline,
|
|
@@ -444,6 +661,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
444
661
|
activeAliases: active,
|
|
445
662
|
groupKeys,
|
|
446
663
|
groupBoxes,
|
|
664
|
+
gridContentBottom,
|
|
447
665
|
};
|
|
448
666
|
}
|
|
449
667
|
|
|
@@ -514,7 +732,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
514
732
|
});
|
|
515
733
|
|
|
516
734
|
// Round 106 (issue #83): group key per alias → shared hue per team.
|
|
517
|
-
const groupKeys =
|
|
735
|
+
const groupKeys = computeGroups([...online, ...offline]);
|
|
518
736
|
|
|
519
737
|
return {
|
|
520
738
|
onlineNodes: online,
|
|
@@ -525,16 +743,70 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
525
743
|
groupKeys,
|
|
526
744
|
// #111: group boxes are a grid-layout feature only — radially scattered
|
|
527
745
|
// ring nodes can't be cleanly boxed. Ring keeps the #83 prefix hue.
|
|
528
|
-
groupBoxes: [] as { key: string; x: number; y: number; w: number; h: number }[],
|
|
746
|
+
groupBoxes: [] as { key: string; count: number; statuses: { working: number; idle: number; offline: number }; x: number; y: number; w: number; h: number }[],
|
|
747
|
+
// ring fits within VIEWBOX_H by construction (offlineRadius=325 + centre at y=330)
|
|
748
|
+
gridContentBottom: 0,
|
|
529
749
|
};
|
|
530
|
-
}, [messages, sessions, sseSessions, layout]);
|
|
750
|
+
}, [messages, sessions, sseSessions, layout, nodeScale]);
|
|
531
751
|
|
|
532
752
|
const workingCount = onlineNodes.filter(s => s.status === 'working').length;
|
|
753
|
+
// Round 6 / Loop: vendor distribution for the header chip — at a glance
|
|
754
|
+
// "what's in the fleet" (A:5 M:2 O:8 书:12 …) without opening a node.
|
|
755
|
+
// Sorted by count desc; "unknown" vendors collapse into a "?" bucket.
|
|
756
|
+
const vendorDist = useMemo(() => {
|
|
757
|
+
const tally = new Map<string, { initial: string; count: number; color: string }>();
|
|
758
|
+
for (const s of [...onlineNodes, ...offlineNodes]) {
|
|
759
|
+
const v = vendorForModel(s.model);
|
|
760
|
+
const key = v.id === 'unknown' ? '?' : v.initial;
|
|
761
|
+
const cur = tally.get(key);
|
|
762
|
+
if (cur) cur.count++;
|
|
763
|
+
else tally.set(key, { initial: key, count: 1, color: v.mono.text });
|
|
764
|
+
}
|
|
765
|
+
return [...tally.values()].sort((a, b) => b.count - a.count);
|
|
766
|
+
}, [onlineNodes, offlineNodes]);
|
|
533
767
|
// Round 109 (Vincent 4582 P0): hover-gated labels above this node count
|
|
534
768
|
// so dense fleets show clean avatars instead of a wall of overlapping
|
|
535
769
|
// label cards. 16 ≈ where the triple-tier rings start to crowd.
|
|
536
770
|
const denseLayout = onlineNodes.length + offlineNodes.length > 16;
|
|
537
771
|
const [hoveredAlias, setHoveredAlias] = useState<string | null>(null);
|
|
772
|
+
// Round 8 / Loop: which group is currently focused. Nodes outside this
|
|
773
|
+
// group + other group boxes fade so the eye locks onto the team you're
|
|
774
|
+
// pointing at. Singletons use their own alias as the group key.
|
|
775
|
+
const hoveredGroup = hoveredAlias ? (groupKeys[hoveredAlias] ?? hoveredAlias) : null;
|
|
776
|
+
// Round 63 / Loop: sticky group focus. R8 dims non-group nodes while
|
|
777
|
+
// a member is hovered, but releasing the hover lets the focus fade.
|
|
778
|
+
// Clicking the group label pins the group key here; activeGroup =
|
|
779
|
+
// hoveredGroup ?? pinnedGroup so hover transiently overrides the
|
|
780
|
+
// pin (handy for spot-comparing teams). Same compose pattern as
|
|
781
|
+
// R60/R61 pinnedStatus.
|
|
782
|
+
const [pinnedGroup, setPinnedGroup] = useState<string | null>(null);
|
|
783
|
+
const activeGroup = hoveredGroup ?? pinnedGroup;
|
|
784
|
+
// Round 49 / Loop: reverse-direction of R40's edge-on-node-hover linkage.
|
|
785
|
+
// R48 widened the flow hitbox to 16 px, so edges are precise enough to
|
|
786
|
+
// serve as a state trigger. When the user hovers a flow edge, light up its
|
|
787
|
+
// two endpoint nodes and dim the rest — "who is this edge between" becomes
|
|
788
|
+
// visible without reading the tooltip. The set is the link's two aliases
|
|
789
|
+
// (null when no edge hovered); node opacity composes this after inFocus.
|
|
790
|
+
const [hoveredEdgeKey, setHoveredEdgeKey] = useState<string | null>(null);
|
|
791
|
+
// Round 55 / Loop: hovering a legend status row dims nodes whose status
|
|
792
|
+
// doesn't match. The legend was passive — "what does this colour mean".
|
|
793
|
+
// Now it answers "show me all of these" the same way R8 group-focus
|
|
794
|
+
// answers "show me this team". Three values match the legend rows.
|
|
795
|
+
const [hoveredStatus, setHoveredStatus] = useState<'working' | 'idle' | 'offline' | null>(null);
|
|
796
|
+
// Round 60 / Loop: sticky variant of `hoveredStatus`. R55 only filters
|
|
797
|
+
// while the user is actively hovering the legend; for sweeping a fleet
|
|
798
|
+
// you want to LOCK the filter. Each segment of the R31 pressure bar
|
|
799
|
+
// toggles a pin — click again on the same segment to release. The node
|
|
800
|
+
// opacity formula reads `activeStatus = hoveredStatus ?? pinnedStatus`
|
|
801
|
+
// so hover transiently overrides a pin (handy for spot-comparison)
|
|
802
|
+
// without nuking it.
|
|
803
|
+
const [pinnedStatus, setPinnedStatus] = useState<'working' | 'idle' | 'offline' | null>(null);
|
|
804
|
+
const activeStatus = hoveredStatus ?? pinnedStatus;
|
|
805
|
+
const hoveredEdgeEndpoints = useMemo<Set<string> | null>(() => {
|
|
806
|
+
if (!hoveredEdgeKey) return null;
|
|
807
|
+
const link = flowLinks.find(l => l.key === hoveredEdgeKey);
|
|
808
|
+
return link ? new Set([link.from, link.to]) : null;
|
|
809
|
+
}, [hoveredEdgeKey, flowLinks]);
|
|
538
810
|
|
|
539
811
|
// --- Round 103 (issue #81): fullscreen + zoom + pan interaction layer ---
|
|
540
812
|
// DIY native (no d3 / svg-pan-zoom): wrap the topology content in a single
|
|
@@ -554,6 +826,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
554
826
|
// Issue #100: singleton chat popover. One alias at a time — clicking
|
|
555
827
|
// another node swaps the target and the conversation switches in place.
|
|
556
828
|
const [chatAlias, setChatAlias] = useState<string | null>(null);
|
|
829
|
+
// Round 14 / Loop: one-shot click ripple — fades outward from the clicked
|
|
830
|
+
// node so the click registers physically before the popover snaps in.
|
|
831
|
+
// Pairs with the Round 11 chat-focus ring: ripple expands, ring locks on.
|
|
832
|
+
// Keyed by ts so re-clicking the same node remounts the <circle> and the
|
|
833
|
+
// SMIL <animate> replays. Cleared 600ms after click (longer than the
|
|
834
|
+
// 500ms animation so a final frame at opacity 0 is still in the tree).
|
|
835
|
+
const [clickRipple, setClickRipple] = useState<{
|
|
836
|
+
ts: number; x: number; y: number; r0: number; color: string;
|
|
837
|
+
} | null>(null);
|
|
557
838
|
// #84: when a node is renamed while its chat popover is open, follow the
|
|
558
839
|
// rename so the conversation keeps targeting a live alias.
|
|
559
840
|
useEffect(() => {
|
|
@@ -584,6 +865,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
584
865
|
} catch {}
|
|
585
866
|
}, []);
|
|
586
867
|
|
|
868
|
+
// Round 28 / Loop: first-paint auto-fit. Round 27's cellH bump means
|
|
869
|
+
// dense grids (≥6-7 rows) overflow the 680-px viewBox at the natural
|
|
870
|
+
// 100% zoom — a new user sees the topology with its bottom rows
|
|
871
|
+
// clipped and no hint that there's more below. Auto-fit on first
|
|
872
|
+
// paint (and only if the user has no persisted view) sets the
|
|
873
|
+
// initial zoom so all content fits. Subsequent zoom changes persist
|
|
874
|
+
// and override the auto-fit on reload; explicit reset (0 key) still
|
|
875
|
+
// goes back to 100% so the gesture's "reset to natural size" semantic
|
|
876
|
+
// is preserved.
|
|
877
|
+
//
|
|
878
|
+
// Capture pre-mount persistence in a useState initializer — the
|
|
879
|
+
// existing `persist` effect (declared below) runs on first render
|
|
880
|
+
// with the default {1,0,0} view and writes it to localStorage before
|
|
881
|
+
// the auto-fit effect (deps on async-arriving sessions) gets a turn.
|
|
882
|
+
// Reading localStorage from the effect would see that write and
|
|
883
|
+
// skip the fit. The useState snapshot fires once, before any effects.
|
|
884
|
+
const [hadPersistedViewOnMount] = useState<boolean>(
|
|
885
|
+
() => typeof window !== 'undefined' && !!localStorage.getItem('anet-topo-view'),
|
|
886
|
+
);
|
|
887
|
+
const autoFitDoneRef = useRef(false);
|
|
888
|
+
useEffect(() => {
|
|
889
|
+
if (autoFitDoneRef.current) return;
|
|
890
|
+
if (hadPersistedViewOnMount) {
|
|
891
|
+
autoFitDoneRef.current = true;
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (layout !== 'grid' || sessions.length === 0 || !gridContentBottom) return;
|
|
895
|
+
if (gridContentBottom <= VIEWBOX_H) {
|
|
896
|
+
autoFitDoneRef.current = true; // no overflow → no fit needed
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const fitZoom = Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));
|
|
900
|
+
setView({ zoom: fitZoom, x: 0, y: 0 });
|
|
901
|
+
autoFitDoneRef.current = true;
|
|
902
|
+
}, [layout, sessions.length, gridContentBottom, hadPersistedViewOnMount]);
|
|
903
|
+
|
|
587
904
|
// persist view
|
|
588
905
|
useEffect(() => {
|
|
589
906
|
try { localStorage.setItem('anet-topo-view', JSON.stringify(view)); } catch {}
|
|
@@ -603,6 +920,14 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
603
920
|
const svg = svgRef.current;
|
|
604
921
|
if (!svg) return;
|
|
605
922
|
const onWheel = (e: WheelEvent) => {
|
|
923
|
+
// Round 23 / Loop: only zoom on Ctrl/Meta+wheel when inline; plain
|
|
924
|
+
// wheel over the canvas keeps scrolling the page (the topo sits at
|
|
925
|
+
// the top of /; trapping page scroll mid-page is the classic
|
|
926
|
+
// "scroll-jail" anti-pattern). In fullscreen the canvas owns the
|
|
927
|
+
// viewport so any wheel zooms — no page scroll to preserve. Pinch-
|
|
928
|
+
// zoom on a trackpad surfaces as ctrlKey=true natively, which is
|
|
929
|
+
// also what we want (zoom the canvas, not the browser).
|
|
930
|
+
if (!isFullscreen && !e.ctrlKey && !e.metaKey) return;
|
|
606
931
|
e.preventDefault();
|
|
607
932
|
const rect = svg.getBoundingClientRect();
|
|
608
933
|
const mx = ((e.clientX - rect.left) / rect.width) * VIEWBOX_W;
|
|
@@ -623,8 +948,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
623
948
|
};
|
|
624
949
|
svg.addEventListener('wheel', onWheel, { passive: false });
|
|
625
950
|
return () => svg.removeEventListener('wheel', onWheel);
|
|
626
|
-
|
|
627
|
-
|
|
951
|
+
// Re-attach when the fullscreen flag toggles so the handler's
|
|
952
|
+
// closure picks up the new value (capture-by-closure means we'd
|
|
953
|
+
// otherwise read the stale boolean forever).
|
|
954
|
+
}, [isFullscreen]);
|
|
955
|
+
|
|
956
|
+
// Round 21 / Loop: pan-aware cursor. Static `grab` gave no tactile cue
|
|
957
|
+
// that the canvas was actually being dragged — "grabbing" while
|
|
958
|
+
// dragRef.current.active = true tells the hand it's moving the
|
|
959
|
+
// viewport. Mirroring dragRef.active into state is the only way to
|
|
960
|
+
// re-render the SVG style; the ref itself doesn't trigger renders.
|
|
961
|
+
const [isPanning, setIsPanning] = useState(false);
|
|
628
962
|
const onPointerDown = (e: React.PointerEvent<SVGSVGElement>) => {
|
|
629
963
|
if (e.button !== 0) return;
|
|
630
964
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
@@ -635,6 +969,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
635
969
|
baseX: viewRef.current.x,
|
|
636
970
|
baseY: viewRef.current.y,
|
|
637
971
|
};
|
|
972
|
+
setIsPanning(true);
|
|
638
973
|
};
|
|
639
974
|
const onPointerMove = (e: React.PointerEvent<SVGSVGElement>) => {
|
|
640
975
|
const d = dragRef.current;
|
|
@@ -649,6 +984,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
649
984
|
const onPointerUp = (e: React.PointerEvent<SVGSVGElement>) => {
|
|
650
985
|
if (!dragRef.current.active) return;
|
|
651
986
|
dragRef.current.active = false;
|
|
987
|
+
setIsPanning(false);
|
|
652
988
|
try {
|
|
653
989
|
(e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
|
|
654
990
|
} catch {}
|
|
@@ -666,6 +1002,64 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
666
1002
|
};
|
|
667
1003
|
const resetView = () => setView({ zoom: 1, x: 0, y: 0 });
|
|
668
1004
|
|
|
1005
|
+
// Round 29 / Loop: `f` = fit-to-content. Shared by the Round 28
|
|
1006
|
+
// first-paint auto-fit effect and the keyboard handler so the math is
|
|
1007
|
+
// in one place. When content already fits at natural zoom, this is
|
|
1008
|
+
// effectively a "recenter" — `f` always lands on a known good view.
|
|
1009
|
+
const fitView = useCallback(() => {
|
|
1010
|
+
const zoom = !gridContentBottom || gridContentBottom <= VIEWBOX_H
|
|
1011
|
+
? 1
|
|
1012
|
+
: Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));
|
|
1013
|
+
setView({ zoom, x: 0, y: 0 });
|
|
1014
|
+
}, [gridContentBottom]);
|
|
1015
|
+
|
|
1016
|
+
// Round 22 / Loop: keyboard zoom — +/= zoom in, - zoom out, 0 reset.
|
|
1017
|
+
// Round 29 / Loop: +f to fit content.
|
|
1018
|
+
// Listen on window so the user doesn't need to focus the SVG first,
|
|
1019
|
+
// but only act when no text input has focus (otherwise typing "-" in
|
|
1020
|
+
// a chat would zoom the topology — surprising and bad). Modifier keys
|
|
1021
|
+
// pass through so Cmd/Ctrl combos still hit their owners. The button
|
|
1022
|
+
// titles below document these shortcuts so users discover them.
|
|
1023
|
+
useEffect(() => {
|
|
1024
|
+
const onKey = (e: KeyboardEvent) => {
|
|
1025
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
1026
|
+
const ae = document.activeElement as HTMLElement | null;
|
|
1027
|
+
if (ae) {
|
|
1028
|
+
const tag = ae.tagName;
|
|
1029
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || ae.isContentEditable) return;
|
|
1030
|
+
}
|
|
1031
|
+
if (e.key === '+' || e.key === '=') { zoomBy(1.2); e.preventDefault(); }
|
|
1032
|
+
else if (e.key === '-' || e.key === '_') { zoomBy(1 / 1.2); e.preventDefault(); }
|
|
1033
|
+
else if (e.key === '0') { resetView(); e.preventDefault(); }
|
|
1034
|
+
else if (e.key === 'f' || e.key === 'F') { fitView(); e.preventDefault(); }
|
|
1035
|
+
// Round 32 / Loop: `l` toggles ring|grid. The vim-style `g l` route
|
|
1036
|
+
// (Audit Log) requires a preceding `g` within 1500ms; a bare `l`
|
|
1037
|
+
// outside that window is free for topology use.
|
|
1038
|
+
else if (e.key === 'l' || e.key === 'L') { toggleLayout(); e.preventDefault(); }
|
|
1039
|
+
// Round 62 / Loop: Esc clears the R60/R61 pinned status filter so
|
|
1040
|
+
// users have a universal-cancel keyboard out. Esc on an open chat
|
|
1041
|
+
// is owned by ChatPopover (which only mounts when chatAlias is
|
|
1042
|
+
// set), so this handler is effectively scoped to "no chat open".
|
|
1043
|
+
// We additionally guard on chatAlias to be explicit — if the chat
|
|
1044
|
+
// closed mid-cycle, the pin can still be cleared on the next Esc.
|
|
1045
|
+
// R63: extends to pinnedGroup too. Clears WHATEVER pin is active
|
|
1046
|
+
// (one Esc collapses all topology pins) so the keyboard escape
|
|
1047
|
+
// route stays a single key, not "Esc maybe Esc again".
|
|
1048
|
+
else if (e.key === 'Escape' && !chatAlias && (pinnedStatus || pinnedGroup)) {
|
|
1049
|
+
if (pinnedStatus) setPinnedStatus(null);
|
|
1050
|
+
if (pinnedGroup) setPinnedGroup(null);
|
|
1051
|
+
e.preventDefault();
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
window.addEventListener('keydown', onKey);
|
|
1055
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
1056
|
+
// zoomBy / resetView are stable wrt setView callback; fitView changes
|
|
1057
|
+
// with gridContentBottom so deps list catches it. R62 adds chatAlias
|
|
1058
|
+
// and pinnedStatus so the Escape branch reads fresh state (re-binding
|
|
1059
|
+
// the listener on these state changes is sub-ms — cheaper than refs).
|
|
1060
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1061
|
+
}, [fitView, chatAlias, pinnedStatus, pinnedGroup]);
|
|
1062
|
+
|
|
669
1063
|
const toggleFullscreen = () => {
|
|
670
1064
|
const el = containerRef.current;
|
|
671
1065
|
if (!el) return;
|
|
@@ -693,6 +1087,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
693
1087
|
<button
|
|
694
1088
|
onClick={() => { if (layout !== 'ring') toggleLayout(); }}
|
|
695
1089
|
aria-pressed={layout === 'ring'}
|
|
1090
|
+
title="Ring layout (l to toggle)"
|
|
696
1091
|
className={`px-2.5 py-1 transition-colors ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300' : 'text-gray-500 hover:text-gray-400'}`}
|
|
697
1092
|
>
|
|
698
1093
|
Ring
|
|
@@ -700,6 +1095,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
700
1095
|
<button
|
|
701
1096
|
onClick={() => { if (layout !== 'grid') toggleLayout(); }}
|
|
702
1097
|
aria-pressed={layout === 'grid'}
|
|
1098
|
+
title="Grid layout (l to toggle)"
|
|
703
1099
|
className={`px-2.5 py-1 border-l border-gray-500/25 transition-colors ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300' : 'text-gray-500 hover:text-gray-400'}`}
|
|
704
1100
|
>
|
|
705
1101
|
Grid
|
|
@@ -711,9 +1107,161 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
711
1107
|
<span className="px-2.5 py-1 rounded-md bg-cyan-500/10 text-cyan-300 border border-cyan-500/20">
|
|
712
1108
|
{onlineNodes.length} online
|
|
713
1109
|
</span>
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1110
|
+
{/* Round 31 / Loop: fleet-health pressure bar. The "X working /
|
|
1111
|
+
Y online" chips already carry the raw counts; the bar lets
|
|
1112
|
+
the eye get the working/idle/offline RATIO in one glance
|
|
1113
|
+
without mental math. Stacked 3-segment chip, ~64 px wide. */}
|
|
1114
|
+
{(() => {
|
|
1115
|
+
const w = workingCount;
|
|
1116
|
+
const i = onlineNodes.length - workingCount; // idle = online - working
|
|
1117
|
+
const o = offlineNodes.length;
|
|
1118
|
+
const total = w + i + o;
|
|
1119
|
+
if (total === 0) return null;
|
|
1120
|
+
// Round 60 / Loop: each segment toggles a sticky filter via
|
|
1121
|
+
// `pinnedStatus`. Click the working segment → all non-working
|
|
1122
|
+
// nodes dim; click again → release. Segments share width with
|
|
1123
|
+
// their proportion of `total`, so a 1-node working share is
|
|
1124
|
+
// ~3 px wide on a 64-px bar. We pad the click target with a
|
|
1125
|
+
// negative-margin overlay wrapper to give thin slices a
|
|
1126
|
+
// 14-px minimum hit area without disturbing the rendered
|
|
1127
|
+
// chip width. cursor:pointer + a one-line title hint at the
|
|
1128
|
+
// affordance.
|
|
1129
|
+
const seg = (n: number, color: string, key: 'working' | 'idle' | 'offline', label: string) => {
|
|
1130
|
+
if (n === 0) return null;
|
|
1131
|
+
const isPinned = pinnedStatus === key;
|
|
1132
|
+
return (
|
|
1133
|
+
<span
|
|
1134
|
+
key={key}
|
|
1135
|
+
data-pressure-seg={key}
|
|
1136
|
+
role="button"
|
|
1137
|
+
tabIndex={0}
|
|
1138
|
+
aria-pressed={isPinned}
|
|
1139
|
+
title={`${n} ${label} — click to ${isPinned ? 'release filter' : 'highlight'}`}
|
|
1140
|
+
style={{
|
|
1141
|
+
width: `${(n / total) * 100}%`,
|
|
1142
|
+
background: color,
|
|
1143
|
+
height: '100%',
|
|
1144
|
+
cursor: 'pointer',
|
|
1145
|
+
boxShadow: isPinned ? `inset 0 0 0 1px ${color}, inset 0 0 0 2px rgba(255,255,255,0.6)` : undefined,
|
|
1146
|
+
}}
|
|
1147
|
+
onClick={(e) => {
|
|
1148
|
+
e.stopPropagation();
|
|
1149
|
+
setPinnedStatus(prev => prev === key ? null : key);
|
|
1150
|
+
}}
|
|
1151
|
+
onKeyDown={(e) => {
|
|
1152
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
1153
|
+
e.preventDefault();
|
|
1154
|
+
setPinnedStatus(prev => prev === key ? null : key);
|
|
1155
|
+
}
|
|
1156
|
+
}}
|
|
1157
|
+
/>
|
|
1158
|
+
);
|
|
1159
|
+
};
|
|
1160
|
+
// Round 47 / Loop: hidden on mobile — at <640px the chip row
|
|
1161
|
+
// wraps to multiple lines and crowds the topology header;
|
|
1162
|
+
// pressure ratio is best read with the working+online raw
|
|
1163
|
+
// counts (kept visible) anyway.
|
|
1164
|
+
return (
|
|
1165
|
+
<span
|
|
1166
|
+
className="hidden sm:inline-flex items-center gap-2 px-2.5 py-1 rounded-md bg-gray-500/10 text-gray-400 border border-gray-500/20 font-mono"
|
|
1167
|
+
title={`${w} working · ${i} idle · ${o} offline`}
|
|
1168
|
+
data-fleet-pressure
|
|
1169
|
+
>
|
|
1170
|
+
<span className="text-[10px] tracking-wide">pressure</span>
|
|
1171
|
+
<span className="inline-flex h-1.5 w-16 rounded-full overflow-hidden" style={{ background: 'rgb(75 85 99 / 0.25)' }}>
|
|
1172
|
+
{seg(w, isLight ? '#059669' : '#22c55e', 'working', 'working')}
|
|
1173
|
+
{seg(i, isLight ? '#0d9488' : '#2dd4bf', 'idle', 'idle')}
|
|
1174
|
+
{seg(o, isLight ? '#94a3b8' : '#6b7280', 'offline', 'offline')}
|
|
1175
|
+
</span>
|
|
1176
|
+
</span>
|
|
1177
|
+
);
|
|
1178
|
+
})()}
|
|
1179
|
+
{/* Round 64 / Loop: active-filter pills. When pinnedStatus or
|
|
1180
|
+
pinnedGroup is set, show a small "filter: <key> ×" pill so
|
|
1181
|
+
the user can see the pin from the chip row even if they
|
|
1182
|
+
scrolled the canvas off-screen. × button clears the
|
|
1183
|
+
specific pin (Esc still clears all — both paths are
|
|
1184
|
+
valid). pinnedStatus and pinnedGroup pin independently so
|
|
1185
|
+
both pills may render simultaneously. */}
|
|
1186
|
+
{pinnedStatus && (
|
|
1187
|
+
<span
|
|
1188
|
+
data-active-filter="status"
|
|
1189
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono text-xs border"
|
|
1190
|
+
style={{
|
|
1191
|
+
background: pinnedStatus === 'working' ? (isLight ? '#05966914' : '#22c55e1f')
|
|
1192
|
+
: pinnedStatus === 'idle' ? (isLight ? '#0d948814' : '#2dd4bf1f')
|
|
1193
|
+
: (isLight ? '#94a3b814' : '#6b72801f'),
|
|
1194
|
+
color: pinnedStatus === 'working' ? (isLight ? '#047857' : '#86efac')
|
|
1195
|
+
: pinnedStatus === 'idle' ? (isLight ? '#0f766e' : '#5eead4')
|
|
1196
|
+
: (isLight ? '#475569' : '#9ca3af'),
|
|
1197
|
+
borderColor: 'currentColor',
|
|
1198
|
+
}}
|
|
1199
|
+
>
|
|
1200
|
+
<span>filter: {pinnedStatus}</span>
|
|
1201
|
+
<button
|
|
1202
|
+
type="button"
|
|
1203
|
+
aria-label={`Clear ${pinnedStatus} filter`}
|
|
1204
|
+
onClick={() => setPinnedStatus(null)}
|
|
1205
|
+
className="ml-0.5 leading-none hover:opacity-70"
|
|
1206
|
+
style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
|
|
1207
|
+
>×</button>
|
|
1208
|
+
</span>
|
|
1209
|
+
)}
|
|
1210
|
+
{pinnedGroup && (
|
|
1211
|
+
<span
|
|
1212
|
+
data-active-filter="group"
|
|
1213
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono text-xs border"
|
|
1214
|
+
style={{
|
|
1215
|
+
background: isLight ? '#67e8f914' : '#67e8f91f',
|
|
1216
|
+
color: pal.legendAccent,
|
|
1217
|
+
borderColor: 'currentColor',
|
|
1218
|
+
}}
|
|
1219
|
+
>
|
|
1220
|
+
<span>filter: {pinnedGroup}</span>
|
|
1221
|
+
<button
|
|
1222
|
+
type="button"
|
|
1223
|
+
aria-label={`Clear group filter ${pinnedGroup}`}
|
|
1224
|
+
onClick={() => setPinnedGroup(null)}
|
|
1225
|
+
className="ml-0.5 leading-none hover:opacity-70"
|
|
1226
|
+
style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
|
|
1227
|
+
>×</button>
|
|
1228
|
+
</span>
|
|
1229
|
+
)}
|
|
1230
|
+
{vendorDist.length > 1 && (
|
|
1231
|
+
<span
|
|
1232
|
+
className="hidden sm:inline-flex items-center gap-2 px-2.5 py-1 rounded-md bg-gray-500/10 text-gray-400 border border-gray-500/20 font-mono"
|
|
1233
|
+
title="Fleet by model vendor"
|
|
1234
|
+
>
|
|
1235
|
+
{vendorDist.map(v => (
|
|
1236
|
+
<span key={v.initial} className="inline-flex items-baseline gap-0.5">
|
|
1237
|
+
<span style={{ color: v.color }}>{v.initial}</span>
|
|
1238
|
+
<span className="text-gray-500">:{v.count}</span>
|
|
1239
|
+
</span>
|
|
1240
|
+
))}
|
|
1241
|
+
</span>
|
|
1242
|
+
)}
|
|
1243
|
+
{/* Round 42 / Loop: extend active-links chip with the timestamp
|
|
1244
|
+
of the most-recent flow event. Tells the operator at a glance
|
|
1245
|
+
whether the topology is currently humming (last 30s) or has
|
|
1246
|
+
been quiet for a while — the visual flow particles and
|
|
1247
|
+
edge brightness only show that there IS traffic, not when
|
|
1248
|
+
the last one was. Reuses Round 38's relativeAgo. */}
|
|
1249
|
+
{(() => {
|
|
1250
|
+
const recent = flowLinks.reduce<number | null>((acc, l) => {
|
|
1251
|
+
if (!l.last_at) return acc;
|
|
1252
|
+
const t = parseHubTime(l.last_at);
|
|
1253
|
+
if (t === null) return acc;
|
|
1254
|
+
return acc === null || t > acc ? t : acc;
|
|
1255
|
+
}, null);
|
|
1256
|
+
const rel = recent !== null ? relativeAgo(new Date(recent).toISOString()) : null;
|
|
1257
|
+
return (
|
|
1258
|
+
<span className="hidden sm:inline px-2.5 py-1 rounded-md bg-gray-500/10 text-gray-400 border border-gray-500/20">
|
|
1259
|
+
{flowLinks.length} active link{flowLinks.length === 1 ? '' : 's'}
|
|
1260
|
+
{rel ? <span className="text-gray-500"> · last {rel}</span> : null}
|
|
1261
|
+
</span>
|
|
1262
|
+
);
|
|
1263
|
+
})()}
|
|
1264
|
+
<FreshnessChip sessions={sessions} />
|
|
717
1265
|
</div>
|
|
718
1266
|
</div>
|
|
719
1267
|
|
|
@@ -733,7 +1281,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
733
1281
|
onPointerMove={onPointerMove}
|
|
734
1282
|
onPointerUp={onPointerUp}
|
|
735
1283
|
onPointerLeave={onPointerUp}
|
|
736
|
-
|
|
1284
|
+
// Round 41 / Loop: the reset-button title (R22) and the Help
|
|
1285
|
+
// overlay (R25) both advertise "double-click the canvas to
|
|
1286
|
+
// reset", but the handler was never actually wired. The text
|
|
1287
|
+
// was lying. Wire it here, guarded so dbl-clicking a node
|
|
1288
|
+
// (which would also trigger the SVG-level handler via event
|
|
1289
|
+
// bubbling) doesn't unexpectedly reset the view on the user.
|
|
1290
|
+
onDoubleClick={(e) => {
|
|
1291
|
+
const t = e.target as Element | null;
|
|
1292
|
+
if (t?.closest('g[data-node]')) return;
|
|
1293
|
+
resetView();
|
|
1294
|
+
}}
|
|
1295
|
+
style={{ cursor: isPanning ? 'grabbing' : 'grab', touchAction: 'none' }}
|
|
737
1296
|
>
|
|
738
1297
|
<defs>
|
|
739
1298
|
<linearGradient id="topo-panel" x1="0" x2="1" y1="0" y2="1">
|
|
@@ -755,9 +1314,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
755
1314
|
</feMerge>
|
|
756
1315
|
</filter>
|
|
757
1316
|
)}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1317
|
+
{/* Round 16 / Loop: 3-tier flow-link arrow markers.
|
|
1318
|
+
The single marker had `markerUnits` defaulting to
|
|
1319
|
+
`strokeWidth`, so heavy edges (stroke=7) rendered 35-user-
|
|
1320
|
+
unit arrowheads — visually dominant, the head outweighed
|
|
1321
|
+
the line. Switching to `userSpaceOnUse` decouples arrow
|
|
1322
|
+
size from stroke; binning by count gives a clearer
|
|
1323
|
+
hierarchy than continuous linear scaling:
|
|
1324
|
+
s (count 1-2) → 12 user units
|
|
1325
|
+
m (count 3-4) → 16 user units (alias for `topo-arrow`)
|
|
1326
|
+
l (count 5+) → 22 user units
|
|
1327
|
+
`topo-arrow` stays bound to the medium tier so the legend
|
|
1328
|
+
swatch (line ~1500) renders without change. */}
|
|
1329
|
+
{[
|
|
1330
|
+
{ id: 'topo-arrow-s', size: 12 },
|
|
1331
|
+
{ id: 'topo-arrow', size: 16 },
|
|
1332
|
+
{ id: 'topo-arrow-l', size: 22 },
|
|
1333
|
+
].map(m => (
|
|
1334
|
+
<marker
|
|
1335
|
+
key={m.id}
|
|
1336
|
+
id={m.id}
|
|
1337
|
+
viewBox="0 0 10 10"
|
|
1338
|
+
refX="8"
|
|
1339
|
+
refY="5"
|
|
1340
|
+
markerWidth={m.size}
|
|
1341
|
+
markerHeight={m.size}
|
|
1342
|
+
markerUnits="userSpaceOnUse"
|
|
1343
|
+
orient="auto-start-reverse"
|
|
1344
|
+
>
|
|
1345
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill={pal.arrowFill} />
|
|
1346
|
+
</marker>
|
|
1347
|
+
))}
|
|
761
1348
|
{/* Round 45: radial radar sweep gradient — bright at center
|
|
762
1349
|
(where it meets the hub) fading to leading edge. */}
|
|
763
1350
|
<radialGradient id="topo-sweep" cx="0%" cy="50%" r="100%">
|
|
@@ -777,13 +1364,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
777
1364
|
grid mode drops it so the concentric rings don't sit behind a
|
|
778
1365
|
rectangular grid. */}
|
|
779
1366
|
{layout === 'ring' && (<>
|
|
780
|
-
|
|
1367
|
+
{/* R52: radar bg is pure decoration — drop its pointer events so
|
|
1368
|
+
the hub <g> under it (and any future inner-disk affordances)
|
|
1369
|
+
can receive clicks. Previously the r=330 disc intercepted
|
|
1370
|
+
the hub click outright. */}
|
|
1371
|
+
<circle cx={cx} cy={cy} r="330" fill="url(#topo-radar)" style={{ pointerEvents: 'none' }} />
|
|
781
1372
|
|
|
782
1373
|
{/* Round 45: subtle star field — 24 deterministic dots scattered
|
|
783
1374
|
across the canvas give the radar bg some depth. Skipped on
|
|
784
1375
|
light theme so the white surface stays clean. */}
|
|
785
1376
|
{!isLight && (
|
|
786
|
-
<g opacity="0.5">
|
|
1377
|
+
<g opacity="0.5" style={{ pointerEvents: 'none' }}>
|
|
787
1378
|
{Array.from({ length: 28 }).map((_, i) => {
|
|
788
1379
|
// Deterministic pseudo-random scatter so positions are
|
|
789
1380
|
// stable between renders (no JS hydration mismatch).
|
|
@@ -805,6 +1396,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
805
1396
|
style={{
|
|
806
1397
|
transformOrigin: `${cx}px ${cy}px`,
|
|
807
1398
|
transformBox: 'view-box',
|
|
1399
|
+
pointerEvents: 'none',
|
|
808
1400
|
}}
|
|
809
1401
|
className="anet-topo-sweep"
|
|
810
1402
|
opacity={isLight ? 0.7 : 1}
|
|
@@ -815,11 +1407,44 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
815
1407
|
/>
|
|
816
1408
|
</g>
|
|
817
1409
|
|
|
818
|
-
{/* radar rings
|
|
1410
|
+
{/* radar rings — pure decoration at fixed radii, independent of
|
|
1411
|
+
node positions so the radar aesthetic is preserved across tier
|
|
1412
|
+
changes. */}
|
|
819
1413
|
{[90, 170, 250, 330].map(radius => (
|
|
820
1414
|
<circle key={radius} cx={cx} cy={cy} r={radius} fill="none" stroke={pal.ringStroke} strokeWidth="1" opacity={isLight ? 0.6 : 0.35} />
|
|
821
1415
|
))}
|
|
822
1416
|
|
|
1417
|
+
{/* Round 54 / Loop: tier-radius guide rings. The radar rings above
|
|
1418
|
+
are decorative and don't match the actual tier radii nodes sit
|
|
1419
|
+
on (single 220 / dual 175,260 / triple 145,215,285). Drawing
|
|
1420
|
+
a faint dashed ring at each ACTIVE tier radius lets the eye
|
|
1421
|
+
anchor "this is the inner / outer ring" without inferring from
|
|
1422
|
+
node spacing. Picked based on online node count so only the
|
|
1423
|
+
tiers currently in use draw — empty tiers stay quiet. pointer-
|
|
1424
|
+
events:none so they never intercept hub or node clicks. The
|
|
1425
|
+
0.7 stroke + dashed pattern reads as guide, not feature. */}
|
|
1426
|
+
{(
|
|
1427
|
+
onlineNodes.length > onlineTripleThreshold
|
|
1428
|
+
? [onlineTripleInnerR, onlineTripleMidR, onlineTripleOuterR]
|
|
1429
|
+
: onlineNodes.length > onlineTierThreshold
|
|
1430
|
+
? [onlineInnerRadius, onlineOuterRadius]
|
|
1431
|
+
: onlineNodes.length > 0
|
|
1432
|
+
? [onlineRadius]
|
|
1433
|
+
: []
|
|
1434
|
+
).map(r => (
|
|
1435
|
+
<circle
|
|
1436
|
+
key={`tier-${r}`}
|
|
1437
|
+
cx={cx} cy={cy} r={r}
|
|
1438
|
+
fill="none"
|
|
1439
|
+
stroke={pal.ringStroke}
|
|
1440
|
+
strokeWidth="0.7"
|
|
1441
|
+
strokeDasharray="2 8"
|
|
1442
|
+
opacity={isLight ? 0.32 : 0.42}
|
|
1443
|
+
style={{ pointerEvents: 'none' }}
|
|
1444
|
+
data-tier-ring={r}
|
|
1445
|
+
/>
|
|
1446
|
+
))}
|
|
1447
|
+
|
|
823
1448
|
{/* Round 50: 4 small particles slowly orbiting the outer ring
|
|
824
1449
|
(r=330). Each starts at a different angle (offset 0/0.25/0.5/0.75
|
|
825
1450
|
of the cycle) so they're evenly spaced. 16s per revolution is
|
|
@@ -889,32 +1514,104 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
889
1514
|
only — groupBoxes is empty in ring mode. Rendered behind the
|
|
890
1515
|
flow links + nodes; pointer-events off so they never intercept
|
|
891
1516
|
a node click. Restrained dashed container + group-name label. */}
|
|
892
|
-
{groupBoxes.map(box =>
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1517
|
+
{groupBoxes.map(box => {
|
|
1518
|
+
const isHovered = activeGroup === box.key;
|
|
1519
|
+
// Round 18 / Loop: group-box hover linkage. The Round 8 fade
|
|
1520
|
+
// already dropped OUT-of-focus groups to 0.28, but the IN-focus
|
|
1521
|
+
// group sat at its baseline appearance — no positive emphasis.
|
|
1522
|
+
// Hovering now upgrades the box to an "accent" treatment:
|
|
1523
|
+
// solid stroke (not dashed), thicker, accent-coloured; brighter
|
|
1524
|
+
// text and slightly stronger fill. Label and box read as one
|
|
1525
|
+
// selected unit. Geometry unchanged → overlap test untouched.
|
|
1526
|
+
return (
|
|
1527
|
+
<g
|
|
1528
|
+
key={`grp-${box.key}`}
|
|
1529
|
+
data-group={box.key}
|
|
1530
|
+
className="transition-opacity"
|
|
1531
|
+
// R63: drop the blanket pointerEvents:'none' that
|
|
1532
|
+
// previously sat here. Chrome's SVG impl doesn't let a
|
|
1533
|
+
// child override a parent's `none` even though the spec
|
|
1534
|
+
// says it should — moving the property onto just the
|
|
1535
|
+
// rect (where it's needed so nodes underneath stay
|
|
1536
|
+
// clickable) lets the label text receive its own click.
|
|
1537
|
+
style={{ opacity: !activeGroup || isHovered ? 1 : 0.28 }}
|
|
913
1538
|
>
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1539
|
+
<rect
|
|
1540
|
+
x={box.x}
|
|
1541
|
+
y={box.y}
|
|
1542
|
+
width={box.w}
|
|
1543
|
+
height={box.h}
|
|
1544
|
+
rx="14"
|
|
1545
|
+
fill={isLight ? '#0f172a' : '#a5b4fc'}
|
|
1546
|
+
fillOpacity={isHovered ? (isLight ? 0.05 : 0.09) : (isLight ? 0.025 : 0.045)}
|
|
1547
|
+
stroke={isHovered ? pal.legendAccent : pal.ringStroke}
|
|
1548
|
+
strokeWidth={isHovered ? 2 : 1.5}
|
|
1549
|
+
strokeDasharray={isHovered ? 'none' : '6 6'}
|
|
1550
|
+
style={{ transition: 'stroke 200ms ease-out, stroke-width 200ms ease-out, fill-opacity 200ms ease-out', pointerEvents: 'none' }}
|
|
1551
|
+
/>
|
|
1552
|
+
{/* R63: wrap label in a clickable <g> with an invisible
|
|
1553
|
+
rect hitbox. The text alone wasn't getting hit-tested
|
|
1554
|
+
reliably — the SVG-wide topo-panel <rect> intercepts
|
|
1555
|
+
at the label's screen position when the label sits at
|
|
1556
|
+
a high viewBox-y (it lands below where the compositor
|
|
1557
|
+
expects the text to paint on top, same gotcha as the
|
|
1558
|
+
recent-signal panel rows in R56). Hitbox rect + the
|
|
1559
|
+
parent <g> taking the click + onPointerDown stop-
|
|
1560
|
+
propagation match the R55/R56/R61 pattern. */}
|
|
1561
|
+
<g
|
|
1562
|
+
role="button"
|
|
1563
|
+
tabIndex={0}
|
|
1564
|
+
aria-pressed={pinnedGroup === box.key}
|
|
1565
|
+
data-group-label-hit={box.key}
|
|
1566
|
+
style={{ pointerEvents: 'all', cursor: 'pointer' }}
|
|
1567
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
1568
|
+
onClick={() => setPinnedGroup(prev => prev === box.key ? null : box.key)}
|
|
1569
|
+
>
|
|
1570
|
+
<rect
|
|
1571
|
+
x={box.x + 6}
|
|
1572
|
+
y={box.y + 2}
|
|
1573
|
+
width={Math.min(box.w - 12, 240)}
|
|
1574
|
+
height={20}
|
|
1575
|
+
fill="transparent"
|
|
1576
|
+
/>
|
|
1577
|
+
<text
|
|
1578
|
+
x={box.x + 12}
|
|
1579
|
+
y={box.y + 14}
|
|
1580
|
+
fill={isHovered ? pal.legendHeadline : pal.legendText}
|
|
1581
|
+
fontSize="13"
|
|
1582
|
+
fontFamily="monospace"
|
|
1583
|
+
fontWeight="700"
|
|
1584
|
+
style={{ transition: 'fill 200ms ease-out' }}
|
|
1585
|
+
data-group-label={box.key}
|
|
1586
|
+
>
|
|
1587
|
+
{box.key}
|
|
1588
|
+
{/* Round 19 / Loop: member-count chip. Inline tspan stays
|
|
1589
|
+
in the single <text> bbox the overlap test reads, so
|
|
1590
|
+
the node↔label guard still catches if the chip ever
|
|
1591
|
+
pushes the label far enough right to clip a node.
|
|
1592
|
+
Smaller + lighter weight reads as metadata, not name. */}
|
|
1593
|
+
<tspan dx="6" fill={pal.legendText} fontSize="11" fontWeight="400">· {box.count}</tspan>
|
|
1594
|
+
{/* Round 58 / Loop: status mix pip strip. Compact text-
|
|
1595
|
+
based chips (e.g. "2w 1i") so the strip stays inside
|
|
1596
|
+
the same <text> bbox the overlap-test reads — keeps
|
|
1597
|
+
the R27 label↔label and R19 node↔label guards intact.
|
|
1598
|
+
Each tier is colour-coded against the legend swatches
|
|
1599
|
+
and only renders when count > 0, so a healthy all-
|
|
1600
|
+
working group reads simply " · 2w". */}
|
|
1601
|
+
{box.statuses.working > 0 && (
|
|
1602
|
+
<tspan dx="8" fill={isLight ? '#059669' : '#22c55e'} fontSize="11" fontWeight="600">{box.statuses.working}w</tspan>
|
|
1603
|
+
)}
|
|
1604
|
+
{box.statuses.idle > 0 && (
|
|
1605
|
+
<tspan dx="4" fill={isLight ? '#0d9488' : '#2dd4bf'} fontSize="11" fontWeight="600">{box.statuses.idle}i</tspan>
|
|
1606
|
+
)}
|
|
1607
|
+
{box.statuses.offline > 0 && (
|
|
1608
|
+
<tspan dx="4" fill={isLight ? '#94a3b8' : '#6b7280'} fontSize="11" fontWeight="600">{box.statuses.offline}o</tspan>
|
|
1609
|
+
)}
|
|
1610
|
+
</text>
|
|
1611
|
+
</g>
|
|
1612
|
+
</g>
|
|
1613
|
+
);
|
|
1614
|
+
})}
|
|
918
1615
|
|
|
919
1616
|
{/* directed message flows */}
|
|
920
1617
|
{flowLinks.map((link, index) => {
|
|
@@ -922,21 +1619,116 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
922
1619
|
const to = nodePositions[link.to];
|
|
923
1620
|
if (!from || !to) return null;
|
|
924
1621
|
|
|
925
|
-
|
|
1622
|
+
// Round 7 / Loop: lift now scales with distance so short links
|
|
1623
|
+
// aren't over-bent (long links keep the ~36px hump), and the
|
|
1624
|
+
// particle period shortens with link.count so busier edges
|
|
1625
|
+
// visibly pulse faster — instant info density on top of the
|
|
1626
|
+
// existing stroke-width-by-count chip.
|
|
1627
|
+
const dist = Math.hypot(to.x - from.x, to.y - from.y);
|
|
1628
|
+
const lift = (index % 2 === 0 ? 1 : -1) * Math.min(36, dist * 0.18);
|
|
926
1629
|
const path = curvePath(from, to, lift);
|
|
927
1630
|
const width = Math.min(2 + link.count, 7);
|
|
928
|
-
const duration = 2.
|
|
929
|
-
|
|
1631
|
+
const duration = Math.max(0.9, 2.6 / Math.sqrt(link.count));
|
|
1632
|
+
// Round 10 / Loop: freshness fade. An edge that fired ≤30s ago
|
|
1633
|
+
// stays at full intensity; over 5 minutes it decays to ~35%.
|
|
1634
|
+
// Surfaces "what's happening now" vs background chatter without
|
|
1635
|
+
// hiding old flow entirely (some context still useful). `now`
|
|
1636
|
+
// captured at useMemo-recompute time (every 5s message refresh)
|
|
1637
|
+
// — accuracy is within the poll interval, plenty.
|
|
1638
|
+
const ageMs = link.last_at ? Math.max(0, Date.now() - Date.parse(link.last_at)) : 0;
|
|
1639
|
+
const fresh = Math.max(0.35, 1 - ageMs / (5 * 60 * 1000));
|
|
1640
|
+
// Round 16 arrow-tier binning — keep `topo-arrow` as the
|
|
1641
|
+
// medium tier id so the legend swatch picks it up unchanged.
|
|
1642
|
+
const arrowId = link.count <= 2 ? 'topo-arrow-s'
|
|
1643
|
+
: link.count <= 4 ? 'topo-arrow'
|
|
1644
|
+
: 'topo-arrow-l';
|
|
1645
|
+
|
|
1646
|
+
// Round 39 / Loop: edge hover tooltip — surface the same
|
|
1647
|
+
// last_at + count info the freshness fade and arrow tier
|
|
1648
|
+
// already encode visually, in plain text. The stroke is the
|
|
1649
|
+
// hover target; SVG `<title>` honours newlines on every
|
|
1650
|
+
// browser the dashboard targets.
|
|
1651
|
+
const lastAt = relativeAgo(link.last_at);
|
|
1652
|
+
const tooltip = `${link.from} → ${link.to}\n${link.count} message${link.count === 1 ? '' : 's'}${lastAt ? ` · last ${lastAt}` : ''}`;
|
|
1653
|
+
// Round 40 / Loop: edges follow node hover — when an alias is
|
|
1654
|
+
// hovered, every edge touching it brightens, the rest fade.
|
|
1655
|
+
// Pairs with the Round 8 group-focus fade on nodes: hover a
|
|
1656
|
+
// node to find "who is this agent talking to" at a glance.
|
|
1657
|
+
// No hover → multiplier is 1.0 (current behaviour preserved).
|
|
1658
|
+
// Round 50 / Loop: edge-on-self priority. When the user hovers
|
|
1659
|
+
// a flow edge directly (R48 widened the hitbox so this is now
|
|
1660
|
+
// a precise gesture), THAT edge gets the strongest boost (2.0)
|
|
1661
|
+
// and every other edge dims to 0.35. Node-hover (R40) keeps
|
|
1662
|
+
// its own ladder. Edge-hover and node-hover are mutually
|
|
1663
|
+
// exclusive in practice — the cursor is over one or the
|
|
1664
|
+
// other — but the order below makes edge-hover win if both
|
|
1665
|
+
// ever read truthy at the same React tick.
|
|
1666
|
+
// Round 53 / Loop: in-group edges follow team focus. R40
|
|
1667
|
+
// brightened only edges touching the exact hovered alias —
|
|
1668
|
+
// but with R8/R18 prefix-clustering, a user hovering one
|
|
1669
|
+
// member is asking about the team. So when BOTH endpoints of
|
|
1670
|
+
// an edge share the hoveredGroup (and neither is the exact
|
|
1671
|
+
// hovered alias — that gets the stronger 1.7×), the edge
|
|
1672
|
+
// boosts to 1.3×. Edges leaving the team (one endpoint in,
|
|
1673
|
+
// one out) still dim to 0.35× per R40 since they read as
|
|
1674
|
+
// background to the focus. Singletons fall through unchanged
|
|
1675
|
+
// (their group key is the alias itself, so bothInHoveredGroup
|
|
1676
|
+
// is impossible for a non-self-edge).
|
|
1677
|
+
const isHoveredEdge = hoveredEdgeKey === link.key;
|
|
1678
|
+
const fromGroup = groupKeys[link.from] ?? link.from;
|
|
1679
|
+
const toGroup = groupKeys[link.to] ?? link.to;
|
|
1680
|
+
const bothInHoveredGroup = !!activeGroup && fromGroup === activeGroup && toGroup === activeGroup;
|
|
1681
|
+
// R63: also gate "no filter" on activeGroup so pinnedGroup
|
|
1682
|
+
// alone activates the in-group 1.3× boost + non-group dim.
|
|
1683
|
+
// When neither a node nor a group is in focus, mul is 1.
|
|
1684
|
+
const edgeOpacityMul = isHoveredEdge
|
|
1685
|
+
? 2.0
|
|
1686
|
+
: hoveredEdgeKey
|
|
1687
|
+
? 0.35
|
|
1688
|
+
: !hoveredAlias && !activeGroup
|
|
1689
|
+
? 1
|
|
1690
|
+
: (link.from === hoveredAlias || link.to === hoveredAlias)
|
|
1691
|
+
? 1.7
|
|
1692
|
+
: bothInHoveredGroup
|
|
1693
|
+
? 1.3
|
|
1694
|
+
: 0.35;
|
|
1695
|
+
// Round 50: the hovered edge also visibly thickens so the eye
|
|
1696
|
+
// tracks it even at low message counts (width was 3 → 4.5 on
|
|
1697
|
+
// hover). 1.4× is enough to read as "lifted" without
|
|
1698
|
+
// breaching the 16-px hitbox bound.
|
|
1699
|
+
const renderWidth = isHoveredEdge ? Math.min(width * 1.4, 10) : width;
|
|
930
1700
|
return (
|
|
931
1701
|
<g key={link.key}>
|
|
1702
|
+
{/* Round 48 / Loop: invisible hover hitbox — visible flow
|
|
1703
|
+
path is 3-7 px wide and damn hard to hover precisely.
|
|
1704
|
+
Stack a transparent 16-px-wide stroke behind it so the
|
|
1705
|
+
cursor only needs to be ~8 px from the line for the
|
|
1706
|
+
tooltip to fire. Native <title> moves here; the
|
|
1707
|
+
visible path no longer needs it. pointer-events on
|
|
1708
|
+
the visible path drop to "none" since the hitbox
|
|
1709
|
+
owns the hover surface. */}
|
|
1710
|
+
<path
|
|
1711
|
+
d={path}
|
|
1712
|
+
fill="none"
|
|
1713
|
+
stroke="transparent"
|
|
1714
|
+
strokeWidth={Math.max(width + 10, 16)}
|
|
1715
|
+
style={{ pointerEvents: 'stroke' }}
|
|
1716
|
+
data-edge-hitbox
|
|
1717
|
+
onMouseEnter={() => setHoveredEdgeKey(link.key)}
|
|
1718
|
+
onMouseLeave={() => setHoveredEdgeKey(prev => prev === link.key ? null : prev)}
|
|
1719
|
+
>
|
|
1720
|
+
<title>{tooltip}</title>
|
|
1721
|
+
</path>
|
|
932
1722
|
<path
|
|
933
1723
|
d={path}
|
|
934
1724
|
fill="none"
|
|
935
1725
|
stroke={pal.flowEdge}
|
|
936
|
-
strokeWidth={
|
|
937
|
-
opacity={isLight ? 0.22 : 0.28}
|
|
1726
|
+
strokeWidth={renderWidth}
|
|
1727
|
+
opacity={Math.min(1, (isLight ? 0.22 : 0.28) * fresh * edgeOpacityMul)}
|
|
938
1728
|
filter={isLight ? undefined : 'url(#topo-glow)'}
|
|
939
|
-
markerEnd=
|
|
1729
|
+
markerEnd={`url(#${arrowId})`}
|
|
1730
|
+
className="transition-opacity duration-300"
|
|
1731
|
+
style={{ pointerEvents: 'none' }}
|
|
940
1732
|
/>
|
|
941
1733
|
<path
|
|
942
1734
|
id={`flow-path-${index}`}
|
|
@@ -945,37 +1737,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
945
1737
|
stroke={pal.flowPath}
|
|
946
1738
|
strokeWidth="1"
|
|
947
1739
|
strokeDasharray="2 12"
|
|
948
|
-
opacity={isLight ? 0.4 : 0.75}
|
|
1740
|
+
opacity={Math.min(1, (isLight ? 0.4 : 0.75) * fresh * edgeOpacityMul)}
|
|
1741
|
+
className="transition-opacity duration-300"
|
|
949
1742
|
/>
|
|
950
|
-
|
|
951
|
-
<
|
|
952
|
-
|
|
1743
|
+
{!reducedMotion && (
|
|
1744
|
+
<circle r="4" fill={pal.flowParticle} filter={isLight ? undefined : 'url(#topo-glow)'} opacity={Math.min(1, fresh * edgeOpacityMul)}>
|
|
1745
|
+
<animateMotion dur={`${duration}s`} repeatCount="indefinite" path={path} />
|
|
1746
|
+
</circle>
|
|
1747
|
+
)}
|
|
953
1748
|
</g>
|
|
954
1749
|
);
|
|
955
1750
|
})}
|
|
956
1751
|
|
|
957
|
-
{/* center hub — round 39
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
the
|
|
963
|
-
|
|
964
|
-
|
|
1752
|
+
{/* center hub — round 39 sized + round 13 restraint.
|
|
1753
|
+
The hub is the control-plane anchor. r39 gave it two outward
|
|
1754
|
+
pulses (r 10→38, 2.4s, double-phase) which read as "loudest
|
|
1755
|
+
node in the room" — wrong semantics: anchors don't emit,
|
|
1756
|
+
they hold. r13 swaps the kinetic pulse for a slow opacity
|
|
1757
|
+
breath on the static halo (4s, ±15% from base), so the hub
|
|
1758
|
+
stays alive without throwing kinetic energy outward. The
|
|
1759
|
+
dual-circle "lit lamp" core is unchanged. */}
|
|
1760
|
+
{layout === 'ring' && (<g
|
|
1761
|
+
data-topo-hub
|
|
1762
|
+
style={{ cursor: 'pointer' }}
|
|
1763
|
+
// Stop pointerdown from reaching the SVG pan handler — same
|
|
1764
|
+
// reason as the node <g>: a captured pointer makes Chromium
|
|
1765
|
+
// fire the follow-up click on the SVG, not this group.
|
|
1766
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
1767
|
+
// Round 52 / Loop: hub is the visual anchor but had no click
|
|
1768
|
+
// action — users discovered fit-to-content only via the `f`
|
|
1769
|
+
// key or the chrome button. Wire the hub to fitView() so the
|
|
1770
|
+
// most prominent element in the canvas is also the "re-
|
|
1771
|
+
// center" affordance. A click-ripple keyed by ts confirms
|
|
1772
|
+
// the gesture; the hub <title> now ends with a hint.
|
|
1773
|
+
onClick={() => {
|
|
1774
|
+
fitView();
|
|
1775
|
+
setClickRipple({ ts: Date.now(), x: cx, y: cy, r0: 18, color: isLight ? '#059669' : '#10b981' });
|
|
1776
|
+
setTimeout(() => setClickRipple(prev => prev && prev.x === cx && prev.y === cy ? null : prev), 600);
|
|
1777
|
+
}}
|
|
1778
|
+
>
|
|
1779
|
+
{/* Round 43 / Loop: hub `<title>` summary — hovering the
|
|
1780
|
+
central glow now answers "what is this?" with a one-line
|
|
1781
|
+
fleet snapshot. Duplicates the header chips by design;
|
|
1782
|
+
hovering the most visually prominent element is the
|
|
1783
|
+
most natural impulse, so satisfy it where the cursor
|
|
1784
|
+
already is. Falls clean: omits sub-clauses when their
|
|
1785
|
+
counts are zero. R52 appends a "click to fit view" hint.
|
|
1786
|
+
*/}
|
|
1787
|
+
<title>{(() => {
|
|
1788
|
+
const total = sessions.length;
|
|
1789
|
+
const parts = [`Network hub`, `${total} session${total === 1 ? '' : 's'}`];
|
|
1790
|
+
if (onlineNodes.length > 0) parts.push(`${onlineNodes.length} online`);
|
|
1791
|
+
if (workingCount > 0) parts.push(`${workingCount} working`);
|
|
1792
|
+
if (flowLinks.length > 0) parts.push(`${flowLinks.length} active link${flowLinks.length === 1 ? '' : 's'}`);
|
|
1793
|
+
return parts.join(' · ') + '\nclick to fit view';
|
|
1794
|
+
})()}</title>
|
|
1795
|
+
{/* grounding halo — now breathes in opacity, no expansion */}
|
|
965
1796
|
<circle
|
|
966
1797
|
cx={cx} cy={cy} r="18"
|
|
967
1798
|
fill={isLight ? '#d1fae5' : '#10b981'}
|
|
968
|
-
opacity={isLight ? 0.
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
<animate attributeName="opacity" values="0.45;0;0.45" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
|
|
1799
|
+
opacity={isLight ? 0.42 : 0.12}
|
|
1800
|
+
>
|
|
1801
|
+
{!reducedMotion && (
|
|
1802
|
+
<animate
|
|
1803
|
+
attributeName="opacity"
|
|
1804
|
+
values={isLight ? '0.32;0.52;0.32' : '0.08;0.16;0.08'}
|
|
1805
|
+
dur="4s"
|
|
1806
|
+
repeatCount="indefinite"
|
|
1807
|
+
/>
|
|
1808
|
+
)}
|
|
979
1809
|
</circle>
|
|
980
1810
|
{/* core — 20px diameter, larger inner highlight reads as a "lit lamp" */}
|
|
981
1811
|
<circle cx={cx} cy={cy} r="10" fill={isLight ? '#059669' : '#10b981'} />
|
|
@@ -983,7 +1813,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
983
1813
|
</g>)}
|
|
984
1814
|
|
|
985
1815
|
{/* agent nodes */}
|
|
986
|
-
{[...onlineNodes, ...offlineNodes].map(session => {
|
|
1816
|
+
{[...onlineNodes, ...offlineNodes].map((session, nodeIdx) => {
|
|
987
1817
|
const pos = nodePositions[session.alias];
|
|
988
1818
|
if (!pos) return null;
|
|
989
1819
|
|
|
@@ -991,7 +1821,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
991
1821
|
const isOnline = session.status !== 'offline' || !!sseCountFor;
|
|
992
1822
|
const status = nodeStatus(session, isOnline, isLight);
|
|
993
1823
|
const isActive = activeAliases.has(session.alias);
|
|
994
|
-
|
|
1824
|
+
// #113: node size scales with the S/M/L control; halos, labels,
|
|
1825
|
+
// badge and avatar all derive from `radius` so they follow.
|
|
1826
|
+
const radius = Math.round((isOnline ? 26 : 18) * nodeScale);
|
|
995
1827
|
// Round 109/110 (Vincent 4582 + 4583 P0): at high node counts
|
|
996
1828
|
// the 100px opaque label cards overlapped each other and
|
|
997
1829
|
// covered neighbouring avatars. But hiding text entirely went
|
|
@@ -1001,12 +1833,82 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1001
1833
|
// occlude an avatar), and the full card appears only for the
|
|
1002
1834
|
// hovered node or once zoomed in past 1.4×.
|
|
1003
1835
|
const showFullLabel = !denseLayout || hoveredAlias === session.alias || view.zoom >= 1.4;
|
|
1836
|
+
// Round 8: when a node in another group is hovered, fade this
|
|
1837
|
+
// one. Same-group nodes (incl. singletons matching the hovered
|
|
1838
|
+
// alias) stay full. Pure visual focus, geometry unchanged.
|
|
1839
|
+
const inFocus = !activeGroup || (groupKeys[session.alias] ?? session.alias) === activeGroup;
|
|
1004
1840
|
|
|
1005
1841
|
return (
|
|
1006
1842
|
<g
|
|
1007
1843
|
key={session.alias}
|
|
1008
|
-
|
|
1009
|
-
|
|
1844
|
+
data-node={session.alias}
|
|
1845
|
+
// Round 3 / Loop: `anet-fade-in` runs once when the <g>
|
|
1846
|
+
// mounts — a new session entering the fleet (or the topology
|
|
1847
|
+
// first rendering) eases in instead of popping. Re-renders of
|
|
1848
|
+
// an existing node don't re-trigger (React preserves the <g>
|
|
1849
|
+
// via the alias key), so status changes don't flicker. The
|
|
1850
|
+
// global prefers-reduced-motion sweep already neutralises it.
|
|
1851
|
+
className="group transition-opacity anet-fade-in"
|
|
1852
|
+
style={{
|
|
1853
|
+
cursor: 'pointer',
|
|
1854
|
+
// Round 17 / Loop: offline nodes drop to 0.6 at rest so
|
|
1855
|
+
// online nodes pop without losing the offline-as-ghost
|
|
1856
|
+
// information. The dashed stroke + smaller radius already
|
|
1857
|
+
// say "offline"; dimming the whole group strengthens the
|
|
1858
|
+
// online-vs-offline hierarchy at first glance. Exempt the
|
|
1859
|
+
// chat-focused node — if the user explicitly opened a
|
|
1860
|
+
// popover targeting an offline alias, that node stays
|
|
1861
|
+
// full-brightness so the focus ring + popover read as one
|
|
1862
|
+
// selected thing rather than a dimmed selection. Group-
|
|
1863
|
+
// hover fade (Round 8) still wins when inFocus is false.
|
|
1864
|
+
// Round 49 / Loop: edge-hover endpoint highlight composes
|
|
1865
|
+
// OVER the inFocus/online formula — a non-endpoint node
|
|
1866
|
+
// dims to 0.28 (just below the inFocus 0.32 to read as a
|
|
1867
|
+
// stronger "not relevant" signal), endpoints keep their
|
|
1868
|
+
// base opacity. chatAlias still wins to keep the focus
|
|
1869
|
+
// ring legible if the user clicked through.
|
|
1870
|
+
// Round 55 / Loop: legend-status hover composes on the
|
|
1871
|
+
// SAME level as edge-hover-endpoint. Non-matching nodes
|
|
1872
|
+
// dim to 0.28; matching nodes stay at their base.
|
|
1873
|
+
// activeStatus matches: working = status==='working',
|
|
1874
|
+
// idle = online but not working, offline = !isOnline.
|
|
1875
|
+
// Round 60 / Loop: activeStatus = hoveredStatus ?? pinnedStatus
|
|
1876
|
+
// so the pressure-bar segment pins (R60) and the legend
|
|
1877
|
+
// row hover (R55) feed the same branch.
|
|
1878
|
+
opacity: hoveredEdgeEndpoints && !hoveredEdgeEndpoints.has(session.alias) && chatAlias !== session.alias
|
|
1879
|
+
? 0.28
|
|
1880
|
+
: activeStatus && chatAlias !== session.alias && !(
|
|
1881
|
+
activeStatus === 'working' ? session.status === 'working'
|
|
1882
|
+
: activeStatus === 'idle' ? (isOnline && session.status !== 'working')
|
|
1883
|
+
: /* offline */ !isOnline
|
|
1884
|
+
)
|
|
1885
|
+
? 0.28
|
|
1886
|
+
: !inFocus
|
|
1887
|
+
? 0.32
|
|
1888
|
+
: chatAlias === session.alias
|
|
1889
|
+
? 1
|
|
1890
|
+
: isOnline ? 1 : 0.6,
|
|
1891
|
+
// Round 9 / Loop: stagger the anet-fade-in so the topology
|
|
1892
|
+
// reveals as a wave on first paint instead of one big pop.
|
|
1893
|
+
// Cap at 24 indices (≈600ms tail) so 50-node fleets still
|
|
1894
|
+
// finish revealing within a beat. CSS animation-delay only
|
|
1895
|
+
// applies during the keyframe — re-renders without a new
|
|
1896
|
+
// mount (same alias key) don't replay, so status changes
|
|
1897
|
+
// never trigger the stagger again.
|
|
1898
|
+
animationDelay: `${Math.min(nodeIdx, 24) * 25}ms`,
|
|
1899
|
+
// Round 51 / Loop: hover micro-lift. The label already
|
|
1900
|
+
// lifts on group-hover (R26). The node body — circle,
|
|
1901
|
+
// avatar, status ring — stays planted, so the gesture
|
|
1902
|
+
// reads "label moves, body doesn't". Now the whole <g>
|
|
1903
|
+
// translates -2px when hovered: avatar + ring + label
|
|
1904
|
+
// move as one unit. 2 px is well inside the inter-row
|
|
1905
|
+
// gap (cellH headroom ≥22), and CSS transform on SVG
|
|
1906
|
+
// <g> never affects the overlap-test geometry (the
|
|
1907
|
+
// test never simulates hover). useReducedMotion drops
|
|
1908
|
+
// the lift to 0 for prefers-reduced-motion users.
|
|
1909
|
+
transform: !reducedMotion && hoveredAlias === session.alias ? 'translateY(-2px)' : undefined,
|
|
1910
|
+
transition: 'transform 180ms cubic-bezier(0.4,0,0.2,1)',
|
|
1911
|
+
}}
|
|
1010
1912
|
// Stop the pointerdown from reaching the SVG pan handler: the
|
|
1011
1913
|
// SVG calls setPointerCapture, and a captured pointer makes
|
|
1012
1914
|
// Chromium fire the follow-up `click` on the SVG instead of
|
|
@@ -1014,21 +1916,106 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1014
1916
|
// Side effect (intended): you pan from empty canvas, not by
|
|
1015
1917
|
// grabbing a node.
|
|
1016
1918
|
onPointerDown={(e) => e.stopPropagation()}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1919
|
+
// Track hover globally (not just under denseLayout) so the
|
|
1920
|
+
// Round 8 group-focus fade works at any fleet size.
|
|
1921
|
+
onPointerEnter={() => setHoveredAlias(session.alias)}
|
|
1922
|
+
onPointerLeave={() => setHoveredAlias(prev => (prev === session.alias ? null : prev))}
|
|
1923
|
+
onClick={() => {
|
|
1924
|
+
setChatAlias(session.alias);
|
|
1925
|
+
// Round 14 ripple — capture position, radius and status
|
|
1926
|
+
// colour at click time so the one-shot circle is self-
|
|
1927
|
+
// contained (no re-render-time recomputation needed).
|
|
1928
|
+
setClickRipple({
|
|
1929
|
+
ts: Date.now(),
|
|
1930
|
+
x: pos.x, y: pos.y, r0: radius,
|
|
1931
|
+
color: status.primary,
|
|
1932
|
+
});
|
|
1933
|
+
setTimeout(() => setClickRipple(prev =>
|
|
1934
|
+
prev && Date.now() - prev.ts >= 590 ? null : prev), 600);
|
|
1935
|
+
}}
|
|
1020
1936
|
>
|
|
1021
1937
|
{/* Issue #96: native hover tooltip — "Vendor · model · Runtime".
|
|
1022
1938
|
Falls back to just the alias when the node reports no
|
|
1023
|
-
model/runtime.
|
|
1024
|
-
|
|
1025
|
-
|
|
1939
|
+
model/runtime.
|
|
1940
|
+
Round 33 / Loop: cwd line answers "what is this agent on?".
|
|
1941
|
+
Round 34 / Loop: for offline nodes, append "last seen: 6m ago"
|
|
1942
|
+
so the operator knows whether to wait or chase. Online nodes
|
|
1943
|
+
skip the line (Round 27's 1 h ghost age-out means anything
|
|
1944
|
+
still online has heartbeated recently — the line would just
|
|
1945
|
+
be noise). Accepts both ISO ("…T06:00:28Z") and SQL-style
|
|
1946
|
+
("… 06:00:28" assumed UTC) formats. */}
|
|
1947
|
+
{(() => {
|
|
1948
|
+
// Round 35 / Loop: TZ-safe parsing via the shared lib helper
|
|
1949
|
+
// — same parseHubTime is used by isGhost above (Round 38
|
|
1950
|
+
// factored it to app/lib/time.ts so both paths interpret SQL
|
|
1951
|
+
// bare timestamps as UTC on every browser).
|
|
1952
|
+
const lastSeen = !isOnline && session.last_seen_at ? relativeAgo(session.last_seen_at) : null;
|
|
1953
|
+
return (
|
|
1954
|
+
<title>{[
|
|
1955
|
+
identityLine(session.model, session.runtime) || session.alias,
|
|
1956
|
+
session.project_dir ? `cwd: ${session.project_dir}` : null,
|
|
1957
|
+
lastSeen ? `last seen: ${lastSeen}` : null,
|
|
1958
|
+
].filter(Boolean).join('\n')}</title>
|
|
1959
|
+
);
|
|
1960
|
+
})()}
|
|
1961
|
+
{/* Round 2 / Loop: hover ring — a thin outer stroke that fades
|
|
1962
|
+
in when the cursor enters the node, signalling clickability
|
|
1963
|
+
(real-user feedback for the chat-popover open). Pure CSS via
|
|
1964
|
+
Tailwind group-hover, so it costs nothing per frame and
|
|
1965
|
+
respects prefers-reduced-motion via the global media query. */}
|
|
1966
|
+
<circle
|
|
1967
|
+
cx={pos.x}
|
|
1968
|
+
cy={pos.y}
|
|
1969
|
+
r={radius + 12}
|
|
1970
|
+
fill="none"
|
|
1971
|
+
stroke={status.primary}
|
|
1972
|
+
// strokeWidth must NOT be 1.5 (offline status ring) or 3
|
|
1973
|
+
// (online status ring) — the overlap test selects by those
|
|
1974
|
+
// exact widths and would mis-count this invisible hover
|
|
1975
|
+
// ring as a node footprint.
|
|
1976
|
+
strokeWidth="2"
|
|
1977
|
+
className="opacity-0 group-hover:opacity-70 transition-opacity duration-150"
|
|
1978
|
+
style={{ pointerEvents: 'none' }}
|
|
1979
|
+
/>
|
|
1980
|
+
{/* Round 11 / Loop: chat-focus ring — when the ChatPopover is
|
|
1981
|
+
open targeting this node, anchor a persistent ring around
|
|
1982
|
+
it so the floating popover visibly links back to its source
|
|
1983
|
+
node. Static (not pulsing) so it reads as "selected state"
|
|
1984
|
+
rather than "this node is active". strokeWidth=2.5 stays
|
|
1985
|
+
clear of the overlap-test selectors (1.5 / 3). Sits just
|
|
1986
|
+
outside the halo radius+8 so it never overlaps a neighbour
|
|
1987
|
+
(halos already pack flush in dense grids). */}
|
|
1988
|
+
{chatAlias === session.alias && (
|
|
1989
|
+
<circle
|
|
1990
|
+
cx={pos.x}
|
|
1991
|
+
cy={pos.y}
|
|
1992
|
+
r={radius + 14}
|
|
1993
|
+
fill="none"
|
|
1994
|
+
stroke={status.primary}
|
|
1995
|
+
strokeWidth="2.5"
|
|
1996
|
+
opacity={isLight ? 0.85 : 0.95}
|
|
1997
|
+
filter={!isLight ? 'url(#topo-glow)' : undefined}
|
|
1998
|
+
className="transition-opacity duration-200"
|
|
1999
|
+
style={{ pointerEvents: 'none' }}
|
|
2000
|
+
/>
|
|
2001
|
+
)}
|
|
2002
|
+
{isActive && !reducedMotion && (
|
|
1026
2003
|
<circle cx={pos.x} cy={pos.y} r={radius + 14} fill={status.primary} opacity={isLight ? 0.08 : 0.12}>
|
|
1027
2004
|
<animate attributeName="r" values={`${radius + 8};${radius + 22};${radius + 8}`} dur="2.4s" repeatCount="indefinite" />
|
|
1028
2005
|
<animate attributeName="opacity" values={isLight ? '0.12;0.02;0.12' : '0.18;0.04;0.18'} dur="2.4s" repeatCount="indefinite" />
|
|
1029
2006
|
</circle>
|
|
1030
2007
|
)}
|
|
1031
|
-
|
|
2008
|
+
{/* Round 4 / Loop: transition-[fill,stroke,opacity] smooths
|
|
2009
|
+
status colour changes so idle↔working↔offline doesn't snap
|
|
2010
|
+
— task replies / node-rename / SSE updates ease in. */}
|
|
2011
|
+
<circle
|
|
2012
|
+
cx={pos.x}
|
|
2013
|
+
cy={pos.y}
|
|
2014
|
+
r={radius + 8}
|
|
2015
|
+
fill={status.halo}
|
|
2016
|
+
opacity={isOnline ? (isLight ? 0.85 : 0.65) : (isLight ? 0.4 : 0.25)}
|
|
2017
|
+
className="transition-[fill,opacity] duration-300 ease-out"
|
|
2018
|
+
/>
|
|
1032
2019
|
<circle
|
|
1033
2020
|
cx={pos.x}
|
|
1034
2021
|
cy={pos.y}
|
|
@@ -1038,6 +2025,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1038
2025
|
strokeWidth={isOnline ? 3 : 1.5}
|
|
1039
2026
|
strokeDasharray={isOnline ? 'none' : '5 5'}
|
|
1040
2027
|
filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
|
|
2028
|
+
className="transition-[fill,stroke] duration-300 ease-out"
|
|
1041
2029
|
/>
|
|
1042
2030
|
{/* Issue #96: node "avatar" is now driven by the model
|
|
1043
2031
|
vendor. Decision order:
|
|
@@ -1050,7 +2038,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1050
2038
|
4. unknown vendor / null model → the prefix-group
|
|
1051
2039
|
hue-hashed initial (#83/#99 behaviour, unchanged). */}
|
|
1052
2040
|
{(() => {
|
|
1053
|
-
const ar = isOnline ? 14 : 10;
|
|
2041
|
+
const ar = Math.round((isOnline ? 14 : 10) * nodeScale);
|
|
1054
2042
|
const size = radius * 2;
|
|
1055
2043
|
const vendor = vendorForModel(session.model);
|
|
1056
2044
|
const internByAlias = /书生|书小生|intern/i.test(session.alias);
|
|
@@ -1075,7 +2063,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1075
2063
|
<circle cx={pos.x} cy={pos.y} r={ar} fill={vendor.mono.bg} stroke={vendor.mono.ring} strokeWidth="1" />
|
|
1076
2064
|
<text
|
|
1077
2065
|
x={pos.x} y={pos.y} dy="0.34em" textAnchor="middle"
|
|
1078
|
-
fill={vendor.mono.text} fontSize={
|
|
2066
|
+
fill={vendor.mono.text} fontSize={ar}
|
|
1079
2067
|
fontFamily="monospace" fontWeight="700"
|
|
1080
2068
|
>
|
|
1081
2069
|
{vendor.initial}
|
|
@@ -1095,7 +2083,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1095
2083
|
dy="0.34em"
|
|
1096
2084
|
textAnchor="middle"
|
|
1097
2085
|
fill={c.text}
|
|
1098
|
-
fontSize={
|
|
2086
|
+
fontSize={ar}
|
|
1099
2087
|
fontFamily="monospace"
|
|
1100
2088
|
fontWeight="700"
|
|
1101
2089
|
>
|
|
@@ -1124,78 +2112,314 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1124
2112
|
</g>
|
|
1125
2113
|
);
|
|
1126
2114
|
})()}
|
|
1127
|
-
{session.status === 'working' && (
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
2115
|
+
{session.status === 'working' && (() => {
|
|
2116
|
+
// Round 24 / Loop: pulse rate ↔ traffic. The dot's dur
|
|
2117
|
+
// was a flat 1.1s — every working node looked equally
|
|
2118
|
+
// busy. Mapping it to sse:N lets the eye land on what's
|
|
2119
|
+
// actually hot. 3 discrete tiers read cleaner than
|
|
2120
|
+
// continuous easing at a glance; thresholds picked so
|
|
2121
|
+
// typical 1-2-SSE Claude/Codex sessions stay in the
|
|
2122
|
+
// "active" tier and only multi-pane orchestrators pop
|
|
2123
|
+
// into the "busy" tier. Geometry unchanged (r=2.5).
|
|
2124
|
+
const sse = sseCountFor ?? 0;
|
|
2125
|
+
const dur = sse >= 4 ? '0.7s' : sse >= 2 ? '0.9s' : '1.2s';
|
|
2126
|
+
return (
|
|
2127
|
+
<circle cx={pos.x} cy={pos.y - (radius - 6)} r="2.5" fill={pal.flowParticle} data-pulse-dur={dur} opacity={reducedMotion ? 0.6 : undefined}>
|
|
2128
|
+
{!reducedMotion && (
|
|
2129
|
+
<animate attributeName="opacity" values="1;0.25;1" dur={dur} repeatCount="indefinite" />
|
|
2130
|
+
)}
|
|
2131
|
+
</circle>
|
|
2132
|
+
);
|
|
2133
|
+
})()}
|
|
1136
2134
|
|
|
1137
2135
|
{/* Round 98 (issue #61): label rect 124px → 100px.
|
|
1138
2136
|
Round 109/110 (Vincent P0): full opaque card below the
|
|
1139
2137
|
density threshold / on hover / when zoomed; otherwise a
|
|
1140
2138
|
lightweight plain-text alias that keeps every node
|
|
1141
|
-
labelled without an opaque box covering its neighbours.
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
2139
|
+
labelled without an opaque box covering its neighbours.
|
|
2140
|
+
Round 15 / Loop: when nodeScale=S the node shrinks 30%
|
|
2141
|
+
but the label card was staying full-size, so the label
|
|
2142
|
+
visually outweighed the small node. Tighten the card
|
|
2143
|
+
frame, alias / sub fontSize, drop-offset and truncate
|
|
2144
|
+
length specifically for S; M and L keep their existing
|
|
2145
|
+
sizes (M ≈ L for labels by design — the S user is the
|
|
2146
|
+
one who explicitly asked for a denser view). */}
|
|
2147
|
+
{(() => {
|
|
2148
|
+
const isSmall = nodeScale < 0.8;
|
|
2149
|
+
const cardW = isSmall ? 88 : 100;
|
|
2150
|
+
const cardH = isSmall ? 36 : 42;
|
|
2151
|
+
const cardTopY = isSmall ? -12 : -14;
|
|
2152
|
+
const aliasFs = isSmall ? 11 : 12;
|
|
2153
|
+
const subFs = isSmall ? 8 : 9;
|
|
2154
|
+
const subY = isSmall ? 15 : 17;
|
|
2155
|
+
const dropY = isSmall ? 18 : 22;
|
|
2156
|
+
const fullMax = isSmall ? 11 : 12;
|
|
2157
|
+
const denseFs = isSmall ? 9 : 10;
|
|
2158
|
+
const denseDrop = isSmall ? 12 : 14;
|
|
2159
|
+
// Round 26 / Loop: micro-lift the label group on hover —
|
|
2160
|
+
// 1.5 px upward, 200 ms ease. Pairs with the Round 18
|
|
2161
|
+
// group-box hover-accent treatment to give the same
|
|
2162
|
+
// "this is the focused element" feedback at the per-
|
|
2163
|
+
// node level. CSS transform stacks onto the SVG
|
|
2164
|
+
// positioning transform attribute (SVG 2 cascade);
|
|
2165
|
+
// bbox is unchanged at rest, so the overlap-test gate
|
|
2166
|
+
// continues to see the geometric layout it expects.
|
|
2167
|
+
return showFullLabel ? (
|
|
2168
|
+
<g transform={`translate(${pos.x}, ${pos.y + radius + dropY})`} style={{ pointerEvents: 'none' }}
|
|
2169
|
+
className="transition-transform duration-200 group-hover:-translate-y-[1.5px]">
|
|
2170
|
+
<rect x={-cardW / 2} y={cardTopY} width={cardW} height={cardH} rx="6" fill={pal.labelBox.fill} stroke={pal.labelBox.stroke} opacity={isLight ? 1 : 0.94} />
|
|
2171
|
+
<text x="0" y="1" textAnchor="middle" fill={status.text} fontSize={aliasFs} fontFamily="monospace" fontWeight="700">
|
|
2172
|
+
{truncate(session.alias, fullMax)}
|
|
2173
|
+
</text>
|
|
2174
|
+
<text x="0" y={subY} textAnchor="middle" fill={status.primary} fontSize={subFs} fontFamily="monospace">
|
|
2175
|
+
{status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
|
|
2176
|
+
</text>
|
|
2177
|
+
</g>
|
|
2178
|
+
) : (
|
|
2179
|
+
<text
|
|
2180
|
+
x={pos.x}
|
|
2181
|
+
y={pos.y + radius + denseDrop}
|
|
2182
|
+
textAnchor="middle"
|
|
2183
|
+
fill={status.text}
|
|
2184
|
+
fontSize={denseFs}
|
|
2185
|
+
fontFamily="monospace"
|
|
2186
|
+
fontWeight="700"
|
|
2187
|
+
opacity={0.9}
|
|
2188
|
+
className="transition-transform duration-200 group-hover:-translate-y-[1.5px]"
|
|
2189
|
+
style={{ pointerEvents: 'none', paintOrder: 'stroke' }}
|
|
2190
|
+
stroke={pal.containerBg}
|
|
2191
|
+
strokeWidth="3"
|
|
2192
|
+
>
|
|
2193
|
+
{truncate(session.alias, isSmall ? 9 : 10)}
|
|
1150
2194
|
</text>
|
|
1151
|
-
|
|
1152
|
-
)
|
|
1153
|
-
<text
|
|
1154
|
-
x={pos.x}
|
|
1155
|
-
y={pos.y + radius + 14}
|
|
1156
|
-
textAnchor="middle"
|
|
1157
|
-
fill={status.text}
|
|
1158
|
-
fontSize="10"
|
|
1159
|
-
fontFamily="monospace"
|
|
1160
|
-
fontWeight="700"
|
|
1161
|
-
opacity={0.9}
|
|
1162
|
-
style={{ pointerEvents: 'none', paintOrder: 'stroke' }}
|
|
1163
|
-
stroke={pal.containerBg}
|
|
1164
|
-
strokeWidth="3"
|
|
1165
|
-
>
|
|
1166
|
-
{truncate(session.alias, 10)}
|
|
1167
|
-
</text>
|
|
1168
|
-
)}
|
|
2195
|
+
);
|
|
2196
|
+
})()}
|
|
1169
2197
|
</g>
|
|
1170
2198
|
);
|
|
1171
2199
|
})}
|
|
1172
2200
|
|
|
2201
|
+
{/* Round 14 / Loop: click ripple — one-shot expanding ring from
|
|
2202
|
+
the clicked node, ~500ms, status-coloured. Sits inside the
|
|
2203
|
+
zoom/pan <g> so it scales / pans with the topology. Keyed by
|
|
2204
|
+
ts so a re-click on any node (same or different) remounts the
|
|
2205
|
+
<circle> and the SMIL <animate> elements replay from t=0.
|
|
2206
|
+
strokeWidth=2 doesn't match the overlap-test selectors. */}
|
|
2207
|
+
{clickRipple && !reducedMotion && (
|
|
2208
|
+
<circle
|
|
2209
|
+
key={clickRipple.ts}
|
|
2210
|
+
cx={clickRipple.x}
|
|
2211
|
+
cy={clickRipple.y}
|
|
2212
|
+
r={clickRipple.r0 + 4}
|
|
2213
|
+
fill="none"
|
|
2214
|
+
stroke={clickRipple.color}
|
|
2215
|
+
strokeWidth="2"
|
|
2216
|
+
opacity="0"
|
|
2217
|
+
style={{ pointerEvents: 'none' }}
|
|
2218
|
+
>
|
|
2219
|
+
<animate attributeName="r" values={`${clickRipple.r0 + 4};${clickRipple.r0 + 30}`} dur="0.5s" fill="freeze" />
|
|
2220
|
+
<animate attributeName="opacity" values="0.7;0" dur="0.5s" fill="freeze" />
|
|
2221
|
+
</circle>
|
|
2222
|
+
)}
|
|
2223
|
+
|
|
2224
|
+
</g>
|
|
2225
|
+
|
|
2226
|
+
{/* #112: overlay panels (recent-signal + legend) render OUTSIDE the
|
|
2227
|
+
zoom/pan <g> so they stay fixed while the topology pans/zooms.
|
|
2228
|
+
They're sized + tucked into the top corners so every corner of
|
|
2229
|
+
each panel is >325px from the canvas centre — i.e. fully outside
|
|
2230
|
+
the outermost (offline) ring. No node on any ring can reach the
|
|
2231
|
+
corner triangles, so the panels never overlap a node, in ring
|
|
2232
|
+
OR grid layout (Vincent 4727 zero-overlap criterion). */}
|
|
1173
2233
|
{/* latest flow labels */}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
<
|
|
1180
|
-
|
|
2234
|
+
{/* Round 57 / Loop: drop-shadow on the panel rects gives them
|
|
2235
|
+
card-like elevation, especially on light theme where the
|
|
2236
|
+
near-white fill on a near-white canvas read as pasted-on.
|
|
2237
|
+
data-topo-panel-elevation tag so the test can verify both
|
|
2238
|
+
panels carry the filter. The filter is on the rect, not
|
|
2239
|
+
the parent <g>, so it doesn't shadow the rows + text inside
|
|
2240
|
+
— only the panel chrome lifts. */}
|
|
2241
|
+
<g transform="translate(16, 16)">
|
|
2242
|
+
<rect
|
|
2243
|
+
x="0" y="0" width="230" height="84" rx="10"
|
|
2244
|
+
fill={pal.legendBox.fill}
|
|
2245
|
+
stroke={pal.legendBox.stroke}
|
|
2246
|
+
opacity={isLight ? 0.97 : 0.92}
|
|
2247
|
+
style={{ filter: isLight ? 'drop-shadow(0 2px 6px rgba(15,23,42,0.08))' : 'drop-shadow(0 2px 6px rgba(0,0,0,0.45))' }}
|
|
2248
|
+
data-topo-panel-elevation="recent"
|
|
2249
|
+
/>
|
|
2250
|
+
<text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight="700">recent signal</text>
|
|
2251
|
+
<text x="150" y="21" fill={pal.legendAccent} fontSize="10" fontFamily="monospace">{messages.length} msgs</text>
|
|
2252
|
+
{/* Round 45 / Loop: empty state. The panel used to render
|
|
2253
|
+
"recent signal" + "0 msgs" with three blank slots below
|
|
2254
|
+
when no flow yet — read as "broken" rather than "quiet".
|
|
2255
|
+
A muted centred placeholder makes the empty state
|
|
2256
|
+
deliberate. Messages count CAN diverge from flowLinks
|
|
2257
|
+
count (raw count vs. deduped pairs), so the placeholder
|
|
2258
|
+
fires on flowLinks.length=0 specifically. */}
|
|
2259
|
+
{flowLinks.length === 0 ? (
|
|
2260
|
+
<text x="115" y="60" textAnchor="middle" fill={pal.legendText} fontSize="10" fontFamily="monospace" fontStyle="italic" opacity={0.65} data-recent-signal-empty>
|
|
2261
|
+
no flow yet
|
|
1181
2262
|
</text>
|
|
1182
|
-
)
|
|
2263
|
+
) : (
|
|
2264
|
+
// Round 56 / Loop: each row is a navigator into the canvas.
|
|
2265
|
+
// Hover a row → set hoveredEdgeKey, which the existing R50
|
|
2266
|
+
// edge-focus + R49 endpoint-highlight ladders consume. The
|
|
2267
|
+
// matching flow edge brightens to 2× + thickens, its two
|
|
2268
|
+
// endpoint nodes stay full opacity, and every other edge +
|
|
2269
|
+
// non-endpoint node dims. Released → all restore. Wrapping
|
|
2270
|
+
// <g> + a transparent 218×14 hitbox so the cursor doesn't
|
|
2271
|
+
// have to land precisely on the truncated text.
|
|
2272
|
+
flowLinks.slice(0, 3).map((link, index) => (
|
|
2273
|
+
<g
|
|
2274
|
+
key={link.key}
|
|
2275
|
+
data-recent-row={link.key}
|
|
2276
|
+
style={{ cursor: 'pointer' }}
|
|
2277
|
+
onMouseEnter={() => setHoveredEdgeKey(link.key)}
|
|
2278
|
+
onMouseLeave={() => setHoveredEdgeKey(prev => prev === link.key ? null : prev)}
|
|
2279
|
+
>
|
|
2280
|
+
<rect x="6" y={38 + index * 16 - 10} width="218" height="14" fill="transparent" />
|
|
2281
|
+
<text
|
|
2282
|
+
x="13" y={38 + index * 16}
|
|
2283
|
+
fill={hoveredEdgeKey === link.key ? pal.legendHeadline : pal.legendText}
|
|
2284
|
+
fontSize="9"
|
|
2285
|
+
fontFamily="monospace"
|
|
2286
|
+
style={{ transition: 'fill 150ms ease-out' }}
|
|
2287
|
+
>
|
|
2288
|
+
{truncate(link.from, 6)} {'->'} {truncate(link.to, 6)} / {link.count} / {truncate(link.content, 12)}
|
|
2289
|
+
</text>
|
|
2290
|
+
</g>
|
|
2291
|
+
))
|
|
2292
|
+
)}
|
|
1183
2293
|
</g>
|
|
1184
2294
|
|
|
1185
|
-
{/* legend
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
<
|
|
1194
|
-
|
|
1195
|
-
|
|
2295
|
+
{/* legend — Round 55 / Loop: each status row is now a hover
|
|
2296
|
+
target. Pointer enter sets `hoveredStatus`; pointer leave
|
|
2297
|
+
clears it. Node opacity formula composes the match below.
|
|
2298
|
+
The row text brightens to legendHeadline while hovered as
|
|
2299
|
+
a small affordance hint. Geometry unchanged — the new
|
|
2300
|
+
<g> wrappers only carry pointer handlers. */}
|
|
2301
|
+
<g transform="translate(760, 16)">
|
|
2302
|
+
{/* R57: matching drop-shadow elevation to the legend panel. */}
|
|
2303
|
+
<rect
|
|
2304
|
+
x="0" y="0" width="224" height="96" rx="10"
|
|
2305
|
+
fill={pal.legendBox.fill}
|
|
2306
|
+
stroke={pal.legendBox.stroke}
|
|
2307
|
+
opacity={isLight ? 0.97 : 0.92}
|
|
2308
|
+
style={{ filter: isLight ? 'drop-shadow(0 2px 6px rgba(15,23,42,0.08))' : 'drop-shadow(0 2px 6px rgba(0,0,0,0.45))' }}
|
|
2309
|
+
data-topo-panel-elevation="legend"
|
|
2310
|
+
/>
|
|
2311
|
+
{([
|
|
2312
|
+
{ key: 'working' as const, y0: 24, y1: 28, fill: isLight ? '#059669' : '#22c55e', label: 'working node' },
|
|
2313
|
+
{ key: 'idle' as const, y0: 48, y1: 52, fill: isLight ? '#0d9488' : '#2dd4bf', label: 'online idle' },
|
|
2314
|
+
{ key: 'offline' as const, y0: 72, y1: 76, fill: isLight ? '#94a3b8' : '#6b7280', label: 'offline / no SSE' },
|
|
2315
|
+
]).map(row => {
|
|
2316
|
+
// Round 61 / Loop: legend rows pin too — symmetric with the
|
|
2317
|
+
// R60 pressure-bar segments. R55 hover stays transient; the
|
|
2318
|
+
// new onClick toggles pinnedStatus so users can lock a
|
|
2319
|
+
// filter without holding the cursor still. Pinned row gets
|
|
2320
|
+
// an inset ring on the swatch (same vocab as R60).
|
|
2321
|
+
const isPinned = pinnedStatus === row.key;
|
|
2322
|
+
return (
|
|
2323
|
+
<g
|
|
2324
|
+
key={row.key}
|
|
2325
|
+
data-legend-status={row.key}
|
|
2326
|
+
role="button"
|
|
2327
|
+
tabIndex={0}
|
|
2328
|
+
aria-pressed={isPinned}
|
|
2329
|
+
style={{ cursor: 'pointer' }}
|
|
2330
|
+
// R61: stopPropagation on pointerdown so the SVG-level
|
|
2331
|
+
// pan handler (R103) doesn't setPointerCapture and
|
|
2332
|
+
// redirect the follow-up click away from this <g>.
|
|
2333
|
+
// Same trick the node <g> uses (and R52 hub).
|
|
2334
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
2335
|
+
onMouseEnter={() => setHoveredStatus(row.key)}
|
|
2336
|
+
onMouseLeave={() => setHoveredStatus(prev => prev === row.key ? null : prev)}
|
|
2337
|
+
onClick={() => setPinnedStatus(prev => prev === row.key ? null : row.key)}
|
|
2338
|
+
>
|
|
2339
|
+
{/* invisible hitbox covers the row so cursor doesn't need
|
|
2340
|
+
to be exactly on the 5-px swatch */}
|
|
2341
|
+
<rect x="6" y={row.y0 - 12} width="170" height="22" fill="transparent" />
|
|
2342
|
+
<circle cx="16" cy={row.y0} r="5.5" fill={row.fill} />
|
|
2343
|
+
{/* R61 pinned-state ring — concentric stroke at r=8 in
|
|
2344
|
+
the row colour, draws OUTSIDE the swatch so it
|
|
2345
|
+
doesn't fight the fill colour the user is matching. */}
|
|
2346
|
+
{isPinned && (
|
|
2347
|
+
<circle cx="16" cy={row.y0} r="8" fill="none" stroke={row.fill} strokeWidth="1.5" />
|
|
2348
|
+
)}
|
|
2349
|
+
<text
|
|
2350
|
+
x="30" y={row.y1}
|
|
2351
|
+
fill={hoveredStatus === row.key || isPinned ? pal.legendHeadline : pal.legendText}
|
|
2352
|
+
fontSize="11"
|
|
2353
|
+
fontFamily="monospace"
|
|
2354
|
+
style={{ transition: 'fill 150ms ease-out' }}
|
|
2355
|
+
>{row.label}</text>
|
|
2356
|
+
</g>
|
|
2357
|
+
);
|
|
2358
|
+
})}
|
|
2359
|
+
{/* Flow-arrow swatch sits at y=72 — same row as the offline
|
|
2360
|
+
hitbox. Drop its pointerEvents so the offline legend row
|
|
2361
|
+
stays hoverable (R55). It's decoration, no need to
|
|
2362
|
+
receive events. */}
|
|
2363
|
+
<path d="M140,72 Q164,48 196,72" fill="none" stroke={pal.flowEdge} strokeWidth="3" markerEnd="url(#topo-arrow)" style={{ pointerEvents: 'none' }} />
|
|
1196
2364
|
</g>
|
|
1197
2365
|
</svg>
|
|
1198
2366
|
|
|
2367
|
+
{/* Round 30 / Loop: minimap. Big fleets in fullscreen mode at high
|
|
2368
|
+
zoom let users lose their position — the minimap shows the
|
|
2369
|
+
whole topology miniaturised plus a viewport rectangle so the
|
|
2370
|
+
user always knows where they are. Click anywhere to recenter
|
|
2371
|
+
the canvas there. Only mounted when the view is non-default
|
|
2372
|
+
(zoomed or panned) since at 1× centered the minimap and the
|
|
2373
|
+
canvas show the same thing. HTML overlay so it stays fixed
|
|
2374
|
+
while the SVG transforms. */}
|
|
2375
|
+
{(() => {
|
|
2376
|
+
const isDefaultView = Math.abs(view.zoom - 1) < 0.01 && Math.abs(view.x) < 1 && Math.abs(view.y) < 1;
|
|
2377
|
+
if (isDefaultView || (onlineNodes.length + offlineNodes.length) === 0) return null;
|
|
2378
|
+
const MW = 120, MH = 82;
|
|
2379
|
+
const sx = MW / VIEWBOX_W, sy = MH / VIEWBOX_H;
|
|
2380
|
+
const rectX = (-view.x / view.zoom) * sx;
|
|
2381
|
+
const rectY = (-view.y / view.zoom) * sy;
|
|
2382
|
+
const rectW = (VIEWBOX_W / view.zoom) * sx;
|
|
2383
|
+
const rectH = (VIEWBOX_H / view.zoom) * sy;
|
|
2384
|
+
return (
|
|
2385
|
+
<div
|
|
2386
|
+
className="absolute right-3 rounded-md border shadow-lg shadow-black/30 overflow-hidden anet-fade-in"
|
|
2387
|
+
style={{ bottom: 56, background: pal.legendBox.fill, borderColor: pal.containerBorder, cursor: 'crosshair' }}
|
|
2388
|
+
role="img"
|
|
2389
|
+
aria-label="Topology minimap — click to recenter"
|
|
2390
|
+
title="Minimap · click to recenter"
|
|
2391
|
+
onClick={(e) => {
|
|
2392
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
2393
|
+
const fx = (e.clientX - r.left) / r.width;
|
|
2394
|
+
const fy = (e.clientY - r.top) / r.height;
|
|
2395
|
+
setView(prev => ({
|
|
2396
|
+
...prev,
|
|
2397
|
+
x: VIEWBOX_W / 2 - fx * VIEWBOX_W * prev.zoom,
|
|
2398
|
+
y: VIEWBOX_H / 2 - fy * VIEWBOX_H * prev.zoom,
|
|
2399
|
+
}));
|
|
2400
|
+
}}
|
|
2401
|
+
data-topo-minimap
|
|
2402
|
+
>
|
|
2403
|
+
<svg width={MW} height={MH} viewBox={`0 0 ${MW} ${MH}`} style={{ display: 'block' }}>
|
|
2404
|
+
{[...onlineNodes, ...offlineNodes].map(s => {
|
|
2405
|
+
const p = nodePositions[s.alias];
|
|
2406
|
+
if (!p) return null;
|
|
2407
|
+
const sseN = (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];
|
|
2408
|
+
const isOn = s.status !== 'offline' || !!sseN;
|
|
2409
|
+
const st = nodeStatus(s, isOn, isLight);
|
|
2410
|
+
return <circle key={s.alias} cx={p.x * sx} cy={p.y * sy} r={isOn ? 1.7 : 1.2} fill={st.primary} opacity={isOn ? 0.9 : 0.5} />;
|
|
2411
|
+
})}
|
|
2412
|
+
{/* viewport rectangle */}
|
|
2413
|
+
<rect
|
|
2414
|
+
x={Math.max(0, rectX)} y={Math.max(0, rectY)}
|
|
2415
|
+
width={Math.max(0, Math.min(MW - Math.max(0, rectX), rectW))}
|
|
2416
|
+
height={Math.max(0, Math.min(MH - Math.max(0, rectY), rectH))}
|
|
2417
|
+
fill="none" stroke={pal.legendAccent} strokeWidth="1" opacity="0.9"
|
|
2418
|
+
/>
|
|
2419
|
+
</svg>
|
|
2420
|
+
</div>
|
|
2421
|
+
);
|
|
2422
|
+
})()}
|
|
1199
2423
|
{/* Round 103 (issue #81): zoom / pan / fullscreen controls — HTML
|
|
1200
2424
|
overlay so they stay fixed while the SVG content transforms.
|
|
1201
2425
|
Round 104: Vincent 实测 — the reset action used to be hidden
|
|
@@ -1203,6 +2427,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1203
2427
|
Split into a plain % readout + an explicit reset button with
|
|
1204
2428
|
its own icon + tooltip. */}
|
|
1205
2429
|
<div className="absolute bottom-3 right-3 flex items-center gap-1.5 text-xs select-none">
|
|
2430
|
+
{/* #113: node size — S / M / L segmented control (Vincent 4727). */}
|
|
2431
|
+
<div
|
|
2432
|
+
className="flex items-center rounded-md border overflow-hidden"
|
|
2433
|
+
style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder }}
|
|
2434
|
+
role="group"
|
|
2435
|
+
aria-label="Node size"
|
|
2436
|
+
>
|
|
2437
|
+
{([['S', 0.7], ['M', 0.84], ['L', 1]] as const).map(([lbl, v], idx) => (
|
|
2438
|
+
<button
|
|
2439
|
+
key={lbl}
|
|
2440
|
+
onClick={() => pickNodeScale(v)}
|
|
2441
|
+
aria-pressed={nodeScale === v}
|
|
2442
|
+
title={`Node size: ${lbl === 'S' ? 'small' : lbl === 'M' ? 'medium' : 'large'}`}
|
|
2443
|
+
className={`px-2 py-1 transition-colors ${idx > 0 ? 'border-l' : ''} ${nodeScale === v ? 'bg-cyan-500/15 text-cyan-300' : 'hover:bg-white/5'}`}
|
|
2444
|
+
style={{ color: nodeScale === v ? undefined : pal.legendText, borderColor: pal.containerBorder }}
|
|
2445
|
+
>
|
|
2446
|
+
{lbl}
|
|
2447
|
+
</button>
|
|
2448
|
+
))}
|
|
2449
|
+
</div>
|
|
1206
2450
|
<div
|
|
1207
2451
|
className="flex items-center rounded-md border overflow-hidden"
|
|
1208
2452
|
style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder }}
|
|
@@ -1212,6 +2456,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1212
2456
|
className="px-2 py-1 hover:bg-white/5 transition-colors"
|
|
1213
2457
|
style={{ color: pal.legendText }}
|
|
1214
2458
|
aria-label="Zoom out"
|
|
2459
|
+
title="Zoom out (−)"
|
|
1215
2460
|
>
|
|
1216
2461
|
<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>
|
|
1217
2462
|
</button>
|
|
@@ -1227,6 +2472,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1227
2472
|
className="px-2 py-1 hover:bg-white/5 transition-colors"
|
|
1228
2473
|
style={{ color: pal.legendText }}
|
|
1229
2474
|
aria-label="Zoom in"
|
|
2475
|
+
title="Zoom in (+)"
|
|
1230
2476
|
>
|
|
1231
2477
|
<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>
|
|
1232
2478
|
</button>
|
|
@@ -1236,7 +2482,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1236
2482
|
className="p-1.5 rounded-md border hover:bg-white/5 transition-colors"
|
|
1237
2483
|
style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
|
|
1238
2484
|
aria-label="Reset view"
|
|
1239
|
-
title="Reset zoom + pan (or double-click the canvas)"
|
|
2485
|
+
title="Reset zoom + pan (0, or double-click the canvas)"
|
|
1240
2486
|
>
|
|
1241
2487
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
1242
2488
|
<path d="M3 12a9 9 0 1 0 9-9 9 9 0 0 0-6.4 2.6L3 8" />
|