@sleep2agi/agent-network-dashboard 0.4.5-preview.9 → 0.4.5-preview.91

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 (319) hide show
  1. package/.mcp.json +11 -0
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/diagnostics/route-bundle-stats.json +48 -48
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/server/app/_global-error.html +1 -1
  8. package/.next/server/app/_global-error.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/server/app/_not-found/page/next-font-manifest.json +1 -1
  15. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/_not-found.html +4 -4
  17. package/.next/server/app/_not-found.rsc +12 -12
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  19. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  20. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  23. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  24. package/.next/server/app/admin/page/next-font-manifest.json +1 -1
  25. package/.next/server/app/admin/page.js.nft.json +1 -1
  26. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  27. package/.next/server/app/admin.html +4 -4
  28. package/.next/server/app/admin.rsc +15 -15
  29. package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
  30. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  31. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  32. package/.next/server/app/admin.segments/_tree.segment.rsc +3 -3
  33. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  34. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  35. package/.next/server/app/index.html +4 -4
  36. package/.next/server/app/index.rsc +15 -15
  37. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  38. package/.next/server/app/index.segments/_full.segment.rsc +15 -15
  39. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  40. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  41. package/.next/server/app/index.segments/_tree.segment.rsc +3 -3
  42. package/.next/server/app/login/page/next-font-manifest.json +1 -1
  43. package/.next/server/app/login/page.js.nft.json +1 -1
  44. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  45. package/.next/server/app/login.html +2 -2
  46. package/.next/server/app/login.rsc +15 -15
  47. package/.next/server/app/login.segments/_full.segment.rsc +15 -15
  48. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  49. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  50. package/.next/server/app/login.segments/_tree.segment.rsc +3 -3
  51. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  52. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  53. package/.next/server/app/logs/page/next-font-manifest.json +1 -1
  54. package/.next/server/app/logs/page.js.nft.json +1 -1
  55. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  56. package/.next/server/app/logs.html +4 -4
  57. package/.next/server/app/logs.rsc +15 -15
  58. package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
  59. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  60. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  61. package/.next/server/app/logs.segments/_tree.segment.rsc +3 -3
  62. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  63. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  64. package/.next/server/app/messages/page/next-font-manifest.json +1 -1
  65. package/.next/server/app/messages/page.js.nft.json +1 -1
  66. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  67. package/.next/server/app/messages.html +4 -4
  68. package/.next/server/app/messages.rsc +15 -15
  69. package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
  70. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  71. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  72. package/.next/server/app/messages.segments/_tree.segment.rsc +3 -3
  73. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  74. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  75. package/.next/server/app/node/page/next-font-manifest.json +1 -1
  76. package/.next/server/app/node/page.js.nft.json +1 -1
  77. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  78. package/.next/server/app/node.html +4 -4
  79. package/.next/server/app/node.rsc +15 -15
  80. package/.next/server/app/node.segments/_full.segment.rsc +15 -15
  81. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  82. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  83. package/.next/server/app/node.segments/_tree.segment.rsc +3 -3
  84. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  85. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  86. package/.next/server/app/nodes/page/next-font-manifest.json +1 -1
  87. package/.next/server/app/nodes/page.js.nft.json +1 -1
  88. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  89. package/.next/server/app/nodes.html +4 -4
  90. package/.next/server/app/nodes.rsc +15 -15
  91. package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
  92. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  93. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  94. package/.next/server/app/nodes.segments/_tree.segment.rsc +3 -3
  95. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  96. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  97. package/.next/server/app/page/next-font-manifest.json +1 -1
  98. package/.next/server/app/page.js.nft.json +1 -1
  99. package/.next/server/app/page_client-reference-manifest.js +1 -1
  100. package/.next/server/app/server-logs/page/next-font-manifest.json +1 -1
  101. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  102. package/.next/server/app/server-logs.html +4 -4
  103. package/.next/server/app/server-logs.rsc +15 -15
  104. package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
  105. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  106. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  107. package/.next/server/app/server-logs.segments/_tree.segment.rsc +3 -3
  108. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  109. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  110. package/.next/server/app/settings/networks/page/next-font-manifest.json +1 -1
  111. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  112. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  113. package/.next/server/app/settings/networks.html +4 -4
  114. package/.next/server/app/settings/networks.rsc +15 -15
  115. package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
  116. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  117. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  118. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +3 -3
  119. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  120. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  121. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  122. package/.next/server/app/settings/page/next-font-manifest.json +1 -1
  123. package/.next/server/app/settings/page.js.nft.json +1 -1
  124. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  125. package/.next/server/app/settings/tokens/page/next-font-manifest.json +1 -1
  126. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  127. package/.next/server/app/settings/tokens.html +4 -4
  128. package/.next/server/app/settings/tokens.rsc +15 -15
  129. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
  130. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  131. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  132. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +3 -3
  133. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  134. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  135. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  136. package/.next/server/app/settings.html +4 -4
  137. package/.next/server/app/settings.rsc +15 -15
  138. package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  139. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  140. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  141. package/.next/server/app/settings.segments/_tree.segment.rsc +3 -3
  142. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  143. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  144. package/.next/server/app/tasks/[id]/page/next-font-manifest.json +1 -1
  145. package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  146. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  147. package/.next/server/app/tasks/page/next-font-manifest.json +1 -1
  148. package/.next/server/app/tasks/page.js.nft.json +1 -1
  149. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  150. package/.next/server/app/tasks.html +4 -4
  151. package/.next/server/app/tasks.rsc +15 -15
  152. package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
  153. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  154. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  155. package/.next/server/app/tasks.segments/_tree.segment.rsc +3 -3
  156. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  157. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  158. package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js +3 -0
  159. package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js.map +1 -0
  160. package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js +3 -0
  161. package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js.map +1 -0
  162. package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js +3 -0
  163. package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js.map +1 -0
  164. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +1 -1
  165. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +1 -1
  166. package/.next/server/chunks/ssr/_0nne7mz._.js +3 -0
  167. package/.next/server/chunks/ssr/_0nne7mz._.js.map +1 -0
  168. package/.next/server/chunks/ssr/app_0.zbtn5._.js +9 -0
  169. package/.next/server/chunks/ssr/app_0.zbtn5._.js.map +1 -0
  170. package/.next/server/chunks/ssr/app_01y-~mp._.js +3 -0
  171. package/.next/server/chunks/ssr/app_01y-~mp._.js.map +1 -0
  172. package/.next/server/chunks/ssr/app_04n78pr._.js +3 -0
  173. package/.next/server/chunks/ssr/app_04n78pr._.js.map +1 -0
  174. package/.next/server/chunks/ssr/app_0_agk0r._.js +3 -0
  175. package/.next/server/chunks/ssr/app_0_agk0r._.js.map +1 -0
  176. package/.next/server/chunks/ssr/app_0aenxef._.js +3 -0
  177. package/.next/server/chunks/ssr/app_0aenxef._.js.map +1 -0
  178. package/.next/server/chunks/ssr/app_0aum_g5._.js +3 -0
  179. package/.next/server/chunks/ssr/app_0aum_g5._.js.map +1 -0
  180. package/.next/server/chunks/ssr/app_0emvkn2._.js +3 -0
  181. package/.next/server/chunks/ssr/app_0emvkn2._.js.map +1 -0
  182. package/.next/server/chunks/ssr/app_0esuljp._.js +3 -0
  183. package/.next/server/chunks/ssr/app_0esuljp._.js.map +1 -0
  184. package/.next/server/chunks/ssr/app_0izi28j._.js +1 -1
  185. package/.next/server/chunks/ssr/app_0izi28j._.js.map +1 -1
  186. package/.next/server/chunks/ssr/app_components_08ys5y3._.js +5 -5
  187. package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +1 -1
  188. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js +1 -1
  189. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js.map +1 -1
  190. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
  191. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
  192. package/.next/server/middleware-build-manifest.js +3 -3
  193. package/.next/server/next-font-manifest.js +1 -1
  194. package/.next/server/next-font-manifest.json +14 -14
  195. package/.next/server/pages/404.html +4 -4
  196. package/.next/server/pages/500.html +1 -1
  197. package/.next/server/server-reference-manifest.js +1 -1
  198. package/.next/server/server-reference-manifest.json +1 -1
  199. package/.next/static/chunks/0-hlcuoemhe7w.js +7 -0
  200. package/.next/static/chunks/02_zpo7taex9o.js +7 -0
  201. package/.next/static/chunks/03b6jkic-1r9u.css +2 -0
  202. package/.next/static/chunks/06fliypy0.f0r.js +1 -0
  203. package/.next/static/chunks/06n3d~fbbtagk.js +1 -0
  204. package/.next/static/chunks/07eba1lz2u3p4.js +1 -0
  205. package/.next/static/chunks/0av2~0.96~d~g.js +1 -0
  206. package/.next/static/chunks/{07okhs5561y5y.js → 0c1bp~g7.8ru..js} +1 -1
  207. package/.next/static/chunks/0g9aof0ob_fcj.js +1 -0
  208. package/.next/static/chunks/0gtbwcilfu6b7.js +1 -0
  209. package/.next/static/chunks/0s8jo6jypzkx5.js +1 -0
  210. package/.next/static/chunks/0ubqqhhpsaez4.js +1 -0
  211. package/.next/static/chunks/0vmfu_gz9tp4f.js +1 -0
  212. package/.next/static/chunks/0~qw9kxksr1v2.js +1 -0
  213. package/.next/static/chunks/12gprw3l0kvvf.js +1 -0
  214. package/.next/static/chunks/12mjtyh-j74~k.js +1 -0
  215. package/.next/static/chunks/12yuo353giomx.js +1 -0
  216. package/.next/static/media/4fa387ec64143e14-s.0.qu-9752pffj.woff2 +0 -0
  217. package/.next/static/media/5ce348bf30bf5439-s.0ee55_hj9qcer.woff2 +0 -0
  218. package/.next/static/media/6306c77e7c8268e4-s.0mao5jbfbduzp.woff2 +0 -0
  219. package/.next/static/media/797e433ab948586e-s.p.09zddjkbdep5a.woff2 +0 -0
  220. package/.next/static/media/7d817b4c03b0c5f1-s.0uzt.a6d44yda.woff2 +0 -0
  221. package/.next/static/media/bbc41e54d2fcbd21-s.0mvwgmnhv29no.woff2 +0 -0
  222. package/.next/trace +2 -2
  223. package/.next/trace-build +1 -1
  224. package/app/admin/page.tsx +97 -27
  225. package/app/components/AgentCard.tsx +19 -5
  226. package/app/components/BroadcastBar.tsx +22 -4
  227. package/app/components/DispatchPanel.tsx +9 -5
  228. package/app/components/EmptyState.tsx +127 -12
  229. package/app/components/HealthBanner.tsx +11 -2
  230. package/app/components/HelpOverlay.tsx +50 -10
  231. package/app/components/InboxPanel.tsx +7 -4
  232. package/app/components/Sidebar.tsx +81 -44
  233. package/app/components/StatsBar.tsx +59 -36
  234. package/app/components/TaskChatPanel.tsx +8 -10
  235. package/app/components/TaskDrawer.tsx +72 -28
  236. package/app/components/TopoGraph.tsx +527 -41
  237. package/app/components/utils.ts +13 -0
  238. package/app/globals.css +90 -0
  239. package/app/lib/status.ts +85 -0
  240. package/app/lib/version.ts +7 -0
  241. package/app/login/page.tsx +40 -7
  242. package/app/logs/page.tsx +70 -21
  243. package/app/messages/page.tsx +34 -9
  244. package/app/node/page.tsx +52 -27
  245. package/app/nodes/page.tsx +45 -21
  246. package/app/page.tsx +103 -32
  247. package/app/server-logs/page.tsx +104 -28
  248. package/app/settings/networks/page.tsx +40 -16
  249. package/app/settings/page.tsx +39 -11
  250. package/app/tasks/[id]/page.tsx +2 -13
  251. package/app/tasks/page.tsx +66 -73
  252. package/package.json +1 -1
  253. package/public/intern_avatar.png +0 -0
  254. package/scripts/empty-state-test.mjs +41 -0
  255. package/scripts/fold-only.mjs +47 -0
  256. package/scripts/intern-alias-test.mjs +46 -0
  257. package/scripts/loop-review.mjs +270 -0
  258. package/scripts/network-derive-shot.mjs +16 -0
  259. package/scripts/network-derive-test.mjs +28 -0
  260. package/scripts/topo-brand-shot.mjs +61 -0
  261. package/scripts/topo-cluster-test.mjs +41 -0
  262. package/scripts/topo-controls-shot.mjs +24 -0
  263. package/scripts/topo-group-test.mjs +37 -0
  264. package/scripts/topo-inspect.mjs +56 -0
  265. package/scripts/topo-interaction-test.mjs +80 -0
  266. package/scripts/topo-mock-large.mjs +58 -0
  267. package/scripts/topo-r104-test.mjs +54 -0
  268. package/scripts/topo-shot.mjs +31 -0
  269. package/.next/server/chunks/ssr/[root-of-the-server]__06w_u7t._.js +0 -3
  270. package/.next/server/chunks/ssr/[root-of-the-server]__06w_u7t._.js.map +0 -1
  271. package/.next/server/chunks/ssr/[root-of-the-server]__0ehu0jb._.js +0 -3
  272. package/.next/server/chunks/ssr/[root-of-the-server]__0ehu0jb._.js.map +0 -1
  273. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js +0 -3
  274. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js.map +0 -1
  275. package/.next/server/chunks/ssr/_13_v7-3._.js +0 -3
  276. package/.next/server/chunks/ssr/_13_v7-3._.js.map +0 -1
  277. package/.next/server/chunks/ssr/app_0.0wpa~._.js +0 -3
  278. package/.next/server/chunks/ssr/app_0.0wpa~._.js.map +0 -1
  279. package/.next/server/chunks/ssr/app_0ap_oiq._.js +0 -3
  280. package/.next/server/chunks/ssr/app_0ap_oiq._.js.map +0 -1
  281. package/.next/server/chunks/ssr/app_0dk5wao._.js +0 -3
  282. package/.next/server/chunks/ssr/app_0dk5wao._.js.map +0 -1
  283. package/.next/server/chunks/ssr/app_0et5yua._.js +0 -3
  284. package/.next/server/chunks/ssr/app_0et5yua._.js.map +0 -1
  285. package/.next/server/chunks/ssr/app_10b5blc._.js +0 -9
  286. package/.next/server/chunks/ssr/app_10b5blc._.js.map +0 -1
  287. package/.next/server/chunks/ssr/app_11vl967._.js +0 -3
  288. package/.next/server/chunks/ssr/app_11vl967._.js.map +0 -1
  289. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js +0 -3
  290. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js.map +0 -1
  291. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +0 -3
  292. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +0 -1
  293. package/.next/static/chunks/0-.hjh--kcdiy.js +0 -1
  294. package/.next/static/chunks/02afyxgpty3uf.js +0 -1
  295. package/.next/static/chunks/02flrewxlf_m_.js +0 -1
  296. package/.next/static/chunks/051gslfjka1kj.js +0 -1
  297. package/.next/static/chunks/07oobuohrpigy.js +0 -1
  298. package/.next/static/chunks/09al3qm76q7nd.js +0 -1
  299. package/.next/static/chunks/0e7c40p~wq665.js +0 -1
  300. package/.next/static/chunks/0g7m7zk.v7g6h.js +0 -1
  301. package/.next/static/chunks/0h.ajsk_uxigl.js +0 -1
  302. package/.next/static/chunks/0msa~bub38ytf.css +0 -2
  303. package/.next/static/chunks/0ogeals5ufhho.js +0 -1
  304. package/.next/static/chunks/10ir.g.n~hre0.js +0 -7
  305. package/.next/static/chunks/10njj7kn9ylkf.js +0 -1
  306. package/.next/static/chunks/12dcb86elsl-e.js +0 -7
  307. package/.next/static/chunks/13.dst7liziwo.js +0 -1
  308. package/.next/static/chunks/132e__998_q9r.js +0 -1
  309. package/.next/static/media/4fa387ec64143e14-s.0q3udbd2bu5yp.woff2 +0 -0
  310. package/.next/static/media/797e433ab948586e-s.p.0.q-h669a_dqa.woff2 +0 -0
  311. package/.next/static/media/bbc41e54d2fcbd21-s.0gw~uztddq1df.woff2 +0 -0
  312. package/public/file.svg +0 -1
  313. package/public/globe.svg +0 -1
  314. package/public/next.svg +0 -1
  315. package/public/vercel.svg +0 -1
  316. package/public/window.svg +0 -1
  317. /package/.next/static/{stSZtBF9uGYc3L399m0h1 → u-HIIfy-O0UVQ02APphkX}/_buildManifest.js +0 -0
  318. /package/.next/static/{stSZtBF9uGYc3L399m0h1 → u-HIIfy-O0UVQ02APphkX}/_clientMiddlewareManifest.js +0 -0
  319. /package/.next/static/{stSZtBF9uGYc3L399m0h1 → u-HIIfy-O0UVQ02APphkX}/_ssgManifest.js +0 -0
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useMemo, useState } from 'react';
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { Session } from './types';
5
+ import { aliasAvatarColors, aliasInitial } from './AliasAvatar';
5
6
 
