@sleep2agi/agent-network-dashboard 0.4.6-preview.5 → 0.4.6-preview.50

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 (842) 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 +4 -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/0l-goaw68dc94.js +1 -0
  518. package/.next/static/chunks/0mqwn-l7wtcdo.js +7 -0
  519. package/.next/static/chunks/0q_7h~ht6ih~7.css +2 -0
  520. package/.next/static/chunks/0r4q73.-1bkyl.js +1 -0
  521. package/.next/static/chunks/0r_c8eccg3z2o.js +1 -0
  522. package/.next/static/chunks/0tvn2l1pc.h65.js +1 -0
  523. package/.next/static/chunks/0u3b93reo9xci.js +1 -0
  524. package/.next/static/chunks/0whesa_4tlk69.js +1 -0
  525. package/.next/static/chunks/0~pgydk0.a5_t.js +2 -0
  526. package/.next/static/chunks/10oi7l-tycgl9.js +1 -0
  527. package/.next/static/chunks/11uk1izuv3~0t.js +1 -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 +43 -7
  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 +995 -156
  540. package/app/components/types.ts +3 -0
  541. package/app/lib/time.ts +36 -0
  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-active-links-chip-test.mjs +73 -0
  548. package/scripts/topo-arrow-tier-test.mjs +61 -0
  549. package/scripts/topo-auto-fit-test.mjs +73 -0
  550. package/scripts/topo-chat-focus-ring-test.mjs +77 -0
  551. package/scripts/topo-click-ripple-test.mjs +66 -0
  552. package/scripts/topo-cwd-tooltip-test.mjs +64 -0
  553. package/scripts/topo-dblclick-reset-test.mjs +74 -0
  554. package/scripts/topo-edge-hover-test.mjs +85 -0
  555. package/scripts/topo-edge-tooltip-test.mjs +70 -0
  556. package/scripts/topo-fit-key-test.mjs +106 -0
  557. package/scripts/topo-group-count-test.mjs +63 -0
  558. package/scripts/topo-group-hover-test.mjs +82 -0
  559. package/scripts/topo-groupbox-test.mjs +118 -0
  560. package/scripts/topo-hub-title-test.mjs +88 -0
  561. package/scripts/topo-label-lift-test.mjs +115 -0
  562. package/scripts/topo-label-scale-test.mjs +78 -0
  563. package/scripts/topo-lastseen-tooltip-test.mjs +59 -0
  564. package/scripts/topo-layout-key-test.mjs +91 -0
  565. package/scripts/topo-layout-test.mjs +1 -0
  566. package/scripts/topo-minimap-test.mjs +68 -0
  567. package/scripts/topo-offline-dim-test.mjs +70 -0
  568. package/scripts/topo-overlap-test.mjs +164 -0
  569. package/scripts/topo-p0round-test.mjs +166 -0
  570. package/scripts/topo-pan-cursor-test.mjs +62 -0
  571. package/scripts/topo-popover-meta-test.mjs +78 -0
  572. package/scripts/topo-pressure-bar-test.mjs +69 -0
  573. package/scripts/topo-pulse-rate-test.mjs +71 -0
  574. package/scripts/topo-reduced-motion-test.mjs +77 -0
  575. package/scripts/topo-rename-test.mjs +1 -0
  576. package/scripts/topo-tz-ghost-test.mjs +69 -0
  577. package/scripts/topo-vendor-test.mjs +1 -0
  578. package/scripts/topo-wheel-zoom-test.mjs +79 -0
  579. package/scripts/topo-zoom-keys-test.mjs +89 -0
  580. package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_06e.r3r._.js +0 -13
  581. package/.next/build/chunks/node_modules_13sb.px._.js.map +0 -47
  582. package/.next/server/chunks/[root-of-the-server]__0-mykaf._.js +0 -3
  583. package/.next/server/chunks/[root-of-the-server]__0-mykaf._.js.map +0 -1
  584. package/.next/server/chunks/[root-of-the-server]__0.d_kqy._.js +0 -3
  585. package/.next/server/chunks/[root-of-the-server]__0.d_kqy._.js.map +0 -1
  586. package/.next/server/chunks/[root-of-the-server]__02pocyg._.js +0 -3
  587. package/.next/server/chunks/[root-of-the-server]__02pocyg._.js.map +0 -1
  588. package/.next/server/chunks/[root-of-the-server]__03ueci3._.js +0 -3
  589. package/.next/server/chunks/[root-of-the-server]__04aud7x._.js +0 -3
  590. package/.next/server/chunks/[root-of-the-server]__04aud7x._.js.map +0 -1
  591. package/.next/server/chunks/[root-of-the-server]__05fnt3n._.js +0 -3
  592. package/.next/server/chunks/[root-of-the-server]__05fnt3n._.js.map +0 -1
  593. package/.next/server/chunks/[root-of-the-server]__077tsd.._.js +0 -20
  594. package/.next/server/chunks/[root-of-the-server]__077tsd.._.js.map +0 -1
  595. package/.next/server/chunks/[root-of-the-server]__08wy1jr._.js +0 -3
  596. package/.next/server/chunks/[root-of-the-server]__08wy1jr._.js.map +0 -1
  597. package/.next/server/chunks/[root-of-the-server]__0adhu1m._.js +0 -3
  598. package/.next/server/chunks/[root-of-the-server]__0adhu1m._.js.map +0 -1
  599. package/.next/server/chunks/[root-of-the-server]__0aqv02v._.js +0 -3
  600. package/.next/server/chunks/[root-of-the-server]__0aqv02v._.js.map +0 -1
  601. package/.next/server/chunks/[root-of-the-server]__0cawhfq._.js +0 -3
  602. package/.next/server/chunks/[root-of-the-server]__0cawhfq._.js.map +0 -1
  603. package/.next/server/chunks/[root-of-the-server]__0d_-_k.._.js +0 -3
  604. package/.next/server/chunks/[root-of-the-server]__0d_-_k.._.js.map +0 -1
  605. package/.next/server/chunks/[root-of-the-server]__0f6bxfl._.js +0 -3
  606. package/.next/server/chunks/[root-of-the-server]__0f6bxfl._.js.map +0 -1
  607. package/.next/server/chunks/[root-of-the-server]__0h15edq._.js +0 -3
  608. package/.next/server/chunks/[root-of-the-server]__0h15edq._.js.map +0 -1
  609. package/.next/server/chunks/[root-of-the-server]__0k.vegl._.js +0 -3
  610. package/.next/server/chunks/[root-of-the-server]__0k.vegl._.js.map +0 -1
  611. package/.next/server/chunks/[root-of-the-server]__0kltbce._.js +0 -3
  612. package/.next/server/chunks/[root-of-the-server]__0kltbce._.js.map +0 -1
  613. package/.next/server/chunks/[root-of-the-server]__0mn04fw._.js +0 -3
  614. package/.next/server/chunks/[root-of-the-server]__0mn04fw._.js.map +0 -1
  615. package/.next/server/chunks/[root-of-the-server]__0o29v8c._.js +0 -3
  616. package/.next/server/chunks/[root-of-the-server]__0o4qfn9._.js +0 -3
  617. package/.next/server/chunks/[root-of-the-server]__0o7ya7r._.js +0 -3
  618. package/.next/server/chunks/[root-of-the-server]__0o7ya7r._.js.map +0 -1
  619. package/.next/server/chunks/[root-of-the-server]__0p3fo6l._.js +0 -6
  620. package/.next/server/chunks/[root-of-the-server]__0p3fo6l._.js.map +0 -1
  621. package/.next/server/chunks/[root-of-the-server]__0p6tutt._.js +0 -3
  622. package/.next/server/chunks/[root-of-the-server]__0pn3wsx._.js +0 -3
  623. package/.next/server/chunks/[root-of-the-server]__0pn3wsx._.js.map +0 -1
  624. package/.next/server/chunks/[root-of-the-server]__0rg9ifx._.js +0 -3
  625. package/.next/server/chunks/[root-of-the-server]__0x.-194._.js +0 -3
  626. package/.next/server/chunks/[root-of-the-server]__0x.-194._.js.map +0 -1
  627. package/.next/server/chunks/[root-of-the-server]__0x.a_yh._.js +0 -3
  628. package/.next/server/chunks/[root-of-the-server]__0x.a_yh._.js.map +0 -1
  629. package/.next/server/chunks/[root-of-the-server]__0~h2uqi._.js +0 -3
  630. package/.next/server/chunks/[root-of-the-server]__139~d6a._.js +0 -3
  631. package/.next/server/chunks/[root-of-the-server]__139~d6a._.js.map +0 -1
  632. package/.next/server/chunks/_next-internal_server_app_api_anet_config_route_actions_0xzy73j.js +0 -3
  633. package/.next/server/chunks/_next-internal_server_app_api_auth_login_route_actions_0zukc38.js +0 -3
  634. package/.next/server/chunks/_next-internal_server_app_api_auth_logout_route_actions_0fm~3ij.js +0 -3
  635. package/.next/server/chunks/_next-internal_server_app_api_auth_v3_route_actions_0ndzs5l.js +0 -3
  636. package/.next/server/chunks/_next-internal_server_app_api_hub_audit-log_route_actions_11k8fqt.js +0 -3
  637. package/.next/server/chunks/_next-internal_server_app_api_hub_auth_route_actions_0gs7ttk.js +0 -3
  638. package/.next/server/chunks/_next-internal_server_app_api_hub_broadcast_route_actions_0ads-kd.js +0 -3
  639. package/.next/server/chunks/_next-internal_server_app_api_hub_events_route_actions_0opktby.js +0 -3
  640. package/.next/server/chunks/_next-internal_server_app_api_hub_health_route_actions_0tzzt~x.js +0 -3
  641. package/.next/server/chunks/_next-internal_server_app_api_hub_inbox_route_actions_0xvslrg.js +0 -3
  642. package/.next/server/chunks/_next-internal_server_app_api_hub_license_route_actions_0i3tlr3.js +0 -3
  643. package/.next/server/chunks/_next-internal_server_app_api_hub_messages_route_actions_08-gi-s.js +0 -3
  644. package/.next/server/chunks/_next-internal_server_app_api_hub_networks_route_actions_02vejq2.js +0 -3
  645. package/.next/server/chunks/_next-internal_server_app_api_hub_nodes_route_actions_0nx8ryn.js +0 -3
  646. package/.next/server/chunks/_next-internal_server_app_api_hub_register_route_actions_086wi_3.js +0 -3
  647. package/.next/server/chunks/_next-internal_server_app_api_hub_send_route_actions_0-ga_ug.js +0 -3
  648. package/.next/server/chunks/_next-internal_server_app_api_hub_server-logs_route_actions_00euexv.js +0 -3
  649. package/.next/server/chunks/_next-internal_server_app_api_hub_session_route_actions_041wa62.js +0 -3
  650. package/.next/server/chunks/_next-internal_server_app_api_hub_stats_route_actions_1242~od.js +0 -3
  651. package/.next/server/chunks/_next-internal_server_app_api_hub_status_route_actions_0aml09a.js +0 -3
  652. package/.next/server/chunks/_next-internal_server_app_api_hub_task-events_route_actions_02v1tmm.js +0 -3
  653. package/.next/server/chunks/_next-internal_server_app_api_hub_tasks_route_actions_0nl77v4.js +0 -3
  654. package/.next/server/chunks/_next-internal_server_app_api_hub_tmux_route_actions_0u~aaza.js +0 -3
  655. package/.next/server/chunks/_next-internal_server_app_api_hub_tokens_route_actions_09hzf_1.js +0 -3
  656. package/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_095lj93.js +0 -3
  657. package/.next/server/chunks/node_modules_next_11synfn._.js +0 -10
  658. package/.next/server/chunks/node_modules_next_11synfn._.js.map +0 -1
  659. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0yubv2d.js +0 -3
  660. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0yubv2d.js.map +0 -1
  661. package/.next/server/chunks/ssr/[root-of-the-server]__014uja_._.js +0 -3
  662. package/.next/server/chunks/ssr/[root-of-the-server]__014uja_._.js.map +0 -1
  663. package/.next/server/chunks/ssr/[root-of-the-server]__02hi65f._.js +0 -3
  664. package/.next/server/chunks/ssr/[root-of-the-server]__02hi65f._.js.map +0 -1
  665. package/.next/server/chunks/ssr/[root-of-the-server]__02xqa5z._.js +0 -3
  666. package/.next/server/chunks/ssr/[root-of-the-server]__02xqa5z._.js.map +0 -1
  667. package/.next/server/chunks/ssr/[root-of-the-server]__04ph1de._.js +0 -3
  668. package/.next/server/chunks/ssr/[root-of-the-server]__04ph1de._.js.map +0 -1
  669. package/.next/server/chunks/ssr/[root-of-the-server]__05wc5-z._.js +0 -3
  670. package/.next/server/chunks/ssr/[root-of-the-server]__05wc5-z._.js.map +0 -1
  671. package/.next/server/chunks/ssr/[root-of-the-server]__06-2p1a._.js +0 -33
  672. package/.next/server/chunks/ssr/[root-of-the-server]__06-2p1a._.js.map +0 -1
  673. package/.next/server/chunks/ssr/[root-of-the-server]__08qr2ji._.js.map +0 -1
  674. package/.next/server/chunks/ssr/[root-of-the-server]__09l8xaa._.js +0 -3
  675. package/.next/server/chunks/ssr/[root-of-the-server]__09l8xaa._.js.map +0 -1
  676. package/.next/server/chunks/ssr/[root-of-the-server]__09z7o2x._.js +0 -19
  677. package/.next/server/chunks/ssr/[root-of-the-server]__09z7o2x._.js.map +0 -1
  678. package/.next/server/chunks/ssr/[root-of-the-server]__0a0ffmv._.js +0 -3
  679. package/.next/server/chunks/ssr/[root-of-the-server]__0a0ffmv._.js.map +0 -1
  680. package/.next/server/chunks/ssr/[root-of-the-server]__0b7uu-_._.js +0 -3
  681. package/.next/server/chunks/ssr/[root-of-the-server]__0b7uu-_._.js.map +0 -1
  682. package/.next/server/chunks/ssr/[root-of-the-server]__0bl_wqk._.js +0 -3
  683. package/.next/server/chunks/ssr/[root-of-the-server]__0bl_wqk._.js.map +0 -1
  684. package/.next/server/chunks/ssr/[root-of-the-server]__0ft0rpu._.js +0 -3
  685. package/.next/server/chunks/ssr/[root-of-the-server]__0ft0rpu._.js.map +0 -1
  686. package/.next/server/chunks/ssr/[root-of-the-server]__0i~g-2t._.js +0 -3
  687. package/.next/server/chunks/ssr/[root-of-the-server]__0i~g-2t._.js.map +0 -1
  688. package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js +0 -3
  689. package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js.map +0 -1
  690. package/.next/server/chunks/ssr/[root-of-the-server]__0mook08._.js +0 -3
  691. package/.next/server/chunks/ssr/[root-of-the-server]__0mook08._.js.map +0 -1
  692. package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js +0 -3
  693. package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js.map +0 -1
  694. package/.next/server/chunks/ssr/[root-of-the-server]__0qchqb~._.js +0 -3
  695. package/.next/server/chunks/ssr/[root-of-the-server]__0qchqb~._.js.map +0 -1
  696. package/.next/server/chunks/ssr/[root-of-the-server]__0rrm~-q._.js +0 -3
  697. package/.next/server/chunks/ssr/[root-of-the-server]__0rrm~-q._.js.map +0 -1
  698. package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js +0 -3
  699. package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js.map +0 -1
  700. package/.next/server/chunks/ssr/[root-of-the-server]__0xjish~._.js +0 -3
  701. package/.next/server/chunks/ssr/[root-of-the-server]__0xjish~._.js.map +0 -1
  702. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +0 -3
  703. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +0 -1
  704. package/.next/server/chunks/ssr/[root-of-the-server]__0ywihy_._.js +0 -8
  705. package/.next/server/chunks/ssr/[root-of-the-server]__0ywihy_._.js.map +0 -1
  706. package/.next/server/chunks/ssr/[root-of-the-server]__12ad1n.._.js +0 -3
  707. package/.next/server/chunks/ssr/[root-of-the-server]__12ad1n.._.js.map +0 -1
  708. package/.next/server/chunks/ssr/_0nne7mz._.js +0 -3
  709. package/.next/server/chunks/ssr/_0nne7mz._.js.map +0 -1
  710. package/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_0k77kol.js +0 -3
  711. package/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_0eq97pa.js +0 -3
  712. package/.next/server/chunks/ssr/_next-internal_server_app_admin_page_actions_0ycnu8f.js +0 -3
  713. package/.next/server/chunks/ssr/_next-internal_server_app_login_page_actions_02kefem.js +0 -3
  714. package/.next/server/chunks/ssr/_next-internal_server_app_logs_page_actions_0rvsl~d.js +0 -3
  715. package/.next/server/chunks/ssr/_next-internal_server_app_messages_page_actions_0gsqep_.js +0 -3
  716. package/.next/server/chunks/ssr/_next-internal_server_app_node_page_actions_0wd8cgl.js +0 -3
  717. package/.next/server/chunks/ssr/_next-internal_server_app_nodes_page_actions_0-.rv67.js +0 -3
  718. package/.next/server/chunks/ssr/_next-internal_server_app_page_actions_09-gtaw.js +0 -3
  719. package/.next/server/chunks/ssr/_next-internal_server_app_server-logs_page_actions_0g1gb5c.js +0 -3
  720. package/.next/server/chunks/ssr/_next-internal_server_app_settings_networks_page_actions_09uw.sw.js +0 -3
  721. package/.next/server/chunks/ssr/_next-internal_server_app_settings_page_actions_0mr68ai.js +0 -3
  722. package/.next/server/chunks/ssr/_next-internal_server_app_settings_tokens_page_actions_0yi54x8.js +0 -3
  723. package/.next/server/chunks/ssr/_next-internal_server_app_tasks_[id]_page_actions_0k_hm9g.js +0 -3
  724. package/.next/server/chunks/ssr/_next-internal_server_app_tasks_page_actions_0_2o_cr.js +0 -3
  725. package/.next/server/chunks/ssr/app_0.zbtn5._.js.map +0 -1
  726. package/.next/server/chunks/ssr/app_01y-~mp._.js +0 -3
  727. package/.next/server/chunks/ssr/app_01y-~mp._.js.map +0 -1
  728. package/.next/server/chunks/ssr/app_04n78pr._.js.map +0 -1
  729. package/.next/server/chunks/ssr/app_0_agk0r._.js.map +0 -1
  730. package/.next/server/chunks/ssr/app_0aenxef._.js +0 -3
  731. package/.next/server/chunks/ssr/app_0aenxef._.js.map +0 -1
  732. package/.next/server/chunks/ssr/app_0aum_g5._.js +0 -3
  733. package/.next/server/chunks/ssr/app_0aum_g5._.js.map +0 -1
  734. package/.next/server/chunks/ssr/app_0emvkn2._.js +0 -3
  735. package/.next/server/chunks/ssr/app_0emvkn2._.js.map +0 -1
  736. package/.next/server/chunks/ssr/app_0esuljp._.js +0 -3
  737. package/.next/server/chunks/ssr/app_0esuljp._.js.map +0 -1
  738. package/.next/server/chunks/ssr/app_0izi28j._.js.map +0 -1
  739. package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +0 -1
  740. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js.map +0 -1
  741. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +0 -3
  742. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +0 -1
  743. package/.next/server/chunks/ssr/node_modules_09w7yel._.js.map +0 -1
  744. package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js +0 -3
  745. package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js.map +0 -1
  746. package/.next/server/chunks/ssr/node_modules_0r_.s40._.js.map +0 -1
  747. package/.next/server/chunks/ssr/node_modules_0vtdjw7._.js +0 -3
  748. package/.next/server/chunks/ssr/node_modules_0vtdjw7._.js.map +0 -1
  749. package/.next/server/chunks/ssr/node_modules_next_0w2y7fz._.js +0 -6
  750. package/.next/server/chunks/ssr/node_modules_next_0w2y7fz._.js.map +0 -1
  751. package/.next/server/chunks/ssr/node_modules_next_dist_0e1izl_._.js +0 -6
  752. package/.next/server/chunks/ssr/node_modules_next_dist_0e1izl_._.js.map +0 -1
  753. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_0inhx6q._.js +0 -3
  754. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_0inhx6q._.js.map +0 -1
  755. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_0ghu-f7.js +0 -3
  756. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_0ghu-f7.js.map +0 -1
  757. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0lgvd_..js +0 -3
  758. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0lgvd_..js.map +0 -1
  759. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_unauthorized_0cjv-23.js +0 -3
  760. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_unauthorized_0cjv-23.js.map +0 -1
  761. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_002l7yi.js.map +0 -1
  762. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_02suzhc.js.map +0 -1
  763. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_07uu-xq.js.map +0 -1
  764. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0d2yq5t.js.map +0 -1
  765. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0dm~kj8.js.map +0 -1
  766. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0foejvu.js.map +0 -1
  767. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0l-fh0i.js.map +0 -1
  768. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0nsg22r.js.map +0 -1
  769. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0o9vt1n.js.map +0 -1
  770. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0qm1-a1.js.map +0 -1
  771. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0uccf9z.js.map +0 -1
  772. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0uv6jyc.js.map +0 -1
  773. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_10~~o-h.js +0 -4
  774. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_10~~o-h.js.map +0 -1
  775. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_13leyhk.js +0 -4
  776. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_13leyhk.js.map +0 -1
  777. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_13s07li.js.map +0 -1
  778. package/.next/static/chunks/0-hlcuoemhe7w.js +0 -7
  779. package/.next/static/chunks/0257pdz1-imal.js +0 -2
  780. package/.next/static/chunks/02_zpo7taex9o.js +0 -7
  781. package/.next/static/chunks/06n3d~fbbtagk.js +0 -1
  782. package/.next/static/chunks/07eba1lz2u3p4.js +0 -1
  783. package/.next/static/chunks/09n9xs.6dvvo2.css +0 -2
  784. package/.next/static/chunks/0av2~0.96~d~g.js +0 -1
  785. package/.next/static/chunks/0c1bp~g7.8ru..js +0 -1
  786. package/.next/static/chunks/0e-.zgw81gela.js +0 -31
  787. package/.next/static/chunks/0exo44x.p_gq7.js +0 -1
  788. package/.next/static/chunks/0g9aof0ob_fcj.js +0 -1
  789. package/.next/static/chunks/0s8jo6jypzkx5.js +0 -1
  790. package/.next/static/chunks/0u0_jo83jrziw.js +0 -1
  791. package/.next/static/chunks/0u359llqm1_01.js +0 -1
  792. package/.next/static/chunks/0ubqqhhpsaez4.js +0 -1
  793. package/.next/static/chunks/0~qw9kxksr1v2.js +0 -1
  794. package/.next/static/chunks/12_8n4k-hg72i.js +0 -1
  795. package/.next/static/chunks/12gprw3l0kvvf.js +0 -1
  796. package/.next/static/chunks/12yuo353giomx.js +0 -1
  797. package/.next/static/chunks/14bsg3_-taiw4.js +0 -1
  798. /package/.next/build/chunks/{[root-of-the-server]__0ubbtyl._.js.map → [root-of-the-server]__0tgc-8p._.js.map} +0 -0
  799. /package/.next/build/chunks/{[turbopack-node]_transforms_postcss_ts_06e.r3r._.js.map → [turbopack-node]_transforms_postcss_ts_0r4x5ys._.js.map} +0 -0
  800. /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
  801. /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
  802. /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
  803. /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
  804. /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
  805. /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
  806. /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
  807. /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
  808. /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
  809. /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
  810. /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
  811. /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
  812. /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
  813. /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
  814. /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
  815. /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
  816. /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
  817. /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
  818. /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
  819. /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
  820. /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
  821. /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
  822. /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
  823. /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
  824. /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
  825. /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
  826. /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
  827. /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
  828. /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
  829. /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
  830. /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
  831. /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
  832. /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
  833. /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
  834. /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
  835. /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
  836. /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
  837. /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
  838. /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
  839. /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
  840. /package/.next/static/{FXhbXa0x4YZ0yjsgUY54q → 2_4eST7GF9IwT6UkQziya}/_buildManifest.js +0 -0
  841. /package/.next/static/{FXhbXa0x4YZ0yjsgUY54q → 2_4eST7GF9IwT6UkQziya}/_clientMiddlewareManifest.js +0 -0
  842. /package/.next/static/{FXhbXa0x4YZ0yjsgUY54q → 2_4eST7GF9IwT6UkQziya}/_ssgManifest.js +0 -0
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { Session } from './types';
5
5
  import { aliasAvatarColors, aliasInitial } from './AliasAvatar';
