@sleep2agi/agent-network-dashboard 0.4.6-preview.4 → 0.4.6-preview.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (834) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/build/chunks/{node_modules_13sb.px._.js → 00jm_02mlcew._.js} +167 -167
  4. package/.next/build/chunks/00jm_02mlcew._.js.map +47 -0
  5. package/.next/build/chunks/{[root-of-the-server]__0ubbtyl._.js → [root-of-the-server]__0tgc-8p._.js} +3 -3
  6. package/.next/build/chunks/{[root-of-the-server]__0d-m0h0._.js → [root-of-the-server]__13z64y.._.js} +7 -7
  7. package/.next/build/chunks/{[root-of-the-server]__0d-m0h0._.js.map → [root-of-the-server]__13z64y.._.js.map} +2 -2
  8. package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_0r4x5ys._.js +13 -0
  9. package/.next/build/chunks/[turbopack]_runtime.js +1 -1
  10. package/.next/build/postcss.js +4 -4
  11. package/.next/build-manifest.json +8 -8
  12. package/.next/diagnostics/route-bundle-stats.json +157 -143
  13. package/.next/fallback-build-manifest.json +3 -3
  14. package/.next/next-minimal-server.js.nft.json +1 -1
  15. package/.next/next-server.js.nft.json +1 -1
  16. package/.next/prerender-manifest.json +3 -3
  17. package/.next/required-server-files.js +4 -4
  18. package/.next/required-server-files.json +4 -4
  19. package/.next/routes-manifest.json +6 -0
  20. package/.next/server/app/_global-error/page/build-manifest.json +5 -5
  21. package/.next/server/app/_global-error/page.js +9 -9
  22. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  23. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  24. package/.next/server/app/_global-error.html +1 -1
  25. package/.next/server/app/_global-error.rsc +8 -8
  26. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  27. package/.next/server/app/_global-error.segments/_full.segment.rsc +8 -8
  28. package/.next/server/app/_global-error.segments/_head.segment.rsc +4 -4
  29. package/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  30. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  31. package/.next/server/app/_not-found/page/build-manifest.json +5 -5
  32. package/.next/server/app/_not-found/page/next-font-manifest.json +1 -1
  33. package/.next/server/app/_not-found/page.js +12 -12
  34. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  35. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/_not-found.html +2 -2
  37. package/.next/server/app/_not-found.rsc +12 -12
  38. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  39. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  40. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  41. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  42. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  43. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  44. package/.next/server/app/admin/page/build-manifest.json +5 -5
  45. package/.next/server/app/admin/page/next-font-manifest.json +1 -1
  46. package/.next/server/app/admin/page.js +13 -13
  47. package/.next/server/app/admin/page.js.nft.json +1 -1
  48. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  49. package/.next/server/app/admin.html +2 -2
  50. package/.next/server/app/admin.rsc +14 -14
  51. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  52. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  53. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  54. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  55. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  56. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  57. package/.next/server/app/api/anet/config/route.js +6 -6
  58. package/.next/server/app/api/anet/config/route.js.nft.json +1 -1
  59. package/.next/server/app/api/auth/login/route.js +7 -7
  60. package/.next/server/app/api/auth/login/route.js.nft.json +1 -1
  61. package/.next/server/app/api/auth/logout/route.js +7 -7
  62. package/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  63. package/.next/server/app/api/auth/v3/route.js +7 -7
  64. package/.next/server/app/api/auth/v3/route.js.nft.json +1 -1
  65. package/.next/server/app/api/hub/audit-log/route.js +6 -6
  66. package/.next/server/app/api/hub/audit-log/route.js.nft.json +1 -1
  67. package/.next/server/app/api/hub/auth/route.js +7 -7
  68. package/.next/server/app/api/hub/auth/route.js.nft.json +1 -1
  69. package/.next/server/app/api/hub/broadcast/route.js +6 -6
  70. package/.next/server/app/api/hub/broadcast/route.js.nft.json +1 -1
  71. package/.next/server/app/api/hub/events/route.js +6 -6
  72. package/.next/server/app/api/hub/events/route.js.nft.json +1 -1
  73. package/.next/server/app/api/hub/health/route.js +6 -6
  74. package/.next/server/app/api/hub/health/route.js.nft.json +1 -1
  75. package/.next/server/app/api/hub/inbox/route.js +6 -6
  76. package/.next/server/app/api/hub/inbox/route.js.nft.json +1 -1
  77. package/.next/server/app/api/hub/license/route.js +6 -6
  78. package/.next/server/app/api/hub/license/route.js.nft.json +1 -1
  79. package/.next/server/app/api/hub/messages/route.js +6 -6
  80. package/.next/server/app/api/hub/messages/route.js.nft.json +1 -1
  81. package/.next/server/app/api/hub/networks/route.js +6 -6
  82. package/.next/server/app/api/hub/networks/route.js.nft.json +1 -1
  83. package/.next/server/app/api/hub/nodes/route.js +6 -6
  84. package/.next/server/app/api/hub/nodes/route.js.nft.json +1 -1
  85. package/.next/server/app/api/hub/register/route.js +6 -6
  86. package/.next/server/app/api/hub/register/route.js.nft.json +1 -1
  87. package/.next/server/app/api/hub/send/route.js +6 -6
  88. package/.next/server/app/api/hub/send/route.js.nft.json +1 -1
  89. package/.next/server/app/api/hub/server-logs/route.js +6 -6
  90. package/.next/server/app/api/hub/server-logs/route.js.nft.json +1 -1
  91. package/.next/server/app/api/hub/servers/route/app-paths-manifest.json +3 -0
  92. package/.next/server/app/api/hub/servers/route/build-manifest.json +9 -0
  93. package/.next/server/app/api/hub/servers/route/server-reference-manifest.json +4 -0
  94. package/.next/server/app/api/hub/servers/route.js +7 -0
  95. package/.next/server/app/api/hub/servers/route.js.map +5 -0
  96. package/.next/server/app/api/hub/servers/route.js.nft.json +1 -0
  97. package/.next/server/app/api/hub/servers/route_client-reference-manifest.js +3 -0
  98. package/.next/server/app/api/hub/session/route.js +6 -6
  99. package/.next/server/app/api/hub/session/route.js.nft.json +1 -1
  100. package/.next/server/app/api/hub/stats/route.js +6 -6
  101. package/.next/server/app/api/hub/stats/route.js.nft.json +1 -1
  102. package/.next/server/app/api/hub/status/route.js +6 -6
  103. package/.next/server/app/api/hub/status/route.js.nft.json +1 -1
  104. package/.next/server/app/api/hub/task-events/route.js +6 -6
  105. package/.next/server/app/api/hub/task-events/route.js.nft.json +1 -1
  106. package/.next/server/app/api/hub/tasks/route.js +6 -6
  107. package/.next/server/app/api/hub/tasks/route.js.nft.json +1 -1
  108. package/.next/server/app/api/hub/tmux/route.js +6 -6
  109. package/.next/server/app/api/hub/tmux/route.js.nft.json +1 -1
  110. package/.next/server/app/api/hub/tokens/route.js +6 -6
  111. package/.next/server/app/api/hub/tokens/route.js.nft.json +1 -1
  112. package/.next/server/app/favicon.ico/route.js +6 -6
  113. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  114. package/.next/server/app/index.html +2 -2
  115. package/.next/server/app/index.rsc +14 -14
  116. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  117. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  118. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  119. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  120. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  121. package/.next/server/app/login/page/build-manifest.json +5 -5
  122. package/.next/server/app/login/page/next-font-manifest.json +1 -1
  123. package/.next/server/app/login/page.js +13 -13
  124. package/.next/server/app/login/page.js.nft.json +1 -1
  125. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  126. package/.next/server/app/login.html +2 -2
  127. package/.next/server/app/login.rsc +14 -14
  128. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  129. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  130. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  131. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  132. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  133. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  134. package/.next/server/app/logs/page/build-manifest.json +5 -5
  135. package/.next/server/app/logs/page/next-font-manifest.json +1 -1
  136. package/.next/server/app/logs/page.js +13 -13
  137. package/.next/server/app/logs/page.js.nft.json +1 -1
  138. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  139. package/.next/server/app/logs.html +2 -2
  140. package/.next/server/app/logs.rsc +14 -14
  141. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  142. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  143. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  144. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  145. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  146. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  147. package/.next/server/app/messages/page/build-manifest.json +5 -5
  148. package/.next/server/app/messages/page/next-font-manifest.json +1 -1
  149. package/.next/server/app/messages/page.js +13 -13
  150. package/.next/server/app/messages/page.js.nft.json +1 -1
  151. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  152. package/.next/server/app/messages.html +2 -2
  153. package/.next/server/app/messages.rsc +14 -14
  154. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  155. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  156. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  157. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  158. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  159. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  160. package/.next/server/app/node/page/build-manifest.json +5 -5
  161. package/.next/server/app/node/page/next-font-manifest.json +1 -1
  162. package/.next/server/app/node/page.js +13 -13
  163. package/.next/server/app/node/page.js.nft.json +1 -1
  164. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  165. package/.next/server/app/node.html +2 -2
  166. package/.next/server/app/node.rsc +14 -14
  167. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  168. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  169. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  170. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  171. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  172. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  173. package/.next/server/app/nodes/page/build-manifest.json +5 -5
  174. package/.next/server/app/nodes/page/next-font-manifest.json +1 -1
  175. package/.next/server/app/nodes/page.js +13 -13
  176. package/.next/server/app/nodes/page.js.nft.json +1 -1
  177. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  178. package/.next/server/app/nodes.html +2 -2
  179. package/.next/server/app/nodes.rsc +14 -14
  180. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  181. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  182. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  183. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  184. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  185. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  186. package/.next/server/app/page/build-manifest.json +5 -5
  187. package/.next/server/app/page/next-font-manifest.json +1 -1
  188. package/.next/server/app/page.js +13 -13
  189. package/.next/server/app/page.js.nft.json +1 -1
  190. package/.next/server/app/page_client-reference-manifest.js +1 -1
  191. package/.next/server/app/server-logs/page/build-manifest.json +5 -5
  192. package/.next/server/app/server-logs/page/next-font-manifest.json +1 -1
  193. package/.next/server/app/server-logs/page.js +13 -13
  194. package/.next/server/app/server-logs/page.js.nft.json +1 -1
  195. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  196. package/.next/server/app/server-logs.html +2 -2
  197. package/.next/server/app/server-logs.rsc +14 -14
  198. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  199. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  200. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  201. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  202. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  203. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  204. package/.next/server/app/settings/networks/page/build-manifest.json +5 -5
  205. package/.next/server/app/settings/networks/page/next-font-manifest.json +1 -1
  206. package/.next/server/app/settings/networks/page.js +13 -13
  207. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  208. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  209. package/.next/server/app/settings/networks.html +2 -2
  210. package/.next/server/app/settings/networks.rsc +14 -14
  211. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  212. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  213. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  214. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  215. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  216. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  217. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  218. package/.next/server/app/settings/page/build-manifest.json +5 -5
  219. package/.next/server/app/settings/page/next-font-manifest.json +1 -1
  220. package/.next/server/app/settings/page.js +13 -13
  221. package/.next/server/app/settings/page.js.nft.json +1 -1
  222. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  223. package/.next/server/app/settings/tokens/page/build-manifest.json +5 -5
  224. package/.next/server/app/settings/tokens/page/next-font-manifest.json +1 -1
  225. package/.next/server/app/settings/tokens/page.js +13 -13
  226. package/.next/server/app/settings/tokens/page.js.nft.json +1 -1
  227. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  228. package/.next/server/app/settings/tokens.html +2 -2
  229. package/.next/server/app/settings/tokens.rsc +14 -14
  230. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  231. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  232. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  233. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  234. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  235. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  236. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  237. package/.next/server/app/settings.html +2 -2
  238. package/.next/server/app/settings.rsc +14 -14
  239. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  240. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  241. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  242. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  243. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  244. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  245. package/.next/server/app/tasks/[id]/page/build-manifest.json +5 -5
  246. package/.next/server/app/tasks/[id]/page/next-font-manifest.json +1 -1
  247. package/.next/server/app/tasks/[id]/page.js +13 -13
  248. package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  249. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  250. package/.next/server/app/tasks/page/build-manifest.json +5 -5
  251. package/.next/server/app/tasks/page/next-font-manifest.json +1 -1
  252. package/.next/server/app/tasks/page.js +13 -13
  253. package/.next/server/app/tasks/page.js.nft.json +1 -1
  254. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  255. package/.next/server/app/tasks.html +2 -2
  256. package/.next/server/app/tasks.rsc +14 -14
  257. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  258. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  259. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  260. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  261. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  262. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  263. package/.next/server/app-paths-manifest.json +1 -0
  264. package/.next/server/chunks/00jm_next_04f3gac._.js +10 -0
  265. package/.next/server/chunks/00jm_next_04f3gac._.js.map +1 -0
  266. package/.next/server/chunks/00jm_next_dist_esm_build_templates_app-route_02f430o.js +3 -0
  267. package/.next/server/chunks/00jm_next_dist_esm_build_templates_app-route_02f430o.js.map +1 -0
  268. package/.next/server/chunks/0ci9_nt-network-dashboard__next-internal_server_app_api_auth_v3_route_actions_0f5nbzr.js +3 -0
  269. package/.next/server/chunks/0ci9_nt-network-dashboard__next-internal_server_app_favicon_ico_route_actions_0t0eu9r.js +3 -0
  270. package/.next/server/chunks/0ykm__next-internal_server_app_api_anet_config_route_actions_0a~v55_.js +3 -0
  271. package/.next/server/chunks/0ykm__next-internal_server_app_api_auth_login_route_actions_0xnxfs4.js +3 -0
  272. package/.next/server/chunks/0ykm__next-internal_server_app_api_auth_logout_route_actions_0p8p0rn.js +3 -0
  273. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_audit-log_route_actions_06uquwi.js +3 -0
  274. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_auth_route_actions_0f_y1ey.js +3 -0
  275. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_broadcast_route_actions_0ao3j43.js +3 -0
  276. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_events_route_actions_0_5ndld.js +3 -0
  277. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_health_route_actions_0mpzl74.js +3 -0
  278. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_inbox_route_actions_0stoyto.js +3 -0
  279. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_license_route_actions_0a4.fuh.js +3 -0
  280. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_messages_route_actions_059rw1o.js +3 -0
  281. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_networks_route_actions_03sehpn.js +3 -0
  282. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_nodes_route_actions_11fbp28.js +3 -0
  283. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_register_route_actions_0l.d7~n.js +3 -0
  284. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_send_route_actions_0.p82vy.js +3 -0
  285. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_server-logs_route_actions_05~c5lb.js +3 -0
  286. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_servers_route_actions_08-m-cs.js +3 -0
  287. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_session_route_actions_0b9ef2i.js +3 -0
  288. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_stats_route_actions_03u65u0.js +3 -0
  289. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_status_route_actions_0_71ear.js +3 -0
  290. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_task-events_route_actions_07md_ee.js +3 -0
  291. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_tasks_route_actions_0er3uvk.js +3 -0
  292. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_tmux_route_actions_0o_1-8u.js +3 -0
  293. package/.next/server/chunks/0ykm__next-internal_server_app_api_hub_tokens_route_actions_09doluz.js +3 -0
  294. package/.next/server/chunks/[root-of-the-server]__0-2le8-._.js +3 -0
  295. package/.next/server/chunks/[root-of-the-server]__0-2le8-._.js.map +1 -0
  296. package/.next/server/chunks/[root-of-the-server]__0-g~15e._.js +3 -0
  297. package/.next/server/chunks/[root-of-the-server]__0-g~15e._.js.map +1 -0
  298. package/.next/server/chunks/[root-of-the-server]__00194.t._.js +3 -0
  299. package/.next/server/chunks/{[root-of-the-server]__03ueci3._.js.map → [root-of-the-server]__00194.t._.js.map} +1 -1
  300. package/.next/server/chunks/[root-of-the-server]__02p2n3n._.js +3 -0
  301. package/.next/server/chunks/[root-of-the-server]__02p2n3n._.js.map +1 -0
  302. package/.next/server/chunks/[root-of-the-server]__02y-i-p._.js +6 -0
  303. package/.next/server/chunks/[root-of-the-server]__02y-i-p._.js.map +1 -0
  304. package/.next/server/chunks/[root-of-the-server]__043tnx2._.js +3 -0
  305. package/.next/server/chunks/{[root-of-the-server]__0o4qfn9._.js.map → [root-of-the-server]__043tnx2._.js.map} +1 -1
  306. package/.next/server/chunks/[root-of-the-server]__04z62zk._.js +3 -0
  307. package/.next/server/chunks/[root-of-the-server]__04z62zk._.js.map +1 -0
  308. package/.next/server/chunks/[root-of-the-server]__06p6kkp._.js +3 -0
  309. package/.next/server/chunks/[root-of-the-server]__06p6kkp._.js.map +1 -0
  310. package/.next/server/chunks/[root-of-the-server]__07ztet2._.js +3 -0
  311. package/.next/server/chunks/[root-of-the-server]__07ztet2._.js.map +1 -0
  312. package/.next/server/chunks/[root-of-the-server]__08uyvdq._.js +20 -0
  313. package/.next/server/chunks/[root-of-the-server]__08uyvdq._.js.map +1 -0
  314. package/.next/server/chunks/[root-of-the-server]__0b7v51y._.js +3 -0
  315. package/.next/server/chunks/[root-of-the-server]__0b7v51y._.js.map +1 -0
  316. package/.next/server/chunks/[root-of-the-server]__0e3ucl1._.js +3 -0
  317. package/.next/server/chunks/{[root-of-the-server]__0rg9ifx._.js.map → [root-of-the-server]__0e3ucl1._.js.map} +1 -1
  318. package/.next/server/chunks/[root-of-the-server]__0e6yail._.js +3 -0
  319. package/.next/server/chunks/[root-of-the-server]__0e6yail._.js.map +1 -0
  320. package/.next/server/chunks/[root-of-the-server]__0gx2_6a._.js +3 -0
  321. package/.next/server/chunks/[root-of-the-server]__0gx2_6a._.js.map +1 -0
  322. package/.next/server/chunks/[root-of-the-server]__0hcnyp.._.js +3 -0
  323. package/.next/server/chunks/{[root-of-the-server]__0o29v8c._.js.map → [root-of-the-server]__0hcnyp.._.js.map} +1 -1
  324. package/.next/server/chunks/[root-of-the-server]__0hg3_8r._.js +3 -0
  325. package/.next/server/chunks/[root-of-the-server]__0hg3_8r._.js.map +1 -0
  326. package/.next/server/chunks/[root-of-the-server]__0mxw4vb._.js +3 -0
  327. package/.next/server/chunks/[root-of-the-server]__0mxw4vb._.js.map +1 -0
  328. package/.next/server/chunks/[root-of-the-server]__0mzopco._.js +3 -0
  329. package/.next/server/chunks/[root-of-the-server]__0mzopco._.js.map +1 -0
  330. package/.next/server/chunks/[root-of-the-server]__0o45a4q._.js +3 -0
  331. package/.next/server/chunks/{[root-of-the-server]__0p6tutt._.js.map → [root-of-the-server]__0o45a4q._.js.map} +1 -1
  332. package/.next/server/chunks/[root-of-the-server]__0q-teo6._.js +3 -0
  333. package/.next/server/chunks/[root-of-the-server]__0q-teo6._.js.map +1 -0
  334. package/.next/server/chunks/[root-of-the-server]__0qcxxzf._.js +3 -0
  335. package/.next/server/chunks/[root-of-the-server]__0qcxxzf._.js.map +1 -0
  336. package/.next/server/chunks/[root-of-the-server]__0rovr5-._.js +3 -0
  337. package/.next/server/chunks/[root-of-the-server]__0rovr5-._.js.map +1 -0
  338. package/.next/server/chunks/[root-of-the-server]__0tx8s8i._.js +3 -0
  339. package/.next/server/chunks/{[root-of-the-server]__0~h2uqi._.js.map → [root-of-the-server]__0tx8s8i._.js.map} +1 -1
  340. package/.next/server/chunks/[root-of-the-server]__0xtp-et._.js +3 -0
  341. package/.next/server/chunks/[root-of-the-server]__0xtp-et._.js.map +1 -0
  342. package/.next/server/chunks/[root-of-the-server]__10pvabe._.js +3 -0
  343. package/.next/server/chunks/[root-of-the-server]__10pvabe._.js.map +1 -0
  344. package/.next/server/chunks/[root-of-the-server]__10t11zq._.js +3 -0
  345. package/.next/server/chunks/[root-of-the-server]__10t11zq._.js.map +1 -0
  346. package/.next/server/chunks/[root-of-the-server]__12r~omy._.js +3 -0
  347. package/.next/server/chunks/[root-of-the-server]__12r~omy._.js.map +1 -0
  348. package/.next/server/chunks/[root-of-the-server]__1389~cs._.js +3 -0
  349. package/.next/server/chunks/[root-of-the-server]__1389~cs._.js.map +1 -0
  350. package/.next/server/chunks/[root-of-the-server]__13n8t..._.js +3 -0
  351. package/.next/server/chunks/[root-of-the-server]__13n8t..._.js.map +1 -0
  352. package/.next/server/chunks/[turbopack]_runtime.js +1 -1
  353. package/.next/server/chunks/ssr/{node_modules_0r_.s40._.js → 00jm_03rn9vn._.js} +2 -2
  354. package/.next/server/chunks/ssr/00jm_03rn9vn._.js.map +1 -0
  355. package/.next/server/chunks/ssr/{node_modules_09w7yel._.js → 00jm_059ns68._.js} +3 -3
  356. package/.next/server/chunks/ssr/00jm_059ns68._.js.map +1 -0
  357. package/.next/server/chunks/ssr/00jm_09ae-q0._.js +3 -0
  358. package/.next/server/chunks/ssr/00jm_09ae-q0._.js.map +1 -0
  359. package/.next/server/chunks/ssr/00jm_0j7__z.._.js +3 -0
  360. package/.next/server/chunks/ssr/00jm_0j7__z.._.js.map +1 -0
  361. package/.next/server/chunks/ssr/00jm_next_01-0urn._.js +6 -0
  362. package/.next/server/chunks/ssr/00jm_next_01-0urn._.js.map +1 -0
  363. package/.next/server/chunks/ssr/00jm_next_dist_05rlh5d._.js +6 -0
  364. package/.next/server/chunks/ssr/00jm_next_dist_05rlh5d._.js.map +1 -0
  365. package/.next/server/chunks/ssr/00jm_next_dist_client_components_0gkudcq._.js +3 -0
  366. package/.next/server/chunks/ssr/00jm_next_dist_client_components_0gkudcq._.js.map +1 -0
  367. package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_forbidden_0ecus3e.js +3 -0
  368. package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_forbidden_0ecus3e.js.map +1 -0
  369. package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_global-error_07n8pg2.js +3 -0
  370. package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_global-error_07n8pg2.js.map +1 -0
  371. package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_unauthorized_0r0uq4j.js +3 -0
  372. package/.next/server/chunks/ssr/00jm_next_dist_client_components_builtin_unauthorized_0r0uq4j.js.map +1 -0
  373. 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
  374. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0..ddwm.js.map +1 -0
  375. 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
  376. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_02bxi6j.js.map +1 -0
  377. 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
  378. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_05oassd.js.map +1 -0
  379. 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
  380. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_05vxtby.js.map +1 -0
  381. 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
  382. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_05~eml4.js.map +1 -0
  383. 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
  384. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0_-g_q2.js.map +1 -0
  385. 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
  386. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0_ec0xu.js.map +1 -0
  387. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0czb6wz.js +4 -0
  388. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0czb6wz.js.map +1 -0
  389. 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
  390. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0e-cn.p.js.map +1 -0
  391. 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
  392. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0k8_sr8.js.map +1 -0
  393. 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
  394. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0lt_3bw.js.map +1 -0
  395. 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
  396. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0uyh3yt.js.map +1 -0
  397. 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
  398. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_0v-a2ps.js.map +1 -0
  399. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_12.4r9~.js +4 -0
  400. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_12.4r9~.js.map +1 -0
  401. 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
  402. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_13mqkay.js.map +1 -0
  403. package/.next/server/chunks/ssr/0dwh_ent-network-dashboard__next-internal_server_app_server-logs_page_actions_0hkk815.js +3 -0
  404. package/.next/server/chunks/ssr/0yas_gent-network-dashboard__next-internal_server_app__not-found_page_actions_0u.f-rz.js +3 -0
  405. package/.next/server/chunks/ssr/0yas_gent-network-dashboard__next-internal_server_app_tasks_[id]_page_actions_0r3~lho.js +3 -0
  406. package/.next/server/chunks/ssr/0ykm__next-internal_server_app__global-error_page_actions_100fxls.js +3 -0
  407. package/.next/server/chunks/ssr/0ykm__next-internal_server_app_settings_networks_page_actions_0jgy57m.js +3 -0
  408. package/.next/server/chunks/ssr/0ykm__next-internal_server_app_settings_tokens_page_actions_0l5h.k~.js +3 -0
  409. package/.next/server/chunks/ssr/[root-of-the-server]__030vg4n._.js +3 -0
  410. package/.next/server/chunks/ssr/[root-of-the-server]__030vg4n._.js.map +1 -0
  411. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js +3 -0
  412. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +1 -0
  413. package/.next/server/chunks/ssr/[root-of-the-server]__063~dul._.js +3 -0
  414. package/.next/server/chunks/ssr/[root-of-the-server]__063~dul._.js.map +1 -0
  415. package/.next/server/chunks/ssr/[root-of-the-server]__07io.d8._.js +3 -0
  416. package/.next/server/chunks/ssr/[root-of-the-server]__07io.d8._.js.map +1 -0
  417. package/.next/server/chunks/ssr/[root-of-the-server]__08d6lp4._.js +3 -0
  418. package/.next/server/chunks/ssr/[root-of-the-server]__08d6lp4._.js.map +1 -0
  419. package/.next/server/chunks/ssr/[root-of-the-server]__09f1wn6._.js +3 -0
  420. package/.next/server/chunks/ssr/[root-of-the-server]__09f1wn6._.js.map +1 -0
  421. package/.next/server/chunks/ssr/[root-of-the-server]__0c4xtkg._.js +3 -0
  422. package/.next/server/chunks/ssr/[root-of-the-server]__0c4xtkg._.js.map +1 -0
  423. package/.next/server/chunks/ssr/[root-of-the-server]__0erfzjn._.js +3 -0
  424. package/.next/server/chunks/ssr/[root-of-the-server]__0erfzjn._.js.map +1 -0
  425. package/.next/server/chunks/ssr/[root-of-the-server]__0ezjwje._.js +3 -0
  426. package/.next/server/chunks/ssr/[root-of-the-server]__0ezjwje._.js.map +1 -0
  427. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js +3 -0
  428. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js.map +1 -0
  429. package/.next/server/chunks/ssr/{[root-of-the-server]__08qr2ji._.js → [root-of-the-server]__0gc.4uk._.js} +3 -3
  430. package/.next/server/chunks/ssr/[root-of-the-server]__0gc.4uk._.js.map +1 -0
  431. package/.next/server/chunks/ssr/[root-of-the-server]__0h_p~-4._.js +19 -0
  432. package/.next/server/chunks/ssr/[root-of-the-server]__0h_p~-4._.js.map +1 -0
  433. package/.next/server/chunks/ssr/[root-of-the-server]__0jdr1e5._.js +3 -0
  434. package/.next/server/chunks/ssr/[root-of-the-server]__0jdr1e5._.js.map +1 -0
  435. package/.next/server/chunks/ssr/[root-of-the-server]__0lw16mu._.js +3 -0
  436. package/.next/server/chunks/ssr/[root-of-the-server]__0lw16mu._.js.map +1 -0
  437. package/.next/server/chunks/ssr/[root-of-the-server]__0mjly~-._.js +33 -0
  438. package/.next/server/chunks/ssr/[root-of-the-server]__0mjly~-._.js.map +1 -0
  439. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js +3 -0
  440. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js.map +1 -0
  441. package/.next/server/chunks/ssr/[root-of-the-server]__0p-95r_._.js +3 -0
  442. package/.next/server/chunks/ssr/[root-of-the-server]__0p-95r_._.js.map +1 -0
  443. package/.next/server/chunks/ssr/[root-of-the-server]__0u5aqkk._.js +8 -0
  444. package/.next/server/chunks/ssr/[root-of-the-server]__0u5aqkk._.js.map +1 -0
  445. package/.next/server/chunks/ssr/[root-of-the-server]__0ue5.fy._.js +3 -0
  446. package/.next/server/chunks/ssr/[root-of-the-server]__0ue5.fy._.js.map +1 -0
  447. package/.next/server/chunks/ssr/[root-of-the-server]__0uggxvx._.js +3 -0
  448. package/.next/server/chunks/ssr/[root-of-the-server]__0uggxvx._.js.map +1 -0
  449. package/.next/server/chunks/ssr/[root-of-the-server]__0zjk5ro._.js +3 -0
  450. package/.next/server/chunks/ssr/[root-of-the-server]__0zjk5ro._.js.map +1 -0
  451. package/.next/server/chunks/ssr/[root-of-the-server]__0zz3v7a._.js +3 -0
  452. package/.next/server/chunks/ssr/[root-of-the-server]__0zz3v7a._.js.map +1 -0
  453. package/.next/server/chunks/ssr/[root-of-the-server]__0~y12ef._.js +3 -0
  454. package/.next/server/chunks/ssr/[root-of-the-server]__0~y12ef._.js.map +1 -0
  455. package/.next/server/chunks/ssr/[root-of-the-server]__129sc_9._.js +3 -0
  456. package/.next/server/chunks/ssr/[root-of-the-server]__129sc_9._.js.map +1 -0
  457. package/.next/server/chunks/ssr/[turbopack]_runtime.js +1 -1
  458. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -0
  459. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -0
  460. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_admin_page_actions_0i~.x~j.js +3 -0
  461. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_login_page_actions_0frwl15.js +3 -0
  462. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_logs_page_actions_0xa2ox_.js +3 -0
  463. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_messages_page_actions_10j3tg4.js +3 -0
  464. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_node_page_actions_0sl4ntd.js +3 -0
  465. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_nodes_page_actions_0cpgkfs.js +3 -0
  466. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_page_actions_08nuo36.js +3 -0
  467. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_settings_page_actions_0iv.lmk.js +3 -0
  468. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_tasks_page_actions_08c7qlx.js +3 -0
  469. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_tasks_page_actions_08c7qlx.js.map +1 -0
  470. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +3 -0
  471. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -0
  472. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js +3 -0
  473. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js.map +1 -0
  474. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +3 -0
  475. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -0
  476. package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js +3 -0
  477. package/.next/server/chunks/ssr/agent-network-dashboard_app_0_d45-d._.js.map +1 -0
  478. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js +3 -0
  479. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js.map +1 -0
  480. package/.next/server/chunks/ssr/{app_0izi28j._.js → agent-network-dashboard_app_0xgney8._.js} +2 -2
  481. package/.next/server/chunks/ssr/agent-network-dashboard_app_0xgney8._.js.map +1 -0
  482. package/.next/server/chunks/ssr/{app_0_agk0r._.js → agent-network-dashboard_app_10hjgv4._.js} +2 -2
  483. package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js.map +1 -0
  484. package/.next/server/chunks/ssr/{app_0.zbtn5._.js → agent-network-dashboard_app_1153xeb._.js} +2 -2
  485. package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js.map +1 -0
  486. package/.next/server/chunks/ssr/{app_04n78pr._.js → agent-network-dashboard_app_12l4oto._.js} +2 -2
  487. package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js.map +1 -0
  488. package/.next/server/chunks/ssr/{app_components_08ys5y3._.js → agent-network-dashboard_app_components_0mvyi-4._.js} +3 -3
  489. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -0
  490. 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
  491. package/.next/server/chunks/ssr/agent-network-dashboard_app_server-logs_page_tsx_0dg.l_8._.js.map +1 -0
  492. package/.next/server/chunks/ssr/agent-network-dashboard_app_tasks_page_tsx_0mwxy4z._.js +3 -0
  493. package/.next/server/chunks/ssr/agent-network-dashboard_app_tasks_page_tsx_0mwxy4z._.js.map +1 -0
  494. package/.next/server/middleware-build-manifest.js +8 -8
  495. package/.next/server/middleware.js +3 -3
  496. package/.next/server/middleware.js.nft.json +1 -1
  497. package/.next/server/next-font-manifest.js +1 -1
  498. package/.next/server/next-font-manifest.json +14 -14
  499. package/.next/server/pages/404.html +2 -2
  500. package/.next/server/pages/500.html +1 -1
  501. package/.next/server/server-reference-manifest.js +1 -1
  502. package/.next/server/server-reference-manifest.json +1 -1
  503. package/.next/static/chunks/0-7iowzu4.798.js +1 -0
  504. package/.next/static/chunks/{0gtbwcilfu6b7.js → 0.66f3.rtcybb.js} +1 -1
  505. package/.next/static/chunks/02t5jr4shh.hy.js +1 -0
  506. package/.next/static/chunks/039-b.v_9-_rm.js +1 -0
  507. package/.next/static/chunks/05oi25hh7b81r.js +31 -0
  508. package/.next/static/chunks/07~8sx3_5uiox.js +1 -0
  509. package/.next/static/chunks/{0pqt~8bl3ukh4.js → 0_pxg.4pebw__.js} +2 -2
  510. package/.next/static/chunks/0_qfg5s5uzs7..js +7 -0
  511. package/.next/static/chunks/0b8wnt4put-5n.js +2 -0
  512. package/.next/static/chunks/0elalb9n_.~di.js +1 -0
  513. package/.next/static/chunks/{01xlw8hd842-c.js → 0eov~nkwaf-qa.js} +1 -1
  514. package/.next/static/chunks/0j_q__o8p6g83.js +1 -0
  515. package/.next/static/chunks/0jl6_w50zh4l5.js +1 -0
  516. package/.next/static/chunks/{0w1zwt8kq_7nb.js → 0ku0fjqlm9mca.js} +1 -1
  517. package/.next/static/chunks/0mqwn-l7wtcdo.js +7 -0
  518. package/.next/static/chunks/0r4q73.-1bkyl.js +1 -0
  519. package/.next/static/chunks/0r_c8eccg3z2o.js +1 -0
  520. package/.next/static/chunks/0tvn2l1pc.h65.js +1 -0
  521. package/.next/static/chunks/0tz5gaflhax.o.js +1 -0
  522. package/.next/static/chunks/0u3b93reo9xci.js +1 -0
  523. package/.next/static/chunks/10kk9kuoj0ns9.js +1 -0
  524. package/.next/static/chunks/10oi7l-tycgl9.js +1 -0
  525. package/.next/static/chunks/11l042tnn68ye.js +1 -0
  526. package/.next/static/chunks/11uk1izuv3~0t.js +1 -0
  527. package/.next/static/chunks/13.zti9ix.1eh.css +2 -0
  528. package/.next/static/chunks/{turbopack-01f1xs~cq_~si.js → turbopack-0.7dvsf9rllt0.js} +1 -1
  529. package/.next/trace +2 -2
  530. package/.next/trace-build +1 -1
  531. package/.next/types/routes.d.ts +2 -1
  532. package/.next/types/validator.ts +9 -0
  533. package/app/api/hub/servers/route.ts +39 -0
  534. package/app/components/AppShell.tsx +2 -0
  535. package/app/components/ChatPopover.tsx +6 -6
  536. package/app/components/HelpOverlay.tsx +18 -0
  537. package/app/components/ServersDrawer.tsx +206 -0
  538. package/app/components/TaskChatPanel.tsx +39 -39
  539. package/app/components/TopoGraph.tsx +909 -148
  540. package/app/components/types.ts +3 -0
  541. package/app/page.tsx +18 -1
  542. package/package.json +1 -1
  543. package/scripts/chat-popover-test.mjs +2 -0
  544. package/scripts/chat-theme-test.mjs +98 -0
  545. package/scripts/help-overlay-topo-test.mjs +44 -0
  546. package/scripts/servers-drawer-live-test.mjs +96 -0
  547. package/scripts/topo-arrow-tier-test.mjs +61 -0
  548. package/scripts/topo-auto-fit-test.mjs +73 -0
  549. package/scripts/topo-chat-focus-ring-test.mjs +77 -0
  550. package/scripts/topo-click-ripple-test.mjs +66 -0
  551. package/scripts/topo-cwd-tooltip-test.mjs +64 -0
  552. package/scripts/topo-fit-key-test.mjs +106 -0
  553. package/scripts/topo-group-count-test.mjs +63 -0
  554. package/scripts/topo-group-hover-test.mjs +82 -0
  555. package/scripts/topo-groupbox-test.mjs +118 -0
  556. package/scripts/topo-label-lift-test.mjs +115 -0
  557. package/scripts/topo-label-scale-test.mjs +78 -0
  558. package/scripts/topo-lastseen-tooltip-test.mjs +59 -0
  559. package/scripts/topo-layout-key-test.mjs +91 -0
  560. package/scripts/topo-layout-test.mjs +1 -0
  561. package/scripts/topo-minimap-test.mjs +68 -0
  562. package/scripts/topo-offline-dim-test.mjs +70 -0
  563. package/scripts/topo-overlap-test.mjs +164 -0
  564. package/scripts/topo-p0round-test.mjs +166 -0
  565. package/scripts/topo-pan-cursor-test.mjs +62 -0
  566. package/scripts/topo-pressure-bar-test.mjs +69 -0
  567. package/scripts/topo-pulse-rate-test.mjs +71 -0
  568. package/scripts/topo-rename-test.mjs +107 -0
  569. package/scripts/topo-vendor-test.mjs +1 -0
  570. package/scripts/topo-wheel-zoom-test.mjs +79 -0
  571. package/scripts/topo-zoom-keys-test.mjs +89 -0
  572. package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_06e.r3r._.js +0 -13
  573. package/.next/build/chunks/node_modules_13sb.px._.js.map +0 -47
  574. package/.next/server/chunks/[root-of-the-server]__0-mykaf._.js +0 -3
  575. package/.next/server/chunks/[root-of-the-server]__0-mykaf._.js.map +0 -1
  576. package/.next/server/chunks/[root-of-the-server]__0.d_kqy._.js +0 -3
  577. package/.next/server/chunks/[root-of-the-server]__0.d_kqy._.js.map +0 -1
  578. package/.next/server/chunks/[root-of-the-server]__02pocyg._.js +0 -3
  579. package/.next/server/chunks/[root-of-the-server]__02pocyg._.js.map +0 -1
  580. package/.next/server/chunks/[root-of-the-server]__03ueci3._.js +0 -3
  581. package/.next/server/chunks/[root-of-the-server]__04aud7x._.js +0 -3
  582. package/.next/server/chunks/[root-of-the-server]__04aud7x._.js.map +0 -1
  583. package/.next/server/chunks/[root-of-the-server]__05fnt3n._.js +0 -3
  584. package/.next/server/chunks/[root-of-the-server]__05fnt3n._.js.map +0 -1
  585. package/.next/server/chunks/[root-of-the-server]__077tsd.._.js +0 -20
  586. package/.next/server/chunks/[root-of-the-server]__077tsd.._.js.map +0 -1
  587. package/.next/server/chunks/[root-of-the-server]__08wy1jr._.js +0 -3
  588. package/.next/server/chunks/[root-of-the-server]__08wy1jr._.js.map +0 -1
  589. package/.next/server/chunks/[root-of-the-server]__0adhu1m._.js +0 -3
  590. package/.next/server/chunks/[root-of-the-server]__0adhu1m._.js.map +0 -1
  591. package/.next/server/chunks/[root-of-the-server]__0aqv02v._.js +0 -3
  592. package/.next/server/chunks/[root-of-the-server]__0aqv02v._.js.map +0 -1
  593. package/.next/server/chunks/[root-of-the-server]__0cawhfq._.js +0 -3
  594. package/.next/server/chunks/[root-of-the-server]__0cawhfq._.js.map +0 -1
  595. package/.next/server/chunks/[root-of-the-server]__0d_-_k.._.js +0 -3
  596. package/.next/server/chunks/[root-of-the-server]__0d_-_k.._.js.map +0 -1
  597. package/.next/server/chunks/[root-of-the-server]__0f6bxfl._.js +0 -3
  598. package/.next/server/chunks/[root-of-the-server]__0f6bxfl._.js.map +0 -1
  599. package/.next/server/chunks/[root-of-the-server]__0h15edq._.js +0 -3
  600. package/.next/server/chunks/[root-of-the-server]__0h15edq._.js.map +0 -1
  601. package/.next/server/chunks/[root-of-the-server]__0k.vegl._.js +0 -3
  602. package/.next/server/chunks/[root-of-the-server]__0k.vegl._.js.map +0 -1
  603. package/.next/server/chunks/[root-of-the-server]__0kltbce._.js +0 -3
  604. package/.next/server/chunks/[root-of-the-server]__0kltbce._.js.map +0 -1
  605. package/.next/server/chunks/[root-of-the-server]__0mn04fw._.js +0 -3
  606. package/.next/server/chunks/[root-of-the-server]__0mn04fw._.js.map +0 -1
  607. package/.next/server/chunks/[root-of-the-server]__0o29v8c._.js +0 -3
  608. package/.next/server/chunks/[root-of-the-server]__0o4qfn9._.js +0 -3
  609. package/.next/server/chunks/[root-of-the-server]__0o7ya7r._.js +0 -3
  610. package/.next/server/chunks/[root-of-the-server]__0o7ya7r._.js.map +0 -1
  611. package/.next/server/chunks/[root-of-the-server]__0p3fo6l._.js +0 -6
  612. package/.next/server/chunks/[root-of-the-server]__0p3fo6l._.js.map +0 -1
  613. package/.next/server/chunks/[root-of-the-server]__0p6tutt._.js +0 -3
  614. package/.next/server/chunks/[root-of-the-server]__0pn3wsx._.js +0 -3
  615. package/.next/server/chunks/[root-of-the-server]__0pn3wsx._.js.map +0 -1
  616. package/.next/server/chunks/[root-of-the-server]__0rg9ifx._.js +0 -3
  617. package/.next/server/chunks/[root-of-the-server]__0x.-194._.js +0 -3
  618. package/.next/server/chunks/[root-of-the-server]__0x.-194._.js.map +0 -1
  619. package/.next/server/chunks/[root-of-the-server]__0x.a_yh._.js +0 -3
  620. package/.next/server/chunks/[root-of-the-server]__0x.a_yh._.js.map +0 -1
  621. package/.next/server/chunks/[root-of-the-server]__0~h2uqi._.js +0 -3
  622. package/.next/server/chunks/[root-of-the-server]__139~d6a._.js +0 -3
  623. package/.next/server/chunks/[root-of-the-server]__139~d6a._.js.map +0 -1
  624. package/.next/server/chunks/_next-internal_server_app_api_anet_config_route_actions_0xzy73j.js +0 -3
  625. package/.next/server/chunks/_next-internal_server_app_api_auth_login_route_actions_0zukc38.js +0 -3
  626. package/.next/server/chunks/_next-internal_server_app_api_auth_logout_route_actions_0fm~3ij.js +0 -3
  627. package/.next/server/chunks/_next-internal_server_app_api_auth_v3_route_actions_0ndzs5l.js +0 -3
  628. package/.next/server/chunks/_next-internal_server_app_api_hub_audit-log_route_actions_11k8fqt.js +0 -3
  629. package/.next/server/chunks/_next-internal_server_app_api_hub_auth_route_actions_0gs7ttk.js +0 -3
  630. package/.next/server/chunks/_next-internal_server_app_api_hub_broadcast_route_actions_0ads-kd.js +0 -3
  631. package/.next/server/chunks/_next-internal_server_app_api_hub_events_route_actions_0opktby.js +0 -3
  632. package/.next/server/chunks/_next-internal_server_app_api_hub_health_route_actions_0tzzt~x.js +0 -3
  633. package/.next/server/chunks/_next-internal_server_app_api_hub_inbox_route_actions_0xvslrg.js +0 -3
  634. package/.next/server/chunks/_next-internal_server_app_api_hub_license_route_actions_0i3tlr3.js +0 -3
  635. package/.next/server/chunks/_next-internal_server_app_api_hub_messages_route_actions_08-gi-s.js +0 -3
  636. package/.next/server/chunks/_next-internal_server_app_api_hub_networks_route_actions_02vejq2.js +0 -3
  637. package/.next/server/chunks/_next-internal_server_app_api_hub_nodes_route_actions_0nx8ryn.js +0 -3
  638. package/.next/server/chunks/_next-internal_server_app_api_hub_register_route_actions_086wi_3.js +0 -3
  639. package/.next/server/chunks/_next-internal_server_app_api_hub_send_route_actions_0-ga_ug.js +0 -3
  640. package/.next/server/chunks/_next-internal_server_app_api_hub_server-logs_route_actions_00euexv.js +0 -3
  641. package/.next/server/chunks/_next-internal_server_app_api_hub_session_route_actions_041wa62.js +0 -3
  642. package/.next/server/chunks/_next-internal_server_app_api_hub_stats_route_actions_1242~od.js +0 -3
  643. package/.next/server/chunks/_next-internal_server_app_api_hub_status_route_actions_0aml09a.js +0 -3
  644. package/.next/server/chunks/_next-internal_server_app_api_hub_task-events_route_actions_02v1tmm.js +0 -3
  645. package/.next/server/chunks/_next-internal_server_app_api_hub_tasks_route_actions_0nl77v4.js +0 -3
  646. package/.next/server/chunks/_next-internal_server_app_api_hub_tmux_route_actions_0u~aaza.js +0 -3
  647. package/.next/server/chunks/_next-internal_server_app_api_hub_tokens_route_actions_09hzf_1.js +0 -3
  648. package/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_095lj93.js +0 -3
  649. package/.next/server/chunks/node_modules_next_11synfn._.js +0 -10
  650. package/.next/server/chunks/node_modules_next_11synfn._.js.map +0 -1
  651. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0yubv2d.js +0 -3
  652. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0yubv2d.js.map +0 -1
  653. package/.next/server/chunks/ssr/[root-of-the-server]__014uja_._.js +0 -3
  654. package/.next/server/chunks/ssr/[root-of-the-server]__014uja_._.js.map +0 -1
  655. package/.next/server/chunks/ssr/[root-of-the-server]__02hi65f._.js +0 -3
  656. package/.next/server/chunks/ssr/[root-of-the-server]__02hi65f._.js.map +0 -1
  657. package/.next/server/chunks/ssr/[root-of-the-server]__02xqa5z._.js +0 -3
  658. package/.next/server/chunks/ssr/[root-of-the-server]__02xqa5z._.js.map +0 -1
  659. package/.next/server/chunks/ssr/[root-of-the-server]__04ph1de._.js +0 -3
  660. package/.next/server/chunks/ssr/[root-of-the-server]__04ph1de._.js.map +0 -1
  661. package/.next/server/chunks/ssr/[root-of-the-server]__05wc5-z._.js +0 -3
  662. package/.next/server/chunks/ssr/[root-of-the-server]__05wc5-z._.js.map +0 -1
  663. package/.next/server/chunks/ssr/[root-of-the-server]__06-2p1a._.js +0 -33
  664. package/.next/server/chunks/ssr/[root-of-the-server]__06-2p1a._.js.map +0 -1
  665. package/.next/server/chunks/ssr/[root-of-the-server]__08qr2ji._.js.map +0 -1
  666. package/.next/server/chunks/ssr/[root-of-the-server]__09l8xaa._.js +0 -3
  667. package/.next/server/chunks/ssr/[root-of-the-server]__09l8xaa._.js.map +0 -1
  668. package/.next/server/chunks/ssr/[root-of-the-server]__09z7o2x._.js +0 -19
  669. package/.next/server/chunks/ssr/[root-of-the-server]__09z7o2x._.js.map +0 -1
  670. package/.next/server/chunks/ssr/[root-of-the-server]__0a0ffmv._.js +0 -3
  671. package/.next/server/chunks/ssr/[root-of-the-server]__0a0ffmv._.js.map +0 -1
  672. package/.next/server/chunks/ssr/[root-of-the-server]__0b7uu-_._.js +0 -3
  673. package/.next/server/chunks/ssr/[root-of-the-server]__0b7uu-_._.js.map +0 -1
  674. package/.next/server/chunks/ssr/[root-of-the-server]__0bl_wqk._.js +0 -3
  675. package/.next/server/chunks/ssr/[root-of-the-server]__0bl_wqk._.js.map +0 -1
  676. package/.next/server/chunks/ssr/[root-of-the-server]__0ft0rpu._.js +0 -3
  677. package/.next/server/chunks/ssr/[root-of-the-server]__0ft0rpu._.js.map +0 -1
  678. package/.next/server/chunks/ssr/[root-of-the-server]__0i~g-2t._.js +0 -3
  679. package/.next/server/chunks/ssr/[root-of-the-server]__0i~g-2t._.js.map +0 -1
  680. package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js +0 -3
  681. package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js.map +0 -1
  682. package/.next/server/chunks/ssr/[root-of-the-server]__0mook08._.js +0 -3
  683. package/.next/server/chunks/ssr/[root-of-the-server]__0mook08._.js.map +0 -1
  684. package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js +0 -3
  685. package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js.map +0 -1
  686. package/.next/server/chunks/ssr/[root-of-the-server]__0qchqb~._.js +0 -3
  687. package/.next/server/chunks/ssr/[root-of-the-server]__0qchqb~._.js.map +0 -1
  688. package/.next/server/chunks/ssr/[root-of-the-server]__0rrm~-q._.js +0 -3
  689. package/.next/server/chunks/ssr/[root-of-the-server]__0rrm~-q._.js.map +0 -1
  690. package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js +0 -3
  691. package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js.map +0 -1
  692. package/.next/server/chunks/ssr/[root-of-the-server]__0xjish~._.js +0 -3
  693. package/.next/server/chunks/ssr/[root-of-the-server]__0xjish~._.js.map +0 -1
  694. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +0 -3
  695. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +0 -1
  696. package/.next/server/chunks/ssr/[root-of-the-server]__0ywihy_._.js +0 -8
  697. package/.next/server/chunks/ssr/[root-of-the-server]__0ywihy_._.js.map +0 -1
  698. package/.next/server/chunks/ssr/[root-of-the-server]__12ad1n.._.js +0 -3
  699. package/.next/server/chunks/ssr/[root-of-the-server]__12ad1n.._.js.map +0 -1
  700. package/.next/server/chunks/ssr/_0nne7mz._.js +0 -3
  701. package/.next/server/chunks/ssr/_0nne7mz._.js.map +0 -1
  702. package/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_0k77kol.js +0 -3
  703. package/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_0eq97pa.js +0 -3
  704. package/.next/server/chunks/ssr/_next-internal_server_app_admin_page_actions_0ycnu8f.js +0 -3
  705. package/.next/server/chunks/ssr/_next-internal_server_app_login_page_actions_02kefem.js +0 -3
  706. package/.next/server/chunks/ssr/_next-internal_server_app_logs_page_actions_0rvsl~d.js +0 -3
  707. package/.next/server/chunks/ssr/_next-internal_server_app_messages_page_actions_0gsqep_.js +0 -3
  708. package/.next/server/chunks/ssr/_next-internal_server_app_node_page_actions_0wd8cgl.js +0 -3
  709. package/.next/server/chunks/ssr/_next-internal_server_app_nodes_page_actions_0-.rv67.js +0 -3
  710. package/.next/server/chunks/ssr/_next-internal_server_app_page_actions_09-gtaw.js +0 -3
  711. package/.next/server/chunks/ssr/_next-internal_server_app_server-logs_page_actions_0g1gb5c.js +0 -3
  712. package/.next/server/chunks/ssr/_next-internal_server_app_settings_networks_page_actions_09uw.sw.js +0 -3
  713. package/.next/server/chunks/ssr/_next-internal_server_app_settings_page_actions_0mr68ai.js +0 -3
  714. package/.next/server/chunks/ssr/_next-internal_server_app_settings_tokens_page_actions_0yi54x8.js +0 -3
  715. package/.next/server/chunks/ssr/_next-internal_server_app_tasks_[id]_page_actions_0k_hm9g.js +0 -3
  716. package/.next/server/chunks/ssr/_next-internal_server_app_tasks_page_actions_0_2o_cr.js +0 -3
  717. package/.next/server/chunks/ssr/app_0.zbtn5._.js.map +0 -1
  718. package/.next/server/chunks/ssr/app_01y-~mp._.js +0 -3
  719. package/.next/server/chunks/ssr/app_01y-~mp._.js.map +0 -1
  720. package/.next/server/chunks/ssr/app_04n78pr._.js.map +0 -1
  721. package/.next/server/chunks/ssr/app_0_agk0r._.js.map +0 -1
  722. package/.next/server/chunks/ssr/app_0aenxef._.js +0 -3
  723. package/.next/server/chunks/ssr/app_0aenxef._.js.map +0 -1
  724. package/.next/server/chunks/ssr/app_0aum_g5._.js +0 -3
  725. package/.next/server/chunks/ssr/app_0aum_g5._.js.map +0 -1
  726. package/.next/server/chunks/ssr/app_0emvkn2._.js +0 -3
  727. package/.next/server/chunks/ssr/app_0emvkn2._.js.map +0 -1
  728. package/.next/server/chunks/ssr/app_0esuljp._.js +0 -3
  729. package/.next/server/chunks/ssr/app_0esuljp._.js.map +0 -1
  730. package/.next/server/chunks/ssr/app_0izi28j._.js.map +0 -1
  731. package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +0 -1
  732. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js.map +0 -1
  733. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +0 -3
  734. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +0 -1
  735. package/.next/server/chunks/ssr/node_modules_09w7yel._.js.map +0 -1
  736. package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js +0 -3
  737. package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js.map +0 -1
  738. package/.next/server/chunks/ssr/node_modules_0r_.s40._.js.map +0 -1
  739. package/.next/server/chunks/ssr/node_modules_0vtdjw7._.js +0 -3
  740. package/.next/server/chunks/ssr/node_modules_0vtdjw7._.js.map +0 -1
  741. package/.next/server/chunks/ssr/node_modules_next_0w2y7fz._.js +0 -6
  742. package/.next/server/chunks/ssr/node_modules_next_0w2y7fz._.js.map +0 -1
  743. package/.next/server/chunks/ssr/node_modules_next_dist_0e1izl_._.js +0 -6
  744. package/.next/server/chunks/ssr/node_modules_next_dist_0e1izl_._.js.map +0 -1
  745. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_0inhx6q._.js +0 -3
  746. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_0inhx6q._.js.map +0 -1
  747. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_0ghu-f7.js +0 -3
  748. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_0ghu-f7.js.map +0 -1
  749. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0lgvd_..js +0 -3
  750. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0lgvd_..js.map +0 -1
  751. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_unauthorized_0cjv-23.js +0 -3
  752. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_unauthorized_0cjv-23.js.map +0 -1
  753. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_002l7yi.js.map +0 -1
  754. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_02suzhc.js.map +0 -1
  755. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_07uu-xq.js.map +0 -1
  756. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0d2yq5t.js.map +0 -1
  757. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0dm~kj8.js.map +0 -1
  758. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0foejvu.js.map +0 -1
  759. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0l-fh0i.js.map +0 -1
  760. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0nsg22r.js.map +0 -1
  761. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0o9vt1n.js.map +0 -1
  762. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0qm1-a1.js.map +0 -1
  763. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0uccf9z.js.map +0 -1
  764. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0uv6jyc.js.map +0 -1
  765. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_10~~o-h.js +0 -4
  766. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_10~~o-h.js.map +0 -1
  767. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_13leyhk.js +0 -4
  768. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_13leyhk.js.map +0 -1
  769. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_13s07li.js.map +0 -1
  770. package/.next/static/chunks/0-hlcuoemhe7w.js +0 -7
  771. package/.next/static/chunks/0257pdz1-imal.js +0 -2
  772. package/.next/static/chunks/02_zpo7taex9o.js +0 -7
  773. package/.next/static/chunks/04p678v0aui~f.js +0 -1
  774. package/.next/static/chunks/06n3d~fbbtagk.js +0 -1
  775. package/.next/static/chunks/07eba1lz2u3p4.js +0 -1
  776. package/.next/static/chunks/07epshqd2k_l_.js +0 -1
  777. package/.next/static/chunks/09n9xs.6dvvo2.css +0 -2
  778. package/.next/static/chunks/0av2~0.96~d~g.js +0 -1
  779. package/.next/static/chunks/0c1bp~g7.8ru..js +0 -1
  780. package/.next/static/chunks/0e-.zgw81gela.js +0 -31
  781. package/.next/static/chunks/0g9aof0ob_fcj.js +0 -1
  782. package/.next/static/chunks/0s8jo6jypzkx5.js +0 -1
  783. package/.next/static/chunks/0sxaea_g05hbz.js +0 -1
  784. package/.next/static/chunks/0u359llqm1_01.js +0 -1
  785. package/.next/static/chunks/0ubqqhhpsaez4.js +0 -1
  786. package/.next/static/chunks/0~qw9kxksr1v2.js +0 -1
  787. package/.next/static/chunks/12gprw3l0kvvf.js +0 -1
  788. package/.next/static/chunks/12yuo353giomx.js +0 -1
  789. package/.next/static/chunks/14bsg3_-taiw4.js +0 -1
  790. /package/.next/build/chunks/{[root-of-the-server]__0ubbtyl._.js.map → [root-of-the-server]__0tgc-8p._.js.map} +0 -0
  791. /package/.next/build/chunks/{[turbopack-node]_transforms_postcss_ts_06e.r3r._.js.map → [turbopack-node]_transforms_postcss_ts_0r4x5ys._.js.map} +0 -0
  792. /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
  793. /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
  794. /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
  795. /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
  796. /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
  797. /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
  798. /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
  799. /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
  800. /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
  801. /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
  802. /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
  803. /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
  804. /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
  805. /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
  806. /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
  807. /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
  808. /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
  809. /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
  810. /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
  811. /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
  812. /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
  813. /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
  814. /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
  815. /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
  816. /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
  817. /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
  818. /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
  819. /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
  820. /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
  821. /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
  822. /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
  823. /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
  824. /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
  825. /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
  826. /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
  827. /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
  828. /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
  829. /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
  830. /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
  831. /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
  832. /package/.next/static/{q2pRPNeRoK1vOr1EX-qOG → zYWeA6KAnt4mH1Rredbmu}/_buildManifest.js +0 -0
  833. /package/.next/static/{q2pRPNeRoK1vOr1EX-qOG → zYWeA6KAnt4mH1Rredbmu}/_clientMiddlewareManifest.js +0 -0
  834. /package/.next/static/{q2pRPNeRoK1vOr1EX-qOG → zYWeA6KAnt4mH1Rredbmu}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
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';
@@ -16,6 +16,9 @@ interface MessageFlow {
16
16
  interface TopoGraphProps {
17
17
  sessions: Session[];
18
18
  sseSessions: Record<string, number>;
19
+ // #84: a node.renamed event from the Overview's SSE listener. When the
20
+ // currently open chat popover targets `from`, it follows the rename to `to`.
21
+ renameSignal?: { from: string; to: string; ts: number } | null;
19
22
  }
20
23
 
21
24
  interface Point {
@@ -29,6 +32,9 @@ interface FlowLink {
29
32
  to: string;
30
33
  count: number;
31
34
  content: string;
35
+ /** ISO timestamp of the most recent message on this edge — drives
36
+ * the Round 10 freshness fade so dormant links recede visually. */
37
+ last_at: string;
32
38
  }
33
39
 
34
40
  const cx = 500;
@@ -82,36 +88,52 @@ function truncate(value: string, max: number) {
82
88
  return value.length > max ? `${value.slice(0, max - 1)}...` : value;
83
89
  }
84
90
 
91
+ /** Round 12 / Loop: status trio audit.
92
+ *
93
+ * Each (status × theme) cell returns a {primary, halo, text} trio. The trio
94
+ * invariant — keep it when adding states or tweaking shades:
95
+ *
96
+ * light: primary = <hue>-600 halo = <hue>-100 * text = <hue>-800/900
97
+ * dark: primary = <hue>-400/500 halo = <hue>-900 text = <hue>-100
98
+ *
99
+ * * offline.halo light deviates intentionally to slate-200 (#e2e8f0):
100
+ * slate-100 (#f1f5f9) is too close to the panel bg (#f8fafc) and the
101
+ * halo would vanish. The other three rows keep the 100-shade.
102
+ *
103
+ * Audit caught one cross-family drift before this round: online-other halo
104
+ * light was #dbeafe (blue-100) while its primary (#0284c7 sky-600) and text
105
+ * (#0c4a6e sky-900) were sky-family — now sky-100 (#e0f2fe). Tiny visual
106
+ * difference; large hygiene win — every trio is now mono-hue. */
85
107
  function nodeStatus(session: Session, isOnline: boolean, isLight: boolean) {
86
108
  if (!isOnline) {
87
109
  return {
88
110
  label: 'offline',
89
- primary: isLight ? '#94a3b8' : '#6b7280',
90
- halo: isLight ? '#e2e8f0' : '#111827',
91
- text: isLight ? '#475569' : '#9ca3af',
111
+ primary: isLight ? '#94a3b8' : '#6b7280', // slate-400 / gray-500
112
+ halo: isLight ? '#e2e8f0' : '#111827', // slate-200* / gray-900
113
+ text: isLight ? '#475569' : '#9ca3af', // slate-600 / gray-400
92
114
  };
93
115
  }
94
116
  if (session.status === 'working') {
95
117
  return {
96
118
  label: 'working',
97
- primary: isLight ? '#059669' : '#22c55e',
98
- halo: isLight ? '#d1fae5' : '#14532d',
99
- text: isLight ? '#065f46' : '#dcfce7',
119
+ primary: isLight ? '#059669' : '#22c55e', // emerald-600 / green-500
120
+ halo: isLight ? '#d1fae5' : '#14532d', // emerald-100 / green-900
121
+ text: isLight ? '#065f46' : '#dcfce7', // emerald-800 / green-100
100
122
  };
101
123
  }
102
124
  if (session.status === 'idle') {
103
125
  return {
104
126
  label: 'idle',
105
- primary: isLight ? '#0d9488' : '#2dd4bf',
106
- halo: isLight ? '#ccfbf1' : '#134e4a',
107
- text: isLight ? '#115e59' : '#ccfbf1',
127
+ primary: isLight ? '#0d9488' : '#2dd4bf', // teal-600 / teal-400
128
+ halo: isLight ? '#ccfbf1' : '#134e4a', // teal-100 / teal-900
129
+ text: isLight ? '#115e59' : '#ccfbf1', // teal-800 / teal-100
108
130
  };
109
131
  }
110
132
  return {
111
133
  label: session.status || 'online',
112
- primary: isLight ? '#0284c7' : '#38bdf8',
113
- halo: isLight ? '#dbeafe' : '#0c4a6e',
114
- text: isLight ? '#0c4a6e' : '#e0f2fe',
134
+ primary: isLight ? '#0284c7' : '#38bdf8', // sky-600 / sky-400
135
+ halo: isLight ? '#e0f2fe' : '#0c4a6e', // sky-100 / sky-900 (was blue-100 — drift fixed Round 12)
136
+ text: isLight ? '#0c4a6e' : '#e0f2fe', // sky-900 / sky-100
115
137
  };
116
138
  }
117
139
 
@@ -237,22 +259,59 @@ function commonPrefix(a: string, b: string): string {
237
259
  return a.slice(0, i);
238
260
  }
239
261
 
240
- function computeGroupKeys(aliases: string[]): Record<string, string> {
241
- const sorted = [...aliases].sort((a, b) => a.localeCompare(b));
262
+ /** #83 + #111: group nodes that share a ≥2-char alias prefix OR a project_dir
263
+ * ("either criterion same group", Vincent 4724). Union-find over the
264
+ * sessions; the component label prefers the shared project_dir's basename,
265
+ * else the common alias prefix. Returns alias → groupKey. */
266
+ function computeGroups(sessions: { alias: string; project_dir?: string | null }[]): Record<string, string> {
267
+ const n = sessions.length;
268
+ const parent = sessions.map((_, i) => i);
269
+ const find = (i: number): number => {
270
+ while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; }
271
+ return i;
272
+ };
273
+ const union = (a: number, b: number) => {
274
+ const ra = find(a), rb = find(b);
275
+ if (ra !== rb) parent[ra] = rb;
276
+ };
277
+
278
+ // union by shared project_dir
279
+ const byDir: Record<string, number[]> = {};
280
+ sessions.forEach((s, i) => {
281
+ const d = s.project_dir?.trim();
282
+ if (d) (byDir[d] ||= []).push(i);
283
+ });
284
+ for (const idxs of Object.values(byDir)) {
285
+ for (let k = 1; k < idxs.length; k++) union(idxs[0], idxs[k]);
286
+ }
287
+
288
+ // union by shared ≥2-char alias prefix — sort, link adjacent pairs
289
+ const order = sessions.map((_, i) => i).sort((a, b) => sessions[a].alias.localeCompare(sessions[b].alias));
290
+ for (let k = 0; k + 1 < order.length; k++) {
291
+ if (commonPrefix(sessions[order[k]].alias, sessions[order[k + 1]].alias).length >= 2) {
292
+ union(order[k], order[k + 1]);
293
+ }
294
+ }
295
+
296
+ // label each connected component
297
+ const comps: Record<number, number[]> = {};
298
+ for (let i = 0; i < n; i++) (comps[find(i)] ||= []).push(i);
242
299
  const keys: Record<string, string> = {};
243
- let i = 0;
244
- while (i < sorted.length) {
245
- let j = i;
246
- let prefix = sorted[i];
247
- while (j + 1 < sorted.length) {
248
- const lcp = commonPrefix(prefix, sorted[j + 1]);
249
- if (lcp.length < 2) break;
250
- prefix = lcp;
251
- j++;
300
+ for (const members of Object.values(comps)) {
301
+ let label: string;
302
+ if (members.length === 1) {
303
+ label = sessions[members[0]].alias;
304
+ } else {
305
+ const dirs = new Set(members.map(i => sessions[i].project_dir?.trim()).filter(Boolean) as string[]);
306
+ if (dirs.size === 1) {
307
+ const d = [...dirs][0];
308
+ label = d.split('/').filter(Boolean).pop() || d;
309
+ } else {
310
+ label = members.map(i => sessions[i].alias).reduce((a, b) => commonPrefix(a, b));
311
+ if (label.length < 2) label = sessions[members[0]].alias;
312
+ }
252
313
  }
253
- const key = j > i ? prefix : sorted[i];
254
- for (let k = i; k <= j; k++) keys[sorted[k]] = key;
255
- i = j + 1;
314
+ for (const i of members) keys[sessions[i].alias] = label;
256
315
  }
257
316
  return keys;
258
317
  }
@@ -272,19 +331,27 @@ function buildFlowLinks(messages: MessageFlow[], positions: Record<string, Point
272
331
  const key = `${message.from_alias}->${message.to_alias}`;
273
332
  const current = links.get(key);
274
333
 
334
+ // Keep the most-recent timestamp per pair so the render can fade
335
+ // dormant edges (Round 10 freshness fade).
336
+ const incoming = message.created_at || '';
337
+ const last_at = !current
338
+ ? incoming
339
+ : (incoming > current.last_at ? incoming : current.last_at);
340
+
275
341
  links.set(key, {
276
342
  key,
277
343
  from: message.from_alias,
278
344
  to: message.to_alias,
279
345
  count: (current?.count || 0) + 1,
280
346
  content: current?.content || message.content,
347
+ last_at,
281
348
  });
282
349
  });
283
350
 
284
351
  return [...links.values()].slice(0, 18);
285
352
  }
286
353
 
287
- export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
354
+ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProps) {
288
355
  const theme = useTheme();
289
356
  const isLight = theme === 'light';
290
357
  const pal = isLight ? LIGHT_PALETTE : DARK_PALETTE;
@@ -307,6 +374,20 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
307
374
  try { localStorage.setItem('anet-topo-layout', next); } catch {}
308
375
  return next;
309
376
  });