6
7
  interface MessageFlow {
7
8
  from_alias: string;
@@ -31,7 +32,19 @@ interface FlowLink {
31
32
  const cx = 500;
32
33
  const cy = 330;
33
34
  const onlineRadius = 220;
34
- const offlineRadius = 315;
35
+ // Round 97 (issue #50): tier into two rings when N > 8; round 98
36
+ // (issue #61): tier into three rings when N > 14. At N=22 (Vincent's
37
+ // network) two rings still leave 11 nodes per ring → inner chord of
38
+ // 88px can't fit a 100px label; three rings give ~⌈N/3⌉ per ring so
39
+ // every tier has enough arc room.
40
+ const onlineTierThreshold = 8;
41
+ const onlineTripleThreshold = 14;
42
+ const onlineInnerRadius = 175;
43
+ const onlineOuterRadius = 260;
44
+ const onlineTripleInnerR = 145;
45
+ const onlineTripleMidR = 215;
46
+ const onlineTripleOuterR = 285;
47
+ const offlineRadius = 325;
35
48
 
36
49
  /** Polar coordinate for a node on a ring. `rotateBy` lets the caller offset
37
50
  * the whole ring so two stacked rings don't align radially (round 25: the
@@ -184,6 +197,64 @@ function useTheme(): 'light' | 'dark' {
184
197
  return theme;
185
198
  }
186
199
 
200
+ /** Round 100 (issue #79): brand showcase mode. Activate via `?brand=intern`
201
+ * on any dashboard page — TopoGraph nodes show the 书小生 mascot instead
202
+ * of alias initials. Stored in localStorage so the flag persists across
203
+ * navigation. Clears with `?brand=none` or `?brand=` (empty). */
204
+ function useBrand(): string | null {
205
+ const [brand, setBrand] = useState<string | null>(null);
206
+ useEffect(() => {
207
+ try {
208
+ const url = new URL(window.location.href);
209
+ const param = url.searchParams.get('brand');
210
+ if (param !== null) {
211
+ if (param === '' || param === 'none') {
212
+ localStorage.removeItem('anet-brand');
213
+ setBrand(null);
214
+ } else {
215
+ localStorage.setItem('anet-brand', param);
216
+ setBrand(param);
217
+ }
218
+ } else {
219
+ setBrand(localStorage.getItem('anet-brand'));
220
+ }
221
+ } catch {}
222
+ }, []);
223
+ return brand;
224
+ }
225
+
226
+ /** Round 106 (issue #83): cluster agents by shared alias prefix so a team
227
+ * reads as one unit in the topology. Adjacent (sorted) aliases that share
228
+ * a ≥2-char prefix join the same group; the group key is the prefix common
229
+ * to every member. Singletons map to their own alias (no hue shift). The
230
+ * group key feeds aliasAvatarColors() so e.g. all 通信* nodes get one hue,
231
+ * all 研究员* another — the "同色相 tint" clustering option from #83. */
232
+ function commonPrefix(a: string, b: string): string {
233
+ let i = 0;
234
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
235
+ return a.slice(0, i);
236
+ }
237
+
238
+ function computeGroupKeys(aliases: string[]): Record<string, string> {
239
+ const sorted = [...aliases].sort((a, b) => a.localeCompare(b));
240
+ const keys: Record<string, string> = {};
241
+ let i = 0;
242
+ while (i < sorted.length) {
243
+ let j = i;
244
+ let prefix = sorted[i];
245
+ while (j + 1 < sorted.length) {
246
+ const lcp = commonPrefix(prefix, sorted[j + 1]);
247
+ if (lcp.length < 2) break;
248
+ prefix = lcp;
249
+ j++;
250
+ }
251
+ const key = j > i ? prefix : sorted[i];
252
+ for (let k = i; k <= j; k++) keys[sorted[k]] = key;
253
+ i = j + 1;
254
+ }
255
+ return keys;
256
+ }
257
+
187
258
  function buildFlowLinks(messages: MessageFlow[], positions: Record<string, Point>) {
188
259
  const links = new Map<string, FlowLink>();
189
260
 
@@ -215,6 +286,8 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
215
286
  const theme = useTheme();
216
287
  const isLight = theme === 'light';
217
288
  const pal = isLight ? LIGHT_PALETTE : DARK_PALETTE;
289
+ const brand = useBrand();
290
+ const isIntern = brand === 'intern';
218
291
  const [messages, setMessages] = useState<MessageFlow[]>([]);
219
292
 
220
293
  useEffect(() => {
@@ -241,24 +314,73 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
241
314
  nodePositions,
242
315
  flowLinks,
243
316
  activeAliases,
317
+ groupKeys,
244
318
  } = useMemo(() => {
245
319
  const sseCount = (s: { alias: string; network_id?: string }) =>
246
320
  (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];
247
- const online = sessions.filter(s => s.status !== 'offline' || sseCount(s));
248
- const offline = sessions.filter(s => s.status === 'offline' && !sseCount(s));
321
+ // Round 106 (issue #83): sort by alias so same-prefix agents
322
+ // (通信龙 / 通信牛 / 通信工程马 …, or 研究员1号 / 研究员2号 …) end up
323
+ // adjacent in the array — the tier layout below assigns angles by
324
+ // index, so contiguous-in-array becomes contiguous-in-ring, i.e.
325
+ // each team visually clusters. localeCompare keeps CJK ordering sane.
326
+ const byAlias = (a: Session, b: Session) => a.alias.localeCompare(b.alias);
327
+ const online = sessions.filter(s => s.status !== 'offline' || sseCount(s)).sort(byAlias);
328
+ const offline = sessions.filter(s => s.status === 'offline' && !sseCount(s)).sort(byAlias);
249
329
  const positions: Record<string, Point> = {};
250
330
 
251
- online.forEach((s, index) => {
252
- positions[s.alias] = polarPoint(index, Math.max(online.length, 1), onlineRadius);
253
- });
331
+ // Round 97 (issue #50) + 98 (issue #61): three layout modes by N.
332
+ // N ≤ 8 → single ring (r=220)
333
+ // 8 < N ≤ 14 → two rings (inner r=175, outer r=260, half-step rot)
334
+ // N > 14 → three rings (r=145/215/285, ⌈N/3⌉ per ring)
335
+ // Each ring's spread is 1.78π so its node count drives chord length:
336
+ // labels are 100px wide so each ring needs ≥110px chord per node.
337
+ const tripleTier = online.length > onlineTripleThreshold;
338
+ const dualTier = !tripleTier && online.length > onlineTierThreshold;
339
+ let outerOnlineCount = online.length;
340
+ if (tripleTier) {
341
+ const per = Math.ceil(online.length / 3);
342
+ const r1 = online.slice(0, per);
343
+ const r2 = online.slice(per, 2 * per);
344
+ const r3 = online.slice(2 * per);
345
+ r1.forEach((s, index) => {
346
+ positions[s.alias] = polarPoint(index, Math.max(r1.length, 1), onlineTripleInnerR);
347
+ });
348
+ const r1Spread = r1.length <= 2 ? Math.PI : Math.PI * 1.78;
349
+ const r1Step = r1.length > 1 ? r1Spread / (r1.length - 1) : 0;
350
+ r2.forEach((s, index) => {
351
+ positions[s.alias] = polarPoint(index, Math.max(r2.length, 1), onlineTripleMidR, r1Step / 2);
352
+ });
353
+ r3.forEach((s, index) => {
354
+ positions[s.alias] = polarPoint(index, Math.max(r3.length, 1), onlineTripleOuterR);
355
+ });
356
+ outerOnlineCount = r3.length;
357
+ } else if (dualTier) {
358
+ const innerCount = Math.ceil(online.length / 2);
359
+ const outerCount = online.length - innerCount;
360
+ const innerNodes = online.slice(0, innerCount);
361
+ const outerNodes = online.slice(innerCount);
362
+ innerNodes.forEach((s, index) => {
363
+ positions[s.alias] = polarPoint(index, Math.max(innerCount, 1), onlineInnerRadius);
364
+ });
365
+ const innerSpread = innerCount <= 2 ? Math.PI : Math.PI * 1.78;
366
+ const innerStep = innerCount > 1 ? innerSpread / (innerCount - 1) : 0;
367
+ outerNodes.forEach((s, index) => {
368
+ positions[s.alias] = polarPoint(index, Math.max(outerCount, 1), onlineOuterRadius, innerStep / 2);
369
+ });
370
+ outerOnlineCount = outerCount;
371
+ } else {
372
+ online.forEach((s, index) => {
373
+ positions[s.alias] = polarPoint(index, Math.max(online.length, 1), onlineRadius);
374
+ });
375
+ }
254
376
 
255
- // Offset the offline ring radially by half the online step so offline
256
- // bubbles sit in the angular gaps between online bubbles instead of
257
- // stacking directly behind them. Also push the outer ring further when
377
+ // Offset the offline ring radially by half the outermost online step so
378
+ // offline bubbles sit in the angular gaps between online bubbles instead
379
+ // of stacking directly behind them. Also push the outer ring further when
258
380
  // there are many offline nodes so labels don't crowd the legend.
259
- const spread = online.length <= 2 ? Math.PI : Math.PI * 1.78;
260
- const onlineStep = online.length > 1 ? spread / (online.length - 1) : 0;
261
- const offlineRotation = online.length > 0 ? onlineStep / 2 : 0;
381
+ const outerSpreadBase = outerOnlineCount <= 2 ? Math.PI : Math.PI * 1.78;
382
+ const outerStep = outerOnlineCount > 1 ? outerSpreadBase / (outerOnlineCount - 1) : 0;
383
+ const offlineRotation = outerOnlineCount > 0 ? outerStep / 2 : 0;
262
384
  const offlineR = offlineRadius + Math.max(0, offline.length - 4) * 6;
263
385
 
264
386
  offline.forEach((s, index) => {
@@ -272,17 +394,151 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
272
394
  active.add(link.to);
273
395
  });
274
396
 
397
+ // Round 106 (issue #83): group key per alias → shared hue per team.
398
+ const groupKeys = computeGroupKeys([...online, ...offline].map(s => s.alias));
399
+
275
400
  return {
276
401
  onlineNodes: online,
277
402
  offlineNodes: offline,
278
403
  nodePositions: positions,
279
404
  flowLinks: links,
280
405
  activeAliases: active,
406
+ groupKeys,
281
407
  };
282
408
  }, [messages, sessions, sseSessions]);
283
409
 
284
410
  const workingCount = onlineNodes.filter(s => s.status === 'working').length;
285
411
 
412
+ // --- Round 103 (issue #81): fullscreen + zoom + pan interaction layer ---
413
+ // DIY native (no d3 / svg-pan-zoom): wrap the topology content in a single
414
+ // <g transform> and drive it with wheel + pointer-drag. The panel <rect>
415
+ // backdrop stays fixed so panning never reveals empty canvas. View state
416
+ // {zoom,x,y} persists to localStorage (same sticky pattern as brand flag).
417
+ const VIEWBOX_W = 1000;
418
+ const VIEWBOX_H = 680;
419
+ const ZOOM_MIN = 0.5;
420
+ const ZOOM_MAX = 4;
421
+ const containerRef = useRef<HTMLDivElement>(null);
422
+ const svgRef = useRef<SVGSVGElement>(null);
423
+ const [view, setView] = useState({ zoom: 1, x: 0, y: 0 });
424
+ const viewRef = useRef(view);
425
+ const dragRef = useRef({ active: false, startX: 0, startY: 0, baseX: 0, baseY: 0 });
426
+ const [isFullscreen, setIsFullscreen] = useState(false);
427
+
428
+ useEffect(() => { viewRef.current = view; }, [view]);
429
+
430
+ // restore persisted view once on mount
431
+ useEffect(() => {
432
+ try {
433
+ const raw = localStorage.getItem('anet-topo-view');
434
+ if (raw) {
435
+ const v = JSON.parse(raw);
436
+ if (typeof v?.zoom === 'number') {
437
+ setView({
438
+ zoom: Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, v.zoom)),
439
+ x: typeof v.x === 'number' ? v.x : 0,
440
+ y: typeof v.y === 'number' ? v.y : 0,
441
+ });
442
+ }
443
+ }
444
+ } catch {}
445
+ }, []);
446
+
447
+ // persist view
448
+ useEffect(() => {
449
+ try { localStorage.setItem('anet-topo-view', JSON.stringify(view)); } catch {}
450
+ }, [view]);
451
+
452
+ // track fullscreen state (button label + Esc-exit sync)
453
+ useEffect(() => {
454
+ const onFsChange = () => setIsFullscreen(document.fullscreenElement === containerRef.current);
455
+ document.addEventListener('fullscreenchange', onFsChange);
456
+ return () => document.removeEventListener('fullscreenchange', onFsChange);
457
+ }, []);
458
+
459
+ // wheel zoom — native non-passive listener so preventDefault() actually
460
+ // stops the page from scrolling. Uses functional setState so the listener
461
+ // never goes stale and can attach once.
462
+ useEffect(() => {
463
+ const svg = svgRef.current;
464
+ if (!svg) return;
465
+ const onWheel = (e: WheelEvent) => {
466
+ e.preventDefault();
467
+ const rect = svg.getBoundingClientRect();
468
+ const mx = ((e.clientX - rect.left) / rect.width) * VIEWBOX_W;
469
+ const my = ((e.clientY - rect.top) / rect.height) * VIEWBOX_H;
470
+ setView(prev => {
471
+ // Round 104 (issue #81 follow-up): Vincent 实测 zoom 太灵敏. The
472
+ // old factor was a fixed 1.15x per wheel *event* — fine for a
473
+ // mouse notch but a trackpad fires dozens of events per gesture
474
+ // so it compounded into huge jumps. Scale the factor by deltaY
475
+ // magnitude with a small coefficient, then clamp the per-event
476
+ // change so no single tick moves more than ~8%.
477
+ const factor = Math.min(1.08, Math.max(0.926, Math.exp(-e.deltaY * 0.0006)));
478
+ const nz = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, prev.zoom * factor));
479
+ const ratio = nz / prev.zoom;
480
+ // keep the point under the cursor stationary
481
+ return { zoom: nz, x: mx - (mx - prev.x) * ratio, y: my - (my - prev.y) * ratio };
482
+ });
483
+ };
484
+ svg.addEventListener('wheel', onWheel, { passive: false });
485
+ return () => svg.removeEventListener('wheel', onWheel);
486
+ }, []);
487
+
488
+ const onPointerDown = (e: React.PointerEvent<SVGSVGElement>) => {
489
+ if (e.button !== 0) return;
490
+ (e.currentTarget as Element).setPointerCapture?.(e.pointerId);
491
+ dragRef.current = {
492
+ active: true,
493
+ startX: e.clientX,
494
+ startY: e.clientY,
495
+ baseX: viewRef.current.x,
496
+ baseY: viewRef.current.y,
497
+ };
498
+ };
499
+ const onPointerMove = (e: React.PointerEvent<SVGSVGElement>) => {
500
+ const d = dragRef.current;
501
+ if (!d.active) return;
502
+ const svg = svgRef.current;
503
+ if (!svg) return;
504
+ const rect = svg.getBoundingClientRect();
505
+ const dx = ((e.clientX - d.startX) / rect.width) * VIEWBOX_W;
506
+ const dy = ((e.clientY - d.startY) / rect.height) * VIEWBOX_H;
507
+ setView(prev => ({ ...prev, x: d.baseX + dx, y: d.baseY + dy }));
508
+ };
509
+ const onPointerUp = (e: React.PointerEvent<SVGSVGElement>) => {
510
+ if (!dragRef.current.active) return;
511
+ dragRef.current.active = false;
512
+ try {
513
+ (e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
514
+ } catch {}
515
+ };
516
+
517
+ // zoom buttons — zoom around the canvas center
518
+ const zoomBy = (factor: number) => {
519
+ setView(prev => {
520
+ const nz = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, prev.zoom * factor));
521
+ const ratio = nz / prev.zoom;
522
+ const cx0 = VIEWBOX_W / 2;
523
+ const cy0 = VIEWBOX_H / 2;
524
+ return { zoom: nz, x: cx0 - (cx0 - prev.x) * ratio, y: cy0 - (cy0 - prev.y) * ratio };
525
+ });
526
+ };
527
+ const resetView = () => setView({ zoom: 1, x: 0, y: 0 });
528
+
529
+ const toggleFullscreen = () => {
530
+ const el = containerRef.current;
531
+ if (!el) return;
532
+ if (document.fullscreenElement) {
533
+ document.exitFullscreen?.();
534
+ } else {
535
+ const req =
536
+ el.requestFullscreen ||
537
+ (el as unknown as { webkitRequestFullscreen?: () => void }).webkitRequestFullscreen;
538
+ req?.call(el);
539
+ }
540
+ };
541
+
286
542
  return (
287
543
  <section className="w-full max-w-6xl mx-auto mb-8">
288
544
  <div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between mb-3 px-1">
@@ -304,12 +560,23 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
304
560
  </div>
305
561
 
306
562
  <div
307
- className={`relative overflow-hidden rounded-lg border shadow-2xl ${isLight ? 'shadow-zinc-900/5' : 'shadow-cyan-950/30'}`}
563
+ ref={containerRef}
564
+ className={`relative overflow-hidden rounded-lg border shadow-2xl ${isLight ? 'shadow-zinc-900/5' : 'shadow-cyan-950/30'} ${isFullscreen ? 'flex items-center justify-center' : ''}`}
308
565
  style={{ background: pal.containerBg, borderColor: pal.containerBorder }}
309
566
  >
310
567
  <div className={`absolute inset-x-0 top-0 h-px bg-gradient-to-r ${pal.topRailGradient}`} />
311
568
 
312
- <svg viewBox="0 0 1000 680" className="w-full h-auto block" preserveAspectRatio="xMidYMid meet">
569
+ <svg
570
+ ref={svgRef}
571
+ viewBox="0 0 1000 680"
572
+ className="w-full h-auto block"
573
+ preserveAspectRatio="xMidYMid meet"
574
+ onPointerDown={onPointerDown}
575
+ onPointerMove={onPointerMove}
576
+ onPointerUp={onPointerUp}
577
+ onPointerLeave={onPointerUp}
578
+ style={{ cursor: 'grab', touchAction: 'none' }}
579
+ >
313
580
  <defs>
314
581
  <linearGradient id="topo-panel" x1="0" x2="1" y1="0" y2="1">
315
582
  <stop offset="0%" stopColor={pal.panelStops[0]} />
@@ -333,15 +600,90 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
333
600
  <marker id="topo-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
334
601
  <path d="M 0 0 L 10 5 L 0 10 z" fill={pal.arrowFill} />
335
602
  </marker>
603
+ {/* Round 45: radial radar sweep gradient — bright at center
604
+ (where it meets the hub) fading to leading edge. */}
605
+ <radialGradient id="topo-sweep" cx="0%" cy="50%" r="100%">
606
+ <stop offset="0%" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity={isLight ? 0.18 : 0.32} />
607
+ <stop offset="70%" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity={isLight ? 0.10 : 0.18} />
608
+ <stop offset="100%" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity="0" />
609
+ </radialGradient>
336
610
  </defs>
337
611
 
612
+ {/* panel backdrop stays fixed — panning never reveals empty canvas */}
338
613
  <rect width="1000" height="680" fill="url(#topo-panel)" />
614
+
615
+ {/* Round 103 (issue #81): everything inside this <g> zooms + pans
616
+ together. transform order = translate then scale. */}
617
+ <g transform={`translate(${view.x} ${view.y}) scale(${view.zoom})`}>
339
618
  <circle cx={cx} cy={cy} r="330" fill="url(#topo-radar)" />
340
619
 
620
+ {/* Round 45: subtle star field — 24 deterministic dots scattered
621
+ across the canvas give the radar bg some depth. Skipped on
622
+ light theme so the white surface stays clean. */}
623
+ {!isLight && (
624
+ <g opacity="0.5">
625
+ {Array.from({ length: 28 }).map((_, i) => {
626
+ // Deterministic pseudo-random scatter so positions are
627
+ // stable between renders (no JS hydration mismatch).
628
+ const seed = i * 9301 + 49297;
629
+ const x = ((seed * 13) % 1000);
630
+ const y = ((seed * 7) % 680);
631
+ const r = (i % 3 === 0) ? 1.2 : 0.7;
632
+ return <circle key={i} cx={x} cy={y} r={r} fill="#a5b4fc" opacity={0.35 + (i % 4) * 0.05} />;
633
+ })}
634
+ </g>
635
+ )}
636
+
637
+ {/* Round 45: rotating radar sweep — a 40° wedge with a soft
638
+ leading-edge gradient. Slow 6s rotation reads as a radar
639
+ scan without being noisy. Inline transform-origin on the
640
+ <g> wrapper ensures Chrome / Firefox rotate around (cx,cy)
641
+ instead of the SVG viewBox corner. */}
642
+ <g
643
+ style={{
644
+ transformOrigin: `${cx}px ${cy}px`,
645
+ transformBox: 'view-box',
646
+ }}
647
+ className="anet-topo-sweep"
648
+ opacity={isLight ? 0.7 : 1}
649
+ >
650
+ <path
651
+ d={`M ${cx} ${cy} L ${cx + 330} ${cy} A 330 330 0 0 0 ${cx + 330 * Math.cos(-Math.PI / 4.5)} ${cy + 330 * Math.sin(-Math.PI / 4.5)} Z`}
652
+ fill="url(#topo-sweep)"
653
+ />
654
+ </g>
655
+
341
656
  {/* radar rings */}
342
657
  {[90, 170, 250, 330].map(radius => (
343
658
  <circle key={radius} cx={cx} cy={cy} r={radius} fill="none" stroke={pal.ringStroke} strokeWidth="1" opacity={isLight ? 0.6 : 0.35} />
344
659
  ))}
660
+
661
+ {/* Round 50: 4 small particles slowly orbiting the outer ring
662
+ (r=330). Each starts at a different angle (offset 0/0.25/0.5/0.75
663
+ of the cycle) so they're evenly spaced. 16s per revolution is
664
+ slow enough to feel ambient, not noisy. Skipped on light theme
665
+ so the white surface stays clean. */}
666
+ {!isLight && [0, 0.25, 0.5, 0.75].map((phase, i) => (
667
+ <g key={`orbit-${i}`}>
668
+ <circle
669
+ cx={cx + 330} cy={cy}
670
+ r={i === 0 ? 2.8 : 2.2}
671
+ fill="#22d3ee"
672
+ opacity={0.9}
673
+ filter="url(#topo-glow)"
674
+ >
675
+ <animateTransform
676
+ attributeName="transform"
677
+ type="rotate"
678
+ from={`${phase * 360} ${cx} ${cy}`}
679
+ to={`${phase * 360 + 360} ${cx} ${cy}`}
680
+ dur="16s"
681
+ repeatCount="indefinite"
682
+ />
683
+ </circle>
684
+ </g>
685
+ ))}
686
+
345
687
  {[0, 30, 60, 90, 120, 150].map(angle => (
346
688
  <line
347
689
  key={angle}
@@ -355,21 +697,27 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
355
697
  />
356
698
  ))}
357
699
 
358
- {/* hub links */}
359
- {onlineNodes.map(session => {
700
+ {/* hub links — round 46: idle spokes now have animated
701
+ stroke-dashoffset so dashes flow outward from the hub
702
+ ("command relay" feel). Active spokes carrying live message
703
+ flow stay as solid bright strokes. */}
704
+ {onlineNodes.map((session, idx) => {
360
705
  const pos = nodePositions[session.alias];
361
706
  if (!pos) return null;
362
707
  const path = curvePath({ x: cx, y: cy }, pos, 0);
708
+ const isActiveSpoke = activeAliases.has(session.alias);
363
709
 
364
710
  return (
365
711
  <path
366
712
  key={`hub-${session.alias}`}
367
713
  d={path}
368
714
  fill="none"
369
- stroke={activeAliases.has(session.alias) ? pal.spokeStroke.active : pal.spokeStroke.idle}
370
- strokeWidth={activeAliases.has(session.alias) ? 2 : 1}
371
- strokeDasharray={activeAliases.has(session.alias) ? 'none' : '8 10'}
372
- opacity={activeAliases.has(session.alias) ? 0.65 : 0.35}
715
+ stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}
716
+ strokeWidth={isActiveSpoke ? 2 : 1}
717
+ strokeDasharray={isActiveSpoke ? 'none' : '6 14'}
718
+ opacity={isActiveSpoke ? 0.7 : 0.45}
719
+ className={isActiveSpoke ? undefined : 'anet-topo-spoke-flow'}
720
+ style={isActiveSpoke ? undefined : { animationDelay: `${-(idx * 0.25)}s` }}
373
721
  />
374
722
  );
375
723
  })}
@@ -412,27 +760,32 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
412
760
  );
413
761
  })}