6
6
  import { ChatPopover } from './ChatPopover';
7
7
  import { vendorForModel, runtimeIdentity, identityLine } from '../lib/vendorIdentity';
8
+ import { parseHubTime, relativeAgo } from '../lib/time';
8
9
 
9
10
  interface MessageFlow {
10
11
  from_alias: string;
@@ -32,6 +33,9 @@ interface FlowLink {
32
33
  to: string;
33
34
  count: number;
34
35
  content: string;
36
+ /** ISO timestamp of the most recent message on this edge — drives
37
+ * the Round 10 freshness fade so dormant links recede visually. */
38
+ last_at: string;
35
39
  }
36
40
 
37
41
  const cx = 500;
@@ -85,36 +89,76 @@ function truncate(value: string, max: number) {
85
89
  return value.length > max ? `${value.slice(0, max - 1)}...` : value;
86
90
  }
87
91
 
92
+ /** Round 36 / Loop: prefers-reduced-motion hook.
93
+ *
94
+ * Round 29's a11y sweep zeroed CSS animations via media query, but SVG
95
+ * SMIL `<animate>` / `<animateMotion>` elements aren't reachable from CSS
96
+ * — they need JS to opt out. This hook reads the media query and listens
97
+ * for changes so the topology's flow particles, pulses, click ripple
98
+ * and hub breath honour the OS-level preference. */
99
+ function useReducedMotion(): boolean {
100
+ const [reduced, setReduced] = useState(false);
101
+ useEffect(() => {
102
+ if (typeof window === 'undefined' || !window.matchMedia) return;
103
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
104
+ setReduced(mq.matches);
105
+ const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);
106
+ mq.addEventListener('change', onChange);
107
+ return () => mq.removeEventListener('change', onChange);
108
+ }, []);
109
+ return reduced;
110
+ }
111
+
112
+ // Round 38 / Loop: parseHubTime + relativeAgo factored to app/lib/time.ts
113
+ // — the Round 35 fix lives there now alongside the Round 37 mirror so
114
+ // any future TZ-safe parse update happens in one place.
115
+
116
+ /** Round 12 / Loop: status trio audit.
117
+ *
118
+ * Each (status × theme) cell returns a {primary, halo, text} trio. The trio
119
+ * invariant — keep it when adding states or tweaking shades:
120
+ *
121
+ * light: primary = <hue>-600 halo = <hue>-100 * text = <hue>-800/900
122
+ * dark: primary = <hue>-400/500 halo = <hue>-900 text = <hue>-100
123
+ *
124
+ * * offline.halo light deviates intentionally to slate-200 (#e2e8f0):
125
+ * slate-100 (#f1f5f9) is too close to the panel bg (#f8fafc) and the
126
+ * halo would vanish. The other three rows keep the 100-shade.
127
+ *
128
+ * Audit caught one cross-family drift before this round: online-other halo
129
+ * light was #dbeafe (blue-100) while its primary (#0284c7 sky-600) and text
130
+ * (#0c4a6e sky-900) were sky-family — now sky-100 (#e0f2fe). Tiny visual
131
+ * difference; large hygiene win — every trio is now mono-hue. */
88
132
  function nodeStatus(session: Session, isOnline: boolean, isLight: boolean) {
89
133
  if (!isOnline) {
90
134
  return {
91
135
  label: 'offline',
92
- primary: isLight ? '#94a3b8' : '#6b7280',
93
- halo: isLight ? '#e2e8f0' : '#111827',
94
- text: isLight ? '#475569' : '#9ca3af',
136
+ primary: isLight ? '#94a3b8' : '#6b7280', // slate-400 / gray-500
137
+ halo: isLight ? '#e2e8f0' : '#111827', // slate-200* / gray-900
138
+ text: isLight ? '#475569' : '#9ca3af', // slate-600 / gray-400
95
139
  };
96
140
  }
97
141
  if (session.status === 'working') {
98
142
  return {
99
143
  label: 'working',
100
- primary: isLight ? '#059669' : '#22c55e',
101
- halo: isLight ? '#d1fae5' : '#14532d',
102
- text: isLight ? '#065f46' : '#dcfce7',
144
+ primary: isLight ? '#059669' : '#22c55e', // emerald-600 / green-500
145
+ halo: isLight ? '#d1fae5' : '#14532d', // emerald-100 / green-900
146
+ text: isLight ? '#065f46' : '#dcfce7', // emerald-800 / green-100
103
147
  };
104
148
  }
105
149
  if (session.status === 'idle') {
106
150
  return {
107
151
  label: 'idle',
108
- primary: isLight ? '#0d9488' : '#2dd4bf',
109
- halo: isLight ? '#ccfbf1' : '#134e4a',
110
- text: isLight ? '#115e59' : '#ccfbf1',
152
+ primary: isLight ? '#0d9488' : '#2dd4bf', // teal-600 / teal-400
153
+ halo: isLight ? '#ccfbf1' : '#134e4a', // teal-100 / teal-900
154
+ text: isLight ? '#115e59' : '#ccfbf1', // teal-800 / teal-100
111
155
  };
112
156
  }
113
157
  return {
114
158
  label: session.status || 'online',
115
- primary: isLight ? '#0284c7' : '#38bdf8',
116
- halo: isLight ? '#dbeafe' : '#0c4a6e',
117
- text: isLight ? '#0c4a6e' : '#e0f2fe',
159
+ primary: isLight ? '#0284c7' : '#38bdf8', // sky-600 / sky-400
160
+ halo: isLight ? '#e0f2fe' : '#0c4a6e', // sky-100 / sky-900 (was blue-100 — drift fixed Round 12)
161
+ text: isLight ? '#0c4a6e' : '#e0f2fe', // sky-900 / sky-100
118
162
  };
119
163
  }