377
+ // Issue #113: node size scale (Vincent 4727 — nodes too big / crowded at
378
+ // ~22 nodes). S/M/L → 0.7/0.84/1.0; default M (one notch down from the old
379
+ // fixed size). Persisted like the layout toggle.
380
+ const [nodeScale, setNodeScale] = useState(0.84);
381
+ useEffect(() => {
382
+ try {
383
+ const saved = parseFloat(localStorage.getItem('anet-topo-nodescale') || '');
384
+ if (saved === 0.7 || saved === 0.84 || saved === 1) setNodeScale(saved);
385
+ } catch {}
386
+ }, []);
387
+ const pickNodeScale = (v: number) => {
388
+ setNodeScale(v);
389
+ try { localStorage.setItem('anet-topo-nodescale', String(v)); } catch {}
390
+ };
310
391
 
311
392
  useEffect(() => {
312
393
  const fetchMessages = async () => {
@@ -333,6 +414,8 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
333
414
  flowLinks,
334
415
  activeAliases,
335
416
  groupKeys,
417
+ groupBoxes,
418
+ gridContentBottom,
336
419
  } = useMemo(() => {
337
420
  const sseCount = (s: { alias: string; network_id?: string }) =>
338
421
  (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];
@@ -342,36 +425,148 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
342
425
  // index, so contiguous-in-array becomes contiguous-in-ring, i.e.
343
426
  // each team visually clusters. localeCompare keeps CJK ordering sane.
344
427
  const byAlias = (a: Session, b: Session) => a.alias.localeCompare(b.alias);
428
+ // #112 umbrella: ghost age-out — an offline node not seen recently is
429
+ // almost certainly a deleted node the server `/api/status` still
430
+ // returns (#74 root cause is server-side; this is the dashboard-side
431
+ // fallback so stale ghosts stop cluttering the topology). A missing
432
+ // last_seen_at is kept (conservative — could be a legitimately new node).
433
+ // Round 27 / P0: the 24h threshold was too lenient — Vincent's preview.29
434
+ // screenshot showed B站马 nodes deleted ~4 h earlier still visible. A
435
+ // healthy agent heartbeats every few seconds; if it's been silent for
436
+ // an hour it's effectively gone. 1 h gives a fresh disconnect time to
437
+ // come back while removing dead nodes well within an operator session.
438
+ const GHOST_MS = 60 * 60 * 1000;
439
+ const now = Date.now();
440
+ const isGhost = (s: Session) => {
441
+ if (s.status !== 'offline' || sseCount(s) || !s.last_seen_at) return false;
442
+ const t = Date.parse(s.last_seen_at);
443
+ return !Number.isNaN(t) && now - t > GHOST_MS;
444
+ };
345
445
  const online = sessions.filter(s => s.status !== 'offline' || sseCount(s)).sort(byAlias);
346
- const offline = sessions.filter(s => s.status === 'offline' && !sseCount(s)).sort(byAlias);
446
+ const offline = sessions.filter(s => s.status === 'offline' && !sseCount(s) && !isGhost(s)).sort(byAlias);
347
447
  const positions: Record<string, Point> = {};
348
448
 
349
449
  if (layout === 'grid') {
350
- // Issue #87: N×M grid layout. All nodes share one sorted array (online
351
- // first, then offline) so same-prefix aliases land in adjacent cells
352
- // (#83 prefix-grouping synergy). cols = ⌈√N⌉; the last partial row is
353
- // centred. Grid spans an inset box so node labels don't clip the edge.
450
+ // Issue #87 + #111: group-banded grid. Nodes are sorted by alias so
451
+ // same-prefix aliases are adjacent; each multi-member prefix group then
452
+ // gets its OWN row(s) starting at column 0, while singletons pack into
453
+ // shared rows. Group-banded placement keeps every group's bounding box
454
+ // (#111) a clean rectangle — no box ever overlaps another group's.
354
455
  const all = [...online, ...offline];
355
- const n = all.length;
356
- const cols = Math.max(1, Math.ceil(Math.sqrt(n)));
357
- const rows = Math.max(1, Math.ceil(n / cols));
358
- const gx0 = 150, gx1 = 850, gy0 = 120, gy1 = 560;
456
+ const groupKeys = computeGroups(all);
457
+ const cols = Math.max(1, Math.ceil(Math.sqrt(all.length)));
458
+ // #112: gy0 starts below the (now compact) recent-signal / legend
459
+ // overlay panels their max bottom edge is y≈112 so grid row 0 is
460
+ // never occluded; gy1 extends near the canvas bottom for breathing room.
461
+ const gx0 = 150, gx1 = 850, gy0 = 126, gy1 = 652;
359
462
  const cellW = (gx1 - gx0) / cols;
360
- const cellH = (gy1 - gy0) / rows;
361
- all.forEach((s, i) => {
362
- const row = Math.floor(i / cols);
363
- const col = i % cols;
364
- const rowCount = row === rows - 1 ? n - row * cols : cols;
365
- const rowInset = ((cols - rowCount) * cellW) / 2;
366
- positions[s.alias] = {
367
- x: gx0 + rowInset + (col + 0.5) * cellW,
368
- y: gy0 + (row + 0.5) * cellH,
369
- };
370
- });
463
+ // largest node radius at the current size scale — drives the group
464
+ // label band + the row-height floor so nothing ever overlaps.
465
+ const nodeR = Math.round(26 * nodeScale);
466
+
467
+ // ordered runs of consecutive same-group-key nodes (≥2 = real group)
468
+ const runs: { key: string; members: Session[] }[] = [];
469
+ for (const s of all) {
470
+ const gk = groupKeys[s.alias];
471
+ const last = runs[runs.length - 1];
472
+ if (last && last.key === gk) last.members.push(s);
473
+ else runs.push({ key: gk, members: [s] });
474
+ }
475
+
476
+ // Pass 1 — assign each run to a band: a multi-member group owns its
477
+ // rows (left-aligned, so its bounding box is a tidy rect); contiguous
478
+ // singletons pack into shared rows (centred). Collect total row count.
479
+ type Band = { members: Session[]; startRow: number; centred: boolean; isGroup: boolean };
480
+ const bands: Band[] = [];
481
+ let row = 0;
482
+ let i = 0;
483
+ while (i < runs.length) {
484
+ if (runs[i].members.length >= 2) {
485
+ bands.push({ members: runs[i].members, startRow: row, centred: false, isGroup: true });
486
+ row += Math.ceil(runs[i].members.length / cols);
487
+ i++;
488
+ } else {
489
+ const singles: Session[] = [];
490
+ while (i < runs.length && runs[i].members.length < 2) {
491
+ singles.push(runs[i].members[0]);
492
+ i++;
493
+ }
494
+ bands.push({ members: singles, startRow: row, centred: true, isGroup: false });
495
+ row += Math.ceil(singles.length / cols);
496
+ }
497
+ }
498
+ const totalRows = Math.max(1, row);
499
+ // #112: the group label sits in a band ABOVE the topmost node, so the
500
+ // band must clear the node radius — GROUP_TOP is node-relative, never
501
+ // cellH-derived (cellH-derived was the label↔node overlap bug Vincent
502
+ // hit). cellH then floors at GROUP_TOP + 30 so the label band + bottom
503
+ // padding always fit and stacked group boxes never touch; past the
504
+ // floor the grid overflows and zoom/pan handles it.
505
+ const GROUP_TOP = nodeR + 20;
506
+ // Round 27 / P0 (Vincent screenshot preview.29): dense plain-text
507
+ // node labels (`pos.y + radius + denseDrop`, with a 3 px containerBg
508
+ // stroke halo for readability) at the BOTTOM row of band N would
509
+ // paint OVER the start of band N+1's group label, creating the
510
+ // "blueleap → eleap", "agent-network-dashboard → t-network / board"
511
+ // visual chopping. Geometry of the collision:
512
+ // dense label visual bottom = node_N.y + radius + denseDrop + halo
513
+ // group label glyph top = boxY + 4 (text y=boxY+14, ~10px ascent)
514
+ // = node_N+1.y - GROUP_TOP + 4
515
+ // node_N+1.y - node_N.y = cellH for consecutive rows, so no-collide:
516
+ // cellH ≥ radius + denseDrop + halo + GROUP_TOP - 4 + buffer
517
+ // With halo=3 buffer=4: cellH ≥ nodeR + denseDrop + GROUP_TOP + 3.
518
+ const denseDrop = nodeScale < 0.8 ? 12 : 14;
519
+ const cellH = Math.max(
520
+ 2 * nodeR + 22, // node + dense label within band
521
+ GROUP_TOP + 12, // group-label band + box padding
522
+ nodeR + denseDrop + GROUP_TOP + 3, // round-27 dense↔group label clearance
523
+ Math.min(100, (gy1 - gy0) / totalRows),
524
+ );
525
+
526
+ // Pass 2 — place each band's members.
527
+ for (const band of bands) {
528
+ band.members.forEach((s, idx) => {
529
+ const rowInBand = Math.floor(idx / cols);
530
+ const c = idx % cols;
531
+ const inRow = Math.min(cols, band.members.length - rowInBand * cols);
532
+ const inset = band.centred ? ((cols - inRow) * cellW) / 2 : 0;
533
+ positions[s.alias] = {
534
+ x: gx0 + inset + (c + 0.5) * cellW,
535
+ y: gy0 + (band.startRow + rowInBand + 0.5) * cellH,
536
+ };
537
+ });
538
+ }
539
+
371
540
  const links = buildFlowLinks(messages, positions);
372
541
  const active = new Set<string>();
373
542
  links.forEach(link => { active.add(link.from); active.add(link.to); });
374
- const groupKeys = computeGroupKeys([...online, ...offline].map(s => s.alias));
543
+ // #111: one bounding box per multi-member group (Vincent 4722). Each
544
+ // group owns its rows; GROUP_PAD fills the row space left below the
545
+ // nodes after the label band, so stacked group boxes always have a
546
+ // gap between them (GROUP_TOP is defined above, with cellH).
547
+ const GROUP_PAD = Math.max(8, Math.min(26, cellH - GROUP_TOP - 8)); // side/bottom
548
+ const groupBoxes = bands
549
+ .filter(b => b.isGroup)
550
+ .map(band => {
551
+ const pts = band.members.map(s => positions[s.alias]).filter(Boolean);
552
+ const xs = pts.map(p => p.x);
553
+ const ys = pts.map(p => p.y);
554
+ const minX = Math.min(...xs), minY = Math.min(...ys);
555
+ return {
556
+ key: band.members.length ? groupKeys[band.members[0].alias] : '',
557
+ count: band.members.length,
558
+ x: minX - GROUP_PAD,
559
+ y: minY - GROUP_TOP,
560
+ w: Math.max(...xs) - minX + GROUP_PAD * 2,
561
+ h: Math.max(...ys) - minY + GROUP_TOP + GROUP_PAD,
562
+ };
563
+ });
564
+ // Round 28 / Loop: surface the grid's natural content bottom so the
565
+ // mount effect can auto-fit zoom when the layout would overflow the
566
+ // viewBox. = bottom of the last node row + its label drop + a small
567
+ // breathing buffer. Round 27's cellH bump makes this overflow more
568
+ // common (30-node fleets reach ~774 px, viewBox is 680).
569
+ const gridContentBottom = gy0 + totalRows * cellH + 8;
375
570
  return {
376
571
  onlineNodes: online,
377
572
  offlineNodes: offline,
@@ -379,6 +574,8 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
379
574
  flowLinks: links,
380
575
  activeAliases: active,
381
576
  groupKeys,
577
+ groupBoxes,
578
+ gridContentBottom,
382
579
  };
383
580
  }
384
581
 
@@ -449,7 +646,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
449
646
  });
450
647
 
451
648
  // Round 106 (issue #83): group key per alias → shared hue per team.
452
- const groupKeys = computeGroupKeys([...online, ...offline].map(s => s.alias));
649
+ const groupKeys = computeGroups([...online, ...offline]);
453
650
 
454
651
  return {
455
652
  onlineNodes: online,
@@ -458,15 +655,38 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
458
655
  flowLinks: links,
459
656
  activeAliases: active,
460
657
  groupKeys,
658
+ // #111: group boxes are a grid-layout feature only — radially scattered
659
+ // ring nodes can't be cleanly boxed. Ring keeps the #83 prefix hue.
660
+ groupBoxes: [] as { key: string; count: number; x: number; y: number; w: number; h: number }[],
661
+ // ring fits within VIEWBOX_H by construction (offlineRadius=325 + centre at y=330)
662
+ gridContentBottom: 0,
461
663
  };
462
- }, [messages, sessions, sseSessions, layout]);
664
+ }, [messages, sessions, sseSessions, layout, nodeScale]);
463
665
 