414
762
 
415
- {/* center hub — 24px pulse signal source.
416
- Previous design (56px filled circle + 42px animated ring +
417
- `+` glyph + "DASHBOARD" / "command relay" labels) over-emphasized
418
- the hub and competed with the agent nodes for visual weight.
419
- The new design is a calm 12px emerald source with two outward
420
- pulse rings — reads as "signal origin" without a placeholder
421
- logo or label. Closes agent-network#5. */}
763
+ {/* center hub — round 39: enlarged from r=6 (12px) to r=10 (20px)
764
+ core so the "control plane" reads as the network's anchor,
765
+ not a stray particle. Static halo ring at r=18 grounds it
766
+ visually; two outward pulses keep the "signal source" idea
767
+ from r17. Light theme gets a paler core to avoid hot-spotting
768
+ the bg. */}
422
769
  <g>
770
+ {/* static grounding halo — sits underneath, low opacity */}
771
+ <circle
772
+ cx={cx} cy={cy} r="18"
773
+ fill={isLight ? '#d1fae5' : '#10b981'}
774
+ opacity={isLight ? 0.35 : 0.10}
775
+ />
423
776
  {/* outer pulse 1 */}
424
- <circle cx={cx} cy={cy} r="6" fill="none" stroke="#10b981" strokeWidth="1.5">
425
- <animate attributeName="r" values="6;28;6" dur="2.4s" repeatCount="indefinite" />
777
+ <circle cx={cx} cy={cy} r="10" fill="none" stroke="#10b981" strokeWidth="1.5">
778
+ <animate attributeName="r" values="10;38;10" dur="2.4s" repeatCount="indefinite" />
426
779
  <animate attributeName="opacity" values="0.55;0;0.55" dur="2.4s" repeatCount="indefinite" />