120
164
 
@@ -240,22 +284,59 @@ function commonPrefix(a: string, b: string): string {
240
284
  return a.slice(0, i);
241
285
  }
242
286
 
243
- function computeGroupKeys(aliases: string[]): Record<string, string> {
244
- const sorted = [...aliases].sort((a, b) => a.localeCompare(b));
287
+ /** #83 + #111: group nodes that share a ≥2-char alias prefix OR a project_dir
288
+ * ("either criterion → same group", Vincent 4724). Union-find over the
289
+ * sessions; the component label prefers the shared project_dir's basename,
290
+ * else the common alias prefix. Returns alias → groupKey. */
291
+ function computeGroups(sessions: { alias: string; project_dir?: string | null }[]): Record<string, string> {
292
+ const n = sessions.length;
293
+ const parent = sessions.map((_, i) => i);
294
+ const find = (i: number): number => {
295
+ while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; }
296
+ return i;
297
+ };
298
+ const union = (a: number, b: number) => {
299
+ const ra = find(a), rb = find(b);
300
+ if (ra !== rb) parent[ra] = rb;
301
+ };
302
+
303
+ // union by shared project_dir
304
+ const byDir: Record<string, number[]> = {};
305
+ sessions.forEach((s, i) => {
306
+ const d = s.project_dir?.trim();
307
+ if (d) (byDir[d] ||= []).push(i);
308
+ });
309
+ for (const idxs of Object.values(byDir)) {
310
+ for (let k = 1; k < idxs.length; k++) union(idxs[0], idxs[k]);
311
+ }
312
+
313
+ // union by shared ≥2-char alias prefix — sort, link adjacent pairs
314
+ const order = sessions.map((_, i) => i).sort((a, b) => sessions[a].alias.localeCompare(sessions[b].alias));
315
+ for (let k = 0; k + 1 < order.length; k++) {
316
+ if (commonPrefix(sessions[order[k]].alias, sessions[order[k + 1]].alias).length >= 2) {
317
+ union(order[k], order[k + 1]);
318
+ }
319
+ }
320
+
321
+ // label each connected component
322
+ const comps: Record<number, number[]> = {};
323
+ for (let i = 0; i < n; i++) (comps[find(i)] ||= []).push(i);
245
324
  const keys: Record<string, string> = {};
246
- let i = 0;
247
- while (i < sorted.length) {
248
- let j = i;
249
- let prefix = sorted[i];
250
- while (j + 1 < sorted.length) {
251
- const lcp = commonPrefix(prefix, sorted[j + 1]);
252
- if (lcp.length < 2) break;
253
- prefix = lcp;
254
- j++;
325
+ for (const members of Object.values(comps)) {
326
+ let label: string;
327
+ if (members.length === 1) {
328
+ label = sessions[members[0]].alias;
329
+ } else {
330
+ const dirs = new Set(members.map(i => sessions[i].project_dir?.trim()).filter(Boolean) as string[]);
331
+ if (dirs.size === 1) {
332
+ const d = [...dirs][0];
333
+ label = d.split('/').filter(Boolean).pop() || d;
334
+ } else {
335
+ label = members.map(i => sessions[i].alias).reduce((a, b) => commonPrefix(a, b));
336
+ if (label.length < 2) label = sessions[members[0]].alias;
337
+ }
255
338
  }
256
- const key = j > i ? prefix : sorted[i];
257
- for (let k = i; k <= j; k++) keys[sorted[k]] = key;
258
- i = j + 1;
339
+ for (const i of members) keys[sessions[i].alias] = label;
259
340
  }
260
341
  return keys;
261
342
  }
@@ -275,12 +356,20 @@ function buildFlowLinks(messages: MessageFlow[], positions: Record<string, Point
275
356
  const key = `${message.from_alias}->${message.to_alias}`;
276
357
  const current = links.get(key);
277
358
 
359
+ // Keep the most-recent timestamp per pair so the render can fade
360
+ // dormant edges (Round 10 freshness fade).
361
+ const incoming = message.created_at || '';
362
+ const last_at = !current
363
+ ? incoming
364
+ : (incoming > current.last_at ? incoming : current.last_at);
365
+
278
366
  links.set(key, {
279
367
  key,
280
368
  from: message.from_alias,
281
369
  to: message.to_alias,
282
370
  count: (current?.count || 0) + 1,
283
371
  content: current?.content || message.content,
372
+ last_at,
284
373
  });
285
374
  });
286
375
 
@@ -290,6 +379,7 @@ function buildFlowLinks(messages: MessageFlow[], positions: Record<string, Point
290
379
  export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProps) {
291
380
  const theme = useTheme();
292
381
  const isLight = theme === 'light';
382
+ const reducedMotion = useReducedMotion();
293
383
  const pal = isLight ? LIGHT_PALETTE : DARK_PALETTE;
294
384
  const brand = useBrand();
295
385
  const isIntern = brand === 'intern';
@@ -310,6 +400,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
310
400
  try { localStorage.setItem('anet-topo-layout', next); } catch {}
311
401
  return next;
312
402
  });
403
+ // Issue #113: node size scale (Vincent 4727 — nodes too big / crowded at
404
+ // ~22 nodes). S/M/L → 0.7/0.84/1.0; default M (one notch down from the old
405
+ // fixed size). Persisted like the layout toggle.
406
+ const [nodeScale, setNodeScale] = useState(0.84);
407
+ useEffect(() => {
408
+ try {
409
+ const saved = parseFloat(localStorage.getItem('anet-topo-nodescale') || '');
410
+ if (saved === 0.7 || saved === 0.84 || saved === 1) setNodeScale(saved);
411
+ } catch {}
412
+ }, []);
413
+ const pickNodeScale = (v: number) => {
414
+ setNodeScale(v);
415
+ try { localStorage.setItem('anet-topo-nodescale', String(v)); } catch {}
416
+ };
313
417
 