464
666
  const workingCount = onlineNodes.filter(s => s.status === 'working').length;
667
+ // Round 6 / Loop: vendor distribution for the header chip — at a glance
668
+ // "what's in the fleet" (A:5 M:2 O:8 书:12 …) without opening a node.
669
+ // Sorted by count desc; "unknown" vendors collapse into a "?" bucket.
670
+ const vendorDist = useMemo(() => {
671
+ const tally = new Map<string, { initial: string; count: number; color: string }>();
672
+ for (const s of [...onlineNodes, ...offlineNodes]) {
673
+ const v = vendorForModel(s.model);
674
+ const key = v.id === 'unknown' ? '?' : v.initial;
675
+ const cur = tally.get(key);
676
+ if (cur) cur.count++;
677
+ else tally.set(key, { initial: key, count: 1, color: v.mono.text });
678
+ }
679
+ return [...tally.values()].sort((a, b) => b.count - a.count);
680
+ }, [onlineNodes, offlineNodes]);
465
681
  // Round 109 (Vincent 4582 P0): hover-gated labels above this node count
466
682
  // so dense fleets show clean avatars instead of a wall of overlapping
467
683
  // label cards. 16 ≈ where the triple-tier rings start to crowd.
468
684
  const denseLayout = onlineNodes.length + offlineNodes.length > 16;
