@sleep2agi/agent-network-dashboard 0.4.5-preview.6 → 0.4.5-preview.61

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 (277) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +49 -49
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/server/app/_global-error.html +1 -1
  7. package/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +4 -4
  15. package/.next/server/app/_not-found.rsc +12 -12
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/admin/page.js.nft.json +1 -1
  23. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  24. package/.next/server/app/admin.html +4 -4
  25. package/.next/server/app/admin.rsc +14 -14
  26. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  27. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  28. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  29. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  30. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  31. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  32. package/.next/server/app/index.html +4 -4
  33. package/.next/server/app/index.rsc +14 -14
  34. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  35. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  36. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  37. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  38. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/server/app/login/page.js.nft.json +1 -1
  40. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  41. package/.next/server/app/login.html +2 -2
  42. package/.next/server/app/login.rsc +14 -14
  43. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  44. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  45. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  46. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  47. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  48. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  49. package/.next/server/app/logs/page.js.nft.json +1 -1
  50. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  51. package/.next/server/app/logs.html +4 -4
  52. package/.next/server/app/logs.rsc +14 -14
  53. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  54. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  55. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  56. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  57. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  58. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  59. package/.next/server/app/messages/page.js.nft.json +1 -1
  60. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  61. package/.next/server/app/messages.html +4 -4
  62. package/.next/server/app/messages.rsc +14 -14
  63. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  64. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  65. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  66. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  67. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  68. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  69. package/.next/server/app/node/page.js.nft.json +1 -1
  70. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  71. package/.next/server/app/node.html +4 -4
  72. package/.next/server/app/node.rsc +14 -14
  73. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  74. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  75. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  76. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  77. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  78. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  79. package/.next/server/app/nodes/page.js.nft.json +1 -1
  80. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  81. package/.next/server/app/nodes.html +4 -4
  82. package/.next/server/app/nodes.rsc +14 -14
  83. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  84. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  85. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  86. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  87. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  88. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  89. package/.next/server/app/page.js.nft.json +1 -1
  90. package/.next/server/app/page_client-reference-manifest.js +1 -1
  91. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  92. package/.next/server/app/server-logs.html +4 -4
  93. package/.next/server/app/server-logs.rsc +14 -14
  94. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  95. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  96. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  97. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  98. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  99. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  100. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  101. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  102. package/.next/server/app/settings/networks.html +4 -4
  103. package/.next/server/app/settings/networks.rsc +14 -14
  104. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  105. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  106. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  107. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  108. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  109. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  110. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  111. package/.next/server/app/settings/page.js.nft.json +1 -1
  112. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  113. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  114. package/.next/server/app/settings/tokens.html +4 -4
  115. package/.next/server/app/settings/tokens.rsc +14 -14
  116. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  117. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  118. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  119. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  120. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  121. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  122. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  123. package/.next/server/app/settings.html +4 -4
  124. package/.next/server/app/settings.rsc +14 -14
  125. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  126. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  127. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  128. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  129. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  130. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  131. package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  132. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  133. package/.next/server/app/tasks/page.js.nft.json +1 -1
  134. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  135. package/.next/server/app/tasks.html +4 -4
  136. package/.next/server/app/tasks.rsc +14 -14
  137. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  138. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  139. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  140. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  141. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  142. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  143. package/.next/server/chunks/ssr/[root-of-the-server]__02ee4km._.js +3 -0
  144. package/.next/server/chunks/ssr/[root-of-the-server]__02ee4km._.js.map +1 -0
  145. package/.next/server/chunks/ssr/[root-of-the-server]__0n4l2is._.js +3 -0
  146. package/.next/server/chunks/ssr/[root-of-the-server]__0n4l2is._.js.map +1 -0
  147. package/.next/server/chunks/ssr/[root-of-the-server]__0o8yj3c._.js +3 -0
  148. package/.next/server/chunks/ssr/[root-of-the-server]__0o8yj3c._.js.map +1 -0
  149. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +1 -1
  150. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +1 -1
  151. package/.next/server/chunks/ssr/_0nne7mz._.js +3 -0
  152. package/.next/server/chunks/ssr/_0nne7mz._.js.map +1 -0
  153. package/.next/server/chunks/ssr/app_0.zbtn5._.js +9 -0
  154. package/.next/server/chunks/ssr/app_0.zbtn5._.js.map +1 -0
  155. package/.next/server/chunks/ssr/app_01y-~mp._.js +3 -0
  156. package/.next/server/chunks/ssr/app_01y-~mp._.js.map +1 -0
  157. package/.next/server/chunks/ssr/app_04n78pr._.js +3 -0
  158. package/.next/server/chunks/ssr/app_04n78pr._.js.map +1 -0
  159. package/.next/server/chunks/ssr/app_0_agk0r._.js +3 -0
  160. package/.next/server/chunks/ssr/app_0_agk0r._.js.map +1 -0
  161. package/.next/server/chunks/ssr/app_0aum_g5._.js +3 -0
  162. package/.next/server/chunks/ssr/app_0aum_g5._.js.map +1 -0
  163. package/.next/server/chunks/ssr/app_0esuljp._.js +3 -0
  164. package/.next/server/chunks/ssr/app_0esuljp._.js.map +1 -0
  165. package/.next/server/chunks/ssr/app_0izi28j._.js +1 -1
  166. package/.next/server/chunks/ssr/app_0izi28j._.js.map +1 -1
  167. package/.next/server/chunks/ssr/app_0py1sns._.js +3 -0
  168. package/.next/server/chunks/ssr/app_0py1sns._.js.map +1 -0
  169. package/.next/server/chunks/ssr/app_0tncr1_._.js +3 -0
  170. package/.next/server/chunks/ssr/app_0tncr1_._.js.map +1 -0
  171. package/.next/server/chunks/ssr/app_components_08ys5y3._.js +5 -5
  172. package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +1 -1
  173. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js +1 -1
  174. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js.map +1 -1
  175. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
  176. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
  177. package/.next/server/middleware-build-manifest.js +3 -3
  178. package/.next/server/pages/404.html +4 -4
  179. package/.next/server/pages/500.html +1 -1
  180. package/.next/server/server-reference-manifest.js +1 -1
  181. package/.next/server/server-reference-manifest.json +1 -1
  182. package/.next/static/chunks/0-27cd~m2b0lg.js +1 -0
  183. package/.next/static/chunks/023enh~oz30n..js +1 -0
  184. package/.next/static/chunks/09.q998r20bia.css +2 -0
  185. package/.next/static/chunks/0985ut18w-kpb.js +1 -0
  186. package/.next/static/chunks/{07okhs5561y5y.js → 0c1bp~g7.8ru..js} +1 -1
  187. package/.next/static/chunks/0edt_z0rlxgfk.js +1 -0
  188. package/.next/static/chunks/0et33r-3bdzlg.js +7 -0
  189. package/.next/static/chunks/0f6w7h-pu0tng.js +1 -0
  190. package/.next/static/chunks/0i89ioku1y61m.js +1 -0
  191. package/.next/static/chunks/0irkayokwc1fm.js +1 -0
  192. package/.next/static/chunks/0l2rkodu2.v~r.js +1 -0
  193. package/.next/static/chunks/0mdu-2u9-dx.4.js +1 -0
  194. package/.next/static/chunks/0rk5zmyp19fv8.js +7 -0
  195. package/.next/static/chunks/0zax.-67r-uuj.js +1 -0
  196. package/.next/static/chunks/12hyhrnzrx~ys.js +1 -0
  197. package/.next/static/chunks/12tlkbch057lv.js +1 -0
  198. package/.next/static/chunks/15c9pv6gxir.5.js +1 -0
  199. package/.next/trace +2 -2
  200. package/.next/trace-build +1 -1
  201. package/app/admin/page.tsx +90 -26
  202. package/app/components/AgentCard.tsx +19 -5
  203. package/app/components/BroadcastBar.tsx +22 -4
  204. package/app/components/DispatchPanel.tsx +9 -5
  205. package/app/components/EmptyState.tsx +77 -4
  206. package/app/components/HealthBanner.tsx +11 -2
  207. package/app/components/HelpOverlay.tsx +50 -10
  208. package/app/components/InboxPanel.tsx +6 -3
  209. package/app/components/Sidebar.tsx +49 -41
  210. package/app/components/StatsBar.tsx +59 -36
  211. package/app/components/TaskChatPanel.tsx +8 -10
  212. package/app/components/TaskDrawer.tsx +72 -28
  213. package/app/components/TopoGraph.tsx +118 -24
  214. package/app/globals.css +90 -0
  215. package/app/lib/status.ts +63 -0
  216. package/app/lib/version.ts +7 -0
  217. package/app/login/page.tsx +40 -7
  218. package/app/logs/page.tsx +72 -21
  219. package/app/messages/page.tsx +12 -3
  220. package/app/node/page.tsx +48 -25
  221. package/app/nodes/page.tsx +22 -6
  222. package/app/page.tsx +105 -34
  223. package/app/server-logs/page.tsx +70 -12
  224. package/app/settings/networks/page.tsx +37 -13
  225. package/app/settings/page.tsx +39 -11
  226. package/app/tasks/[id]/page.tsx +2 -13
  227. package/app/tasks/page.tsx +98 -93
  228. package/package.json +1 -1
  229. package/scripts/loop-review.mjs +224 -0
  230. package/.next/server/chunks/ssr/[root-of-the-server]__06pu~4t._.js +0 -3
  231. package/.next/server/chunks/ssr/[root-of-the-server]__06pu~4t._.js.map +0 -1
  232. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js +0 -3
  233. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js.map +0 -1
  234. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js +0 -3
  235. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js.map +0 -1
  236. package/.next/server/chunks/ssr/_0d-fuox._.js +0 -3
  237. package/.next/server/chunks/ssr/_0d-fuox._.js.map +0 -1
  238. package/.next/server/chunks/ssr/app_0.0wpa~._.js +0 -3
  239. package/.next/server/chunks/ssr/app_0.0wpa~._.js.map +0 -1
  240. package/.next/server/chunks/ssr/app_0ap_oiq._.js +0 -3
  241. package/.next/server/chunks/ssr/app_0ap_oiq._.js.map +0 -1
  242. package/.next/server/chunks/ssr/app_0dk5wao._.js +0 -3
  243. package/.next/server/chunks/ssr/app_0dk5wao._.js.map +0 -1
  244. package/.next/server/chunks/ssr/app_0et5yua._.js +0 -3
  245. package/.next/server/chunks/ssr/app_0et5yua._.js.map +0 -1
  246. package/.next/server/chunks/ssr/app_0k8xbx~._.js +0 -9
  247. package/.next/server/chunks/ssr/app_0k8xbx~._.js.map +0 -1
  248. package/.next/server/chunks/ssr/app_11vl967._.js +0 -3
  249. package/.next/server/chunks/ssr/app_11vl967._.js.map +0 -1
  250. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js +0 -3
  251. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js.map +0 -1
  252. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +0 -3
  253. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +0 -1
  254. package/.next/static/chunks/02flrewxlf_m_.js +0 -1
  255. package/.next/static/chunks/051gslfjka1kj.js +0 -1
  256. package/.next/static/chunks/06t_vzei8s9_h.js +0 -1
  257. package/.next/static/chunks/07oobuohrpigy.js +0 -1
  258. package/.next/static/chunks/09al3qm76q7nd.js +0 -1
  259. package/.next/static/chunks/0e7c40p~wq665.js +0 -1
  260. package/.next/static/chunks/0g7m7zk.v7g6h.js +0 -1
  261. package/.next/static/chunks/0m328uz90rp81.js +0 -1
  262. package/.next/static/chunks/0ogeals5ufhho.js +0 -1
  263. package/.next/static/chunks/0p4_o2jw2v9n_.css +0 -2
  264. package/.next/static/chunks/0v~9m62eciynd.js +0 -1
  265. package/.next/static/chunks/0z.pzy8zpa~js.js +0 -7
  266. package/.next/static/chunks/1031v6nw33ud3.js +0 -1
  267. package/.next/static/chunks/10ir.g.n~hre0.js +0 -7
  268. package/.next/static/chunks/10njj7kn9ylkf.js +0 -1
  269. package/.next/static/chunks/13.dst7liziwo.js +0 -1
  270. package/public/file.svg +0 -1
  271. package/public/globe.svg +0 -1
  272. package/public/next.svg +0 -1
  273. package/public/vercel.svg +0 -1
  274. package/public/window.svg +0 -1
  275. /package/.next/static/{aliOmgYOuDD7xT9T9MLOA → wt8PpIEMpmwboFwbqXVtO}/_buildManifest.js +0 -0
  276. /package/.next/static/{aliOmgYOuDD7xT9T9MLOA → wt8PpIEMpmwboFwbqXVtO}/_clientMiddlewareManifest.js +0 -0
  277. /package/.next/static/{aliOmgYOuDD7xT9T9MLOA → wt8PpIEMpmwboFwbqXVtO}/_ssgManifest.js +0 -0
