@sleep2agi/agent-network-dashboard 0.4.4-preview.0 → 0.4.5-preview.0

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 (261) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +6 -6
  3. package/.next/diagnostics/route-bundle-stats.json +106 -92
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/server/app/_global-error/page/build-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/build-manifest.json +3 -3
  15. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/_not-found.html +2 -2
  18. package/.next/server/app/_not-found.rsc +12 -12
  19. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  20. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  21. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  22. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  23. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  24. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  25. package/.next/server/app/admin/page/build-manifest.json +3 -3
  26. package/.next/server/app/admin/page.js.nft.json +1 -1
  27. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  28. package/.next/server/app/admin.html +2 -2
  29. package/.next/server/app/admin.rsc +14 -14
  30. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  31. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  32. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  33. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  34. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  35. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  36. package/.next/server/app/index.html +2 -2
  37. package/.next/server/app/index.rsc +14 -14
  38. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  39. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  40. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  41. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  42. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  43. package/.next/server/app/login/page/build-manifest.json +3 -3
  44. package/.next/server/app/login/page.js.nft.json +1 -1
  45. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  46. package/.next/server/app/login.html +2 -2
  47. package/.next/server/app/login.rsc +14 -14
  48. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  49. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  50. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  51. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  52. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  53. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  54. package/.next/server/app/logs/page/build-manifest.json +3 -3
  55. package/.next/server/app/logs/page.js.nft.json +1 -1
  56. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/logs.html +2 -2
  58. package/.next/server/app/logs.rsc +14 -14
  59. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  60. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  61. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  62. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  64. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  65. package/.next/server/app/messages/page/build-manifest.json +3 -3
  66. package/.next/server/app/messages/page.js.nft.json +1 -1
  67. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  68. package/.next/server/app/messages.html +2 -2
  69. package/.next/server/app/messages.rsc +14 -14
  70. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  71. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  72. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  73. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  74. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  75. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  76. package/.next/server/app/node/page/build-manifest.json +3 -3
  77. package/.next/server/app/node/page.js.nft.json +1 -1
  78. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  79. package/.next/server/app/node.html +2 -2
  80. package/.next/server/app/node.rsc +14 -14
  81. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  82. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  83. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  84. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  85. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  86. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  87. package/.next/server/app/nodes/page/build-manifest.json +3 -3
  88. package/.next/server/app/nodes/page.js.nft.json +1 -1
  89. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  90. package/.next/server/app/nodes.html +2 -2
  91. package/.next/server/app/nodes.rsc +14 -14
  92. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  93. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  94. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  95. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  96. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  97. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  98. package/.next/server/app/page/build-manifest.json +3 -3
  99. package/.next/server/app/page.js.nft.json +1 -1
  100. package/.next/server/app/page_client-reference-manifest.js +1 -1
  101. package/.next/server/app/server-logs/page/build-manifest.json +3 -3
  102. package/.next/server/app/server-logs/page.js.nft.json +1 -1
  103. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/server-logs.html +2 -2
  105. package/.next/server/app/server-logs.rsc +14 -14
  106. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  107. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  108. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  109. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  110. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  111. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  112. package/.next/server/app/settings/networks/page/build-manifest.json +3 -3
  113. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  114. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  115. package/.next/server/app/settings/networks.html +2 -2
  116. package/.next/server/app/settings/networks.rsc +14 -14
  117. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  118. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  119. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  120. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  121. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  122. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  123. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  124. package/.next/server/app/settings/page/build-manifest.json +3 -3
  125. package/.next/server/app/settings/page.js.nft.json +1 -1
  126. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  127. package/.next/server/app/settings/tokens/page/build-manifest.json +3 -3
  128. package/.next/server/app/settings/tokens/page.js.nft.json +1 -1
  129. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  130. package/.next/server/app/settings/tokens.html +2 -2
  131. package/.next/server/app/settings/tokens.rsc +14 -14
  132. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  133. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  134. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  135. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  136. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  137. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  138. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  139. package/.next/server/app/settings.html +2 -2
  140. package/.next/server/app/settings.rsc +14 -14
  141. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  142. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  143. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  144. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  145. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  146. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  147. package/.next/server/app/tasks/[id]/page/build-manifest.json +3 -3
  148. package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  149. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  150. package/.next/server/app/tasks/page/build-manifest.json +3 -3
  151. package/.next/server/app/tasks/page.js.nft.json +1 -1
  152. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  153. package/.next/server/app/tasks.html +2 -2
  154. package/.next/server/app/tasks.rsc +14 -14
  155. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  156. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  157. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  158. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  159. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  160. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  161. package/.next/server/chunks/ssr/[root-of-the-server]__013i9e9._.js +9 -0
  162. package/.next/server/chunks/ssr/[root-of-the-server]__013i9e9._.js.map +1 -0
  163. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js +3 -0
  164. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js.map +1 -0
  165. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js +3 -0
  166. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js.map +1 -0
  167. package/.next/server/chunks/ssr/_0d-fuox._.js +3 -0
  168. package/.next/server/chunks/ssr/_0d-fuox._.js.map +1 -0
  169. package/.next/server/chunks/ssr/app_0.0wpa~._.js +3 -0
  170. package/.next/server/chunks/ssr/app_0.0wpa~._.js.map +1 -0
  171. package/.next/server/chunks/ssr/app_0dk5wao._.js +1 -1
  172. package/.next/server/chunks/ssr/app_0dk5wao._.js.map +1 -1
  173. package/.next/server/chunks/ssr/app_0et5yua._.js +3 -0
  174. package/.next/server/chunks/ssr/app_0et5yua._.js.map +1 -0
  175. package/.next/server/chunks/ssr/app_0ff~kjj._.js +9 -0
  176. package/.next/server/chunks/ssr/app_0ff~kjj._.js.map +1 -0
  177. package/.next/server/chunks/ssr/app_0izi28j._.js +3 -0
  178. package/.next/server/chunks/ssr/app_0izi28j._.js.map +1 -0
  179. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js +1 -1
  180. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js.map +1 -1
  181. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +1 -1
  182. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +1 -1
  183. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
  184. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
  185. package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js +3 -0
  186. package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js.map +1 -0
  187. package/.next/server/middleware-build-manifest.js +6 -6
  188. package/.next/server/pages/404.html +2 -2
  189. package/.next/server/pages/500.html +1 -1
  190. package/.next/server/server-reference-manifest.js +1 -1
  191. package/.next/server/server-reference-manifest.json +1 -1
  192. package/.next/static/chunks/0257pdz1-imal.js +2 -0
  193. package/.next/static/chunks/02flrewxlf_m_.js +1 -0
  194. package/.next/static/chunks/051gslfjka1kj.js +1 -0
  195. package/.next/static/chunks/06t_vzei8s9_h.js +1 -0
  196. package/.next/static/chunks/0e-.zgw81gela.js +31 -0
  197. package/.next/static/chunks/0e7c40p~wq665.js +1 -0
  198. package/.next/static/chunks/0g7m7zk.v7g6h.js +1 -0
  199. package/.next/static/chunks/0hyhubmh-__wg.js +1 -0
  200. package/.next/static/chunks/0khpyxpdrp~ty.js +1 -0
  201. package/.next/static/chunks/0lo_g.q1fm-hr.js +7 -0
  202. package/.next/static/chunks/0snd~q2ucn.h1.css +2 -0
  203. package/.next/static/chunks/0w1zwt8kq_7nb.js +1 -0
  204. package/.next/static/chunks/0xn9.0ejo1s_a.js +7 -0
  205. package/.next/static/chunks/0y0lo-~k2c~00.js +1 -0
  206. package/.next/static/chunks/13.dst7liziwo.js +1 -0
  207. package/.next/static/chunks/{turbopack-0vsvjs02l449s.js → turbopack-01f1xs~cq_~si.js} +1 -1
  208. package/.next/trace +2 -2
  209. package/.next/trace-build +1 -1
  210. package/app/components/EmptyState.tsx +189 -0
  211. package/app/components/LoadingSkeleton.tsx +3 -18
  212. package/app/components/Sidebar.tsx +72 -11
  213. package/app/components/TopoGraph.tsx +151 -62
  214. package/app/globals.css +35 -0
  215. package/app/login/page.tsx +26 -13
  216. package/app/logs/page.tsx +2 -5
  217. package/app/messages/page.tsx +2 -5
  218. package/app/nodes/page.tsx +6 -1
  219. package/app/page.tsx +36 -12
  220. package/app/settings/networks/page.tsx +6 -1
  221. package/app/settings/page.tsx +56 -29
  222. package/app/settings/tokens/page.tsx +6 -1
  223. package/app/tasks/page.tsx +7 -9
  224. package/docs/loop-design-rounds.md +502 -0
  225. package/docs/oss-readiness-report.md +189 -0
  226. package/package.json +1 -1
  227. package/.next/server/chunks/ssr/[root-of-the-server]__093ymui._.js +0 -3
  228. package/.next/server/chunks/ssr/[root-of-the-server]__093ymui._.js.map +0 -1
  229. package/.next/server/chunks/ssr/[root-of-the-server]__0i1gizm._.js +0 -9
  230. package/.next/server/chunks/ssr/[root-of-the-server]__0i1gizm._.js.map +0 -1
  231. package/.next/server/chunks/ssr/[root-of-the-server]__0~yad5w._.js +0 -3
  232. package/.next/server/chunks/ssr/[root-of-the-server]__0~yad5w._.js.map +0 -1
  233. package/.next/server/chunks/ssr/_06_xh1~._.js +0 -3
  234. package/.next/server/chunks/ssr/_06_xh1~._.js.map +0 -1
  235. package/.next/server/chunks/ssr/app_01vhyfx._.js +0 -9
  236. package/.next/server/chunks/ssr/app_01vhyfx._.js.map +0 -1
  237. package/.next/server/chunks/ssr/app_023a94i._.js +0 -3
  238. package/.next/server/chunks/ssr/app_023a94i._.js.map +0 -1
  239. package/.next/server/chunks/ssr/app_0basiqb._.js +0 -3
  240. package/.next/server/chunks/ssr/app_0basiqb._.js.map +0 -1
  241. package/.next/server/chunks/ssr/app_0pljk5o._.js +0 -3
  242. package/.next/server/chunks/ssr/app_0pljk5o._.js.map +0 -1
  243. package/.next/server/chunks/ssr/node_modules_next_dist_0t-hj0x._.js +0 -3
  244. package/.next/server/chunks/ssr/node_modules_next_dist_0t-hj0x._.js.map +0 -1
  245. package/.next/static/chunks/0.z~9klrgd.t-.js +0 -1
  246. package/.next/static/chunks/08aj9~75raaah.js +0 -7
  247. package/.next/static/chunks/08iom_z8w-wmz.js +0 -7
  248. package/.next/static/chunks/08v0-ki6vwpxf.js +0 -1
  249. package/.next/static/chunks/0dkkv90do9a4r.js +0 -1
  250. package/.next/static/chunks/0e-wrhqh4h3r5.js +0 -2
  251. package/.next/static/chunks/0ejz4zwhu3a3y.js +0 -1
  252. package/.next/static/chunks/0o5zj~5vjeks3.js +0 -1
  253. package/.next/static/chunks/0uqfv6rc-xdru.js +0 -1
  254. package/.next/static/chunks/0u~_.32f.midj.css +0 -2
  255. package/.next/static/chunks/0xrnwfefpke-g.js +0 -31
  256. package/.next/static/chunks/0y_po~f~fk~ap.js +0 -1
  257. package/.next/static/chunks/14sm.ta0zylsj.js +0 -1
  258. package/.next/static/chunks/18ac93ii3mjlj.js +0 -1
  259. /package/.next/static/{jTh5m77FIfQccc0gftvik → 2m1yIfZCIYDWMYrrJEKSC}/_buildManifest.js +0 -0
  260. /package/.next/static/{jTh5m77FIfQccc0gftvik → 2m1yIfZCIYDWMYrrJEKSC}/_clientMiddlewareManifest.js +0 -0
  261. /package/.next/static/{jTh5m77FIfQccc0gftvik → 2m1yIfZCIYDWMYrrJEKSC}/_ssgManifest.js +0 -0