314
418
  useEffect(() => {
315
419
  const fetchMessages = async () => {
@@ -336,6 +440,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
336
440
  flowLinks,
337
441
  activeAliases,
338
442
  groupKeys,
443
+ groupBoxes,
444
+ gridContentBottom,
339
445
  } = useMemo(() => {
340
446
  const sseCount = (s: { alias: string; network_id?: string }) =>
341
447
  (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];
@@ -345,36 +451,151 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
345
451
  // index, so contiguous-in-array becomes contiguous-in-ring, i.e.
346
452
  // each team visually clusters. localeCompare keeps CJK ordering sane.
347
453
  const byAlias = (a: Session, b: Session) => a.alias.localeCompare(b.alias);
454
+ // #112 umbrella: ghost age-out — an offline node not seen recently is
455
+ // almost certainly a deleted node the server `/api/status` still
456
+ // returns (#74 root cause is server-side; this is the dashboard-side
457
+ // fallback so stale ghosts stop cluttering the topology). A missing
458
+ // last_seen_at is kept (conservative — could be a legitimately new node).
459
+ // Round 27 / P0: the 24h threshold was too lenient — Vincent's preview.29
460
+ // screenshot showed B站马 nodes deleted ~4 h earlier still visible. A
461
+ // healthy agent heartbeats every few seconds; if it's been silent for
462
+ // an hour it's effectively gone. 1 h gives a fresh disconnect time to
463
+ // come back while removing dead nodes well within an operator session.
464
+ const GHOST_MS = 60 * 60 * 1000;
465
+ const now = Date.now();
466
+ const isGhost = (s: Session) => {
467
+ if (s.status !== 'offline' || sseCount(s) || !s.last_seen_at) return false;
468
+ // Round 35 / Loop: parseHubTime normalises SQL-style timestamps to UTC
469
+ // before parsing so non-UTC browsers don't see a phantom 8-hour skew
470
+ // and ghost freshly-disconnected nodes.
471
+ const t = parseHubTime(s.last_seen_at);
472
+ return t !== null && now - t > GHOST_MS;
473
+ };
348
474
  const online = sessions.filter(s => s.status !== 'offline' || sseCount(s)).sort(byAlias);
349
- const offline = sessions.filter(s => s.status === 'offline' && !sseCount(s)).sort(byAlias);
475
+ const offline = sessions.filter(s => s.status === 'offline' && !sseCount(s) && !isGhost(s)).sort(byAlias);
350
476
  const positions: Record<string, Point> = {};
351
477
 
352
478
  if (layout === 'grid') {
353
- // Issue #87: N×M grid layout. All nodes share one sorted array (online
354
- // first, then offline) so same-prefix aliases land in adjacent cells
355
- // (#83 prefix-grouping synergy). cols = ⌈√N⌉; the last partial row is
356
- // centred. Grid spans an inset box so node labels don't clip the edge.
479
+ // Issue #87 + #111: group-banded grid. Nodes are sorted by alias so
480
+ // same-prefix aliases are adjacent; each multi-member prefix group then
481
+ // gets its OWN row(s) starting at column 0, while singletons pack into
482
+ // shared rows. Group-banded placement keeps every group's bounding box
483
+ // (#111) a clean rectangle — no box ever overlaps another group's.
357
484
  const all = [...online, ...offline];
358
- const n = all.length;
359
- const cols = Math.max(1, Math.ceil(Math.sqrt(n)));
360
- const rows = Math.max(1, Math.ceil(n / cols));
361
- const gx0 = 150, gx1 = 850, gy0 = 120, gy1 = 560;
485
+ const groupKeys = computeGroups(all);
486
+ const cols = Math.max(1, Math.ceil(Math.sqrt(all.length)));
487
+ // #112: gy0 starts below the (now compact) recent-signal / legend
488
+ // overlay panels — their max bottom edge is y≈112 — so grid row 0 is
489
+ // never occluded; gy1 extends near the canvas bottom for breathing room.
490
+ const gx0 = 150, gx1 = 850, gy0 = 126, gy1 = 652;
362
491
  const cellW = (gx1 - gx0) / cols;
363
- const cellH = (gy1 - gy0) / rows;
364
- all.forEach((s, i) => {
365
- const row = Math.floor(i / cols);
366
- const col = i % cols;
367
- const rowCount = row === rows - 1 ? n - row * cols : cols;
368
- const rowInset = ((cols - rowCount) * cellW) / 2;
369
- positions[s.alias] = {
370
- x: gx0 + rowInset + (col + 0.5) * cellW,
371
- y: gy0 + (row + 0.5) * cellH,
372
- };
373
- });
492
+ // largest node radius at the current size scale — drives the group
493
+ // label band + the row-height floor so nothing ever overlaps.
494
+ const nodeR = Math.round(26 * nodeScale);
495
+
496
+ // ordered runs of consecutive same-group-key nodes (≥2 = real group)
497
+ const runs: { key: string; members: Session[] }[] = [];
498
+ for (const s of all) {
499
+ const gk = groupKeys[s.alias];
500
+ const last = runs[runs.length - 1];
501
+ if (last && last.key === gk) last.members.push(s);
502
+ else runs.push({ key: gk, members: [s] });
503
+ }
504
+
505
+ // Pass 1 — assign each run to a band: a multi-member group owns its
506
+ // rows (left-aligned, so its bounding box is a tidy rect); contiguous
507
+ // singletons pack into shared rows (centred). Collect total row count.
508
+ type Band = { members: Session[]; startRow: number; centred: boolean; isGroup: boolean };
509
+ const bands: Band[] = [];
510
+ let row = 0;
511
+ let i = 0;
512
+ while (i < runs.length) {
513
+ if (runs[i].members.length >= 2) {
514
+ bands.push({ members: runs[i].members, startRow: row, centred: false, isGroup: true });
515
+ row += Math.ceil(runs[i].members.length / cols);
516
+ i++;
517
+ } else {
518
+ const singles: Session[] = [];
519
+ while (i < runs.length && runs[i].members.length < 2) {
520
+ singles.push(runs[i].members[0]);
521
+ i++;
522
+ }
523
+ bands.push({ members: singles, startRow: row, centred: true, isGroup: false });
524
+ row += Math.ceil(singles.length / cols);
525
+ }
526
+ }
527
+ const totalRows = Math.max(1, row);
528
+ // #112: the group label sits in a band ABOVE the topmost node, so the
529
+ // band must clear the node radius — GROUP_TOP is node-relative, never
530
+ // cellH-derived (cellH-derived was the label↔node overlap bug Vincent
531
+ // hit). cellH then floors at GROUP_TOP + 30 so the label band + bottom
532
+ // padding always fit and stacked group boxes never touch; past the
533
+ // floor the grid overflows and zoom/pan handles it.
534
+ const GROUP_TOP = nodeR + 20;
535
+ // Round 27 / P0 (Vincent screenshot preview.29): dense plain-text
536
+ // node labels (`pos.y + radius + denseDrop`, with a 3 px containerBg
537
+ // stroke halo for readability) at the BOTTOM row of band N would
538
+ // paint OVER the start of band N+1's group label, creating the
539
+ // "blueleap → eleap", "agent-network-dashboard → t-network / board"
540
+ // visual chopping. Geometry of the collision:
541
+ // dense label visual bottom = node_N.y + radius + denseDrop + halo
542
+ // group label glyph top = boxY + 4 (text y=boxY+14, ~10px ascent)
543
+ // = node_N+1.y - GROUP_TOP + 4
544
+ // node_N+1.y - node_N.y = cellH for consecutive rows, so no-collide:
545
+ // cellH ≥ radius + denseDrop + halo + GROUP_TOP - 4 + buffer
546
+ // With halo=3 buffer=4: cellH ≥ nodeR + denseDrop + GROUP_TOP + 3.
547
+ const denseDrop = nodeScale < 0.8 ? 12 : 14;
548
+ const cellH = Math.max(
549
+ 2 * nodeR + 22, // node + dense label within band
550
+ GROUP_TOP + 12, // group-label band + box padding
551
+ nodeR + denseDrop + GROUP_TOP + 3, // round-27 dense↔group label clearance
552
+ Math.min(100, (gy1 - gy0) / totalRows),
553
+ );
554
+
555
+ // Pass 2 — place each band's members.
556
+ for (const band of bands) {
557
+ band.members.forEach((s, idx) => {
558
+ const rowInBand = Math.floor(idx / cols);
559
+ const c = idx % cols;
560
+ const inRow = Math.min(cols, band.members.length - rowInBand * cols);
561
+ const inset = band.centred ? ((cols - inRow) * cellW) / 2 : 0;
562
+ positions[s.alias] = {
563
+ x: gx0 + inset + (c + 0.5) * cellW,
564
+ y: gy0 + (band.startRow + rowInBand + 0.5) * cellH,
565
+ };
566
+ });
567
+ }
568
+
374
569
  const links = buildFlowLinks(messages, positions);
375
570
  const active = new Set<string>();
376
571
  links.forEach(link => { active.add(link.from); active.add(link.to); });
377
- const groupKeys = computeGroupKeys([...online, ...offline].map(s => s.alias));
572
+ // #111: one bounding box per multi-member group (Vincent 4722). Each
573
+ // group owns its rows; GROUP_PAD fills the row space left below the
574
+ // nodes after the label band, so stacked group boxes always have a
575
+ // gap between them (GROUP_TOP is defined above, with cellH).
576
+ const GROUP_PAD = Math.max(8, Math.min(26, cellH - GROUP_TOP - 8)); // side/bottom
577
+ const groupBoxes = bands
578
+ .filter(b => b.isGroup)
579
+ .map(band => {
580
+ const pts = band.members.map(s => positions[s.alias]).filter(Boolean);
581
+ const xs = pts.map(p => p.x);
582
+ const ys = pts.map(p => p.y);
583
+ const minX = Math.min(...xs), minY = Math.min(...ys);
584
+ return {
585
+ key: band.members.length ? groupKeys[band.members[0].alias] : '',
586
+ count: band.members.length,
587
+ x: minX - GROUP_PAD,
588
+ y: minY - GROUP_TOP,
589
+ w: Math.max(...xs) - minX + GROUP_PAD * 2,
590
+ h: Math.max(...ys) - minY + GROUP_TOP + GROUP_PAD,
591
+ };
592
+ });
593
+ // Round 28 / Loop: surface the grid's natural content bottom so the
594
+ // mount effect can auto-fit zoom when the layout would overflow the
595
+ // viewBox. = bottom of the last node row + its label drop + a small
596
+ // breathing buffer. Round 27's cellH bump makes this overflow more
597
+ // common (30-node fleets reach ~774 px, viewBox is 680).
598
+ const gridContentBottom = gy0 + totalRows * cellH + 8;
378
599
  return {
379
600
  onlineNodes: online,
380
601
  offlineNodes: offline,
@@ -382,6 +603,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
382
603
  flowLinks: links,
383
604
  activeAliases: active,
384
605
  groupKeys,
606
+ groupBoxes,
607
+ gridContentBottom,
385
608
  };
386
609
  }
387
610
 
@@ -452,7 +675,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
452
675
  });
453
676
 
454
677
  // Round 106 (issue #83): group key per alias → shared hue per team.
455
- const groupKeys = computeGroupKeys([...online, ...offline].map(s => s.alias));
678
+ const groupKeys = computeGroups([...online, ...offline]);
456
679
 
457
680
  return {
458
681
  onlineNodes: online,
@@ -461,15 +684,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
461
684
  flowLinks: links,
462
685
  activeAliases: active,
463
686
  groupKeys,
687
+ // #111: group boxes are a grid-layout feature only — radially scattered
688
+ // ring nodes can't be cleanly boxed. Ring keeps the #83 prefix hue.
689
+ groupBoxes: [] as { key: string; count: number; x: number; y: number; w: number; h: number }[],
690
+ // ring fits within VIEWBOX_H by construction (offlineRadius=325 + centre at y=330)
691
+ gridContentBottom: 0,
464
692
  };
465
- }, [messages, sessions, sseSessions, layout]);
693
+ }, [messages, sessions, sseSessions, layout, nodeScale]);
466
694
 
467
695
  const workingCount = onlineNodes.filter(s => s.status === 'working').length;
696
+ // Round 6 / Loop: vendor distribution for the header chip — at a glance
697
+ // "what's in the fleet" (A:5 M:2 O:8 书:12 …) without opening a node.
698
+ // Sorted by count desc; "unknown" vendors collapse into a "?" bucket.
699
+ const vendorDist = useMemo(() => {
700
+ const tally = new Map<string, { initial: string; count: number; color: string }>();
701
+ for (const s of [...onlineNodes, ...offlineNodes]) {
702
+ const v = vendorForModel(s.model);
703
+ const key = v.id === 'unknown' ? '?' : v.initial;
704
+ const cur = tally.get(key);
705
+ if (cur) cur.count++;
706
+ else tally.set(key, { initial: key, count: 1, color: v.mono.text });
707
+ }
708
+ return [...tally.values()].sort((a, b) => b.count - a.count);
709
+ }, [onlineNodes, offlineNodes]);
468
710
  // Round 109 (Vincent 4582 P0): hover-gated labels above this node count
469
711
  // so dense fleets show clean avatars instead of a wall of overlapping
470
712
  // label cards. 16 ≈ where the triple-tier rings start to crowd.
471
713
  const denseLayout = onlineNodes.length + offlineNodes.length > 16;
472
714
  const [hoveredAlias, setHoveredAlias] = useState<string | null>(null);
715
+ // Round 8 / Loop: which group is currently focused. Nodes outside this
716
+ // group + other group boxes fade so the eye locks onto the team you're
717
+ // pointing at. Singletons use their own alias as the group key.
718
+ const hoveredGroup = hoveredAlias ? (groupKeys[hoveredAlias] ?? hoveredAlias) : null;
473
719
 
474
720
  // --- Round 103 (issue #81): fullscreen + zoom + pan interaction layer ---
475
721
  // DIY native (no d3 / svg-pan-zoom): wrap the topology content in a single
@@ -489,6 +735,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
489
735
  // Issue #100: singleton chat popover. One alias at a time — clicking
490
736
  // another node swaps the target and the conversation switches in place.
491
737
  const [chatAlias, setChatAlias] = useState<string | null>(null);
738
+ // Round 14 / Loop: one-shot click ripple — fades outward from the clicked
739
+ // node so the click registers physically before the popover snaps in.
740
+ // Pairs with the Round 11 chat-focus ring: ripple expands, ring locks on.
741
+ // Keyed by ts so re-clicking the same node remounts the <circle> and the
742
+ // SMIL <animate> replays. Cleared 600ms after click (longer than the
743
+ // 500ms animation so a final frame at opacity 0 is still in the tree).
744
+ const [clickRipple, setClickRipple] = useState<{
745
+ ts: number; x: number; y: number; r0: number; color: string;
746
+ } | null>(null);
492
747
  // #84: when a node is renamed while its chat popover is open, follow the
493
748
  // rename so the conversation keeps targeting a live alias.
494
749
  useEffect(() => {
@@ -519,6 +774,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
519
774
  } catch {}
520
775
  }, []);
521
776
 
777
+ // Round 28 / Loop: first-paint auto-fit. Round 27's cellH bump means
778
+ // dense grids (≥6-7 rows) overflow the 680-px viewBox at the natural
779
+ // 100% zoom — a new user sees the topology with its bottom rows
780
+ // clipped and no hint that there's more below. Auto-fit on first
781
+ // paint (and only if the user has no persisted view) sets the
782
+ // initial zoom so all content fits. Subsequent zoom changes persist
783
+ // and override the auto-fit on reload; explicit reset (0 key) still
784
+ // goes back to 100% so the gesture's "reset to natural size" semantic
785
+ // is preserved.
786
+ //
787
+ // Capture pre-mount persistence in a useState initializer — the
788
+ // existing `persist` effect (declared below) runs on first render
789
+ // with the default {1,0,0} view and writes it to localStorage before
790
+ // the auto-fit effect (deps on async-arriving sessions) gets a turn.
791
+ // Reading localStorage from the effect would see that write and
792
+ // skip the fit. The useState snapshot fires once, before any effects.
793
+ const [hadPersistedViewOnMount] = useState<boolean>(
794
+ () => typeof window !== 'undefined' && !!localStorage.getItem('anet-topo-view'),
795
+ );
796
+ const autoFitDoneRef = useRef(false);
797
+ useEffect(() => {
798
+ if (autoFitDoneRef.current) return;
799
+ if (hadPersistedViewOnMount) {
800
+ autoFitDoneRef.current = true;
801
+ return;
802
+ }
803
+ if (layout !== 'grid' || sessions.length === 0 || !gridContentBottom) return;
804
+ if (gridContentBottom <= VIEWBOX_H) {
805
+ autoFitDoneRef.current = true; // no overflow → no fit needed
806
+ return;
807
+ }
808
+ const fitZoom = Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));
809
+ setView({ zoom: fitZoom, x: 0, y: 0 });
810
+ autoFitDoneRef.current = true;
811
+ }, [layout, sessions.length, gridContentBottom, hadPersistedViewOnMount]);
812
+
522
813
  // persist view
523
814
  useEffect(() => {
524
815
  try { localStorage.setItem('anet-topo-view', JSON.stringify(view)); } catch {}
@@ -538,6 +829,14 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
538
829
  const svg = svgRef.current;
539
830
  if (!svg) return;
540
831
  const onWheel = (e: WheelEvent) => {
832
+ // Round 23 / Loop: only zoom on Ctrl/Meta+wheel when inline; plain
833
+ // wheel over the canvas keeps scrolling the page (the topo sits at
834
+ // the top of /; trapping page scroll mid-page is the classic
835
+ // "scroll-jail" anti-pattern). In fullscreen the canvas owns the
836
+ // viewport so any wheel zooms — no page scroll to preserve. Pinch-
837
+ // zoom on a trackpad surfaces as ctrlKey=true natively, which is
838
+ // also what we want (zoom the canvas, not the browser).
839
+ if (!isFullscreen && !e.ctrlKey && !e.metaKey) return;
541
840
  e.preventDefault();
542
841
  const rect = svg.getBoundingClientRect();
543
842
  const mx = ((e.clientX - rect.left) / rect.width) * VIEWBOX_W;
@@ -558,8 +857,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
558
857
  };
559
858
  svg.addEventListener('wheel', onWheel, { passive: false });
560
859
  return () => svg.removeEventListener('wheel', onWheel);
561
- }, []);
562
-
860
+ // Re-attach when the fullscreen flag toggles so the handler's
861
+ // closure picks up the new value (capture-by-closure means we'd
862
+ // otherwise read the stale boolean forever).
863
+ }, [isFullscreen]);
864
+
865
+ // Round 21 / Loop: pan-aware cursor. Static `grab` gave no tactile cue
866
+ // that the canvas was actually being dragged — "grabbing" while
867
+ // dragRef.current.active = true tells the hand it's moving the
868
+ // viewport. Mirroring dragRef.active into state is the only way to
869
+ // re-render the SVG style; the ref itself doesn't trigger renders.
870
+ const [isPanning, setIsPanning] = useState(false);
563
871
  const onPointerDown = (e: React.PointerEvent<SVGSVGElement>) => {
564
872
  if (e.button !== 0) return;
565
873
  (e.currentTarget as Element).setPointerCapture?.(e.pointerId);
@@ -570,6 +878,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
570
878
  baseX: viewRef.current.x,
571
879
  baseY: viewRef.current.y,
572
880
  };