469
685
  const [hoveredAlias, setHoveredAlias] = useState<string | null>(null);
686
+ // Round 8 / Loop: which group is currently focused. Nodes outside this
687
+ // group + other group boxes fade so the eye locks onto the team you're
688
+ // pointing at. Singletons use their own alias as the group key.
689
+ const hoveredGroup = hoveredAlias ? (groupKeys[hoveredAlias] ?? hoveredAlias) : null;
470
690
 
471
691
  // --- Round 103 (issue #81): fullscreen + zoom + pan interaction layer ---
472
692
  // DIY native (no d3 / svg-pan-zoom): wrap the topology content in a single
@@ -486,6 +706,25 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
486
706
  // Issue #100: singleton chat popover. One alias at a time — clicking
487
707
  // another node swaps the target and the conversation switches in place.
488
708
  const [chatAlias, setChatAlias] = useState<string | null>(null);
709
+ // Round 14 / Loop: one-shot click ripple — fades outward from the clicked
710
+ // node so the click registers physically before the popover snaps in.
711
+ // Pairs with the Round 11 chat-focus ring: ripple expands, ring locks on.
712
+ // Keyed by ts so re-clicking the same node remounts the <circle> and the
713
+ // SMIL <animate> replays. Cleared 600ms after click (longer than the
714
+ // 500ms animation so a final frame at opacity 0 is still in the tree).
715
+ const [clickRipple, setClickRipple] = useState<{
716
+ ts: number; x: number; y: number; r0: number; color: string;
717
+ } | null>(null);
718
+ // #84: when a node is renamed while its chat popover is open, follow the
719
+ // rename so the conversation keeps targeting a live alias.
720
+ useEffect(() => {
721
+ if (renameSignal && renameSignal.from === chatAlias) {
722
+ setChatAlias(renameSignal.to);
723
+ }
724
+ // chatAlias intentionally omitted — only react to a new rename signal,
725
+ // not to the user opening/closing the popover.
726
+ // eslint-disable-next-line react-hooks/exhaustive-deps
727
+ }, [renameSignal]);
489
728
 