427
780
  </circle>
428
781
  {/* outer pulse 2 (delayed 1.2s) */}
429
- <circle cx={cx} cy={cy} r="6" fill="none" stroke="#10b981" strokeWidth="1.5" opacity="0">
430
- <animate attributeName="r" values="6;28;6" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
782
+ <circle cx={cx} cy={cy} r="10" fill="none" stroke="#10b981" strokeWidth="1.5" opacity="0">
783
+ <animate attributeName="r" values="10;38;10" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
431
784
  <animate attributeName="opacity" values="0.45;0;0.45" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
432
785
  </circle>
433
- {/* core dot — 12px diameter (r=6), emerald with inner highlight */}
434
- <circle cx={cx} cy={cy} r="6" fill="#10b981" />
435
- <circle cx={cx} cy={cy} r="3" fill="#d1fae5" opacity="0.9" />
786
+ {/* core — 20px diameter, larger inner highlight reads as a "lit lamp" */}
787
+ <circle cx={cx} cy={cy} r="10" fill={isLight ? '#059669' : '#10b981'} />
788
+ <circle cx={cx} cy={cy} r="5" fill="#d1fae5" opacity="0.9" />
436
789
  </g>
437
790
 
438
791
  {/* agent nodes */}
@@ -454,7 +807,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
454
807
  <animate attributeName="opacity" values={isLight ? '0.12;0.02;0.12' : '0.18;0.04;0.18'} dur="2.4s" repeatCount="indefinite" />