881
+ setIsPanning(true);
573
882
  };
574
883
  const onPointerMove = (e: React.PointerEvent<SVGSVGElement>) => {
575
884
  const d = dragRef.current;
@@ -584,6 +893,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
584
893
  const onPointerUp = (e: React.PointerEvent<SVGSVGElement>) => {
585
894
  if (!dragRef.current.active) return;
586
895
  dragRef.current.active = false;
896
+ setIsPanning(false);
587
897
  try {
588
898
  (e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
589
899
  } catch {}
@@ -601,6 +911,48 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
601
911
  };
602
912
  const resetView = () => setView({ zoom: 1, x: 0, y: 0 });
603
913
 
914
+ // Round 29 / Loop: `f` = fit-to-content. Shared by the Round 28
915
+ // first-paint auto-fit effect and the keyboard handler so the math is
916
+ // in one place. When content already fits at natural zoom, this is
917
+ // effectively a "recenter" — `f` always lands on a known good view.
918
+ const fitView = useCallback(() => {
919
+ const zoom = !gridContentBottom || gridContentBottom <= VIEWBOX_H
920
+ ? 1
921
+ : Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));
922
+ setView({ zoom, x: 0, y: 0 });
923
+ }, [gridContentBottom]);
924
+
925
+ // Round 22 / Loop: keyboard zoom — +/= zoom in, - zoom out, 0 reset.
926
+ // Round 29 / Loop: +f to fit content.
927
+ // Listen on window so the user doesn't need to focus the SVG first,
928
+ // but only act when no text input has focus (otherwise typing "-" in
929
+ // a chat would zoom the topology — surprising and bad). Modifier keys
930
+ // pass through so Cmd/Ctrl combos still hit their owners. The button
931
+ // titles below document these shortcuts so users discover them.
932
+ useEffect(() => {
933
+ const onKey = (e: KeyboardEvent) => {
934
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
935
+ const ae = document.activeElement as HTMLElement | null;
936
+ if (ae) {
937
+ const tag = ae.tagName;
938
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || ae.isContentEditable) return;
939
+ }
940
+ if (e.key === '+' || e.key === '=') { zoomBy(1.2); e.preventDefault(); }
941
+ else if (e.key === '-' || e.key === '_') { zoomBy(1 / 1.2); e.preventDefault(); }
942
+ else if (e.key === '0') { resetView(); e.preventDefault(); }
943
+ else if (e.key === 'f' || e.key === 'F') { fitView(); e.preventDefault(); }
944
+ // Round 32 / Loop: `l` toggles ring|grid. The vim-style `g l` route
945
+ // (Audit Log) requires a preceding `g` within 1500ms; a bare `l`
946
+ // outside that window is free for topology use.
947
+ else if (e.key === 'l' || e.key === 'L') { toggleLayout(); e.preventDefault(); }
948
+ };
949
+ window.addEventListener('keydown', onKey);
950
+ return () => window.removeEventListener('keydown', onKey);
951
+ // zoomBy / resetView are stable wrt setView callback; fitView changes
952
+ // with gridContentBottom so deps list catches it.
953
+ // eslint-disable-next-line react-hooks/exhaustive-deps
954
+ }, [fitView]);
955
+
604
956
  const toggleFullscreen = () => {
605
957
  const el = containerRef.current;
606
958
  if (!el) return;
@@ -628,6 +980,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
628
980
  <button
629
981
  onClick={() => { if (layout !== 'ring') toggleLayout(); }}
630
982
  aria-pressed={layout === 'ring'}
983
+ title="Ring layout (l to toggle)"
631
984
  className={`px-2.5 py-1 transition-colors ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300' : 'text-gray-500 hover:text-gray-400'}`}
632
985
  >
633
986
  Ring
@@ -635,6 +988,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
635
988
  <button
636
989
  onClick={() => { if (layout !== 'grid') toggleLayout(); }}
637
990
  aria-pressed={layout === 'grid'}
991
+ title="Grid layout (l to toggle)"
638
992
  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'}`}
639
993
  >
640
994
  Grid
@@ -646,9 +1000,69 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
646
1000
  <span className="px-2.5 py-1 rounded-md bg-cyan-500/10 text-cyan-300 border border-cyan-500/20">
647
1001
  {onlineNodes.length} online
648
1002
  </span>
649
- <span className="px-2.5 py-1 rounded-md bg-gray-500/10 text-gray-400 border border-gray-500/20">
650
- {flowLinks.length} active links
651
- </span>
1003
+ {/* Round 31 / Loop: fleet-health pressure bar. The "X working /
1004
+ Y online" chips already carry the raw counts; the bar lets
1005
+ the eye get the working/idle/offline RATIO in one glance
1006
+ without mental math. Stacked 3-segment chip, ~64 px wide. */}
1007
+ {(() => {
1008
+ const w = workingCount;
1009
+ const i = onlineNodes.length - workingCount; // idle = online - working
1010
+ const o = offlineNodes.length;
1011
+ const total = w + i + o;
1012
+ if (total === 0) return null;
1013
+ const seg = (n: number, color: string, key: string) => {
1014
+ if (n === 0) return null;
1015
+ return <span key={key} style={{ width: `${(n / total) * 100}%`, background: color, height: '100%' }} />;
1016
+ };
1017
+ return (
1018
+ <span
1019
+ 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"
1020
+ title={`${w} working · ${i} idle · ${o} offline`}
1021
+ data-fleet-pressure
1022
+ >
1023
+ <span className="text-[10px] tracking-wide">pressure</span>
1024
+ <span className="inline-flex h-1.5 w-16 rounded-full overflow-hidden" style={{ background: 'rgb(75 85 99 / 0.25)' }}>
1025
+ {seg(w, isLight ? '#059669' : '#22c55e', 'w')}
1026
+ {seg(i, isLight ? '#0d9488' : '#2dd4bf', 'i')}
1027
+ {seg(o, isLight ? '#94a3b8' : '#6b7280', 'o')}
1028
+ </span>
1029
+ </span>
1030
+ );
1031
+ })()}
1032
+ {vendorDist.length > 1 && (
1033
+ <span
1034
+ 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"
1035
+ title="Fleet by model vendor"
1036
+ >
1037
+ {vendorDist.map(v => (
1038
+ <span key={v.initial} className="inline-flex items-baseline gap-0.5">
1039
+ <span style={{ color: v.color }}>{v.initial}</span>
1040
+ <span className="text-gray-500">:{v.count}</span>
1041
+ </span>
1042
+ ))}
1043
+ </span>
1044
+ )}
1045
+ {/* Round 42 / Loop: extend active-links chip with the timestamp
1046
+ of the most-recent flow event. Tells the operator at a glance
1047
+ whether the topology is currently humming (last 30s) or has
1048
+ been quiet for a while — the visual flow particles and
1049
+ edge brightness only show that there IS traffic, not when
1050
+ the last one was. Reuses Round 38's relativeAgo. */}
1051
+ {(() => {
1052
+ const recent = flowLinks.reduce<number | null>((acc, l) => {
1053
+ if (!l.last_at) return acc;
1054
+ const t = parseHubTime(l.last_at);
1055
+ if (t === null) return acc;
1056
+ return acc === null || t > acc ? t : acc;
1057
+ }, null);
1058
+ const rel = recent !== null ? relativeAgo(new Date(recent).toISOString()) : null;
1059
+ return (
1060
+ <span className="px-2.5 py-1 rounded-md bg-gray-500/10 text-gray-400 border border-gray-500/20">
1061
+ {flowLinks.length} active link{flowLinks.length === 1 ? '' : 's'}
1062
+ {rel ? <span className="text-gray-500"> · last {rel}</span> : null}
1063
+ </span>
1064
+ );
1065
+ })()}
652
1066
  </div>
653
1067
  </div>
654
1068
 
@@ -668,7 +1082,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
668
1082
  onPointerMove={onPointerMove}
669
1083
  onPointerUp={onPointerUp}
670
1084
  onPointerLeave={onPointerUp}
671
- style={{ cursor: 'grab', touchAction: 'none' }}
1085
+ // Round 41 / Loop: the reset-button title (R22) and the Help
1086
+ // overlay (R25) both advertise "double-click the canvas to
1087
+ // reset", but the handler was never actually wired. The text
1088
+ // was lying. Wire it here, guarded so dbl-clicking a node
1089
+ // (which would also trigger the SVG-level handler via event
1090
+ // bubbling) doesn't unexpectedly reset the view on the user.
1091
+ onDoubleClick={(e) => {
1092
+ const t = e.target as Element | null;
1093
+ if (t?.closest('g[data-node]')) return;
1094
+ resetView();
1095
+ }}
1096
+ style={{ cursor: isPanning ? 'grabbing' : 'grab', touchAction: 'none' }}
672
1097
  >
673
1098
  <defs>
674
1099
  <linearGradient id="topo-panel" x1="0" x2="1" y1="0" y2="1">
@@ -690,9 +1115,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
690
1115
  </feMerge>
691
1116
  </filter>
692
1117
  )}
693
- <marker id="topo-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
694
- <path d="M 0 0 L 10 5 L 0 10 z" fill={pal.arrowFill} />
695
- </marker>
1118
+ {/* Round 16 / Loop: 3-tier flow-link arrow markers.
1119
+ The single marker had `markerUnits` defaulting to
1120
+ `strokeWidth`, so heavy edges (stroke=7) rendered 35-user-
1121
+ unit arrowheads — visually dominant, the head outweighed
1122
+ the line. Switching to `userSpaceOnUse` decouples arrow
1123
+ size from stroke; binning by count gives a clearer
1124
+ hierarchy than continuous linear scaling:
1125
+ s (count 1-2) → 12 user units
1126
+ m (count 3-4) → 16 user units (alias for `topo-arrow`)
1127
+ l (count 5+) → 22 user units
1128
+ `topo-arrow` stays bound to the medium tier so the legend
1129
+ swatch (line ~1500) renders without change. */}
1130
+ {[
1131
+ { id: 'topo-arrow-s', size: 12 },
1132
+ { id: 'topo-arrow', size: 16 },
1133
+ { id: 'topo-arrow-l', size: 22 },
1134
+ ].map(m => (
1135
+ <marker
1136
+ key={m.id}
1137
+ id={m.id}
1138
+ viewBox="0 0 10 10"
1139
+ refX="8"
1140
+ refY="5"
1141
+ markerWidth={m.size}
1142
+ markerHeight={m.size}
1143
+ markerUnits="userSpaceOnUse"
1144
+ orient="auto-start-reverse"
1145
+ >
1146
+ <path d="M 0 0 L 10 5 L 0 10 z" fill={pal.arrowFill} />
1147
+ </marker>
1148
+ ))}
696
1149
  {/* Round 45: radial radar sweep gradient — bright at center
697
1150
  (where it meets the hub) fading to leading edge. */}
698
1151
  <radialGradient id="topo-sweep" cx="0%" cy="50%" r="100%">
@@ -820,17 +1273,107 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
820
1273
  );
821
1274
  })}
822
1275
 
1276
+ {/* #111: prefix-group boundary boxes (Vincent 4722). Grid layout
1277
+ only — groupBoxes is empty in ring mode. Rendered behind the
1278
+ flow links + nodes; pointer-events off so they never intercept
1279
+ a node click. Restrained dashed container + group-name label. */}
1280
+ {groupBoxes.map(box => {
1281
+ const isHovered = hoveredGroup === box.key;
1282
+ // Round 18 / Loop: group-box hover linkage. The Round 8 fade
1283
+ // already dropped OUT-of-focus groups to 0.28, but the IN-focus
1284
+ // group sat at its baseline appearance — no positive emphasis.
1285
+ // Hovering now upgrades the box to an "accent" treatment:
1286
+ // solid stroke (not dashed), thicker, accent-coloured; brighter
1287
+ // text and slightly stronger fill. Label and box read as one
1288
+ // selected unit. Geometry unchanged → overlap test untouched.
1289
+ return (
1290
+ <g
1291
+ key={`grp-${box.key}`}
1292
+ data-group={box.key}
1293
+ className="transition-opacity"
1294
+ style={{ pointerEvents: 'none', opacity: !hoveredGroup || isHovered ? 1 : 0.28 }}
1295
+ >
1296
+ <rect
1297
+ x={box.x}
1298
+ y={box.y}
1299
+ width={box.w}
1300
+ height={box.h}
1301
+ rx="14"
1302
+ fill={isLight ? '#0f172a' : '#a5b4fc'}
1303
+ fillOpacity={isHovered ? (isLight ? 0.05 : 0.09) : (isLight ? 0.025 : 0.045)}
1304
+ stroke={isHovered ? pal.legendAccent : pal.ringStroke}
1305
+ strokeWidth={isHovered ? 2 : 1.5}
1306
+ strokeDasharray={isHovered ? 'none' : '6 6'}
1307
+ style={{ transition: 'stroke 200ms ease-out, stroke-width 200ms ease-out, fill-opacity 200ms ease-out' }}
1308
+ />
1309
+ <text
1310
+ x={box.x + 12}
1311
+ y={box.y + 14}
1312
+ fill={isHovered ? pal.legendHeadline : pal.legendText}
1313
+ fontSize="13"
1314
+ fontFamily="monospace"
1315
+ fontWeight="700"
1316
+ style={{ transition: 'fill 200ms ease-out' }}
1317
+ >
1318
+ {box.key}
1319
+ {/* Round 19 / Loop: member-count chip. Inline tspan stays
1320
+ in the single <text> bbox the overlap test reads, so
1321
+ the node↔label guard still catches if the chip ever
1322
+ pushes the label far enough right to clip a node.
1323
+ Smaller + lighter weight reads as metadata, not name. */}
1324
+ <tspan dx="6" fill={pal.legendText} fontSize="11" fontWeight="400">· {box.count}</tspan>
1325
+ </text>
1326
+ </g>
1327
+ );
1328
+ })}
1329
+
823
1330
  {/* directed message flows */}