490
729
  useEffect(() => { viewRef.current = view; }, [view]);
491
730
 
@@ -506,6 +745,42 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
506
745
  } catch {}
507
746
  }, []);
508
747
 
748
+ // Round 28 / Loop: first-paint auto-fit. Round 27's cellH bump means
749
+ // dense grids (≥6-7 rows) overflow the 680-px viewBox at the natural
750
+ // 100% zoom — a new user sees the topology with its bottom rows
751
+ // clipped and no hint that there's more below. Auto-fit on first
752
+ // paint (and only if the user has no persisted view) sets the
753
+ // initial zoom so all content fits. Subsequent zoom changes persist
754
+ // and override the auto-fit on reload; explicit reset (0 key) still
755
+ // goes back to 100% so the gesture's "reset to natural size" semantic
756
+ // is preserved.
757
+ //
758
+ // Capture pre-mount persistence in a useState initializer — the
759
+ // existing `persist` effect (declared below) runs on first render
760
+ // with the default {1,0,0} view and writes it to localStorage before
761
+ // the auto-fit effect (deps on async-arriving sessions) gets a turn.
762
+ // Reading localStorage from the effect would see that write and
763
+ // skip the fit. The useState snapshot fires once, before any effects.
764
+ const [hadPersistedViewOnMount] = useState<boolean>(
765
+ () => typeof window !== 'undefined' && !!localStorage.getItem('anet-topo-view'),
766
+ );
767
+ const autoFitDoneRef = useRef(false);
768
+ useEffect(() => {
769
+ if (autoFitDoneRef.current) return;
770
+ if (hadPersistedViewOnMount) {
771
+ autoFitDoneRef.current = true;
772
+ return;
773
+ }
774
+ if (layout !== 'grid' || sessions.length === 0 || !gridContentBottom) return;
775
+ if (gridContentBottom <= VIEWBOX_H) {
776
+ autoFitDoneRef.current = true; // no overflow → no fit needed
777
+ return;
778
+ }
779
+ const fitZoom = Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));
780
+ setView({ zoom: fitZoom, x: 0, y: 0 });
781
+ autoFitDoneRef.current = true;
782
+ }, [layout, sessions.length, gridContentBottom, hadPersistedViewOnMount]);
783
+
509
784
  // persist view