package/app/node/page.tsx CHANGED
@@ -6,6 +6,7 @@ import Link from 'next/link';
6
6
  import { Suspense } from 'react';
7
7
  import { TaskChatPanel } from '../components/TaskChatPanel';
8
8
  import { timeAgo } from '../components/utils';
9
+ import { AliasAvatar } from '../components/AliasAvatar';
9
10
 
10
11
  interface SessionDetail {
11
12
  resume_id: string;
@@ -150,15 +151,23 @@ function NodeDetailContent() {
150
151
 
151
152
  return (
152
153
  <div className="min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono">
153
- {/* Header */}
154
- <div className="flex items-center gap-4 mb-6">
155
- <Link href="/nodes" className="text-gray-500 hover:text-gray-300 text-sm lg:ml-0 ml-10">&larr; Nodes</Link>
156
- <div className="flex items-center gap-3">
157
- <span className={`inline-block w-3 h-3 rounded-full ${
158
- sse > 0 ? (session?.status === 'working' ? 'bg-green-500 animate-pulse' : 'bg-emerald-400') : 'bg-gray-500'
159
- }`} />
160
- <h1 className="text-2xl font-bold text-white">{alias}</h1>
161
- <span className={`text-sm ${statusColor}`}>{session?.status || 'unknown'}</span>
154
+ {/* Header — round 40: avatar joins the alias for cross-page hue
155
+ consistency; agent type lives in a subtitle so users know what
156
+ kind of node they're looking at. */}
157
+ <div className="flex items-center gap-3 mb-6 lg:ml-0 ml-10">
158
+ <Link href="/nodes" className="text-gray-500 hover:text-gray-300 text-sm shrink-0">&larr; Nodes</Link>
159
+ <AliasAvatar alias={alias} size={32} />
160
+ <div className="min-w-0 flex-1">
161
+ <div className="flex items-center gap-2">
162
+ <h1 className="text-xl sm:text-2xl font-bold text-white truncate">{alias}</h1>
163
+ <span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${
164
+ sse > 0 ? (session?.status === 'working' ? 'bg-green-500 animate-pulse' : 'bg-emerald-400') : 'bg-gray-500'
165
+ }`} />
166
+ <span className={`text-[10px] uppercase tracking-wide font-semibold ${statusColor}`}>{session?.status || 'unknown'}</span>
167
+ </div>
168
+ {session?.agent && (
169
+ <div className="text-xs text-gray-500 truncate">{session.agent}{session.server ? <> <span className="text-gray-700 mx-1">·</span> {session.server}</> : null}</div>
170
+ )}
162
171
  </div>
163
172
  </div>
164
173
 
@@ -190,14 +199,22 @@ function NodeFullPanel({ alias, session, sse, sendMsg, setSendMsg, sending, send
190
199
 
191
200
  return (
192
201
  <div className="bg-[#0d0d1a] border border-[#2a2a4a] rounded-xl overflow-hidden h-[calc(100vh-140px)] flex flex-col">
193
- {/* Tab bar */}
202
+ {/* Tab bar — round 40: emoji icons (💬 📋 📊) replaced with stroke
203
+ SVG to drop the "AI generated" tell. */}
194
204
  <div className="flex border-b border-[#2a2a4a] bg-[#0a0a15] shrink-0">
195
- {(['chat', 'events', 'info'] as const).map(t => (
196
- <button key={t} onClick={() => setTab(t)}
197
- className={`px-5 py-3 text-sm font-medium border-b-2 transition-colors ${
198
- tab === t ? 'border-cyan-400 text-cyan-300' : 'border-transparent text-gray-500 hover:text-gray-300'
205
+ {([
206
+ { id: 'chat', label: 'Chat', icon: 'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z' },
207
+ { id: 'events', label: 'Events', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6 M16 13H8 M16 17H8 M10 9H8' },
208
+ { id: 'info', label: 'Info', icon: 'M12 21a9 9 0 100-18 9 9 0 000 18z M12 8h.01 M11 12h1v4h1' },
209
+ ] as const).map(t => (
210
+ <button key={t.id} onClick={() => setTab(t.id)}
211
+ className={`flex items-center gap-2 px-5 py-3 text-sm font-medium border-b-2 transition-colors ${
212
+ tab === t.id ? 'border-cyan-400 text-cyan-300' : 'border-transparent text-gray-500 hover:text-gray-300'
199
213
  }`}>
200
- {t === 'chat' ? '💬 Chat' : t === 'events' ? '📋 Events' : '📊 Info'}
214
+ <svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
215
+ <path d={t.icon} />
216
+ </svg>
217
+ <span>{t.label}</span>
201
218
  </button>
202
219
  ))}
203
220
  </div>
@@ -299,13 +316,16 @@ function NodeFullPanel({ alias, session, sse, sendMsg, setSendMsg, sending, send
299
316
  );
300
317
  }
301
318
 
302
- const EVENT_COLORS: Record<string, { dot: string; line: string; icon: string }> = {
303
- delivered: { dot: 'bg-blue-400', line: 'bg-blue-500/30', icon: '📬' },
304
- acked: { dot: 'bg-yellow-400', line: 'bg-yellow-500/30', icon: '✅' },
305
- running: { dot: 'bg-green-400', line: 'bg-green-500/30', icon: '⚡' },
306
- replied: { dot: 'bg-purple-400', line: 'bg-purple-500/30', icon: '💬' },
307
- failed: { dot: 'bg-red-400', line: 'bg-red-500/30', icon: '' },
308
- created: { dot: 'bg-gray-400', line: 'bg-gray-500/30', icon: '📝' },
319
+ /** Round 59: emoji icons replaced with stroke SVG paths to drop the
320
+ * "AI generated" tell. Each path is sized to fit inside the 18px
321
+ * colored event dot. */
322
+ const EVENT_COLORS: Record<string, { dot: string; line: string; path: string }> = {
323
+ delivered: { dot: 'bg-blue-400', line: 'bg-blue-500/30', path: 'M12 5v14 M5 12l7 7 7-7' }, // down arrow
324
+ acked: { dot: 'bg-yellow-400', line: 'bg-yellow-500/30', path: 'M20 6 9 17l-5-5' }, // check
325
+ running: { dot: 'bg-green-400', line: 'bg-green-500/30', path: 'M13 2L4 14h6l-1 8 10-12h-6l1-8z' }, // bolt
326
+ replied: { dot: 'bg-purple-400', line: 'bg-purple-500/30', path: 'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z' }, // chat bubble
327
+ failed: { dot: 'bg-red-400', line: 'bg-red-500/30', path: 'M18 6 6 18 M6 6l12 12' }, // X
328
+ created: { dot: 'bg-gray-400', line: 'bg-gray-500/30', path: 'M12 5v14 M5 12h14' }, // plus
309
329
  };
310
330
 
311
331
  function EventsTimeline({ events, loading }: { events: Array<{ id: number; event_type: string; from_status: string; to_status: string; detail: string; created_at: string; task_id?: string }>; loading: boolean }) {
@@ -334,9 +354,12 @@ function EventsTimeline({ events, loading }: { events: Array<{ id: number; event
334
354
  const cfg = EVENT_COLORS[e.to_status] || EVENT_COLORS.created;
335
355
  return (
336
356
  <div key={e.id} className="relative pb-4 last:pb-0">
337
- {/* Dot on line */}
338
- <div className={`absolute -left-6 top-0.5 w-[18px] h-[18px] rounded-full ${cfg.dot} flex items-center justify-center text-[9px] z-10 border-2 border-[#111128]`}>
339
- {cfg.icon.length <= 2 ? cfg.icon : ''}
357
+ {/* Dot on line — round 59: emoji icons replaced with
358
+ a 10px stroke SVG path matching event type. */}
359
+ <div className={`absolute -left-6 top-0.5 w-[18px] h-[18px] rounded-full ${cfg.dot} flex items-center justify-center z-10 border-2 border-[#111128]`}>
360
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#0a0a15" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
361
+ <path d={cfg.path} />
362
+ </svg>
340
363
  </div>
341
364
  {/* Content */}
342
365
  <div className="ml-2">
@@ -5,7 +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
+ import { EmptyState, NodesEmptyState } from '../components/EmptyState';
9
9
  import { AliasAvatar } from '../components/AliasAvatar';
10
10
  import type { Session } from '../components/types';
11
11
 
@@ -121,6 +121,12 @@ export default function NodesPage() {
121
121
  );
122
122
  })()}
123
123
 
124
+ {/* Round 74: hide the filter+view chrome when there are no nodes
125
+ anywhere — these controls have nothing to act on, and they only
126
+ push the onboarding CTA further down. When at least one session
127
+ exists, the chrome is back even if the current filter happens
128
+ to hide everything (so users can clear filters). */}
129
+ {sessions.length > 0 && (
124
130
  <div className="flex flex-wrap gap-3 mb-6">
125
131
  <input
126
132
  type="text"
@@ -159,17 +165,27 @@ export default function NodesPage() {
159
165
  ))}
160
166
  </div>
161
167
  </div>
168
+ )}
162
169
 
163
170
  {loading ? (
164
171
  <div className="animate-pulse space-y-2">
165
172
  {[1,2,3,4].map(i => <div key={i} className="h-16 bg-gray-800/20 rounded-lg" />)}
166
173
  </div>
167
174
  ) : filtered.length === 0 ? (
168
- <EmptyState
169
- variant="nodes"
170
- title="No nodes match your filters"
171
- sub="Try clearing search or status filters, or wait for an agent to register."
172
- />
175
+ sessions.length === 0 ? (
176
+ /* Round 73: first-run case — no agents anywhere. Use the same
177
+ onboarding empty state as the Overview (NodesEmptyState
178
+ includes the `npx init` quickstart command). Previously this
179
+ was the "No nodes match your filters" copy, which was wrong
180
+ for new users who have no filters applied. */
181
+ <NodesEmptyState />
182
+ ) : (
183
+ <EmptyState
184
+ variant="nodes"
185
+ title="No nodes match your filters"
186
+ sub="Try clearing search or status filters, or wait for an agent to register."
187
+ />
188
+ )
173
189
  ) : viewMode === 'grid' ? (
174
190
  <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
175
191
  {filtered.map(s => {
package/app/page.tsx CHANGED
@@ -10,6 +10,8 @@ import { AgentCard } from './components/AgentCard';
10
10
  import { InboxPanel } from './components/InboxPanel';
11
11
  import { LoadingSkeleton } from './components/LoadingSkeleton';
12
12
  import { NodesEmptyState as EmptyState } from './components/EmptyState';
13
+ import { AliasAvatar } from './components/AliasAvatar';
14
+ import { STATUS_DOT_HEX, STATUS_CHIP_CLASS } from './lib/status';
13
15
  import { UserBar } from './components/UserBar';
14
16
  import { CommandCenter, useCommandCenter } from './components/CommandCenter';
15
17
  import { DispatchPanel } from './components/DispatchPanel';
@@ -40,6 +42,7 @@ export default function Dashboard() {
40
42
  const cmd = useCommandCenter();
41
43
  const [showDispatch, setShowDispatch] = useState(false);
42
44
  const [inbox, setInbox] = useState<InboxMessage[]>([]);
45
+ const [agentFilter, setAgentFilter] = useState<'all' | 'working' | 'idle' | 'offline'>('all');
43
46
  const { mutate } = useSWRConfig();
44
47
 
45
48
  // SSE: instant revalidation on CommHub events
@@ -113,18 +116,27 @@ export default function Dashboard() {
113
116
  return bWorking - aWorking;
114
117
  });
115
118
 
119
+ /** Round 70: when the fleet is empty, the Overview reorders to lead with
120
+ * the "Spin up your first agent" CTA and hides the Quick Navigation /
121
+ * Nav rail / Broadcast bar that would otherwise occupy prime real estate
122
+ * with zeros and dead-end links. Computed once, used as a gate below. */
123
+ const fleetEmpty = sessions.length === 0 && !sessError;
124
+
116
125
  return (
117
126
  <div className="min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono">
118
127
  <div className="lg:ml-0 ml-10">
119
128
  <StatsBar online={online} working={working} total={total} version={version} uptime={uptime} />
120
129
  </div>
121
130
 
122
- {/* Dispatch + User Bar */}
123
- <div className="flex items-center gap-3 mb-2">
124
- <button onClick={() => setShowDispatch(true)}
125
- className="px-4 py-2 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white text-sm font-medium rounded-xl shadow-lg shadow-cyan-500/10 transition-all active:scale-95 shrink-0">
126
- Dispatch
127
- </button>
131
+ {/* Dispatch + User Bar — Dispatch hidden when fleet empty (nothing to
132
+ dispatch to); UserBar still useful (account/sign-out menu). */}
133
+ <div className="flex items-center gap-3 mb-4">
134
+ {!fleetEmpty && (
135
+ <button onClick={() => setShowDispatch(true)}
136
+ className="px-4 py-2 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white text-sm font-medium rounded-xl shadow-lg shadow-cyan-500/10 transition-all active:scale-95 shrink-0">
137
+ ⚡ Dispatch
138
+ </button>
139
+ )}
128
140
  <div className="flex-1"><UserBar /></div>
129
141
  </div>
130
142
 
@@ -167,18 +179,15 @@ export default function Dashboard() {
167
179
  <Link href="/tasks" prefetch={false} className="text-xs text-cyan-400 hover:text-cyan-300">View all &rarr;</Link>
168
180
  </div>
169
181
  <div className="flex flex-wrap gap-2">
170
- {[
171
- { key: 'running', color: 'bg-green-500/10 text-green-300 border-green-500/20' },
172
- { key: 'delivered', color: 'bg-blue-500/10 text-blue-300 border-blue-500/20' },
173
- { key: 'acked', color: 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20' },
174
- { key: 'replied', color: 'bg-purple-500/10 text-purple-300 border-purple-500/20' },
175
- { key: 'failed', color: 'bg-red-500/10 text-red-300 border-red-500/20' },
176
- { key: 'cancelled', color: 'bg-yellow-500/10 text-yellow-300 border-yellow-500/20' },
177
- { key: 'expired', color: 'bg-orange-500/10 text-orange-300 border-orange-500/20' },
178
- { key: 'closed', color: 'bg-gray-500/10 text-gray-500 border-gray-500/20' },
179
- ].filter(({ key }) => taskStats[key])
180
- .map(({ key, color }) => (
181
- <Link key={key} href={`/tasks?status=${key}`} prefetch={false} className={`px-2.5 py-1 rounded-md border text-xs ${color} hover:opacity-80 transition-opacity`}>
182
+ {/* Round 69: order is "hot first" — active flow before terminal
183
+ states intentionally different from the lifecycle order on
184
+ /tasks. Colors come from shared STATUS_CHIP_CLASS so a
185
+ palette tweak in app/lib/status.ts updates here too.
186
+ 'created' added in r69 (was missing — enum-coverage bug). */}
187
+ {(['running', 'delivered', 'acked', 'replied', 'created', 'failed', 'cancelled', 'expired', 'closed'] as const)
188
+ .filter((key) => taskStats[key])
189
+ .map((key) => (
190
+ <Link key={key} href={`/tasks?status=${key}`} prefetch={false} className={`px-2.5 py-1 rounded-md border text-xs ${STATUS_CHIP_CLASS[key]} hover:opacity-80 transition-opacity`}>
182
191
  {key}: {taskStats[key]}
183
192
  </Link>
184
193
  ))}
@@ -189,9 +198,13 @@ export default function Dashboard() {
189
198
  {/* Quick Actions — split into two distinct intents:
190
199
  (1) Top: live stat cards (carry data, drill-in on click)
191
200
  (2) Bottom: pure nav rail (no number, icon + label)
192
- Previously these were mixed in one row of 6, mixing data cells
193
- ("0/0", "--", "0") with placeholder arrows ("→ Messages").
194
- Users couldn't tell which were stats vs which were navigation. */}
201
+ Round 24 wrap both in a labelled block so the rhythm reads as
202
+ "here are the main jumps" instead of two disconnected strips.
203
+ Round 70 entire Quick Nav + Nav rail + Broadcast + Recent
204
+ Activity block is hidden when the fleet is empty so the
205
+ onboarding CTA gets the page above the fold. */}
206
+ {!fleetEmpty && <>
207
+ <div className="text-[10px] uppercase tracking-[0.12em] text-gray-600 mb-2">Quick navigation</div>
195
208
  <section className="mb-3 grid grid-cols-3 gap-2 sm:gap-3">
196
209
  {(() => {
197
210
  // Build breakdown popover content per card. Pure data — pure CSS
@@ -311,21 +324,25 @@ export default function Dashboard() {
311
324
  <div className="space-y-2 max-h-40 overflow-y-auto">
312
325
  {tasks.slice(0, 5).map((t: { task_id: string; from_name: string; to_name: string; status: string; content: string; created_at: string }) => (
313
326
  <div key={t.task_id} className="flex items-center gap-2 text-xs">
314
- <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${
315
- t.status === 'replied' ? 'bg-purple-400' : t.status === 'running' ? 'bg-green-400' : t.status === 'failed' ? 'bg-red-400' : 'bg-blue-400'
316
- }`} />
317
- <span className="text-blue-400 shrink-0">{t.from_name || '?'}</span>
327
+ <span
328
+ className="w-1.5 h-1.5 rounded-full shrink-0"
329
+ style={{ backgroundColor: STATUS_DOT_HEX[t.status] || '#6b7280' }}
330
+ />
331
+ {t.from_name && <AliasAvatar alias={t.from_name} size={14} />}
332
+ <span className="text-gray-300 shrink-0 max-w-[20%] truncate">{t.from_name || '?'}</span>
318
333
  <span className="text-gray-600">&rarr;</span>
319
- <span className="text-cyan-400 shrink-0">{t.to_name || '?'}</span>
334
+ {t.to_name && <AliasAvatar alias={t.to_name} size={14} />}
335
+ <span className="text-gray-300 shrink-0 max-w-[20%] truncate">{t.to_name || '?'}</span>
320
336
  <span className="text-gray-500 truncate flex-1">{t.content?.slice(0, 40) || '--'}</span>
321
337
  <span className={`shrink-0 px-1.5 py-0.5 rounded text-[10px] border ${
322
- t.status === 'replied' ? 'text-purple-300 border-purple-500/20' : t.status === 'failed' ? 'text-red-300 border-red-500/20' : 'text-gray-500 border-gray-700/30'
338
+ STATUS_CHIP_CLASS[t.status] || 'text-gray-500 border-gray-700/30'
323
339
  }`}>{t.status}</span>
324
340
  </div>
325
341
  ))}
326
342
  </div>
327
343
  </section>
328
344
  )}
345
+ </>}
329
346
 
330
347
  {sessError && (
331
348
  <div className="bg-red-900/20 border border-red-800/40 text-red-300 px-4 py-3 rounded-lg mb-6 text-sm flex items-center justify-between" role="alert">
@@ -356,13 +373,67 @@ export default function Dashboard() {
356
373
 
357
374
  {sessions.length === 0 && !sessError ? (
358
375
  <EmptyState hint={sessHint} />
359
- ) : (
360
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 sm:gap-4">
361
- {sortedSessions.map(s => (
362
- <AgentCard key={s.alias} session={s} hasSse={isOnline(s)} sseCount={sseLookup(s) || 0} onChat={cmd.openTab} />
363
- ))}
364
- </div>
365
- )}
376
+ ) : (() => {
377
+ const counts = {
378
+ all: sortedSessions.length,
379
+ working: sortedSessions.filter(s => isOnline(s) && s.status === 'working').length,
380
+ idle: sortedSessions.filter(s => isOnline(s) && s.status !== 'working').length,
381
+ offline: sortedSessions.filter(s => !isOnline(s)).length,
382
+ };
383
+ const filtered = sortedSessions.filter(s => {
384
+ if (agentFilter === 'all') return true;
385
+ if (agentFilter === 'offline') return !isOnline(s);
386
+ if (agentFilter === 'working') return isOnline(s) && s.status === 'working';
387
+ if (agentFilter === 'idle') return isOnline(s) && s.status !== 'working';
388
+ return true;
389
+ });
390
+ // Filter chip color keyed to status (round 34): working=green, idle=cyan,
391
+ // offline=gray, all=neutral. Inline hex dots avoid Tailwind v4 purge.
392
+ const chips: { key: typeof agentFilter; label: string; dot?: string }[] = [
393
+ { key: 'all', label: 'All' },
394
+ { key: 'working', label: 'Working', dot: '#4ade80' },
395
+ { key: 'idle', label: 'Idle', dot: '#22d3ee' },
396
+ { key: 'offline', label: 'Offline', dot: '#6b7280' },
397
+ ];
398
+ return (
399
+ <>
400
+ <div className="mb-3 flex flex-wrap items-center gap-1">
401
+ {chips.map(c => (
402
+ <button
403
+ key={c.key}
404
+ type="button"
405
+ onClick={() => setAgentFilter(c.key)}
406
+ disabled={counts[c.key] === 0 && c.key !== 'all'}
407
+ className={`flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs transition-colors disabled:opacity-30 disabled:cursor-not-allowed ${
408
+ agentFilter === c.key
409
+ ? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/30'
410
+ : 'text-gray-500 hover:text-gray-200 hover:bg-[#1a1a2a] border border-transparent'
411
+ }`}
412
+ >
413
+ {c.dot && <span aria-hidden className="inline-block w-1.5 h-1.5 rounded-full" style={{ backgroundColor: c.dot }} />}
414
+ <span>{c.label}</span>
415
+ <span className={`text-[10px] tabular-nums ${agentFilter === c.key ? 'text-cyan-400' : 'text-gray-600'}`}>{counts[c.key]}</span>
416
+ </button>
417
+ ))}
418
+ </div>
419
+ {/* Round 48: previous breakpoints had `lg:grid-cols-2 xl:grid-cols-3`
420
+ which kept lg (1024-1279px) at only 2 columns even though
421
+ each AgentCard is fine ≥260px wide. With the sidebar (208px),
422
+ main area at lg is ~816px so 3 cols at ~272px each fits.
423
+ xl breakpoint auto-inherits lg=3 cols; 2xl bumps to 4. */}
424
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-3 sm:gap-4">
425
+ {filtered.map(s => (
426
+ <AgentCard key={s.alias} session={s} hasSse={isOnline(s)} sseCount={sseLookup(s) || 0} onChat={cmd.openTab} />
427
+ ))}
428
+ </div>
429
+ {filtered.length === 0 && (
430
+ <div className="mt-4 text-center text-xs text-gray-600">
431
+ No agents match "{agentFilter}" — <button onClick={() => setAgentFilter('all')} className="underline hover:text-gray-400">Show all</button>
432
+ </div>
433
+ )}
434
+ </>
435
+ );
436
+ })()}
366
437
 
367
438
  <InboxPanel messages={inbox} />
368
439
 
@@ -23,6 +23,30 @@ const LEVEL_BADGE: Record<LogLine['level'], string> = {
23
23
  error: 'bg-red-500/10 text-red-300 border-red-500/30',
24
24
  };
25
25
 
26
+ /** Left-rail strip color per log level — gives the eye a vertical guide
27
+ * when scanning long log walls. Plain hex so Tailwind purge can't drop it. */
28
+ const LEVEL_STRIPE: Record<LogLine['level'], string> = {
29
+ log: 'transparent',
30
+ info: '#22d3ee',
31
+ warn: '#f59e0b',
32
+ error: '#ef4444',
33
+ };
34
+
35
+ /** Highlight search-matched substring inside a log line. Case-insensitive
36
+ * first-match wrap; mark inherits color so log-level coloring stays. */
37
+ function highlightSearch(line: string, q: string) {
38
+ if (!q) return line;
39
+ const idx = line.toLowerCase().indexOf(q.toLowerCase());
40
+ if (idx < 0) return line;
41
+ return (
42
+ <>
43
+ {line.slice(0, idx)}
44
+ <mark className="bg-yellow-500/30 text-yellow-100 rounded-sm px-0.5">{line.slice(idx, idx + q.length)}</mark>
45
+ {line.slice(idx + q.length)}
46
+ </>
47
+ );
48
+ }
49
+
26
50
  function shortTime(iso: string) {
27
51
  try {
28
52
  const d = new Date(iso);
@@ -93,24 +117,48 @@ export default function ServerLogsPage() {
93
117
 
94
118
  return (
95
119
  <div className="p-6 max-w-[1600px] mx-auto">
96
- <div className="flex items-baseline justify-between mb-4">
97
- <div className="flex items-baseline gap-3">
120
+ <div className="flex items-center justify-between mb-4 gap-3">
121
+ <div className="flex items-center gap-3 min-w-0">
98
122
  <h1 className="text-2xl font-bold text-white">Server Logs</h1>
99
- <span className="text-xs text-gray-500">CommHub stdout/stderr · 最新在上</span>
123
+ {logs.length > 0 && (
124
+ <span className="text-xs bg-blue-900/30 text-blue-400 px-2 py-0.5 rounded-full border border-blue-800/30 tabular-nums">
125
+ {logs.length}
126
+ </span>
127
+ )}
128
+ <span className="text-xs text-gray-500 hidden sm:inline">CommHub stdout/stderr</span>
100
129
  </div>
101
- <div className="flex items-center gap-2 text-xs">
130
+ <div className="flex items-center gap-2 text-xs shrink-0">
102
131
  <button
103
132
  onClick={() => setPaused(p => !p)}
104
- className={`px-3 py-1 rounded border ${paused ? 'bg-amber-500/10 text-amber-300 border-amber-500/30' : 'bg-cyan-500/10 text-cyan-300 border-cyan-500/30'}`}
133
+ className={`flex items-center gap-1.5 px-3 py-1 rounded border ${paused ? 'bg-amber-500/10 text-amber-300 border-amber-500/30' : 'bg-cyan-500/10 text-cyan-300 border-cyan-500/30'}`}
105
134
  >
106
- {paused ? '▶ Resume' : '⏸ Pause'}
135
+ {paused ? (
136
+ <>
137
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" aria-hidden><path d="M8 5v14l11-7z" /></svg>
138
+ Resume
139
+ </>
140
+ ) : (
141
+ <>
142
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
143
+ <rect x="6" y="5" width="4" height="14" rx="0.5" />
144
+ <rect x="14" y="5" width="4" height="14" rx="0.5" />
145
+ </svg>
146
+ Pause
147
+ </>
148
+ )}
107
149
  </button>
108
150
  <label className="flex items-center gap-1 text-gray-400 select-none">
109
151
  <input type="checkbox" checked={autoRefresh} onChange={e => setAutoRefresh(e.target.checked)} className="accent-cyan-500" />
110
- auto-refresh
152
+ <span className="hidden sm:inline">auto-refresh</span>
111
153
  </label>
112
- <button onClick={() => fetchLogs(true)} className="px-3 py-1 rounded border bg-gray-500/10 text-gray-300 border-gray-500/20 hover:bg-gray-500/20">
113
- Reload
154
+ <button onClick={() => fetchLogs(true)} className="flex items-center gap-1.5 px-3 py-1 rounded border bg-gray-500/10 text-gray-300 border-gray-500/20 hover:bg-gray-500/20">
155
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
156
+ <path d="M3 12a9 9 0 0 1 15.5-6.4L21 8" />
157
+ <path d="M21 3v5h-5" />
158
+ <path d="M21 12a9 9 0 0 1-15.5 6.4L3 16" />
159
+ <path d="M3 21v-5h5" />
160
+ </svg>
161
+ <span className="hidden sm:inline">Reload</span>
114
162
  </button>
115
163
  </div>
116
164
  </div>
@@ -182,12 +230,22 @@ export default function ServerLogsPage() {
182
230
  ) : (
183
231
  <div className="divide-y divide-[#0d0d18] max-h-[calc(100vh-260px)] overflow-y-auto">
184
232
  {filtered.map((l, i) => (
185
- <div key={`${l.ts}-${i}`} className="px-3 py-1 hover:bg-[#0a0a14] flex gap-3">
186
- <span className="text-gray-600 shrink-0 w-[110px] text-[10px]">{shortTime(l.ts)}</span>
233
+ <div key={`${l.ts}-${i}`} className="relative pl-3 pr-3 py-1 hover:bg-[#0a0a14] flex gap-3">
234
+ {/* Round 32: 2px left rail keyed to level. Makes warn/error
235
+ spike visible in a wall of `log` lines without users having
236
+ to read the level chip on every row. */}
237
+ <span
238
+ aria-hidden
239
+ className="absolute left-0 top-0 bottom-0 w-0.5"
240
+ style={{ backgroundColor: LEVEL_STRIPE[l.level] }}
241
+ />
242
+ <span className="text-gray-600 shrink-0 w-[100px] text-[10px] tabular-nums">{shortTime(l.ts)}</span>
187
243
  <span className={`shrink-0 px-1.5 rounded border text-[9px] uppercase ${LEVEL_BADGE[l.level]}`}>
188
244
  {l.level}
189
245
  </span>
190
- <span className={`break-all whitespace-pre-wrap ${LEVEL_COLOR[l.level]}`}>{l.line}</span>
246
+ <span className={`break-all whitespace-pre-wrap ${LEVEL_COLOR[l.level]}`}>
247
+ {highlightSearch(l.line, search)}
248
+ </span>
191
249
  </div>
192
250
  ))}
193
251
  </div>
@@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { timeAgo } from '../../components/utils';
6
6
  import { EmptyState } from '../../components/EmptyState';
7
+ import { AliasAvatar } from '../../components/AliasAvatar';
7
8
  import useSWR from 'swr';
8
9
 
9
10
  const fetcher = (url: string) => fetch(url).then(r => r.json());
@@ -123,7 +124,12 @@ export default function NetworksPage() {
123
124
 
124
125
  {/* List */}
125
126
  <section className="bg-[#111128] border border-[#2a2a4a] rounded-xl p-5">
126
- <h2 className="text-sm font-semibold text-gray-300 mb-3">My Networks</h2>
127
+ <div className="flex items-center justify-between mb-3">
128
+ <h2 className="text-sm font-semibold text-gray-300">My Networks</h2>
129
+ {networks.length > 0 && (
130
+ <span className="text-[10px] text-gray-600 uppercase tracking-wide">{networks.length} total</span>
131
+ )}
132
+ </div>
127
133
  {loading ? (
128
134
  <div className="animate-pulse space-y-2">{[1,2].map(i => <div key={i} className="h-14 bg-gray-800/20 rounded" />)}</div>
129
135
  ) : networks.length === 0 ? (
@@ -136,11 +142,14 @@ export default function NetworksPage() {
136
142
  <div className="space-y-3">
137
143
  {networks.map(n => (
138
144
  <div key={n.network_id} className="bg-[#0a0a15] rounded-lg px-4 py-3 border border-[#1a1a2a]">
139
- <div className="flex items-center justify-between">
140
- <div className="min-w-0">
141
- <div className="text-sm text-white font-medium">{n.network_name}</div>
142
- <div className="text-[10px] text-gray-500">{n.network_id} · {timeAgo(n.created_at)}</div>
143
- {n.description && <div className="text-xs text-gray-400 mt-1">{n.description}</div>}
145
+ <div className="flex items-start justify-between gap-3">
146
+ <div className="flex items-start gap-3 min-w-0">
147
+ <AliasAvatar alias={n.network_name} size={28} />
148
+ <div className="min-w-0">
149
+ <div className="text-sm text-white font-medium truncate">{n.network_name}</div>
150
+ <div className="text-[10px] text-gray-500 font-mono truncate" title={n.network_id}>{n.network_id} · {timeAgo(n.created_at)}</div>
151
+ {n.description && <div className="text-xs text-gray-400 mt-1">{n.description}</div>}
152
+ </div>
144
153
  </div>
145
154
  <div className="flex gap-2 shrink-0">
146
155
  <button onClick={() => {
@@ -169,14 +178,20 @@ export default function NetworksPage() {
169
178
  {(members[n.network_id] || []).length === 0 ? (
170
179
  <div className="text-[10px] text-gray-600">No members or V3 auth required</div>
171
180
  ) : (
172
- (members[n.network_id] || []).map(m => (
173
- <div key={m.user_id} className="flex items-center justify-between text-xs py-1">
174
- <span className="text-gray-300">{m.username || m.display_name}</span>
175
- <span className={`px-1.5 py-0.5 rounded border text-[10px] ${
181
+ (members[n.network_id] || []).map(m => {
182
+ const name = m.username || m.display_name || '?';
183
+ return (
184
+ <div key={m.user_id} className="flex items-center gap-2 justify-between text-xs py-1">
185
+ <div className="flex items-center gap-2 min-w-0">
186
+ <AliasAvatar alias={name} size={16} />
187
+ <span className="text-gray-300 truncate">{name}</span>
188
+ </div>
189
+ <span className={`px-1.5 py-0.5 rounded border text-[10px] shrink-0 ${
176
190
  m.role === 'owner' ? 'text-yellow-300 border-yellow-500/20' : 'text-gray-400 border-gray-600/20'
177
191
  }`}>{m.role}</span>
178
192
  </div>
179
- ))
193
+ );
194
+ })
180
195
  )}
181
196
 
182
197
  {/* Invite */}
@@ -196,8 +211,17 @@ export default function NetworksPage() {
196
211
  </div>
197
212
  {inviteCode && (
198
213
  <div className="mt-2 bg-green-500/5 border border-green-500/20 rounded px-3 py-2">
199
- <div className="text-[10px] text-green-400 mb-1">Invite code:</div>
200
- <code className="text-xs text-green-300 select-all">{inviteCode}</code>
214
+ <div className="flex items-center justify-between mb-1">
215
+ <div className="text-[10px] text-green-400 uppercase tracking-wide">Invite code</div>
216
+ <button
217
+ type="button"
218
+ onClick={() => { navigator.clipboard?.writeText(inviteCode).catch(() => {}); }}
219
+ className="text-[10px] text-green-300 hover:text-green-200 px-1.5 py-0.5 rounded border border-green-500/20 hover:bg-green-500/10"
220
+ >
221
+ Copy
222
+ </button>
223
+ </div>
224
+ <code className="text-xs text-green-300 select-all break-all">{inviteCode}</code>
201
225
  </div>
202
226
  )}
203
227
  </div>