824
1331
  {flowLinks.map((link, index) => {
825
1332
  const from = nodePositions[link.from];
826
1333
  const to = nodePositions[link.to];
827
1334
  if (!from || !to) return null;
828
1335
 
829
- const lift = index % 2 === 0 ? 36 : -36;
1336
+ // Round 7 / Loop: lift now scales with distance so short links
1337
+ // aren't over-bent (long links keep the ~36px hump), and the
1338
+ // particle period shortens with link.count so busier edges
1339
+ // visibly pulse faster — instant info density on top of the
1340
+ // existing stroke-width-by-count chip.
1341
+ const dist = Math.hypot(to.x - from.x, to.y - from.y);
1342
+ const lift = (index % 2 === 0 ? 1 : -1) * Math.min(36, dist * 0.18);
830
1343
  const path = curvePath(from, to, lift);
831
1344
  const width = Math.min(2 + link.count, 7);
832
- const duration = 2.2 + (index % 5) * 0.32;
833
-
1345
+ const duration = Math.max(0.9, 2.6 / Math.sqrt(link.count));
1346
+ // Round 10 / Loop: freshness fade. An edge that fired ≤30s ago
1347
+ // stays at full intensity; over 5 minutes it decays to ~35%.
1348
+ // Surfaces "what's happening now" vs background chatter without
1349
+ // hiding old flow entirely (some context still useful). `now`
1350
+ // captured at useMemo-recompute time (every 5s message refresh)
1351
+ // — accuracy is within the poll interval, plenty.
1352
+ const ageMs = link.last_at ? Math.max(0, Date.now() - Date.parse(link.last_at)) : 0;
1353
+ const fresh = Math.max(0.35, 1 - ageMs / (5 * 60 * 1000));
1354
+ // Round 16 arrow-tier binning — keep `topo-arrow` as the
1355
+ // medium tier id so the legend swatch picks it up unchanged.
1356
+ const arrowId = link.count <= 2 ? 'topo-arrow-s'
1357
+ : link.count <= 4 ? 'topo-arrow'
1358
+ : 'topo-arrow-l';
1359
+
1360
+ // Round 39 / Loop: edge hover tooltip — surface the same
1361
+ // last_at + count info the freshness fade and arrow tier
1362
+ // already encode visually, in plain text. The stroke is the
1363
+ // hover target; SVG `<title>` honours newlines on every
1364
+ // browser the dashboard targets.
1365
+ const lastAt = relativeAgo(link.last_at);
1366
+ const tooltip = `${link.from} → ${link.to}\n${link.count} message${link.count === 1 ? '' : 's'}${lastAt ? ` · last ${lastAt}` : ''}`;
1367
+ // Round 40 / Loop: edges follow node hover — when an alias is
1368
+ // hovered, every edge touching it brightens, the rest fade.
1369
+ // Pairs with the Round 8 group-focus fade on nodes: hover a
1370
+ // node to find "who is this agent talking to" at a glance.
1371
+ // No hover → multiplier is 1.0 (current behaviour preserved).
1372
+ const edgeOpacityMul = !hoveredAlias
1373
+ ? 1
1374
+ : (link.from === hoveredAlias || link.to === hoveredAlias)
1375
+ ? 1.7 // boost the touching edge
1376
+ : 0.35; // dim the rest
834
1377
  return (
835
1378
  <g key={link.key}>
836
1379
  <path
@@ -838,10 +1381,14 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
838
1381
  fill="none"
839
1382
  stroke={pal.flowEdge}
840
1383
  strokeWidth={width}
841
- opacity={isLight ? 0.22 : 0.28}
1384
+ opacity={Math.min(1, (isLight ? 0.22 : 0.28) * fresh * edgeOpacityMul)}
842
1385
  filter={isLight ? undefined : 'url(#topo-glow)'}
843
- markerEnd="url(#topo-arrow)"
844
- />
1386
+ markerEnd={`url(#${arrowId})`}
1387
+ className="transition-opacity duration-300"
1388
+ style={{ pointerEvents: 'stroke' }}
1389
+ >
1390
+ <title>{tooltip}</title>
1391
+ </path>
845
1392
  <path
846
1393
  id={`flow-path-${index}`}
847
1394
  d={path}
@@ -849,37 +1396,56 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
849
1396
  stroke={pal.flowPath}
850
1397
  strokeWidth="1"
851
1398
  strokeDasharray="2 12"
852
- opacity={isLight ? 0.4 : 0.75}
1399
+ opacity={Math.min(1, (isLight ? 0.4 : 0.75) * fresh * edgeOpacityMul)}
1400
+ className="transition-opacity duration-300"
853
1401
  />
854
- <circle r="4" fill={pal.flowParticle} filter={isLight ? undefined : 'url(#topo-glow)'}>
855
- <animateMotion dur={`${duration}s`} repeatCount="indefinite" path={path} />
856
- </circle>
1402
+ {!reducedMotion && (
1403
+ <circle r="4" fill={pal.flowParticle} filter={isLight ? undefined : 'url(#topo-glow)'} opacity={Math.min(1, fresh * edgeOpacityMul)}>
1404
+ <animateMotion dur={`${duration}s`} repeatCount="indefinite" path={path} />
1405
+ </circle>
1406
+ )}
857
1407
  </g>
858
1408
  );
859
1409
  })}
860
1410
 