455
808
  </circle>
456
809
  )}
457
- <circle cx={pos.x} cy={pos.y} r={radius + 8} fill={status.halo} opacity={isOnline ? (isLight ? 0.85 : 0.55) : (isLight ? 0.4 : 0.25)} />
810
+ <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)} />
458
811
  <circle
459
812
  cx={pos.x}
460
813
  cy={pos.y}
@@ -465,17 +818,83 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
465
818
  strokeDasharray={isOnline ? 'none' : '5 5'}
466
819
  filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
467
820
  />
468
- <circle cx={pos.x} cy={pos.y} r={isOnline ? 8 : 5} fill={status.primary} />
821
+ {/* Round 99 (issue #79): node "avatar" — hue-hashed
822
+ initials circle inside the status ring. Round 100:
823
+ when brand=intern, swap the initials for the 书小生
824
+ mascot image (asset from 群星马 task 51dd0d1d). The
825
+ image already has a transparent bg so it sits cleanly
826
+ on the node's dark/light fill; the outer status ring
827
+ still carries working/idle/etc. */}
828
+ {(() => {
829
+ const ar = isOnline ? 14 : 10;
830
+ // Round 108 (issue #79 reopened): show the 书小生 avatar
831
+ // coin for any agent whose alias marks it as an Intern /
832
+ // 书生 runtime — not only under the global ?brand=intern
833
+ // flag. Vincent 4565: a fleet of 书生N号 nodes should SHOW
834
+ // 书生, not a generic "书" initial. ?brand=intern still
835
+ // forces the coin on every node (full brand showcase).
836
+ const isInternNode = isIntern || /书生|书小生|intern/i.test(session.alias);
837
+ if (isInternNode) {
838
+ // Round 102: self-contained "avatar coin" — 书小生 figure
839
+ // keyed off its white background and composited onto a
840
+ // cream circular backplate. The cream coin gives the
841
+ // dark-haired figure consistent contrast on BOTH the
842
+ // dark cyber theme and light theme. Render at the node
843
+ // diameter so the coin fills inside the status ring.
844
+ const size = radius * 2;
845
+ return (
846
+ <image
847
+ href="/intern_avatar.png"
848
+ x={pos.x - size / 2}
849
+ y={pos.y - size / 2}
850
+ width={size}
851
+ height={size}
852
+ preserveAspectRatio="xMidYMid meet"
853
+ />
854
+ );
855
+ }
856
+ // Round 106 (issue #83): hue keyed to the prefix group,
857
+ // not the full alias — every 通信* node shares one color,
858
+ // every 研究员* another, so teams cluster visually even
859
+ // when the tier layout spreads them across rings.
860
+ const c = aliasAvatarColors(groupKeys[session.alias] || session.alias);
861
+ const fs = isOnline ? 14 : 10;
862
+ return (
863
+ <>
864
+ <circle cx={pos.x} cy={pos.y} r={ar} fill={c.bg} stroke={c.ring} strokeWidth="1" />
865
+ <text
866
+ x={pos.x}
867
+ y={pos.y}
868
+ dy="0.34em"
869
+ textAnchor="middle"
870
+ fill={c.text}
871
+ fontSize={fs}
872
+ fontFamily="monospace"
873
+ fontWeight="700"
874
+ >
875
+ {aliasInitial(session.alias)}
876
+ </text>
877
+ </>
878
+ );
879
+ })()}
469
880
  {session.status === 'working' && (
470
- <path d={`M${pos.x - 9},${pos.y + 11} h18`} stroke={pal.flowParticle} strokeWidth="3" strokeLinecap="round">
881
+ // working pulse — small dot just inside the status ring,
882
+ // above the avatar text. Was a 18px horizontal bar at
883
+ // y+11 which now collides with the avatar; the dot reads
884
+ // as the same "active" cue without occluding the initial.
885
+ <circle cx={pos.x} cy={pos.y - (isOnline ? 18 : 13)} r="2.5" fill={pal.flowParticle}>
471
886
  <animate attributeName="opacity" values="1;0.25;1" dur="1.1s" repeatCount="indefinite" />
472
- </path>
887
+ </circle>
473
888
  )}