510
785
  useEffect(() => {
511
786
  try { localStorage.setItem('anet-topo-view', JSON.stringify(view)); } catch {}
@@ -525,6 +800,14 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
525
800
  const svg = svgRef.current;
526
801
  if (!svg) return;
527
802
  const onWheel = (e: WheelEvent) => {
803
+ // Round 23 / Loop: only zoom on Ctrl/Meta+wheel when inline; plain
804
+ // wheel over the canvas keeps scrolling the page (the topo sits at
805
+ // the top of /; trapping page scroll mid-page is the classic
806
+ // "scroll-jail" anti-pattern). In fullscreen the canvas owns the
807
+ // viewport so any wheel zooms — no page scroll to preserve. Pinch-
808
+ // zoom on a trackpad surfaces as ctrlKey=true natively, which is
809
+ // also what we want (zoom the canvas, not the browser).
810
+ if (!isFullscreen && !e.ctrlKey && !e.metaKey) return;
528
811
  e.preventDefault();
529
812
  const rect = svg.getBoundingClientRect();
530
813
  const mx = ((e.clientX - rect.left) / rect.width) * VIEWBOX_W;
@@ -545,8 +828,17 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
545
828
  };
546
829
  svg.addEventListener('wheel', onWheel, { passive: false });
547
830
  return () => svg.removeEventListener('wheel', onWheel);
548
- }, []);
549
-
831
+ // Re-attach when the fullscreen flag toggles so the handler's
832
+ // closure picks up the new value (capture-by-closure means we'd
833
+ // otherwise read the stale boolean forever).
834
+ }, [isFullscreen]);
835
+
836
+ // Round 21 / Loop: pan-aware cursor. Static `grab` gave no tactile cue
837
+ // that the canvas was actually being dragged — "grabbing" while
838
+ // dragRef.current.active = true tells the hand it's moving the
839
+ // viewport. Mirroring dragRef.active into state is the only way to
840
+ // re-render the SVG style; the ref itself doesn't trigger renders.
841
+ const [isPanning, setIsPanning] = useState(false);
550
842
  const onPointerDown = (e: React.PointerEvent<SVGSVGElement>) => {
551
843
  if (e.button !== 0) return;
552
844
  (e.currentTarget as Element).setPointerCapture?.(e.pointerId);
@@ -557,6 +849,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
557
849
  baseX: viewRef.current.x,
558
850
  baseY: viewRef.current.y,
559
851
  };
852
+ setIsPanning(true);
560
853
  };
561
854
  const onPointerMove = (e: React.PointerEvent<SVGSVGElement>) => {
562
855
  const d = dragRef.current;
@@ -571,6 +864,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
571
864
  const onPointerUp = (e: React.PointerEvent<SVGSVGElement>) => {
572
865
  if (!dragRef.current.active) return;
573
866
  dragRef.current.active = false;
867
+ setIsPanning(false);
574
868
  try {
575
869
  (e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
576
870
  } catch {}
@@ -588,6 +882,48 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
588
882
  };
589
883
  const resetView = () => setView({ zoom: 1, x: 0, y: 0 });
590
884
 
885
+ // Round 29 / Loop: `f` = fit-to-content. Shared by the Round 28
886
+ // first-paint auto-fit effect and the keyboard handler so the math is
887
+ // in one place. When content already fits at natural zoom, this is
888
+ // effectively a "recenter" — `f` always lands on a known good view.
889
+ const fitView = useCallback(() => {
890
+ const zoom = !gridContentBottom || gridContentBottom <= VIEWBOX_H
891
+ ? 1
892
+ : Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));
893
+ setView({ zoom, x: 0, y: 0 });
894
+ }, [gridContentBottom]);
895
+
896
+ // Round 22 / Loop: keyboard zoom — +/= zoom in, - zoom out, 0 reset.
897
+ // Round 29 / Loop: +f to fit content.
898
+ // Listen on window so the user doesn't need to focus the SVG first,
899
+ // but only act when no text input has focus (otherwise typing "-" in
900
+ // a chat would zoom the topology — surprising and bad). Modifier keys
901
+ // pass through so Cmd/Ctrl combos still hit their owners. The button
902
+ // titles below document these shortcuts so users discover them.
903
+ useEffect(() => {
904
+ const onKey = (e: KeyboardEvent) => {
905
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
906
+ const ae = document.activeElement as HTMLElement | null;
907
+ if (ae) {
908
+ const tag = ae.tagName;
909
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || ae.isContentEditable) return;
910
+ }
911
+ if (e.key === '+' || e.key === '=') { zoomBy(1.2); e.preventDefault(); }
912
+ else if (e.key === '-' || e.key === '_') { zoomBy(1 / 1.2); e.preventDefault(); }
913
+ else if (e.key === '0') { resetView(); e.preventDefault(); }
914
+ else if (e.key === 'f' || e.key === 'F') { fitView(); e.preventDefault(); }
915
+ // Round 32 / Loop: `l` toggles ring|grid. The vim-style `g l` route
916
+ // (Audit Log) requires a preceding `g` within 1500ms; a bare `l`
917
+ // outside that window is free for topology use.
918
+ else if (e.key === 'l' || e.key === 'L') { toggleLayout(); e.preventDefault(); }
919
+ };
920
+ window.addEventListener('keydown', onKey);
921
+ return () => window.removeEventListener('keydown', onKey);
922
+ // zoomBy / resetView are stable wrt setView callback; fitView changes
923
+ // with gridContentBottom so deps list catches it.
924
+ // eslint-disable-next-line react-hooks/exhaustive-deps
925
+ }, [fitView]);
926
+
591
927
  const toggleFullscreen = () => {
592
928
  const el = containerRef.current;
593
929
  if (!el) return;
@@ -615,6 +951,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
615
951
  <button
616
952
  onClick={() => { if (layout !== 'ring') toggleLayout(); }}
617
953
  aria-pressed={layout === 'ring'}
954
+ title="Ring layout (l to toggle)"
618
955
  className={`px-2.5 py-1 transition-colors ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300' : 'text-gray-500 hover:text-gray-400'}`}
619
956
  >
620
957
  Ring
@@ -622,6 +959,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
622
959
  <button
623
960
  onClick={() => { if (layout !== 'grid') toggleLayout(); }}
624
961
  aria-pressed={layout === 'grid'}
962
+ title="Grid layout (l to toggle)"
625
963
  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'}`}
626
964
  >
627
965
  Grid
@@ -633,6 +971,48 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
633
971
  <span className="px-2.5 py-1 rounded-md bg-cyan-500/10 text-cyan-300 border border-cyan-500/20">
634
972
  {onlineNodes.length} online
635
973
  </span>
974
+ {/* Round 31 / Loop: fleet-health pressure bar. The "X working /
975
+ Y online" chips already carry the raw counts; the bar lets
976
+ the eye get the working/idle/offline RATIO in one glance
977
+ without mental math. Stacked 3-segment chip, ~64 px wide. */}
978
+ {(() => {
979
+ const w = workingCount;
980
+ const i = onlineNodes.length - workingCount; // idle = online - working
981
+ const o = offlineNodes.length;
982
+ const total = w + i + o;
983
+ if (total === 0) return null;
984
+ const seg = (n: number, color: string, key: string) => {
985
+ if (n === 0) return null;
986
+ return <span key={key} style={{ width: `${(n / total) * 100}%`, background: color, height: '100%' }} />;
987
+ };
988
+ return (
989
+ <span
990
+ className="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"
991
+ title={`${w} working · ${i} idle · ${o} offline`}
992
+ data-fleet-pressure
993
+ >
994
+ <span className="text-[10px] tracking-wide">pressure</span>
995
+ <span className="inline-flex h-1.5 w-16 rounded-full overflow-hidden" style={{ background: 'rgb(75 85 99 / 0.25)' }}>
996
+ {seg(w, isLight ? '#059669' : '#22c55e', 'w')}
997
+ {seg(i, isLight ? '#0d9488' : '#2dd4bf', 'i')}
998
+ {seg(o, isLight ? '#94a3b8' : '#6b7280', 'o')}
999
+ </span>
1000
+ </span>
1001
+ );
1002
+ })()}
1003
+ {vendorDist.length > 1 && (
1004
+ <span
1005
+ className="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"
1006
+ title="Fleet by model vendor"
1007
+ >
1008
+ {vendorDist.map(v => (
1009
+ <span key={v.initial} className="inline-flex items-baseline gap-0.5">
1010
+ <span style={{ color: v.color }}>{v.initial}</span>
1011
+ <span className="text-gray-500">:{v.count}</span>
1012
+ </span>
1013
+ ))}
1014
+ </span>
1015
+ )}
636
1016
  <span className="px-2.5 py-1 rounded-md bg-gray-500/10 text-gray-400 border border-gray-500/20">
637
1017
  {flowLinks.length} active links
638
1018
  </span>
@@ -655,7 +1035,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
655
1035
  onPointerMove={onPointerMove}
656
1036
  onPointerUp={onPointerUp}
657
1037
  onPointerLeave={onPointerUp}
658
- style={{ cursor: 'grab', touchAction: 'none' }}
1038
+ style={{ cursor: isPanning ? 'grabbing' : 'grab', touchAction: 'none' }}
659
1039
  >
660
1040
  <defs>
661
1041
  <linearGradient id="topo-panel" x1="0" x2="1" y1="0" y2="1">
@@ -677,9 +1057,37 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
677
1057
  </feMerge>
678
1058
  </filter>
679
1059
  )}
680
- <marker id="topo-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
681
- <path d="M 0 0 L 10 5 L 0 10 z" fill={pal.arrowFill} />
682
- </marker>
1060
+ {/* Round 16 / Loop: 3-tier flow-link arrow markers.
1061
+ The single marker had `markerUnits` defaulting to
1062
+ `strokeWidth`, so heavy edges (stroke=7) rendered 35-user-
1063
+ unit arrowheads — visually dominant, the head outweighed
1064
+ the line. Switching to `userSpaceOnUse` decouples arrow
1065
+ size from stroke; binning by count gives a clearer
1066
+ hierarchy than continuous linear scaling:
1067
+ s (count 1-2) → 12 user units
1068
+ m (count 3-4) → 16 user units (alias for `topo-arrow`)
1069
+ l (count 5+) → 22 user units
1070
+ `topo-arrow` stays bound to the medium tier so the legend
1071
+ swatch (line ~1500) renders without change. */}
1072
+ {[
1073
+ { id: 'topo-arrow-s', size: 12 },
1074
+ { id: 'topo-arrow', size: 16 },
1075
+ { id: 'topo-arrow-l', size: 22 },
1076
+ ].map(m => (
1077
+ <marker
1078
+ key={m.id}
1079
+ id={m.id}
1080
+ viewBox="0 0 10 10"
1081
+ refX="8"
1082
+ refY="5"
1083
+ markerWidth={m.size}
1084
+ markerHeight={m.size}
1085
+ markerUnits="userSpaceOnUse"
1086
+ orient="auto-start-reverse"
1087
+ >
1088
+ <path d="M 0 0 L 10 5 L 0 10 z" fill={pal.arrowFill} />
1089
+ </marker>
1090
+ ))}
683
1091
  {/* Round 45: radial radar sweep gradient — bright at center
684
1092
  (where it meets the hub) fading to leading edge. */}
685
1093
  <radialGradient id="topo-sweep" cx="0%" cy="50%" r="100%">
@@ -807,16 +1215,89 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
807
1215
  );
808
1216
  })}
809
1217
 
1218
+ {/* #111: prefix-group boundary boxes (Vincent 4722). Grid layout
1219
+ only — groupBoxes is empty in ring mode. Rendered behind the
1220
+ flow links + nodes; pointer-events off so they never intercept
1221
+ a node click. Restrained dashed container + group-name label. */}
1222
+ {groupBoxes.map(box => {
1223
+ const isHovered = hoveredGroup === box.key;
1224
+ // Round 18 / Loop: group-box hover linkage. The Round 8 fade
1225
+ // already dropped OUT-of-focus groups to 0.28, but the IN-focus
1226
+ // group sat at its baseline appearance — no positive emphasis.
1227
+ // Hovering now upgrades the box to an "accent" treatment:
1228
+ // solid stroke (not dashed), thicker, accent-coloured; brighter
1229
+ // text and slightly stronger fill. Label and box read as one
1230
+ // selected unit. Geometry unchanged → overlap test untouched.
1231
+ return (
1232
+ <g
1233
+ key={`grp-${box.key}`}
1234
+ data-group={box.key}
1235
+ className="transition-opacity"
1236
+ style={{ pointerEvents: 'none', opacity: !hoveredGroup || isHovered ? 1 : 0.28 }}
1237
+ >
1238
+ <rect
1239
+ x={box.x}
1240
+ y={box.y}
1241
+ width={box.w}
1242
+ height={box.h}
1243
+ rx="14"
1244
+ fill={isLight ? '#0f172a' : '#a5b4fc'}
1245
+ fillOpacity={isHovered ? (isLight ? 0.05 : 0.09) : (isLight ? 0.025 : 0.045)}
1246
+ stroke={isHovered ? pal.legendAccent : pal.ringStroke}
1247
+ strokeWidth={isHovered ? 2 : 1.5}
1248
+ strokeDasharray={isHovered ? 'none' : '6 6'}
1249
+ style={{ transition: 'stroke 200ms ease-out, stroke-width 200ms ease-out, fill-opacity 200ms ease-out' }}
1250
+ />
1251
+ <text
1252
+ x={box.x + 12}
1253
+ y={box.y + 14}
1254
+ fill={isHovered ? pal.legendHeadline : pal.legendText}
1255
+ fontSize="13"
1256
+ fontFamily="monospace"
1257
+ fontWeight="700"
1258
+ style={{ transition: 'fill 200ms ease-out' }}
1259
+ >
1260
+ {box.key}
1261
+ {/* Round 19 / Loop: member-count chip. Inline tspan stays
1262
+ in the single <text> bbox the overlap test reads, so
1263
+ the node↔label guard still catches if the chip ever
1264
+ pushes the label far enough right to clip a node.
1265
+ Smaller + lighter weight reads as metadata, not name. */}
1266
+ <tspan dx="6" fill={pal.legendText} fontSize="11" fontWeight="400">· {box.count}</tspan>
1267
+ </text>
1268
+ </g>
1269
+ );
1270
+ })}
1271
+
810
1272
  {/* directed message flows */}
811
1273
  {flowLinks.map((link, index) => {
812
1274
  const from = nodePositions[link.from];
813
1275
  const to = nodePositions[link.to];
814
1276
  if (!from || !to) return null;
815
1277
 
816
- const lift = index % 2 === 0 ? 36 : -36;
1278
+ // Round 7 / Loop: lift now scales with distance so short links
1279
+ // aren't over-bent (long links keep the ~36px hump), and the
1280
+ // particle period shortens with link.count so busier edges
1281
+ // visibly pulse faster — instant info density on top of the
1282
+ // existing stroke-width-by-count chip.
1283
+ const dist = Math.hypot(to.x - from.x, to.y - from.y);
1284
+ const lift = (index % 2 === 0 ? 1 : -1) * Math.min(36, dist * 0.18);
817
1285
  const path = curvePath(from, to, lift);
818
1286
  const width = Math.min(2 + link.count, 7);
819
- const duration = 2.2 + (index % 5) * 0.32;
1287
+ const duration = Math.max(0.9, 2.6 / Math.sqrt(link.count));
1288
+ // Round 10 / Loop: freshness fade. An edge that fired ≤30s ago
1289
+ // stays at full intensity; over 5 minutes it decays to ~35%.
1290
+ // Surfaces "what's happening now" vs background chatter without
1291
+ // hiding old flow entirely (some context still useful). `now`
1292
+ // captured at useMemo-recompute time (every 5s message refresh)
1293
+ // — accuracy is within the poll interval, plenty.
1294
+ const ageMs = link.last_at ? Math.max(0, Date.now() - Date.parse(link.last_at)) : 0;
1295
+ const fresh = Math.max(0.35, 1 - ageMs / (5 * 60 * 1000));
1296
+ // Round 16 arrow-tier binning — keep `topo-arrow` as the
1297
+ // medium tier id so the legend swatch picks it up unchanged.
1298
+ const arrowId = link.count <= 2 ? 'topo-arrow-s'
1299
+ : link.count <= 4 ? 'topo-arrow'
1300
+ : 'topo-arrow-l';
820
1301
 
821
1302
  return (
822
1303
  <g key={link.key}>
@@ -825,9 +1306,10 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
825
1306
  fill="none"
826
1307
  stroke={pal.flowEdge}
827
1308
  strokeWidth={width}
828
- opacity={isLight ? 0.22 : 0.28}
1309
+ opacity={(isLight ? 0.22 : 0.28) * fresh}
829
1310
  filter={isLight ? undefined : 'url(#topo-glow)'}
830
- markerEnd="url(#topo-arrow)"
1311
+ markerEnd={`url(#${arrowId})`}
1312
+ className="transition-opacity duration-500"
831
1313
  />
832
1314
  <path
833
1315
  id={`flow-path-${index}`}
@@ -836,37 +1318,37 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
836
1318
  stroke={pal.flowPath}
837
1319
  strokeWidth="1"
838
1320
  strokeDasharray="2 12"
839
- opacity={isLight ? 0.4 : 0.75}
1321
+ opacity={(isLight ? 0.4 : 0.75) * fresh}
1322
+ className="transition-opacity duration-500"
840
1323
  />
841
- <circle r="4" fill={pal.flowParticle} filter={isLight ? undefined : 'url(#topo-glow)'}>
1324
+ <circle r="4" fill={pal.flowParticle} filter={isLight ? undefined : 'url(#topo-glow)'} opacity={fresh}>
842
1325
  <animateMotion dur={`${duration}s`} repeatCount="indefinite" path={path} />
843
1326
  </circle>
844
1327
  </g>
845
1328
  );
846
1329
  })}