@@ -45,21 +45,6 @@ export function LoadingSkeleton() {
45
45
  );
46
46
  }
47
47
 
48
- export function EmptyState({ hint }: { hint?: { global_count?: number; filtered_network?: string } }) {
49
- return (
50
- <div className="text-center py-16">
51
- <div className="text-gray-600 text-4xl mb-4">--</div>
52
- <h3 className="text-gray-400 text-lg font-medium mb-2">No agents in this network</h3>
53
- {hint?.global_count ? (
54
- <p className="text-gray-500 text-sm max-w-md mx-auto">
55
- Server has <span className="text-cyan-400 font-medium">{hint.global_count}</span> nodes globally,
56
- but none are registered to the current network. Switch network or contact admin.
57
- </p>
58
- ) : (
59
- <p className="text-gray-600 text-sm max-w-md mx-auto">
60
- Agent sessions will appear here once they connect to the CommHub.
61
- </p>
62
- )}
63
- </div>
64
- );
65
- }
48
+ // EmptyState export removed in 0.4.5 replaced by EmptyState.tsx with
49
+ // per-variant glyphs and a NodesEmptyState wrapper for the Overview
50
+ // hint-aware behavior. See app/components/EmptyState.tsx.
@@ -101,17 +101,11 @@ export function Sidebar() {
101
101
  ${mobileOpen ? 'translate-x-0' : '-translate-x-full'}
102
102
  lg:translate-x-0 lg:static lg:shrink-0
103
103
  `}>
104
- {/* Logo */}
105
- <div className={`px-4 py-5 border-b border-[#2a2a4a] ${collapsed ? 'text-center' : ''}`}>
106
- {collapsed ? (
107
- <span className="text-cyan-400 font-bold text-lg">A</span>
108
- ) : (
109
- <div>
110
- <div className="text-white font-bold text-sm">Agent Network</div>
111
- <div className="text-gray-600 text-xs">Dashboard</div>
112
- </div>
113
- )}
114
- </div>
104
+ {/* Brand header — round 4: 3-node mesh mark matches /login,
105
+ with an inline live "online" pulse so every page surfaces
106
+ fleet health without leaving for /nodes. */}
107
+ <SidebarBrand collapsed={collapsed} />
108
+ <div className={`border-b border-[#2a2a4a]`} />
115
109
 
116
110
  {/* Network list */}
117
111
  {!collapsed && networks.length > 0 && (
@@ -174,3 +168,70 @@ export function Sidebar() {
174
168
  </>
175
169
  );
176
170
  }
171
+
172
+ interface StatusData {
173
+ sessions?: Array<{ status?: string; alias?: string }>;
174
+ }
175
+
176
+ const statusFetcher = (url: string) =>
177
+ fetch(url).then(r => r.ok ? r.json() as Promise<StatusData> : { sessions: [] });
178
+
179
+ function SidebarBrand({ collapsed }: { collapsed: boolean }) {
180
+ // Live online count — re-uses the same /api/hub/status endpoint the
181
+ // Overview already polls, so SWR dedupes the request. Pulse is muted —
182
+ // emerald dot at 4px with a 1.6s slow opacity pulse, no glow, no halo.
183
+ const { data } = useSWR<StatusData>('/api/hub/status', statusFetcher, {
184
+ refreshInterval: 10000,
185
+ dedupingInterval: 4000,
186
+ });
187
+ const sessions = data?.sessions || [];
188
+ const online = sessions.filter(s => s.status && s.status !== 'offline').length;
189
+ const total = sessions.length;
190
+
191
+ if (collapsed) {
192
+ return (
193
+ <Link href="/" className="block px-3 py-4 flex justify-center hover:opacity-80 transition-opacity" aria-label="Agent Network — home">
194
+ <BrandMark size={32} />
195
+ </Link>
196
+ );
197
+ }
198
+
199
+ return (
200
+ <Link
201
+ href="/"
202
+ className="block px-4 py-4 flex items-center gap-3 hover:bg-[#11112a]/40 transition-colors rounded-r-xl"
203
+ aria-label="Agent Network — home"
204
+ >
205
+ <BrandMark size={32} />
206
+ <div className="min-w-0">
207
+ <div className="text-white text-[13px] font-semibold leading-tight">Agent Network</div>
208
+ <div className="text-[10px] text-gray-500 flex items-center gap-1.5 mt-0.5">
209
+ <span
210
+ className={`inline-block w-1.5 h-1.5 rounded-full ${online > 0 ? 'bg-emerald-400 anet-brand-pulse' : 'bg-gray-600'}`}
211
+ aria-hidden
212
+ />
213
+ {total === 0 ? (
214
+ <span>waiting for agents</span>
215
+ ) : (
216
+ <span><span className="text-gray-300 font-medium tabular-nums">{online}</span><span className="text-gray-600">/{total}</span> online</span>
217
+ )}
218
+ </div>
219
+ </div>
220
+ </Link>
221
+ );
222
+ }
223
+
224
+ /** 3-node mesh mark — same SVG as /login, reusable. */
225
+ function BrandMark({ size }: { size: number }) {
226
+ return (
227
+ <svg width={size} height={size} viewBox="0 0 32 32" aria-hidden className="shrink-0">
228
+ <circle cx="16" cy="16" r="10" fill="none" stroke="currentColor" strokeWidth="1.5" opacity="0.2" className="text-cyan-400" />
229
+ <line x1="16" y1="10" x2="10" y2="20" stroke="currentColor" strokeWidth="1" opacity="0.45" className="text-cyan-400" />
230
+ <line x1="16" y1="10" x2="22" y2="20" stroke="currentColor" strokeWidth="1" opacity="0.45" className="text-cyan-400" />
231
+ <line x1="10" y1="20" x2="22" y2="20" stroke="currentColor" strokeWidth="1" opacity="0.45" className="text-cyan-400" />
232
+ <circle cx="16" cy="10" r="2.5" fill="currentColor" className="text-cyan-400" />
233
+ <circle cx="10" cy="20" r="2.5" fill="currentColor" className="text-green-400" />
234
+ <circle cx="22" cy="20" r="2.5" fill="currentColor" className="text-violet-400" />
235
+ </svg>
236
+ );
237
+ }
@@ -63,42 +63,123 @@ function truncate(value: string, max: number) {
63
63
  return value.length > max ? `${value.slice(0, max - 1)}...` : value;
64
64
  }
65
65
 
66
- function nodeStatus(session: Session, isOnline: boolean) {
66
+ function nodeStatus(session: Session, isOnline: boolean, isLight: boolean) {
67
67
  if (!isOnline) {
68
68
  return {
69
69
  label: 'offline',
70
- primary: '#6b7280',
71
- halo: '#111827',
72
- text: '#9ca3af',
70
+ primary: isLight ? '#94a3b8' : '#6b7280',
71
+ halo: isLight ? '#e2e8f0' : '#111827',
72
+ text: isLight ? '#475569' : '#9ca3af',
73
73
  };
74
74
  }
75
-
76
75
  if (session.status === 'working') {
77
76
  return {
78
77
  label: 'working',
79
- primary: '#22c55e',
80
- halo: '#14532d',
81
- text: '#dcfce7',
78
+ primary: isLight ? '#059669' : '#22c55e',
79
+ halo: isLight ? '#d1fae5' : '#14532d',
80
+ text: isLight ? '#065f46' : '#dcfce7',
82
81
  };
83
82
  }
84
-
85
83
  if (session.status === 'idle') {
86
84
  return {
87
85
  label: 'idle',
88
- primary: '#2dd4bf',
89
- halo: '#134e4a',
90
- text: '#ccfbf1',
86
+ primary: isLight ? '#0d9488' : '#2dd4bf',
87
+ halo: isLight ? '#ccfbf1' : '#134e4a',
88
+ text: isLight ? '#115e59' : '#ccfbf1',
91
89
  };
92
90
  }
93
-
94
91
  return {
95
92
  label: session.status || 'online',
96
- primary: '#38bdf8',
97
- halo: '#0c4a6e',
98
- text: '#e0f2fe',
93
+ primary: isLight ? '#0284c7' : '#38bdf8',
94
+ halo: isLight ? '#dbeafe' : '#0c4a6e',
95
+ text: isLight ? '#0c4a6e' : '#e0f2fe',
99
96
  };
100
97
  }
101
98
 
99
+ /** Theme-aware color palette for the topology SVG. */
100
+ interface Palette {
101
+ panelStops: [string, string, string];
102
+ radarStops: { color: string; opacity: number }[];
103
+ arrowFill: string;
104
+ ringStroke: string;
105
+ spokeStroke: { active: string; idle: string };
106
+ flowEdge: string;
107
+ flowPath: string;
108
+ flowParticle: string;
109
+ nodeFill: { online: string; offline: string };
110
+ labelBox: { fill: string; stroke: string };
111
+ legendBox: { fill: string; stroke: string };
112
+ legendText: string;
113
+ legendHeadline: string;
114
+ legendAccent: string;
115
+ containerBg: string;
116
+ containerBorder: string;
117
+ topRailGradient: string;
118
+ }
119
+
120
+ const DARK_PALETTE: Palette = {
121
+ panelStops: ['#0b1220', '#080814', '#101018'],
122
+ radarStops: [
123
+ { color: '#22d3ee', opacity: 0.18 },
124
+ { color: '#22c55e', opacity: 0.045 },
125
+ { color: '#020617', opacity: 0 },
126
+ ],
127
+ arrowFill: '#67e8f9',
128
+ ringStroke: '#164e63',
129
+ spokeStroke: { active: '#22d3ee', idle: '#155e75' },
130
+ flowEdge: '#67e8f9',
131
+ flowPath: '#e0f2fe',
132
+ flowParticle: '#fef08a',
133
+ nodeFill: { online: '#020617', offline: '#080814' },
134
+ labelBox: { fill: '#020617', stroke: '#1f2937' },
135
+ legendBox: { fill: '#020617', stroke: '#1f2937' },
136
+ legendText: '#94a3b8',
137
+ legendHeadline: '#e5e7eb',
138
+ legendAccent: '#67e8f9',
139
+ containerBg: '#080814',
140
+ containerBorder: '#2a2a4a',
141
+ topRailGradient: 'from-transparent via-cyan-400/70 to-transparent',
142
+ };
143
+
144
+ const LIGHT_PALETTE: Palette = {
145
+ panelStops: ['#f8fafc', '#ffffff', '#f1f5f9'],
146
+ radarStops: [
147
+ { color: '#10b981', opacity: 0.06 },
148
+ { color: '#3b82f6', opacity: 0.03 },
149
+ { color: '#ffffff', opacity: 0 },
150
+ ],
151
+ arrowFill: '#10b981',
152
+ ringStroke: '#cbd5e1',
153
+ spokeStroke: { active: '#10b981', idle: '#cbd5e1' },
154
+ flowEdge: '#10b981',
155
+ flowPath: '#475569',
156
+ flowParticle: '#f59e0b',
157
+ nodeFill: { online: '#ffffff', offline: '#f8fafc' },
158
+ labelBox: { fill: '#ffffff', stroke: '#e2e8f0' },
159
+ legendBox: { fill: '#ffffff', stroke: '#e2e8f0' },
160
+ legendText: '#475569',
161
+ legendHeadline: '#0f172a',
162
+ legendAccent: '#0d9488',
163
+ containerBg: '#ffffff',
164
+ containerBorder: '#e3e6eb',
165
+ topRailGradient: 'from-transparent via-emerald-500/40 to-transparent',
166
+ };
167
+
168
+ function useTheme(): 'light' | 'dark' {
169
+ const [theme, setTheme] = useState<'light' | 'dark'>('dark');
170
+ useEffect(() => {
171
+ const read = () => {
172
+ const t = document.documentElement.getAttribute('data-theme') || 'cyber';
173
+ setTheme(t === 'light' || t === 'mint' ? 'light' : 'dark');
174
+ };
175
+ read();
176
+ const obs = new MutationObserver(read);
177
+ obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
178
+ return () => obs.disconnect();
179
+ }, []);
180
+ return theme;
181
+ }
182
+
102
183
  function buildFlowLinks(messages: MessageFlow[], positions: Record<string, Point>) {
103
184
  const links = new Map<string, FlowLink>();
104
185
 
@@ -127,6 +208,9 @@ function buildFlowLinks(messages: MessageFlow[], positions: Record<string, Point
127
208
  }
128
209
 
129
210
  export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
211
+ const theme = useTheme();
212
+ const isLight = theme === 'light';
213
+ const pal = isLight ? LIGHT_PALETTE : DARK_PALETTE;
130
214
  const [messages, setMessages] = useState<MessageFlow[]>([]);
131
215
 
132
216
  useEffect(() => {
@@ -206,30 +290,35 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
206
290
  </div>
207
291
  </div>
208
292
 
209
- <div className="relative overflow-hidden rounded-lg border border-[#2a2a4a] bg-[#080814] shadow-2xl shadow-cyan-950/30">
210
- <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-cyan-400/70 to-transparent" />
293
+ <div
294
+ className={`relative overflow-hidden rounded-lg border shadow-2xl ${isLight ? 'shadow-zinc-900/5' : 'shadow-cyan-950/30'}`}
295
+ style={{ background: pal.containerBg, borderColor: pal.containerBorder }}
296
+ >
297
+ <div className={`absolute inset-x-0 top-0 h-px bg-gradient-to-r ${pal.topRailGradient}`} />
211
298
 
212
299
  <svg viewBox="0 0 1000 680" className="w-full h-auto block" preserveAspectRatio="xMidYMid meet">
213
300
  <defs>
214
301
  <linearGradient id="topo-panel" x1="0" x2="1" y1="0" y2="1">
215
- <stop offset="0%" stopColor="#0b1220" />
216
- <stop offset="48%" stopColor="#080814" />
217
- <stop offset="100%" stopColor="#101018" />
302
+ <stop offset="0%" stopColor={pal.panelStops[0]} />
303
+ <stop offset="48%" stopColor={pal.panelStops[1]} />
304
+ <stop offset="100%" stopColor={pal.panelStops[2]} />
218
305
  </linearGradient>
219
306
  <radialGradient id="topo-radar" cx="50%" cy="50%" r="55%">
220
- <stop offset="0%" stopColor="#22d3ee" stopOpacity="0.18" />
221
- <stop offset="45%" stopColor="#22c55e" stopOpacity="0.045" />
222
- <stop offset="100%" stopColor="#020617" stopOpacity="0" />
307
+ <stop offset="0%" stopColor={pal.radarStops[0].color} stopOpacity={pal.radarStops[0].opacity} />
308
+ <stop offset="45%" stopColor={pal.radarStops[1].color} stopOpacity={pal.radarStops[1].opacity} />
309
+ <stop offset="100%" stopColor={pal.radarStops[2].color} stopOpacity={pal.radarStops[2].opacity} />
223
310
  </radialGradient>
224
- <filter id="topo-glow">
225
- <feGaussianBlur stdDeviation="4" result="blur" />
226
- <feMerge>
227
- <feMergeNode in="blur" />
228
- <feMergeNode in="SourceGraphic" />
229
- </feMerge>
230
- </filter>
311
+ {!isLight && (
312
+ <filter id="topo-glow">
313
+ <feGaussianBlur stdDeviation="4" result="blur" />
314
+ <feMerge>
315
+ <feMergeNode in="blur" />
316
+ <feMergeNode in="SourceGraphic" />
317
+ </feMerge>
318
+ </filter>
319
+ )}
231
320
  <marker id="topo-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
232
- <path d="M 0 0 L 10 5 L 0 10 z" fill="#67e8f9" />
321
+ <path d="M 0 0 L 10 5 L 0 10 z" fill={pal.arrowFill} />
233
322
  </marker>
234
323
  </defs>
235
324
 
@@ -238,7 +327,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
238
327
 
239
328
  {/* radar rings */}
240
329
  {[90, 170, 250, 330].map(radius => (
241
- <circle key={radius} cx={cx} cy={cy} r={radius} fill="none" stroke="#164e63" strokeWidth="1" opacity="0.35" />
330
+ <circle key={radius} cx={cx} cy={cy} r={radius} fill="none" stroke={pal.ringStroke} strokeWidth="1" opacity={isLight ? 0.6 : 0.35} />
242
331
  ))}
243
332
  {[0, 30, 60, 90, 120, 150].map(angle => (
244
333
  <line
@@ -247,9 +336,9 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
247
336
  y1={cy - 360 * Math.sin(angle * Math.PI / 180)}
248
337
  x2={cx + 360 * Math.cos(angle * Math.PI / 180)}
249
338
  y2={cy + 360 * Math.sin(angle * Math.PI / 180)}
250
- stroke="#164e63"
339
+ stroke={pal.ringStroke}
251
340
  strokeWidth="1"
252
- opacity="0.18"
341
+ opacity={isLight ? 0.35 : 0.18}
253
342
  />
254
343
  ))}
255
344
 
@@ -264,7 +353,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
264
353
  key={`hub-${session.alias}`}
265
354
  d={path}
266
355
  fill="none"
267
- stroke={activeAliases.has(session.alias) ? '#22d3ee' : '#155e75'}
356
+ stroke={activeAliases.has(session.alias) ? pal.spokeStroke.active : pal.spokeStroke.idle}
268
357
  strokeWidth={activeAliases.has(session.alias) ? 2 : 1}
269
358
  strokeDasharray={activeAliases.has(session.alias) ? 'none' : '8 10'}
270
359
  opacity={activeAliases.has(session.alias) ? 0.65 : 0.35}
@@ -288,22 +377,22 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
288
377
  <path
289
378
  d={path}
290
379
  fill="none"
291
- stroke="#67e8f9"
380
+ stroke={pal.flowEdge}
292
381
  strokeWidth={width}
293
- opacity="0.28"
294
- filter="url(#topo-glow)"
382
+ opacity={isLight ? 0.22 : 0.28}
383
+ filter={isLight ? undefined : 'url(#topo-glow)'}
295
384
  markerEnd="url(#topo-arrow)"
296
385
  />
297
386
  <path
298
387
  id={`flow-path-${index}`}
299
388
  d={path}
300
389
  fill="none"
301
- stroke="#e0f2fe"
390
+ stroke={pal.flowPath}
302
391
  strokeWidth="1"
303
392
  strokeDasharray="2 12"
304
- opacity="0.75"
393
+ opacity={isLight ? 0.4 : 0.75}
305
394
  />
306
- <circle r="4" fill="#fef08a" filter="url(#topo-glow)">
395
+ <circle r="4" fill={pal.flowParticle} filter={isLight ? undefined : 'url(#topo-glow)'}>
307
396
  <animateMotion dur={`${duration}s`} repeatCount="indefinite" path={path} />
308
397
  </circle>
309
398
  </g>
@@ -340,38 +429,38 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
340
429
 
341
430
  const sseCountFor = (session.network_id ? sseSessions[`${session.network_id}:${session.alias}`] : undefined) ?? sseSessions[session.alias];
342
431
  const isOnline = session.status !== 'offline' || !!sseCountFor;
343
- const status = nodeStatus(session, isOnline);
432
+ const status = nodeStatus(session, isOnline, isLight);
344
433
  const isActive = activeAliases.has(session.alias);
345
434
  const radius = isOnline ? 26 : 18;
346
435
 
347
436
  return (
348
437
  <g key={session.alias} className="transition-opacity">
349
438
  {isActive && (
350
- <circle cx={pos.x} cy={pos.y} r={radius + 14} fill={status.primary} opacity="0.12">
439
+ <circle cx={pos.x} cy={pos.y} r={radius + 14} fill={status.primary} opacity={isLight ? 0.08 : 0.12}>
351
440
  <animate attributeName="r" values={`${radius + 8};${radius + 22};${radius + 8}`} dur="2.4s" repeatCount="indefinite" />
352
- <animate attributeName="opacity" values="0.18;0.04;0.18" dur="2.4s" repeatCount="indefinite" />
441
+ <animate attributeName="opacity" values={isLight ? '0.12;0.02;0.12' : '0.18;0.04;0.18'} dur="2.4s" repeatCount="indefinite" />
353
442
  </circle>
354
443
  )}
355
- <circle cx={pos.x} cy={pos.y} r={radius + 8} fill={status.halo} opacity={isOnline ? 0.55 : 0.25} />
444
+ <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)} />
356
445
  <circle
357
446
  cx={pos.x}
358
447
  cy={pos.y}
359
448
  r={radius}
360
- fill={isOnline ? '#020617' : '#080814'}
449
+ fill={isOnline ? pal.nodeFill.online : pal.nodeFill.offline}
361
450
  stroke={status.primary}
362
451
  strokeWidth={isOnline ? 3 : 1.5}
363
452
  strokeDasharray={isOnline ? 'none' : '5 5'}
364
- filter={isOnline ? 'url(#topo-glow)' : undefined}
453
+ filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
365
454
  />
366
455
  <circle cx={pos.x} cy={pos.y} r={isOnline ? 8 : 5} fill={status.primary} />
367
456
  {session.status === 'working' && (
368
- <path d={`M${pos.x - 9},${pos.y + 11} h18`} stroke="#fef08a" strokeWidth="3" strokeLinecap="round">
457
+ <path d={`M${pos.x - 9},${pos.y + 11} h18`} stroke={pal.flowParticle} strokeWidth="3" strokeLinecap="round">
369
458
  <animate attributeName="opacity" values="1;0.25;1" dur="1.1s" repeatCount="indefinite" />
370
459
  </path>
371
460
  )}
372
461
 
373
462
  <g transform={`translate(${pos.x}, ${pos.y + radius + 22})`}>
374
- <rect x="-62" y="-14" width="124" height="42" rx="6" fill="#020617" stroke="#1f2937" opacity="0.94" />
463
+ <rect x="-62" y="-14" width="124" height="42" rx="6" fill={pal.labelBox.fill} stroke={pal.labelBox.stroke} opacity={isLight ? 1 : 0.94} />
375
464
  <text x="0" y="1" textAnchor="middle" fill={status.text} fontSize="12" fontFamily="monospace" fontWeight="700">
376
465
  {truncate(session.alias, 14)}
377
466
  </text>
@@ -385,11 +474,11 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
385
474
 
386
475
  {/* latest flow labels */}
387
476
  <g transform="translate(28, 34)">
388
- <rect x="0" y="0" width="285" height={60 + Math.min(flowLinks.length, 4) * 20} rx="8" fill="#020617" stroke="#1f2937" opacity="0.94" />
389
- <text x="16" y="26" fill="#e5e7eb" fontSize="13" fontFamily="monospace" fontWeight="700">recent signal</text>
390
- <text x="178" y="26" fill="#67e8f9" fontSize="11" fontFamily="monospace">{messages.length} messages</text>
477
+ <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} />
478
+ <text x="16" y="26" fill={pal.legendHeadline} fontSize="13" fontFamily="monospace" fontWeight="700">recent signal</text>
479
+ <text x="178" y="26" fill={pal.legendAccent} fontSize="11" fontFamily="monospace">{messages.length} messages</text>
391
480
  {flowLinks.slice(0, 4).map((link, index) => (
392
- <text key={link.key} x="16" y={54 + index * 20} fill="#94a3b8" fontSize="10" fontFamily="monospace">
481
+ <text key={link.key} x="16" y={54 + index * 20} fill={pal.legendText} fontSize="10" fontFamily="monospace">
393
482
  {truncate(link.from, 8)} {'->'} {truncate(link.to, 8)} / {link.count} / {truncate(link.content, 18)}
394
483
  </text>
395
484
  ))}
@@ -397,14 +486,14 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
397
486
 
398
487
  {/* legend */}
399
488
  <g transform="translate(720, 34)">
400
- <rect x="0" y="0" width="250" height="112" rx="8" fill="#020617" stroke="#1f2937" opacity="0.94" />
401
- <circle cx="18" cy="26" r="6" fill="#22c55e" />
402
- <text x="34" y="30" fill="#94a3b8" fontSize="11" fontFamily="monospace">working node</text>
403
- <circle cx="18" cy="52" r="6" fill="#2dd4bf" />
404
- <text x="34" y="56" fill="#94a3b8" fontSize="11" fontFamily="monospace">online idle</text>
405
- <circle cx="18" cy="78" r="6" fill="#6b7280" />
406
- <text x="34" y="82" fill="#94a3b8" fontSize="11" fontFamily="monospace">offline / no SSE</text>
407
- <path d="M150,78 Q176,52 210,78" fill="none" stroke="#67e8f9" strokeWidth="3" markerEnd="url(#topo-arrow)" />
489
+ <rect x="0" y="0" width="250" height="112" rx="8" fill={pal.legendBox.fill} stroke={pal.legendBox.stroke} opacity={isLight ? 1 : 0.94} />
490
+ <circle cx="18" cy="26" r="6" fill={isLight ? '#059669' : '#22c55e'} />
491
+ <text x="34" y="30" fill={pal.legendText} fontSize="11" fontFamily="monospace">working node</text>
492
+ <circle cx="18" cy="52" r="6" fill={isLight ? '#0d9488' : '#2dd4bf'} />
493
+ <text x="34" y="56" fill={pal.legendText} fontSize="11" fontFamily="monospace">online idle</text>
494
+ <circle cx="18" cy="78" r="6" fill={isLight ? '#94a3b8' : '#6b7280'} />
495
+ <text x="34" y="82" fill={pal.legendText} fontSize="11" fontFamily="monospace">offline / no SSE</text>
496
+ <path d="M150,78 Q176,52 210,78" fill="none" stroke={pal.flowEdge} strokeWidth="3" markerEnd="url(#topo-arrow)" />
408
497
  </g>
409
498
  </svg>
410
499
  </div>
package/app/globals.css CHANGED
@@ -517,6 +517,41 @@ body {
517
517
  box-shadow: 0 6px 18px -10px rgba(15, 23, 42, 0.12);
518
518
  }
519
519
 
520
+ /* ─────────────────────────────────────────────────────────────────────
521
+ Login surface (round 1 polish)
522
+ Restrained, minimal — first impression for new Agent Network users.
523
+ ─────────────────────────────────────────────────────────────────── */
524
+ .anet-login-bg {
525
+ background:
526
+ radial-gradient(circle at 50% 30%, rgba(34, 211, 238, 0.05), transparent 55%),
527
+ radial-gradient(circle at 33% 75%, rgba(59, 130, 246, 0.045), transparent 55%);
528
+ }
529
+ [data-theme="light"] .anet-login-bg,
530
+ [data-theme="mint"] .anet-login-bg {
531
+ background-image:
532
+ radial-gradient(circle at 50% 35%, rgba(0, 158, 126, 0.06), transparent 60%),
533
+ radial-gradient(circle, rgba(15, 23, 42, 0.05) 1px, transparent 1px);
534
+ background-size: 100% 100%, 24px 24px;
535
+ }
536
+ [data-theme="light"] .anet-login-mark,
537
+ [data-theme="mint"] .anet-login-mark {
538
+ background: var(--bg-secondary) !important;
539
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), 0 0 0 1px var(--border);
540
+ }
541
+
542
+ /* Sidebar brand "online" pulse — slow, restrained, NO glow.
543
+ 1.6s opacity drift between 1.0 and 0.5; nothing scales, nothing blurs. */
544
+ @keyframes anet-brand-pulse-kf {
545
+ 0%, 100% { opacity: 1; }
546
+ 50% { opacity: 0.4; }
547
+ }
548
+ .anet-brand-pulse {
549
+ animation: anet-brand-pulse-kf 1.6s ease-in-out infinite;
550
+ }
551
+ @media (prefers-reduced-motion: reduce) {
552
+ .anet-brand-pulse { animation: none; }
553
+ }
554
+
520
555
  /* P0-3 baseline: error / warning chip colors for light themes.
521
556
  The component-level copy (replacing raw JSON with human text) is fixed
522
557
  in the Logs / Server-Logs page components separately. */
@@ -44,21 +44,32 @@ export default function LoginPage() {
44
44
  };
45
45
 
46
46
  return (
47
- <main className="min-h-screen bg-[#0a0a1a] text-gray-100 font-mono flex items-center justify-center relative overflow-hidden">
48
- <div className="absolute inset-0 pointer-events-none">
49
- <div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-[600px] h-[600px] rounded-full bg-cyan-500/5 blur-[120px]" />
50
- <div className="absolute bottom-1/4 left-1/3 w-[400px] h-[400px] rounded-full bg-blue-500/5 blur-[100px]" />
51
- </div>
47
+ <main className="min-h-screen bg-[#0a0a1a] text-gray-100 font-mono flex items-center justify-center relative overflow-hidden px-4 py-10 sm:py-16">
48
+ {/* Subtle off-grid background — restraint over AI-glow.
49
+ Dark themes get a faint radial wash; light themes show a low-contrast
50
+ dotted grid for surface texture without noise. */}
51
+ <div className="absolute inset-0 pointer-events-none anet-login-bg" aria-hidden />
52
52
 
53
- <div className="relative w-full max-w-sm mx-4">
54
- <div className="text-center mb-8">
55
- <div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-cyan-500/20 to-blue-500/20 border border-cyan-500/20 mb-4">
56
- <svg className="w-8 h-8 text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
57
- <path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
53
+ <div className="relative w-full max-w-sm">
54
+ <div className="text-center mb-7">
55
+ {/* Brand mark uses the favicon 3-node mesh SVG which IS the
56
+ agent-network concept (nodes + edges). Replaces the previous
57
+ flat emerald square placeholder. */}
58
+ <div className="inline-flex items-center justify-center w-16 h-16 mb-4 rounded-2xl bg-[#111128] border border-[#2a2a4a] anet-login-mark">
59
+ <svg className="w-9 h-9" viewBox="0 0 32 32" aria-hidden>
60
+ <circle cx="16" cy="16" r="10" fill="none" stroke="currentColor" strokeWidth="1.5" opacity="0.25" className="text-cyan-400 anet-login-mark-ring" />
61
+ <line x1="16" y1="10" x2="10" y2="20" stroke="currentColor" strokeWidth="1" opacity="0.5" className="text-cyan-400 anet-login-mark-edge" />
62
+ <line x1="16" y1="10" x2="22" y2="20" stroke="currentColor" strokeWidth="1" opacity="0.5" className="text-cyan-400 anet-login-mark-edge" />
63
+ <line x1="10" y1="20" x2="22" y2="20" stroke="currentColor" strokeWidth="1" opacity="0.5" className="text-cyan-400 anet-login-mark-edge" />
64
+ <circle cx="16" cy="10" r="3" fill="currentColor" className="text-cyan-400 anet-login-mark-node-a" />
65
+ <circle cx="10" cy="20" r="3" fill="currentColor" className="text-green-400 anet-login-mark-node-b" />
66
+ <circle cx="22" cy="20" r="3" fill="currentColor" className="text-violet-400 anet-login-mark-node-c" />
58
67
  </svg>
59
68
  </div>
60
- <h1 className="text-2xl font-bold text-white tracking-tight">Agent Network</h1>
61
- <p className="text-sm text-gray-500 mt-1">Dashboard</p>
69
+ <h1 className="text-[22px] sm:text-2xl font-semibold text-white tracking-tight">Agent Network</h1>
70
+ <p className="text-[13px] text-gray-500 mt-1.5 leading-snug">
71
+ One control plane for your AI agents · <span className="text-gray-600">Tasks · Mesh · Messages</span>
72
+ </p>
62
73
  </div>
63
74
 
64
75
  {/* Mode toggle */}
@@ -96,7 +107,9 @@ export default function LoginPage() {
96
107
  </button>
97
108
  </form>
98
109
 
99
- <p className="text-center text-xs text-gray-700 mt-6">Powered by Sleep2AGI</p>
110
+ <p className="text-center text-[11px] text-gray-700 mt-6">
111
+ Sleep2AGI · Apache-2.0 · <a href="https://anet.sh" className="hover:text-gray-500 transition-colors">anet.sh</a>
112
+ </p>
100
113
  </div>
101
114
  </main>
102
115
  );
package/app/logs/page.tsx CHANGED
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { timeAgo } from '../components/utils';
6
6
  import { useNetworkId } from '../lib/network-context';
7
+ import { EmptyState } from '../components/EmptyState';
7
8
 
8
9
  interface AuditLog {
9
10
  id: string;
@@ -121,11 +122,7 @@ export default function LogsPage() {
121
122
  {[1,2,3,4,5].map(i => <div key={i} className="h-14 bg-gray-800/20 rounded-lg" />)}
122
123
  </div>
123
124
  ) : logs.length === 0 ? (
124
- <div className="text-center py-16">
125
- <div className="text-gray-600 text-4xl mb-4">--</div>
126
- <h3 className="text-gray-400 text-lg font-medium mb-2">No audit logs</h3>
127
- <p className="text-gray-600 text-sm">Events will appear here when users register, login, or perform actions.</p>
128
- </div>
125
+ <EmptyState variant="logs" />
129
126
  ) : (
130
127
  <div className="space-y-2">
131
128
  {logs.map(log => (
@@ -3,6 +3,7 @@
3
3
  import { useMemo, useState } from 'react';
4
4
  import { useMessages } from '../lib/hooks';
5
5
  import { timeAgo } from '../components/utils';
6
+ import { EmptyState } from '../components/EmptyState';
6
7
 
7
8
  interface MessageItem {
8
9
  id: string;
@@ -186,11 +187,7 @@ export default function MessagesPage() {
186
187
  {[1, 2, 3, 4, 5].map(i => <div key={i} className="h-20 rounded-2xl bg-gray-800/20" />)}
187
188
  </div>
188
189
  ) : filtered.length === 0 ? (
189
- <div className="text-center py-16">
190
- <div className="text-gray-600 text-4xl mb-4">--</div>
191
- <h3 className="text-gray-400 text-lg font-medium mb-2">No messages</h3>
192
- <p className="text-gray-600 text-sm">Messages between agents will appear here.</p>
193
- </div>
190
+ <EmptyState variant="messages" />
194
191
  ) : viewMode === 'grouped' ? (
195
192
  <div className="space-y-4">
196
193
  {conversations.map((conv, ci) => (
@@ -5,6 +5,7 @@ import Link from 'next/link';
5
5
  import { timeAgo } from '../components/utils';
6
6
  import { useSessions, useHealth } from '../lib/hooks';
7
7
  import { TaskChatPanel } from '../components/TaskChatPanel';
8
+ import { EmptyState } from '../components/EmptyState';
8
9
  import type { Session } from '../components/types';
9
10
 
10
11
  const STATUS_COLORS: Record<string, string> = {
@@ -163,7 +164,11 @@ export default function NodesPage() {
163
164
  {[1,2,3,4].map(i => <div key={i} className="h-16 bg-gray-800/20 rounded-lg" />)}
164
165
  </div>
165
166
  ) : filtered.length === 0 ? (
166
- <div className="text-center py-16 text-gray-600">No nodes match your filters.</div>
167
+ <EmptyState
168
+ variant="nodes"
169
+ title="No nodes match your filters"
170
+ sub="Try clearing search or status filters, or wait for an agent to register."
171
+ />
167
172
  ) : viewMode === 'grid' ? (
168
173
  <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
169
174
  {filtered.map(s => {