474
889
 
890
+ {/* Round 98 (issue #61): label rect 124px → 100px so a
891
+ 3-ring layout (inner r=145, 8 nodes) still has room for
892
+ the label without overlapping a neighbour. Truncate
893
+ tightened from 14 → 12 chars to match. */}
475
894
  <g transform={`translate(${pos.x}, ${pos.y + radius + 22})`}>
476
- <rect x="-62" y="-14" width="124" height="42" rx="6" fill={pal.labelBox.fill} stroke={pal.labelBox.stroke} opacity={isLight ? 1 : 0.94} />
895
+ <rect x="-50" y="-14" width="100" height="42" rx="6" fill={pal.labelBox.fill} stroke={pal.labelBox.stroke} opacity={isLight ? 1 : 0.94} />
477
896
  <text x="0" y="1" textAnchor="middle" fill={status.text} fontSize="12" fontFamily="monospace" fontWeight="700">
478
- {truncate(session.alias, 14)}
897
+ {truncate(session.alias, 12)}
479
898
  </text>
480
899
  <text x="0" y="17" textAnchor="middle" fill={status.primary} fontSize="9" fontFamily="monospace">
481
900
  {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
@@ -508,7 +927,74 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
508
927
  <text x="34" y="82" fill={pal.legendText} fontSize="11" fontFamily="monospace">offline / no SSE</text>
509
928
  <path d="M150,78 Q176,52 210,78" fill="none" stroke={pal.flowEdge} strokeWidth="3" markerEnd="url(#topo-arrow)" />
510
929
  </g>
930
+ </g>
511
931
  </svg>
932
+
933
+ {/* Round 103 (issue #81): zoom / pan / fullscreen controls — HTML
934
+ overlay so they stay fixed while the SVG content transforms.
935
+ Round 104: Vincent 实测 — the reset action used to be hidden
936
+ behind the "%" label (looked like an indicator, not a button).
937
+ Split into a plain % readout + an explicit reset button with
938
+ its own icon + tooltip. */}
939
+ <div className="absolute bottom-3 right-3 flex items-center gap-1.5 text-xs select-none">
940
+ <div
941
+ className="flex items-center rounded-md border overflow-hidden"
942
+ style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder }}
943
+ >
944
+ <button
945
+ onClick={() => zoomBy(1 / 1.2)}
946
+ className="px-2 py-1 hover:bg-white/5 transition-colors"
947
+ style={{ color: pal.legendText }}
948
+ aria-label="Zoom out"
949
+ >
950
+ <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>
951
+ </button>
952
+ <span
953
+ className="px-2 py-1 tabular-nums border-x text-center"
954
+ style={{ color: pal.legendText, borderColor: pal.containerBorder, minWidth: 46 }}
955
+ title="Current zoom level"
956
+ >
957
+ {Math.round(view.zoom * 100)}%
958
+ </span>
959
+ <button
960
+ onClick={() => zoomBy(1.2)}
961
+ className="px-2 py-1 hover:bg-white/5 transition-colors"
962
+ style={{ color: pal.legendText }}
963
+ aria-label="Zoom in"
964
+ >
965
+ <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>
966
+ </button>
967
+ </div>
968
+ <button
969
+ onClick={resetView}
970
+ className="p-1.5 rounded-md border hover:bg-white/5 transition-colors"
971
+ style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
972
+ aria-label="Reset view"
973
+ title="Reset zoom + pan (or double-click the canvas)"
974
+ >
975
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
976
+ <path d="M3 12a9 9 0 1 0 9-9 9 9 0 0 0-6.4 2.6L3 8" />
977
+ <path d="M3 3v5h5" />
978
+ </svg>
979
+ </button>
980
+ <button
981
+ onClick={toggleFullscreen}
982
+ className="p-1.5 rounded-md border hover:bg-white/5 transition-colors"
983
+ style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
984
+ aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
985
+ title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
986
+ >
987
+ {isFullscreen ? (
988
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
989
+ <path d="M8 3v4a1 1 0 0 1-1 1H3M21 8h-4a1 1 0 0 1-1-1V3M3 16h4a1 1 0 0 1 1 1v4M16 21v-4a1 1 0 0 1 1-1h4" />
990
+ </svg>
991
+ ) : (
992
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
993
+ <path d="M3 8V5a2 2 0 0 1 2-2h3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M21 16v3a2 2 0 0 1-2 2h-3" />
994
+ </svg>
995
+ )}
996
+ </button>
997
+ </div>
512
998
  </div>
513
999
  </section>
514
1000
  );