861
- {/* center hub — round 39: enlarged from r=6 (12px) to r=10 (20px)
862
- core so the "control plane" reads as the network's anchor,
863
- not a stray particle. Static halo ring at r=18 grounds it
864
- visually; two outward pulses keep the "signal source" idea
865
- from r17. Light theme gets a paler core to avoid hot-spotting
866
- the bg. */}
867
- {layout === 'ring' && (<g>
868
- {/* static grounding halo — sits underneath, low opacity */}
1411
+ {/* center hub — round 39 sized + round 13 restraint.
1412
+ The hub is the control-plane anchor. r39 gave it two outward
1413
+ pulses (r 10→38, 2.4s, double-phase) which read as "loudest
1414
+ node in the room" — wrong semantics: anchors don't emit,
1415
+ they hold. r13 swaps the kinetic pulse for a slow opacity
1416
+ breath on the static halo (4s, ±15% from base), so the hub
1417
+ stays alive without throwing kinetic energy outward. The
1418
+ dual-circle "lit lamp" core is unchanged. */}
1419
+ {layout === 'ring' && (<g data-topo-hub>
1420
+ {/* Round 43 / Loop: hub `<title>` summary — hovering the
1421
+ central glow now answers "what is this?" with a one-line
1422
+ fleet snapshot. Duplicates the header chips by design;
1423
+ hovering the most visually prominent element is the
1424
+ most natural impulse, so satisfy it where the cursor
1425
+ already is. Falls clean: omits sub-clauses when their
1426
+ counts are zero. */}
1427
+ <title>{(() => {
1428
+ const total = sessions.length;
1429
+ const parts = [`Network hub`, `${total} session${total === 1 ? '' : 's'}`];
1430
+ if (onlineNodes.length > 0) parts.push(`${onlineNodes.length} online`);
1431
+ if (workingCount > 0) parts.push(`${workingCount} working`);
1432
+ if (flowLinks.length > 0) parts.push(`${flowLinks.length} active link${flowLinks.length === 1 ? '' : 's'}`);
1433
+ return parts.join(' · ');
1434
+ })()}</title>
1435
+ {/* grounding halo — now breathes in opacity, no expansion */}
869
1436
  <circle
870
1437
  cx={cx} cy={cy} r="18"
871
1438
  fill={isLight ? '#d1fae5' : '#10b981'}
872
- opacity={isLight ? 0.35 : 0.10}
873
- />
874
- {/* outer pulse 1 */}
875
- <circle cx={cx} cy={cy} r="10" fill="none" stroke="#10b981" strokeWidth="1.5">
876
- <animate attributeName="r" values="10;38;10" dur="2.4s" repeatCount="indefinite" />
877
- <animate attributeName="opacity" values="0.55;0;0.55" dur="2.4s" repeatCount="indefinite" />
878
- </circle>
879
- {/* outer pulse 2 (delayed 1.2s) */}
880
- <circle cx={cx} cy={cy} r="10" fill="none" stroke="#10b981" strokeWidth="1.5" opacity="0">
881
- <animate attributeName="r" values="10;38;10" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
882
- <animate attributeName="opacity" values="0.45;0;0.45" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
1439
+ opacity={isLight ? 0.42 : 0.12}
1440
+ >
1441
+ {!reducedMotion && (
1442
+ <animate
1443
+ attributeName="opacity"
1444
+ values={isLight ? '0.32;0.52;0.32' : '0.08;0.16;0.08'}
1445
+ dur="4s"
1446
+ repeatCount="indefinite"
1447
+ />
1448
+ )}
883
1449
  </circle>
884
1450
  {/* core — 20px diameter, larger inner highlight reads as a "lit lamp" */}
885
1451
  <circle cx={cx} cy={cy} r="10" fill={isLight ? '#059669' : '#10b981'} />
@@ -887,7 +1453,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
887
1453
  </g>)}
888
1454
 
889
1455
  {/* agent nodes */}
890
- {[...onlineNodes, ...offlineNodes].map(session => {
1456
+ {[...onlineNodes, ...offlineNodes].map((session, nodeIdx) => {
891
1457
  const pos = nodePositions[session.alias];
892
1458
  if (!pos) return null;
893
1459
 
@@ -895,7 +1461,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
895
1461
  const isOnline = session.status !== 'offline' || !!sseCountFor;
896
1462
  const status = nodeStatus(session, isOnline, isLight);
897
1463
  const isActive = activeAliases.has(session.alias);
898
- const radius = isOnline ? 26 : 18;
1464
+ // #113: node size scales with the S/M/L control; halos, labels,
1465
+ // badge and avatar all derive from `radius` so they follow.
1466
+ const radius = Math.round((isOnline ? 26 : 18) * nodeScale);
899
1467
  // Round 109/110 (Vincent 4582 + 4583 P0): at high node counts
900
1468
  // the 100px opaque label cards overlapped each other and
901
1469
  // covered neighbouring avatars. But hiding text entirely went
@@ -905,12 +1473,48 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
905
1473
  // occlude an avatar), and the full card appears only for the
906
1474
  // hovered node or once zoomed in past 1.4×.
907
1475
  const showFullLabel = !denseLayout || hoveredAlias === session.alias || view.zoom >= 1.4;
1476
+ // Round 8: when a node in another group is hovered, fade this
1477
+ // one. Same-group nodes (incl. singletons matching the hovered
1478
+ // alias) stay full. Pure visual focus, geometry unchanged.
1479
+ const inFocus = !hoveredGroup || (groupKeys[session.alias] ?? session.alias) === hoveredGroup;
908
1480
 
909
1481
  return (
910
1482
  <g
911
1483
  key={session.alias}
912
- className="transition-opacity"
913
- style={{ cursor: 'pointer' }}
1484
+ data-node={session.alias}
1485
+ // Round 3 / Loop: `anet-fade-in` runs once when the <g>
1486
+ // mounts — a new session entering the fleet (or the topology
1487
+ // first rendering) eases in instead of popping. Re-renders of
1488
+ // an existing node don't re-trigger (React preserves the <g>
1489
+ // via the alias key), so status changes don't flicker. The
1490
+ // global prefers-reduced-motion sweep already neutralises it.
1491
+ className="group transition-opacity anet-fade-in"
1492
+ style={{
1493
+ cursor: 'pointer',
1494
+ // Round 17 / Loop: offline nodes drop to 0.6 at rest so
1495
+ // online nodes pop without losing the offline-as-ghost
1496
+ // information. The dashed stroke + smaller radius already
1497
+ // say "offline"; dimming the whole group strengthens the
1498
+ // online-vs-offline hierarchy at first glance. Exempt the
1499
+ // chat-focused node — if the user explicitly opened a
1500
+ // popover targeting an offline alias, that node stays
1501
+ // full-brightness so the focus ring + popover read as one
1502
+ // selected thing rather than a dimmed selection. Group-
1503
+ // hover fade (Round 8) still wins when inFocus is false.
1504
+ opacity: !inFocus
1505
+ ? 0.32
1506
+ : chatAlias === session.alias
1507
+ ? 1
1508
+ : isOnline ? 1 : 0.6,
1509
+ // Round 9 / Loop: stagger the anet-fade-in so the topology
1510
+ // reveals as a wave on first paint instead of one big pop.
1511
+ // Cap at 24 indices (≈600ms tail) so 50-node fleets still
1512
+ // finish revealing within a beat. CSS animation-delay only
1513
+ // applies during the keyframe — re-renders without a new
1514
+ // mount (same alias key) don't replay, so status changes
1515
+ // never trigger the stagger again.
1516
+ animationDelay: `${Math.min(nodeIdx, 24) * 25}ms`,
1517
+ }}
914
1518
  // Stop the pointerdown from reaching the SVG pan handler: the
915
1519
  // SVG calls setPointerCapture, and a captured pointer makes
916
1520
  // Chromium fire the follow-up `click` on the SVG instead of
@@ -918,21 +1522,106 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
918
1522
  // Side effect (intended): you pan from empty canvas, not by
919
1523
  // grabbing a node.
920
1524
  onPointerDown={(e) => e.stopPropagation()}
921
- onPointerEnter={() => denseLayout && setHoveredAlias(session.alias)}
922
- onPointerLeave={() => denseLayout && setHoveredAlias(prev => (prev === session.alias ? null : prev))}
923
- onClick={() => setChatAlias(session.alias)}
1525
+ // Track hover globally (not just under denseLayout) so the
1526
+ // Round 8 group-focus fade works at any fleet size.
1527
+ onPointerEnter={() => setHoveredAlias(session.alias)}
1528
+ onPointerLeave={() => setHoveredAlias(prev => (prev === session.alias ? null : prev))}
1529
+ onClick={() => {
1530
+ setChatAlias(session.alias);
1531
+ // Round 14 ripple — capture position, radius and status
1532
+ // colour at click time so the one-shot circle is self-
1533
+ // contained (no re-render-time recomputation needed).
1534
+ setClickRipple({
1535
+ ts: Date.now(),
1536
+ x: pos.x, y: pos.y, r0: radius,
1537
+ color: status.primary,
1538
+ });
1539
+ setTimeout(() => setClickRipple(prev =>
1540
+ prev && Date.now() - prev.ts >= 590 ? null : prev), 600);
1541
+ }}
924
1542
  >
925
1543
  {/* Issue #96: native hover tooltip — "Vendor · model · Runtime".
926
1544
  Falls back to just the alias when the node reports no
927
- model/runtime. */}
928
- <title>{identityLine(session.model, session.runtime) || session.alias}</title>
929
- {isActive && (
1545
+ model/runtime.
1546
+ Round 33 / Loop: cwd line answers "what is this agent on?".
1547
+ Round 34 / Loop: for offline nodes, append "last seen: 6m ago"
1548
+ so the operator knows whether to wait or chase. Online nodes
1549
+ skip the line (Round 27's 1 h ghost age-out means anything
1550
+ still online has heartbeated recently — the line would just
1551
+ be noise). Accepts both ISO ("…T06:00:28Z") and SQL-style
1552
+ ("… 06:00:28" assumed UTC) formats. */}
1553
+ {(() => {
1554
+ // Round 35 / Loop: TZ-safe parsing via the shared lib helper
1555
+ // — same parseHubTime is used by isGhost above (Round 38
1556
+ // factored it to app/lib/time.ts so both paths interpret SQL
1557
+ // bare timestamps as UTC on every browser).
1558
+ const lastSeen = !isOnline && session.last_seen_at ? relativeAgo(session.last_seen_at) : null;
1559
+ return (
1560
+ <title>{[
1561
+ identityLine(session.model, session.runtime) || session.alias,
1562
+ session.project_dir ? `cwd: ${session.project_dir}` : null,
1563
+ lastSeen ? `last seen: ${lastSeen}` : null,
1564
+ ].filter(Boolean).join('\n')}</title>
1565
+ );
1566
+ })()}
1567
+ {/* Round 2 / Loop: hover ring — a thin outer stroke that fades
1568
+ in when the cursor enters the node, signalling clickability
1569
+ (real-user feedback for the chat-popover open). Pure CSS via
1570
+ Tailwind group-hover, so it costs nothing per frame and
1571
+ respects prefers-reduced-motion via the global media query. */}
1572
+ <circle
1573
+ cx={pos.x}
1574
+ cy={pos.y}
1575
+ r={radius + 12}
1576
+ fill="none"
1577
+ stroke={status.primary}
1578
+ // strokeWidth must NOT be 1.5 (offline status ring) or 3
1579
+ // (online status ring) — the overlap test selects by those
1580
+ // exact widths and would mis-count this invisible hover
1581
+ // ring as a node footprint.
1582
+ strokeWidth="2"
1583
+ className="opacity-0 group-hover:opacity-70 transition-opacity duration-150"
1584
+ style={{ pointerEvents: 'none' }}
1585
+ />
1586
+ {/* Round 11 / Loop: chat-focus ring — when the ChatPopover is
1587
+ open targeting this node, anchor a persistent ring around
1588
+ it so the floating popover visibly links back to its source
1589
+ node. Static (not pulsing) so it reads as "selected state"
1590
+ rather than "this node is active". strokeWidth=2.5 stays
1591
+ clear of the overlap-test selectors (1.5 / 3). Sits just
1592
+ outside the halo radius+8 so it never overlaps a neighbour
1593
+ (halos already pack flush in dense grids). */}
1594
+ {chatAlias === session.alias && (
1595
+ <circle
1596
+ cx={pos.x}
1597
+ cy={pos.y}
1598
+ r={radius + 14}
1599
+ fill="none"
1600
+ stroke={status.primary}
1601
+ strokeWidth="2.5"
1602
+ opacity={isLight ? 0.85 : 0.95}
1603
+ filter={!isLight ? 'url(#topo-glow)' : undefined}
1604
+ className="transition-opacity duration-200"
1605
+ style={{ pointerEvents: 'none' }}
1606
+ />
1607
+ )}
1608
+ {isActive && !reducedMotion && (
930
1609
  <circle cx={pos.x} cy={pos.y} r={radius + 14} fill={status.primary} opacity={isLight ? 0.08 : 0.12}>
931
1610
  <animate attributeName="r" values={`${radius + 8};${radius + 22};${radius + 8}`} dur="2.4s" repeatCount="indefinite" />
932
1611
  <animate attributeName="opacity" values={isLight ? '0.12;0.02;0.12' : '0.18;0.04;0.18'} dur="2.4s" repeatCount="indefinite" />
933
1612
  </circle>
934
1613
  )}
935
- <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)} />
1614
+ {/* Round 4 / Loop: transition-[fill,stroke,opacity] smooths
1615
+ status colour changes so idle↔working↔offline doesn't snap
1616
+ — task replies / node-rename / SSE updates ease in. */}
1617
+ <circle
1618
+ cx={pos.x}
1619
+ cy={pos.y}
1620
+ r={radius + 8}
1621
+ fill={status.halo}
1622
+ opacity={isOnline ? (isLight ? 0.85 : 0.65) : (isLight ? 0.4 : 0.25)}
1623
+ className="transition-[fill,opacity] duration-300 ease-out"
1624
+ />
936
1625
  <circle
937
1626
  cx={pos.x}
938
1627
  cy={pos.y}
@@ -942,6 +1631,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
942
1631
  strokeWidth={isOnline ? 3 : 1.5}
943
1632
  strokeDasharray={isOnline ? 'none' : '5 5'}
944
1633
  filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
1634
+ className="transition-[fill,stroke] duration-300 ease-out"
945
1635
  />
946
1636
  {/* Issue #96: node "avatar" is now driven by the model
947
1637
  vendor. Decision order:
@@ -954,7 +1644,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
954
1644
  4. unknown vendor / null model → the prefix-group
955
1645
  hue-hashed initial (#83/#99 behaviour, unchanged). */}
956
1646
  {(() => {
957
- const ar = isOnline ? 14 : 10;
1647
+ const ar = Math.round((isOnline ? 14 : 10) * nodeScale);
958
1648
  const size = radius * 2;
959
1649
  const vendor = vendorForModel(session.model);
960
1650
  const internByAlias = /书生|书小生|intern/i.test(session.alias);
@@ -979,7 +1669,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
979
1669
  <circle cx={pos.x} cy={pos.y} r={ar} fill={vendor.mono.bg} stroke={vendor.mono.ring} strokeWidth="1" />
980
1670
  <text
981
1671
  x={pos.x} y={pos.y} dy="0.34em" textAnchor="middle"
982
- fill={vendor.mono.text} fontSize={isOnline ? 14 : 10}
1672
+ fill={vendor.mono.text} fontSize={ar}
983
1673
  fontFamily="monospace" fontWeight="700"
984
1674
  >
985
1675
  {vendor.initial}
@@ -999,7 +1689,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
999
1689
  dy="0.34em"
1000
1690
  textAnchor="middle"
1001
1691
  fill={c.text}
1002
- fontSize={isOnline ? 14 : 10}
1692
+ fontSize={ar}
1003
1693
  fontFamily="monospace"
1004
1694
  fontWeight="700"
1005
1695
  >
@@ -1028,78 +1718,205 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1028
1718
  </g>
1029
1719
  );
1030
1720
  })()}
1031
- {session.status === 'working' && (
1032
- // working pulse — small dot just inside the status ring,
1033
- // above the avatar text. Was a 18px horizontal bar at
1034
- // y+11 which now collides with the avatar; the dot reads
1035
- // as the same "active" cue without occluding the initial.
1036
- <circle cx={pos.x} cy={pos.y - (isOnline ? 18 : 13)} r="2.5" fill={pal.flowParticle}>
1037
- <animate attributeName="opacity" values="1;0.25;1" dur="1.1s" repeatCount="indefinite" />
1038
- </circle>
1039
- )}
1721
+ {session.status === 'working' && (() => {
1722
+ // Round 24 / Loop: pulse rate ↔ traffic. The dot's dur
1723
+ // was a flat 1.1s — every working node looked equally
1724
+ // busy. Mapping it to sse:N lets the eye land on what's
1725
+ // actually hot. 3 discrete tiers read cleaner than
1726
+ // continuous easing at a glance; thresholds picked so
1727
+ // typical 1-2-SSE Claude/Codex sessions stay in the
1728
+ // "active" tier and only multi-pane orchestrators pop
1729
+ // into the "busy" tier. Geometry unchanged (r=2.5).
1730
+ const sse = sseCountFor ?? 0;
1731
+ const dur = sse >= 4 ? '0.7s' : sse >= 2 ? '0.9s' : '1.2s';
1732
+ return (
1733
+ <circle cx={pos.x} cy={pos.y - (radius - 6)} r="2.5" fill={pal.flowParticle} data-pulse-dur={dur} opacity={reducedMotion ? 0.6 : undefined}>
1734
+ {!reducedMotion && (
1735
+ <animate attributeName="opacity" values="1;0.25;1" dur={dur} repeatCount="indefinite" />
1736
+ )}
1737
+ </circle>
1738
+ );
1739
+ })()}
1040
1740
 
1041
1741
  {/* Round 98 (issue #61): label rect 124px → 100px.
1042
1742
  Round 109/110 (Vincent P0): full opaque card below the
1043
1743
  density threshold / on hover / when zoomed; otherwise a
1044
1744
  lightweight plain-text alias that keeps every node
1045
- labelled without an opaque box covering its neighbours. */}
1046
- {showFullLabel ? (
1047
- <g transform={`translate(${pos.x}, ${pos.y + radius + 22})`} style={{ pointerEvents: 'none' }}>
1048
- <rect x="-50" y="-14" width="100" height="42" rx="6" fill={pal.labelBox.fill} stroke={pal.labelBox.stroke} opacity={isLight ? 1 : 0.94} />
1049
- <text x="0" y="1" textAnchor="middle" fill={status.text} fontSize="12" fontFamily="monospace" fontWeight="700">
1050
- {truncate(session.alias, 12)}
1051
- </text>
1052
- <text x="0" y="17" textAnchor="middle" fill={status.primary} fontSize="9" fontFamily="monospace">
1053
- {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
1745
+ labelled without an opaque box covering its neighbours.
1746
+ Round 15 / Loop: when nodeScale=S the node shrinks 30%
1747
+ but the label card was staying full-size, so the label
1748
+ visually outweighed the small node. Tighten the card
1749
+ frame, alias / sub fontSize, drop-offset and truncate
1750
+ length specifically for S; M and L keep their existing
1751
+ sizes (M ≈ L for labels by design — the S user is the
1752
+ one who explicitly asked for a denser view). */}
1753
+ {(() => {
1754
+ const isSmall = nodeScale < 0.8;
1755
+ const cardW = isSmall ? 88 : 100;
1756
+ const cardH = isSmall ? 36 : 42;
1757
+ const cardTopY = isSmall ? -12 : -14;
1758
+ const aliasFs = isSmall ? 11 : 12;
1759
+ const subFs = isSmall ? 8 : 9;
1760
+ const subY = isSmall ? 15 : 17;
1761
+ const dropY = isSmall ? 18 : 22;
1762
+ const fullMax = isSmall ? 11 : 12;
1763
+ const denseFs = isSmall ? 9 : 10;
1764
+ const denseDrop = isSmall ? 12 : 14;
1765
+ // Round 26 / Loop: micro-lift the label group on hover —
1766
+ // 1.5 px upward, 200 ms ease. Pairs with the Round 18
1767
+ // group-box hover-accent treatment to give the same
1768
+ // "this is the focused element" feedback at the per-
1769
+ // node level. CSS transform stacks onto the SVG
1770
+ // positioning transform attribute (SVG 2 cascade);
1771
+ // bbox is unchanged at rest, so the overlap-test gate
1772
+ // continues to see the geometric layout it expects.
1773
+ return showFullLabel ? (
1774
+ <g transform={`translate(${pos.x}, ${pos.y + radius + dropY})`} style={{ pointerEvents: 'none' }}
1775
+ className="transition-transform duration-200 group-hover:-translate-y-[1.5px]">
1776
+ <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} />
1777
+ <text x="0" y="1" textAnchor="middle" fill={status.text} fontSize={aliasFs} fontFamily="monospace" fontWeight="700">
1778
+ {truncate(session.alias, fullMax)}
1779
+ </text>
1780
+ <text x="0" y={subY} textAnchor="middle" fill={status.primary} fontSize={subFs} fontFamily="monospace">
1781
+ {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
1782
+ </text>
1783
+ </g>
1784
+ ) : (
1785
+ <text
1786
+ x={pos.x}
1787
+ y={pos.y + radius + denseDrop}
1788
+ textAnchor="middle"
1789
+ fill={status.text}
1790
+ fontSize={denseFs}
1791
+ fontFamily="monospace"
1792
+ fontWeight="700"
1793
+ opacity={0.9}
1794
+ className="transition-transform duration-200 group-hover:-translate-y-[1.5px]"
1795
+ style={{ pointerEvents: 'none', paintOrder: 'stroke' }}
1796
+ stroke={pal.containerBg}
1797
+ strokeWidth="3"
1798
+ >
1799
+ {truncate(session.alias, isSmall ? 9 : 10)}
1054
1800
  </text>
1055
- </g>
1056
- ) : (
1057
- <text
1058
- x={pos.x}
1059
- y={pos.y + radius + 14}
1060
- textAnchor="middle"
1061
- fill={status.text}
1062
- fontSize="10"
1063
- fontFamily="monospace"
1064
- fontWeight="700"
1065
- opacity={0.9}
1066
- style={{ pointerEvents: 'none', paintOrder: 'stroke' }}
1067
- stroke={pal.containerBg}
1068
- strokeWidth="3"
1069
- >
1070
- {truncate(session.alias, 10)}
1071
- </text>
1072
- )}
1801
+ );
1802
+ })()}
1073
1803
  </g>
1074
1804
  );
1075
1805
  })}
1076
1806
 
1807
+ {/* Round 14 / Loop: click ripple — one-shot expanding ring from
1808
+ the clicked node, ~500ms, status-coloured. Sits inside the
1809
+ zoom/pan <g> so it scales / pans with the topology. Keyed by
1810
+ ts so a re-click on any node (same or different) remounts the
1811
+ <circle> and the SMIL <animate> elements replay from t=0.
1812
+ strokeWidth=2 doesn't match the overlap-test selectors. */}
1813
+ {clickRipple && !reducedMotion && (
1814
+ <circle
1815
+ key={clickRipple.ts}
1816
+ cx={clickRipple.x}
1817
+ cy={clickRipple.y}
1818
+ r={clickRipple.r0 + 4}
1819
+ fill="none"
1820
+ stroke={clickRipple.color}
1821
+ strokeWidth="2"
1822
+ opacity="0"
1823
+ style={{ pointerEvents: 'none' }}
1824
+ >
1825
+ <animate attributeName="r" values={`${clickRipple.r0 + 4};${clickRipple.r0 + 30}`} dur="0.5s" fill="freeze" />
1826
+ <animate attributeName="opacity" values="0.7;0" dur="0.5s" fill="freeze" />
1827
+ </circle>
1828
+ )}
1829
+
1830
+ </g>
1831
+
1832
+ {/* #112: overlay panels (recent-signal + legend) render OUTSIDE the
1833
+ zoom/pan <g> so they stay fixed while the topology pans/zooms.
1834
+ They're sized + tucked into the top corners so every corner of
1835
+ each panel is >325px from the canvas centre — i.e. fully outside
1836
+ the outermost (offline) ring. No node on any ring can reach the
1837
+ corner triangles, so the panels never overlap a node, in ring
1838
+ OR grid layout (Vincent 4727 zero-overlap criterion). */}
1077
1839
  {/* latest flow labels */}
1078
- <g transform="translate(28, 34)">
1079
- <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} />
1080
- <text x="16" y="26" fill={pal.legendHeadline} fontSize="13" fontFamily="monospace" fontWeight="700">recent signal</text>
1081
- <text x="178" y="26" fill={pal.legendAccent} fontSize="11" fontFamily="monospace">{messages.length} messages</text>
1082
- {flowLinks.slice(0, 4).map((link, index) => (
1083
- <text key={link.key} x="16" y={54 + index * 20} fill={pal.legendText} fontSize="10" fontFamily="monospace">
1084
- {truncate(link.from, 8)} {'->'} {truncate(link.to, 8)} / {link.count} / {truncate(link.content, 18)}
1840
+ <g transform="translate(16, 16)">
1841
+ <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} />
1842
+ <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight="700">recent signal</text>
1843
+ <text x="150" y="21" fill={pal.legendAccent} fontSize="10" fontFamily="monospace">{messages.length} msgs</text>
1844
+ {flowLinks.slice(0, 3).map((link, index) => (
1845
+ <text key={link.key} x="13" y={38 + index * 16} fill={pal.legendText} fontSize="9" fontFamily="monospace">
1846
+ {truncate(link.from, 6)} {'->'} {truncate(link.to, 6)} / {link.count} / {truncate(link.content, 12)}
1085
1847
  </text>
1086
1848
  ))}
1087
1849
  </g>
1088
1850
 
1089
1851
  {/* legend */}
1090
- <g transform="translate(720, 34)">
1091
- <rect x="0" y="0" width="250" height="112" rx="8" fill={pal.legendBox.fill} stroke={pal.legendBox.stroke} opacity={isLight ? 1 : 0.94} />
1092
- <circle cx="18" cy="26" r="6" fill={isLight ? '#059669' : '#22c55e'} />
1093
- <text x="34" y="30" fill={pal.legendText} fontSize="11" fontFamily="monospace">working node</text>
1094
- <circle cx="18" cy="52" r="6" fill={isLight ? '#0d9488' : '#2dd4bf'} />
1095
- <text x="34" y="56" fill={pal.legendText} fontSize="11" fontFamily="monospace">online idle</text>
1096
- <circle cx="18" cy="78" r="6" fill={isLight ? '#94a3b8' : '#6b7280'} />
1097
- <text x="34" y="82" fill={pal.legendText} fontSize="11" fontFamily="monospace">offline / no SSE</text>
1098
- <path d="M150,78 Q176,52 210,78" fill="none" stroke={pal.flowEdge} strokeWidth="3" markerEnd="url(#topo-arrow)" />
1099
- </g>
1852
+ <g transform="translate(760, 16)">
1853
+ <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} />
1854
+ <circle cx="16" cy="24" r="5.5" fill={isLight ? '#059669' : '#22c55e'} />
1855
+ <text x="30" y="28" fill={pal.legendText} fontSize="11" fontFamily="monospace">working node</text>
1856
+ <circle cx="16" cy="48" r="5.5" fill={isLight ? '#0d9488' : '#2dd4bf'} />
1857
+ <text x="30" y="52" fill={pal.legendText} fontSize="11" fontFamily="monospace">online idle</text>
1858
+ <circle cx="16" cy="72" r="5.5" fill={isLight ? '#94a3b8' : '#6b7280'} />
1859
+ <text x="30" y="76" fill={pal.legendText} fontSize="11" fontFamily="monospace">offline / no SSE</text>
1860
+ <path d="M140,72 Q164,48 196,72" fill="none" stroke={pal.flowEdge} strokeWidth="3" markerEnd="url(#topo-arrow)" />
1100
1861
  </g>
1101
1862
  </svg>
1102
1863
 
1864
+ {/* Round 30 / Loop: minimap. Big fleets in fullscreen mode at high
1865
+ zoom let users lose their position — the minimap shows the
1866
+ whole topology miniaturised plus a viewport rectangle so the
1867
+ user always knows where they are. Click anywhere to recenter
1868
+ the canvas there. Only mounted when the view is non-default
1869
+ (zoomed or panned) since at 1× centered the minimap and the
1870
+ canvas show the same thing. HTML overlay so it stays fixed
1871
+ while the SVG transforms. */}
1872
+ {(() => {
1873
+ const isDefaultView = Math.abs(view.zoom - 1) < 0.01 && Math.abs(view.x) < 1 && Math.abs(view.y) < 1;
1874
+ if (isDefaultView || (onlineNodes.length + offlineNodes.length) === 0) return null;
1875
+ const MW = 120, MH = 82;
1876
+ const sx = MW / VIEWBOX_W, sy = MH / VIEWBOX_H;
1877
+ const rectX = (-view.x / view.zoom) * sx;
1878
+ const rectY = (-view.y / view.zoom) * sy;
1879
+ const rectW = (VIEWBOX_W / view.zoom) * sx;
1880
+ const rectH = (VIEWBOX_H / view.zoom) * sy;
1881
+ return (
1882
+ <div
1883
+ className="absolute right-3 rounded-md border shadow-lg shadow-black/30 overflow-hidden anet-fade-in"
1884
+ style={{ bottom: 56, background: pal.legendBox.fill, borderColor: pal.containerBorder, cursor: 'crosshair' }}
1885
+ role="img"
1886
+ aria-label="Topology minimap — click to recenter"
1887
+ title="Minimap · click to recenter"
1888
+ onClick={(e) => {
1889
+ const r = e.currentTarget.getBoundingClientRect();
1890
+ const fx = (e.clientX - r.left) / r.width;
1891
+ const fy = (e.clientY - r.top) / r.height;
1892
+ setView(prev => ({
1893
+ ...prev,
1894
+ x: VIEWBOX_W / 2 - fx * VIEWBOX_W * prev.zoom,
1895
+ y: VIEWBOX_H / 2 - fy * VIEWBOX_H * prev.zoom,
1896
+ }));
1897
+ }}
1898
+ data-topo-minimap
1899
+ >
1900
+ <svg width={MW} height={MH} viewBox={`0 0 ${MW} ${MH}`} style={{ display: 'block' }}>
1901
+ {[...onlineNodes, ...offlineNodes].map(s => {
1902
+ const p = nodePositions[s.alias];
1903
+ if (!p) return null;
1904
+ const sseN = (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];
1905
+ const isOn = s.status !== 'offline' || !!sseN;
1906
+ const st = nodeStatus(s, isOn, isLight);
1907
+ 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} />;
1908
+ })}
1909
+ {/* viewport rectangle */}
1910
+ <rect
1911
+ x={Math.max(0, rectX)} y={Math.max(0, rectY)}
1912
+ width={Math.max(0, Math.min(MW - Math.max(0, rectX), rectW))}
1913
+ height={Math.max(0, Math.min(MH - Math.max(0, rectY), rectH))}
1914
+ fill="none" stroke={pal.legendAccent} strokeWidth="1" opacity="0.9"
1915
+ />
1916
+ </svg>
1917
+ </div>
1918
+ );
1919
+ })()}
1103
1920
  {/* Round 103 (issue #81): zoom / pan / fullscreen controls — HTML
1104
1921
  overlay so they stay fixed while the SVG content transforms.
1105
1922
  Round 104: Vincent 实测 — the reset action used to be hidden
@@ -1107,6 +1924,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1107
1924
  Split into a plain % readout + an explicit reset button with
1108
1925
  its own icon + tooltip. */}
1109
1926
  <div className="absolute bottom-3 right-3 flex items-center gap-1.5 text-xs select-none">
1927
+ {/* #113: node size — S / M / L segmented control (Vincent 4727). */}
1928
+ <div
1929
+ className="flex items-center rounded-md border overflow-hidden"
1930
+ style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder }}
1931
+ role="group"
1932
+ aria-label="Node size"
1933
+ >
1934
+ {([['S', 0.7], ['M', 0.84], ['L', 1]] as const).map(([lbl, v], idx) => (
1935
+ <button
1936
+ key={lbl}
1937
+ onClick={() => pickNodeScale(v)}
1938
+ aria-pressed={nodeScale === v}
1939
+ title={`Node size: ${lbl === 'S' ? 'small' : lbl === 'M' ? 'medium' : 'large'}`}
1940
+ className={`px-2 py-1 transition-colors ${idx > 0 ? 'border-l' : ''} ${nodeScale === v ? 'bg-cyan-500/15 text-cyan-300' : 'hover:bg-white/5'}`}
1941
+ style={{ color: nodeScale === v ? undefined : pal.legendText, borderColor: pal.containerBorder }}
1942
+ >
1943
+ {lbl}
1944
+ </button>
1945
+ ))}
1946
+ </div>
1110
1947
  <div