847
1330
 
848
- {/* center hub — round 39: enlarged from r=6 (12px) to r=10 (20px)
849
- core so the "control plane" reads as the network's anchor,
850
- not a stray particle. Static halo ring at r=18 grounds it
851
- visually; two outward pulses keep the "signal source" idea
852
- from r17. Light theme gets a paler core to avoid hot-spotting
853
- the bg. */}
1331
+ {/* center hub — round 39 sized + round 13 restraint.
1332
+ The hub is the control-plane anchor. r39 gave it two outward
1333
+ pulses (r 10→38, 2.4s, double-phase) which read as "loudest
1334
+ node in the room" wrong semantics: anchors don't emit,
1335
+ they hold. r13 swaps the kinetic pulse for a slow opacity
1336
+ breath on the static halo (4s, ±15% from base), so the hub
1337
+ stays alive without throwing kinetic energy outward. The
1338
+ dual-circle "lit lamp" core is unchanged. */}
854
1339
  {layout === 'ring' && (<g>
855
- {/* static grounding halo — sits underneath, low opacity */}
1340
+ {/* grounding halo — now breathes in opacity, no expansion */}
856
1341
  <circle
857
1342
  cx={cx} cy={cy} r="18"
858
1343
  fill={isLight ? '#d1fae5' : '#10b981'}
859
- opacity={isLight ? 0.35 : 0.10}
860
- />
861
- {/* outer pulse 1 */}
862
- <circle cx={cx} cy={cy} r="10" fill="none" stroke="#10b981" strokeWidth="1.5">
863
- <animate attributeName="r" values="10;38;10" dur="2.4s" repeatCount="indefinite" />
864
- <animate attributeName="opacity" values="0.55;0;0.55" dur="2.4s" repeatCount="indefinite" />
865
- </circle>
866
- {/* outer pulse 2 (delayed 1.2s) */}
867
- <circle cx={cx} cy={cy} r="10" fill="none" stroke="#10b981" strokeWidth="1.5" opacity="0">
868
- <animate attributeName="r" values="10;38;10" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
869
- <animate attributeName="opacity" values="0.45;0;0.45" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
1344
+ opacity={isLight ? 0.42 : 0.12}
1345
+ >
1346
+ <animate
1347
+ attributeName="opacity"
1348
+ values={isLight ? '0.32;0.52;0.32' : '0.08;0.16;0.08'}
1349
+ dur="4s"
1350
+ repeatCount="indefinite"
1351
+ />
870
1352
  </circle>
871
1353
  {/* core — 20px diameter, larger inner highlight reads as a "lit lamp" */}
872
1354
  <circle cx={cx} cy={cy} r="10" fill={isLight ? '#059669' : '#10b981'} />
@@ -874,7 +1356,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
874
1356
  </g>)}
875
1357
 
876
1358
  {/* agent nodes */}