1111
1948
  className="flex items-center rounded-md border overflow-hidden"
1112
1949
  style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder }}
@@ -1116,6 +1953,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1116
1953
  className="px-2 py-1 hover:bg-white/5 transition-colors"
1117
1954
  style={{ color: pal.legendText }}
1118
1955
  aria-label="Zoom out"
1956
+ title="Zoom out (−)"
1119
1957
  >
1120
1958
  <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>
1121
1959
  </button>
@@ -1131,6 +1969,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1131
1969
  className="px-2 py-1 hover:bg-white/5 transition-colors"
1132
1970
  style={{ color: pal.legendText }}
1133
1971
  aria-label="Zoom in"
1972
+ title="Zoom in (+)"
1134
1973
  >
1135
1974
  <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>
1136
1975
  </button>
@@ -1140,7 +1979,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1140
1979
  className="p-1.5 rounded-md border hover:bg-white/5 transition-colors"
1141
1980
  style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
1142
1981
  aria-label="Reset view"
1143
- title="Reset zoom + pan (or double-click the canvas)"
1982
+ title="Reset zoom + pan (0, or double-click the canvas)"
1144
1983
  >
1145
1984
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
1146
1985
  <path d="M3 12a9 9 0 1 0 9-9 9 9 0 0 0-6.4 2.6L3 8" />