877
- {[...onlineNodes, ...offlineNodes].map(session => {
1359
+ {[...onlineNodes, ...offlineNodes].map((session, nodeIdx) => {
878
1360
  const pos = nodePositions[session.alias];
879
1361
  if (!pos) return null;
880
1362
 
@@ -882,7 +1364,9 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
882
1364
  const isOnline = session.status !== 'offline' || !!sseCountFor;
883
1365
  const status = nodeStatus(session, isOnline, isLight);
884
1366
  const isActive = activeAliases.has(session.alias);
885
- const radius = isOnline ? 26 : 18;
1367
+ // #113: node size scales with the S/M/L control; halos, labels,
1368
+ // badge and avatar all derive from `radius` so they follow.
1369
+ const radius = Math.round((isOnline ? 26 : 18) * nodeScale);
886
1370
  // Round 109/110 (Vincent 4582 + 4583 P0): at high node counts
887
1371
  // the 100px opaque label cards overlapped each other and
888
1372
  // covered neighbouring avatars. But hiding text entirely went
@@ -892,12 +1376,48 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
892
1376
  // occlude an avatar), and the full card appears only for the
893
1377
  // hovered node or once zoomed in past 1.4×.
894
1378
  const showFullLabel = !denseLayout || hoveredAlias === session.alias || view.zoom >= 1.4;
1379
+ // Round 8: when a node in another group is hovered, fade this
1380
+ // one. Same-group nodes (incl. singletons matching the hovered
1381
+ // alias) stay full. Pure visual focus, geometry unchanged.
1382
+ const inFocus = !hoveredGroup || (groupKeys[session.alias] ?? session.alias) === hoveredGroup;
895
1383
 
896
1384
  return (
897
1385
  <g
898
1386
  key={session.alias}
899
- className="transition-opacity"
900
- style={{ cursor: 'pointer' }}
1387
+ data-node={session.alias}
1388
+ // Round 3 / Loop: `anet-fade-in` runs once when the <g>
1389
+ // mounts — a new session entering the fleet (or the topology
1390
+ // first rendering) eases in instead of popping. Re-renders of
1391
+ // an existing node don't re-trigger (React preserves the <g>
1392
+ // via the alias key), so status changes don't flicker. The
1393
+ // global prefers-reduced-motion sweep already neutralises it.
1394
+ className="group transition-opacity anet-fade-in"
1395
+ style={{
1396
+ cursor: 'pointer',
1397
+ // Round 17 / Loop: offline nodes drop to 0.6 at rest so
1398
+ // online nodes pop without losing the offline-as-ghost
1399
+ // information. The dashed stroke + smaller radius already
1400
+ // say "offline"; dimming the whole group strengthens the
1401
+ // online-vs-offline hierarchy at first glance. Exempt the
1402
+ // chat-focused node — if the user explicitly opened a
1403
+ // popover targeting an offline alias, that node stays
1404
+ // full-brightness so the focus ring + popover read as one
1405
+ // selected thing rather than a dimmed selection. Group-
1406
+ // hover fade (Round 8) still wins when inFocus is false.
1407
+ opacity: !inFocus
1408
+ ? 0.32
1409
+ : chatAlias === session.alias
1410
+ ? 1
1411
+ : isOnline ? 1 : 0.6,
1412
+ // Round 9 / Loop: stagger the anet-fade-in so the topology
1413
+ // reveals as a wave on first paint instead of one big pop.
1414
+ // Cap at 24 indices (≈600ms tail) so 50-node fleets still
1415
+ // finish revealing within a beat. CSS animation-delay only
1416
+ // applies during the keyframe — re-renders without a new
1417
+ // mount (same alias key) don't replay, so status changes
1418
+ // never trigger the stagger again.
1419
+ animationDelay: `${Math.min(nodeIdx, 24) * 25}ms`,
1420
+ }}
901
1421
  // Stop the pointerdown from reaching the SVG pan handler: the
902
1422
  // SVG calls setPointerCapture, and a captured pointer makes
903
1423
  // Chromium fire the follow-up `click` on the SVG instead of
@@ -905,21 +1425,114 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
905
1425
  // Side effect (intended): you pan from empty canvas, not by
906
1426
  // grabbing a node.
907
1427
  onPointerDown={(e) => e.stopPropagation()}
908
- onPointerEnter={() => denseLayout && setHoveredAlias(session.alias)}
909
- onPointerLeave={() => denseLayout && setHoveredAlias(prev => (prev === session.alias ? null : prev))}
910
- onClick={() => setChatAlias(session.alias)}
1428
+ // Track hover globally (not just under denseLayout) so the
1429
+ // Round 8 group-focus fade works at any fleet size.
1430
+ onPointerEnter={() => setHoveredAlias(session.alias)}
1431
+ onPointerLeave={() => setHoveredAlias(prev => (prev === session.alias ? null : prev))}
1432
+ onClick={() => {
1433
+ setChatAlias(session.alias);
1434
+ // Round 14 ripple — capture position, radius and status
1435
+ // colour at click time so the one-shot circle is self-
1436
+ // contained (no re-render-time recomputation needed).
1437
+ setClickRipple({
1438
+ ts: Date.now(),
1439
+ x: pos.x, y: pos.y, r0: radius,
1440
+ color: status.primary,
1441
+ });
1442
+ setTimeout(() => setClickRipple(prev =>
1443
+ prev && Date.now() - prev.ts >= 590 ? null : prev), 600);
1444
+ }}
911
1445
  >
912
1446
  {/* Issue #96: native hover tooltip — "Vendor · model · Runtime".
913
1447
  Falls back to just the alias when the node reports no
914
- model/runtime. */}
915
- <title>{identityLine(session.model, session.runtime) || session.alias}</title>
1448
+ model/runtime.
1449
+ Round 33 / Loop: cwd line answers "what is this agent on?".
1450
+ Round 34 / Loop: for offline nodes, append "last seen: 6m ago"
1451
+ so the operator knows whether to wait or chase. Online nodes
1452
+ skip the line (Round 27's 1 h ghost age-out means anything
1453
+ still online has heartbeated recently — the line would just
1454
+ be noise). Accepts both ISO ("…T06:00:28Z") and SQL-style
1455
+ ("… 06:00:28" assumed UTC) formats. */}
1456
+ {(() => {
1457
+ const formatLastSeen = (dateStr: string): string | null => {
1458
+ if (!dateStr) return null;
1459
+ const iso = dateStr.includes('T') ? dateStr : dateStr.replace(' ', 'T') + 'Z';
1460
+ const t = Date.parse(iso);
1461
+ if (!Number.isFinite(t)) return null;
1462
+ const s = Math.floor((Date.now() - t) / 1000);
1463
+ if (s < 0) return 'just now';
1464
+ if (s < 60) return `${s}s ago`;
1465
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
1466
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
1467
+ return `${Math.floor(s / 86400)}d ago`;
1468
+ };
1469
+ const lastSeen = !isOnline && session.last_seen_at ? formatLastSeen(session.last_seen_at) : null;
1470
+ return (
1471
+ <title>{[
1472
+ identityLine(session.model, session.runtime) || session.alias,
1473
+ session.project_dir ? `cwd: ${session.project_dir}` : null,
1474
+ lastSeen ? `last seen: ${lastSeen}` : null,
1475
+ ].filter(Boolean).join('\n')}</title>
1476
+ );
1477
+ })()}
1478
+ {/* Round 2 / Loop: hover ring — a thin outer stroke that fades
1479
+ in when the cursor enters the node, signalling clickability
1480
+ (real-user feedback for the chat-popover open). Pure CSS via
1481
+ Tailwind group-hover, so it costs nothing per frame and
1482
+ respects prefers-reduced-motion via the global media query. */}
1483
+ <circle
1484
+ cx={pos.x}
1485
+ cy={pos.y}
1486
+ r={radius + 12}
1487
+ fill="none"
1488
+ stroke={status.primary}
1489
+ // strokeWidth must NOT be 1.5 (offline status ring) or 3
1490
+ // (online status ring) — the overlap test selects by those
1491
+ // exact widths and would mis-count this invisible hover
1492
+ // ring as a node footprint.
1493
+ strokeWidth="2"
1494
+ className="opacity-0 group-hover:opacity-70 transition-opacity duration-150"
1495
+ style={{ pointerEvents: 'none' }}
1496
+ />
1497
+ {/* Round 11 / Loop: chat-focus ring — when the ChatPopover is
1498
+ open targeting this node, anchor a persistent ring around
1499
+ it so the floating popover visibly links back to its source
1500
+ node. Static (not pulsing) so it reads as "selected state"
1501
+ rather than "this node is active". strokeWidth=2.5 stays
1502
+ clear of the overlap-test selectors (1.5 / 3). Sits just
1503
+ outside the halo radius+8 so it never overlaps a neighbour
1504
+ (halos already pack flush in dense grids). */}
1505
+ {chatAlias === session.alias && (
1506
+ <circle
1507
+ cx={pos.x}
1508
+ cy={pos.y}
1509
+ r={radius + 14}
1510
+ fill="none"
1511
+ stroke={status.primary}
1512
+ strokeWidth="2.5"
1513
+ opacity={isLight ? 0.85 : 0.95}
1514
+ filter={!isLight ? 'url(#topo-glow)' : undefined}
1515
+ className="transition-opacity duration-200"
1516
+ style={{ pointerEvents: 'none' }}
1517
+ />
1518
+ )}
916
1519
  {isActive && (
917
1520
  <circle cx={pos.x} cy={pos.y} r={radius + 14} fill={status.primary} opacity={isLight ? 0.08 : 0.12}>
918
1521
  <animate attributeName="r" values={`${radius + 8};${radius + 22};${radius + 8}`} dur="2.4s" repeatCount="indefinite" />
919
1522
  <animate attributeName="opacity" values={isLight ? '0.12;0.02;0.12' : '0.18;0.04;0.18'} dur="2.4s" repeatCount="indefinite" />
920
1523
  </circle>
921
1524
  )}
922
- <circle cx={pos.x} cy={pos.y} r={radius + 8} fill={status.halo} opacity={isOnline ? (isLight ? 0.85 : 0.65) : (isLight ? 0.4 : 0.25)} />
1525
+ {/* Round 4 / Loop: transition-[fill,stroke,opacity] smooths
1526
+ status colour changes so idle↔working↔offline doesn't snap
1527
+ — task replies / node-rename / SSE updates ease in. */}
1528
+ <circle
1529
+ cx={pos.x}
1530
+ cy={pos.y}
1531
+ r={radius + 8}
1532
+ fill={status.halo}
1533
+ opacity={isOnline ? (isLight ? 0.85 : 0.65) : (isLight ? 0.4 : 0.25)}
1534
+ className="transition-[fill,opacity] duration-300 ease-out"
1535
+ />
923
1536
  <circle
924
1537
  cx={pos.x}
925
1538
  cy={pos.y}
@@ -929,6 +1542,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
929
1542
  strokeWidth={isOnline ? 3 : 1.5}
930
1543
  strokeDasharray={isOnline ? 'none' : '5 5'}
931
1544
  filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
1545
+ className="transition-[fill,stroke] duration-300 ease-out"
932
1546
  />
933
1547
  {/* Issue #96: node "avatar" is now driven by the model
934
1548
  vendor. Decision order:
@@ -941,7 +1555,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
941
1555
  4. unknown vendor / null model → the prefix-group
942
1556
  hue-hashed initial (#83/#99 behaviour, unchanged). */}
943
1557
  {(() => {
944
- const ar = isOnline ? 14 : 10;
1558
+ const ar = Math.round((isOnline ? 14 : 10) * nodeScale);
945
1559
  const size = radius * 2;
946
1560
  const vendor = vendorForModel(session.model);
947
1561
  const internByAlias = /书生|书小生|intern/i.test(session.alias);
@@ -966,7 +1580,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
966
1580
  <circle cx={pos.x} cy={pos.y} r={ar} fill={vendor.mono.bg} stroke={vendor.mono.ring} strokeWidth="1" />
967
1581
  <text
968
1582
  x={pos.x} y={pos.y} dy="0.34em" textAnchor="middle"
969
- fill={vendor.mono.text} fontSize={isOnline ? 14 : 10}
1583
+ fill={vendor.mono.text} fontSize={ar}
970
1584
  fontFamily="monospace" fontWeight="700"
971
1585
  >
972
1586
  {vendor.initial}
@@ -986,7 +1600,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
986
1600
  dy="0.34em"
987
1601
  textAnchor="middle"
988
1602
  fill={c.text}
989
- fontSize={isOnline ? 14 : 10}
1603
+ fontSize={ar}
990
1604
  fontFamily="monospace"
991
1605
  fontWeight="700"
992
1606
  >
@@ -1015,78 +1629,203 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
1015
1629
  </g>
1016
1630
  );
1017
1631
  })()}
1018
- {session.status === 'working' && (
1019
- // working pulse small dot just inside the status ring,
1020
- // above the avatar text. Was a 18px horizontal bar at
1021
- // y+11 which now collides with the avatar; the dot reads
1022
- // as the same "active" cue without occluding the initial.
1023
- <circle cx={pos.x} cy={pos.y - (isOnline ? 18 : 13)} r="2.5" fill={pal.flowParticle}>
1024
- <animate attributeName="opacity" values="1;0.25;1" dur="1.1s" repeatCount="indefinite" />
1025
- </circle>
1026
- )}
1632
+ {session.status === 'working' && (() => {
1633
+ // Round 24 / Loop: pulse rate traffic. The dot's dur
1634
+ // was a flat 1.1s every working node looked equally
1635
+ // busy. Mapping it to sse:N lets the eye land on what's
1636
+ // actually hot. 3 discrete tiers read cleaner than
1637
+ // continuous easing at a glance; thresholds picked so
1638
+ // typical 1-2-SSE Claude/Codex sessions stay in the
1639
+ // "active" tier and only multi-pane orchestrators pop
1640
+ // into the "busy" tier. Geometry unchanged (r=2.5).
1641
+ const sse = sseCountFor ?? 0;
1642
+ const dur = sse >= 4 ? '0.7s' : sse >= 2 ? '0.9s' : '1.2s';
1643
+ return (
1644
+ <circle cx={pos.x} cy={pos.y - (radius - 6)} r="2.5" fill={pal.flowParticle} data-pulse-dur={dur}>
1645
+ <animate attributeName="opacity" values="1;0.25;1" dur={dur} repeatCount="indefinite" />
1646
+ </circle>
1647
+ );
1648
+ })()}
1027
1649
 
1028
1650
  {/* Round 98 (issue #61): label rect 124px → 100px.
1029
1651
  Round 109/110 (Vincent P0): full opaque card below the
1030
1652
  density threshold / on hover / when zoomed; otherwise a
1031
1653
  lightweight plain-text alias that keeps every node
1032
- labelled without an opaque box covering its neighbours. */}
1033
- {showFullLabel ? (
1034
- <g transform={`translate(${pos.x}, ${pos.y + radius + 22})`} style={{ pointerEvents: 'none' }}>
1035
- <rect x="-50" y="-14" width="100" height="42" rx="6" fill={pal.labelBox.fill} stroke={pal.labelBox.stroke} opacity={isLight ? 1 : 0.94} />
1036
- <text x="0" y="1" textAnchor="middle" fill={status.text} fontSize="12" fontFamily="monospace" fontWeight="700">
1037
- {truncate(session.alias, 12)}
1038
- </text>
1039
- <text x="0" y="17" textAnchor="middle" fill={status.primary} fontSize="9" fontFamily="monospace">
1040
- {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
1654
+ labelled without an opaque box covering its neighbours.
1655
+ Round 15 / Loop: when nodeScale=S the node shrinks 30%
1656
+ but the label card was staying full-size, so the label
1657
+ visually outweighed the small node. Tighten the card
1658
+ frame, alias / sub fontSize, drop-offset and truncate
1659
+ length specifically for S; M and L keep their existing
1660
+ sizes (M ≈ L for labels by design — the S user is the
1661
+ one who explicitly asked for a denser view). */}
1662
+ {(() => {
1663
+ const isSmall = nodeScale < 0.8;
1664
+ const cardW = isSmall ? 88 : 100;
1665
+ const cardH = isSmall ? 36 : 42;
1666
+ const cardTopY = isSmall ? -12 : -14;
1667
+ const aliasFs = isSmall ? 11 : 12;
1668
+ const subFs = isSmall ? 8 : 9;
1669
+ const subY = isSmall ? 15 : 17;
1670
+ const dropY = isSmall ? 18 : 22;
1671
+ const fullMax = isSmall ? 11 : 12;
1672
+ const denseFs = isSmall ? 9 : 10;
1673
+ const denseDrop = isSmall ? 12 : 14;
1674
+ // Round 26 / Loop: micro-lift the label group on hover —
1675
+ // 1.5 px upward, 200 ms ease. Pairs with the Round 18
1676
+ // group-box hover-accent treatment to give the same
1677
+ // "this is the focused element" feedback at the per-
1678
+ // node level. CSS transform stacks onto the SVG
1679
+ // positioning transform attribute (SVG 2 cascade);
1680
+ // bbox is unchanged at rest, so the overlap-test gate
1681
+ // continues to see the geometric layout it expects.
1682
+ return showFullLabel ? (
1683
+ <g transform={`translate(${pos.x}, ${pos.y + radius + dropY})`} style={{ pointerEvents: 'none' }}
1684
+ className="transition-transform duration-200 group-hover:-translate-y-[1.5px]">
1685
+ <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} />
1686
+ <text x="0" y="1" textAnchor="middle" fill={status.text} fontSize={aliasFs} fontFamily="monospace" fontWeight="700">
1687
+ {truncate(session.alias, fullMax)}
1688
+ </text>
1689
+ <text x="0" y={subY} textAnchor="middle" fill={status.primary} fontSize={subFs} fontFamily="monospace">
1690
+ {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
1691
+ </text>
1692
+ </g>
1693
+ ) : (
1694
+ <text
1695
+ x={pos.x}
1696
+ y={pos.y + radius + denseDrop}
1697
+ textAnchor="middle"
1698
+ fill={status.text}
1699
+ fontSize={denseFs}
1700
+ fontFamily="monospace"
1701
+ fontWeight="700"
1702
+ opacity={0.9}
1703
+ className="transition-transform duration-200 group-hover:-translate-y-[1.5px]"
1704
+ style={{ pointerEvents: 'none', paintOrder: 'stroke' }}
1705
+ stroke={pal.containerBg}
1706
+ strokeWidth="3"
1707
+ >
1708
+ {truncate(session.alias, isSmall ? 9 : 10)}
1041
1709
  </text>
1042
- </g>
1043
- ) : (
1044
- <text
1045
- x={pos.x}
1046
- y={pos.y + radius + 14}
1047
- textAnchor="middle"
1048
- fill={status.text}
1049
- fontSize="10"
1050
- fontFamily="monospace"
1051
- fontWeight="700"
1052
- opacity={0.9}
1053
- style={{ pointerEvents: 'none', paintOrder: 'stroke' }}
1054
- stroke={pal.containerBg}
1055
- strokeWidth="3"
1056
- >
1057
- {truncate(session.alias, 10)}
1058
- </text>
1059
- )}
1710
+ );
1711
+ })()}
1060
1712
  </g>
1061
1713
  );
1062
1714
  })}
1063
1715
 
1716
+ {/* Round 14 / Loop: click ripple — one-shot expanding ring from
1717
+ the clicked node, ~500ms, status-coloured. Sits inside the
1718
+ zoom/pan <g> so it scales / pans with the topology. Keyed by
1719
+ ts so a re-click on any node (same or different) remounts the
1720
+ <circle> and the SMIL <animate> elements replay from t=0.
1721
+ strokeWidth=2 doesn't match the overlap-test selectors. */}
1722
+ {clickRipple && (
1723
+ <circle
1724
+ key={clickRipple.ts}
1725
+ cx={clickRipple.x}
1726
+ cy={clickRipple.y}
1727
+ r={clickRipple.r0 + 4}
1728
+ fill="none"
1729
+ stroke={clickRipple.color}
1730
+ strokeWidth="2"
1731
+ opacity="0"
1732
+ style={{ pointerEvents: 'none' }}
1733
+ >
1734
+ <animate attributeName="r" values={`${clickRipple.r0 + 4};${clickRipple.r0 + 30}`} dur="0.5s" fill="freeze" />
1735
+ <animate attributeName="opacity" values="0.7;0" dur="0.5s" fill="freeze" />
1736
+ </circle>
1737
+ )}
1738
+
1739
+ </g>
1740
+
1741
+ {/* #112: overlay panels (recent-signal + legend) render OUTSIDE the
1742
+ zoom/pan <g> so they stay fixed while the topology pans/zooms.
1743
+ They're sized + tucked into the top corners so every corner of
1744
+ each panel is >325px from the canvas centre — i.e. fully outside
1745
+ the outermost (offline) ring. No node on any ring can reach the
1746
+ corner triangles, so the panels never overlap a node, in ring
1747
+ OR grid layout (Vincent 4727 zero-overlap criterion). */}
1064
1748
  {/* latest flow labels */}
1065
- <g transform="translate(28, 34)">
1066
- <rect x="0" y="0" width="285" height={60 + Math.min(flowLinks.length, 4) * 20} rx="8" fill={pal.legendBox.fill} stroke={pal.legendBox.stroke} opacity={isLight ? 1 : 0.94} />
1067
- <text x="16" y="26" fill={pal.legendHeadline} fontSize="13" fontFamily="monospace" fontWeight="700">recent signal</text>
1068
- <text x="178" y="26" fill={pal.legendAccent} fontSize="11" fontFamily="monospace">{messages.length} messages</text>
1069
- {flowLinks.slice(0, 4).map((link, index) => (
1070
- <text key={link.key} x="16" y={54 + index * 20} fill={pal.legendText} fontSize="10" fontFamily="monospace">
1071
- {truncate(link.from, 8)} {'->'} {truncate(link.to, 8)} / {link.count} / {truncate(link.content, 18)}
1749
+ <g transform="translate(16, 16)">
1750
+ <rect x="0" y="0" width="230" height="84" rx="10" fill={pal.legendBox.fill} stroke={pal.legendBox.stroke} opacity={isLight ? 0.97 : 0.92} />
1751
+ <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight="700">recent signal</text>
1752
+ <text x="150" y="21" fill={pal.legendAccent} fontSize="10" fontFamily="monospace">{messages.length} msgs</text>
1753
+ {flowLinks.slice(0, 3).map((link, index) => (
1754
+ <text key={link.key} x="13" y={38 + index * 16} fill={pal.legendText} fontSize="9" fontFamily="monospace">
1755
+ {truncate(link.from, 6)} {'->'} {truncate(link.to, 6)} / {link.count} / {truncate(link.content, 12)}
1072
1756
  </text>
1073
1757
  ))}
1074
1758
  </g>
1075
1759
 
1076
1760
  {/* legend */}
1077
- <g transform="translate(720, 34)">
1078
- <rect x="0" y="0" width="250" height="112" rx="8" fill={pal.legendBox.fill} stroke={pal.legendBox.stroke} opacity={isLight ? 1 : 0.94} />
1079
- <circle cx="18" cy="26" r="6" fill={isLight ? '#059669' : '#22c55e'} />
1080
- <text x="34" y="30" fill={pal.legendText} fontSize="11" fontFamily="monospace">working node</text>
1081
- <circle cx="18" cy="52" r="6" fill={isLight ? '#0d9488' : '#2dd4bf'} />
1082
- <text x="34" y="56" fill={pal.legendText} fontSize="11" fontFamily="monospace">online idle</text>
1083
- <circle cx="18" cy="78" r="6" fill={isLight ? '#94a3b8' : '#6b7280'} />
1084
- <text x="34" y="82" fill={pal.legendText} fontSize="11" fontFamily="monospace">offline / no SSE</text>
1085
- <path d="M150,78 Q176,52 210,78" fill="none" stroke={pal.flowEdge} strokeWidth="3" markerEnd="url(#topo-arrow)" />
1086
- </g>
1761
+ <g transform="translate(760, 16)">
1762
+ <rect x="0" y="0" width="224" height="96" rx="10" fill={pal.legendBox.fill} stroke={pal.legendBox.stroke} opacity={isLight ? 0.97 : 0.92} />
1763
+ <circle cx="16" cy="24" r="5.5" fill={isLight ? '#059669' : '#22c55e'} />
1764
+ <text x="30" y="28" fill={pal.legendText} fontSize="11" fontFamily="monospace">working node</text>
1765
+ <circle cx="16" cy="48" r="5.5" fill={isLight ? '#0d9488' : '#2dd4bf'} />
1766
+ <text x="30" y="52" fill={pal.legendText} fontSize="11" fontFamily="monospace">online idle</text>
1767
+ <circle cx="16" cy="72" r="5.5" fill={isLight ? '#94a3b8' : '#6b7280'} />
1768
+ <text x="30" y="76" fill={pal.legendText} fontSize="11" fontFamily="monospace">offline / no SSE</text>
1769
+ <path d="M140,72 Q164,48 196,72" fill="none" stroke={pal.flowEdge} strokeWidth="3" markerEnd="url(#topo-arrow)" />
1087
1770
  </g>
1088
1771
  </svg>
1089
1772
 
1773
+ {/* Round 30 / Loop: minimap. Big fleets in fullscreen mode at high
1774
+ zoom let users lose their position — the minimap shows the
1775
+ whole topology miniaturised plus a viewport rectangle so the
1776
+ user always knows where they are. Click anywhere to recenter
1777
+ the canvas there. Only mounted when the view is non-default
1778
+ (zoomed or panned) since at 1× centered the minimap and the
1779
+ canvas show the same thing. HTML overlay so it stays fixed
1780
+ while the SVG transforms. */}
1781
+ {(() => {
1782
+ const isDefaultView = Math.abs(view.zoom - 1) < 0.01 && Math.abs(view.x) < 1 && Math.abs(view.y) < 1;
1783
+ if (isDefaultView || (onlineNodes.length + offlineNodes.length) === 0) return null;
1784
+ const MW = 120, MH = 82;
1785
+ const sx = MW / VIEWBOX_W, sy = MH / VIEWBOX_H;
1786
+ const rectX = (-view.x / view.zoom) * sx;
1787
+ const rectY = (-view.y / view.zoom) * sy;
1788
+ const rectW = (VIEWBOX_W / view.zoom) * sx;
1789
+ const rectH = (VIEWBOX_H / view.zoom) * sy;
1790
+ return (
1791
+ <div
1792
+ className="absolute right-3 rounded-md border shadow-lg shadow-black/30 overflow-hidden anet-fade-in"
1793
+ style={{ bottom: 56, background: pal.legendBox.fill, borderColor: pal.containerBorder, cursor: 'crosshair' }}
1794
+ role="img"
1795
+ aria-label="Topology minimap — click to recenter"
1796
+ title="Minimap · click to recenter"
1797
+ onClick={(e) => {
1798
+ const r = e.currentTarget.getBoundingClientRect();
1799
+ const fx = (e.clientX - r.left) / r.width;
1800
+ const fy = (e.clientY - r.top) / r.height;
1801
+ setView(prev => ({
1802
+ ...prev,
1803
+ x: VIEWBOX_W / 2 - fx * VIEWBOX_W * prev.zoom,
1804
+ y: VIEWBOX_H / 2 - fy * VIEWBOX_H * prev.zoom,
1805
+ }));
1806
+ }}
1807
+ data-topo-minimap
1808
+ >
1809
+ <svg width={MW} height={MH} viewBox={`0 0 ${MW} ${MH}`} style={{ display: 'block' }}>
1810
+ {[...onlineNodes, ...offlineNodes].map(s => {
1811
+ const p = nodePositions[s.alias];
1812
+ if (!p) return null;
1813
+ const sseN = (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];
1814
+ const isOn = s.status !== 'offline' || !!sseN;
1815
+ const st = nodeStatus(s, isOn, isLight);
1816
+ 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} />;
1817
+ })}
1818
+ {/* viewport rectangle */}
1819
+ <rect
1820
+ x={Math.max(0, rectX)} y={Math.max(0, rectY)}
1821
+ width={Math.max(0, Math.min(MW - Math.max(0, rectX), rectW))}
1822
+ height={Math.max(0, Math.min(MH - Math.max(0, rectY), rectH))}
1823
+ fill="none" stroke={pal.legendAccent} strokeWidth="1" opacity="0.9"
1824
+ />
1825
+ </svg>
1826
+ </div>
1827
+ );
1828
+ })()}
1090
1829
  {/* Round 103 (issue #81): zoom / pan / fullscreen controls — HTML
1091
1830
  overlay so they stay fixed while the SVG content transforms.
1092
1831
  Round 104: Vincent 实测 — the reset action used to be hidden
@@ -1094,6 +1833,26 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
1094
1833
  Split into a plain % readout + an explicit reset button with
1095
1834
  its own icon + tooltip. */}
1096
1835
  <div className="absolute bottom-3 right-3 flex items-center gap-1.5 text-xs select-none">
1836
+ {/* #113: node size — S / M / L segmented control (Vincent 4727). */}
1837
+ <div
1838
+ className="flex items-center rounded-md border overflow-hidden"
1839
+ style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder }}
1840
+ role="group"
1841
+ aria-label="Node size"
1842
+ >
1843
+ {([['S', 0.7], ['M', 0.84], ['L', 1]] as const).map(([lbl, v], idx) => (
1844
+ <button
1845
+ key={lbl}
1846
+ onClick={() => pickNodeScale(v)}
1847
+ aria-pressed={nodeScale === v}
1848
+ title={`Node size: ${lbl === 'S' ? 'small' : lbl === 'M' ? 'medium' : 'large'}`}
1849
+ className={`px-2 py-1 transition-colors ${idx > 0 ? 'border-l' : ''} ${nodeScale === v ? 'bg-cyan-500/15 text-cyan-300' : 'hover:bg-white/5'}`}
1850
+ style={{ color: nodeScale === v ? undefined : pal.legendText, borderColor: pal.containerBorder }}
1851
+ >
1852
+ {lbl}
1853
+ </button>
1854
+ ))}
1855
+ </div>
1097
1856
  <div
1098
1857
  className="flex items-center rounded-md border overflow-hidden"
1099
1858
  style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder }}
@@ -1103,6 +1862,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
1103
1862
  className="px-2 py-1 hover:bg-white/5 transition-colors"
1104
1863
  style={{ color: pal.legendText }}
1105
1864
  aria-label="Zoom out"
1865
+ title="Zoom out (−)"
1106
1866
  >
1107
1867
  <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>
1108
1868
  </button>
@@ -1118,6 +1878,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
1118
1878
  className="px-2 py-1 hover:bg-white/5 transition-colors"
1119
1879
  style={{ color: pal.legendText }}
1120
1880
  aria-label="Zoom in"
1881
+ title="Zoom in (+)"
1121
1882
  >
1122
1883
  <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>
1123
1884
  </button>
@@ -1127,7 +1888,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
1127
1888
  className="p-1.5 rounded-md border hover:bg-white/5 transition-colors"
1128
1889
  style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
1129
1890
  aria-label="Reset view"
1130
- title="Reset zoom + pan (or double-click the canvas)"
1891
+ title="Reset zoom + pan (0, or double-click the canvas)"
1131
1892
  >
1132
1893
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
1133
1894
  <path d="M3 12a9 9 0 1 0 9-9 9 9 0 0 0-6.4 2.6L3 8" />