@sleep2agi/agent-network-dashboard 0.4.5-preview.8 → 0.4.5-preview.80

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 (281) hide show
  1. package/.mcp.json +11 -0
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/diagnostics/route-bundle-stats.json +48 -48
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/server/app/_global-error.html +1 -1
  8. package/.next/server/app/_global-error.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/_not-found.html +4 -4
  16. package/.next/server/app/_not-found.rsc +12 -12
  17. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  18. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  19. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  20. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/server/app/admin/page.js.nft.json +1 -1
  24. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  25. package/.next/server/app/admin.html +4 -4
  26. package/.next/server/app/admin.rsc +14 -14
  27. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  28. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  29. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  30. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  31. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  32. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  33. package/.next/server/app/index.html +4 -4
  34. package/.next/server/app/index.rsc +14 -14
  35. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  36. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  37. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  38. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  39. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  40. package/.next/server/app/login/page.js.nft.json +1 -1
  41. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  42. package/.next/server/app/login.html +2 -2
  43. package/.next/server/app/login.rsc +14 -14
  44. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  45. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  46. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  47. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  48. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  49. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  50. package/.next/server/app/logs/page.js.nft.json +1 -1
  51. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  52. package/.next/server/app/logs.html +4 -4
  53. package/.next/server/app/logs.rsc +14 -14
  54. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  55. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  56. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  57. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  58. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  59. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  60. package/.next/server/app/messages/page.js.nft.json +1 -1
  61. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  62. package/.next/server/app/messages.html +4 -4
  63. package/.next/server/app/messages.rsc +14 -14
  64. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  65. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  66. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  67. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  68. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  69. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  70. package/.next/server/app/node/page.js.nft.json +1 -1
  71. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  72. package/.next/server/app/node.html +4 -4
  73. package/.next/server/app/node.rsc +14 -14
  74. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  75. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  76. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  77. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  78. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  79. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  80. package/.next/server/app/nodes/page.js.nft.json +1 -1
  81. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  82. package/.next/server/app/nodes.html +4 -4
  83. package/.next/server/app/nodes.rsc +14 -14
  84. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  85. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  86. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  87. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  88. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  89. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  90. package/.next/server/app/page.js.nft.json +1 -1
  91. package/.next/server/app/page_client-reference-manifest.js +1 -1
  92. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app/server-logs.html +4 -4
  94. package/.next/server/app/server-logs.rsc +14 -14
  95. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  96. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  97. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  98. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  99. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  100. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  101. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  102. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  103. package/.next/server/app/settings/networks.html +4 -4
  104. package/.next/server/app/settings/networks.rsc +14 -14
  105. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  106. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  107. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  108. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  109. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  110. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  111. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  112. package/.next/server/app/settings/page.js.nft.json +1 -1
  113. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  114. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  115. package/.next/server/app/settings/tokens.html +4 -4
  116. package/.next/server/app/settings/tokens.rsc +14 -14
  117. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  118. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  119. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  120. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  121. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  122. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  123. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  124. package/.next/server/app/settings.html +4 -4
  125. package/.next/server/app/settings.rsc +14 -14
  126. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  127. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  128. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  129. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  130. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  131. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  132. package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  133. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  134. package/.next/server/app/tasks/page.js.nft.json +1 -1
  135. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  136. package/.next/server/app/tasks.html +4 -4
  137. package/.next/server/app/tasks.rsc +14 -14
  138. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  139. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  140. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  141. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  142. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  143. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  144. package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js +3 -0
  145. package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js.map +1 -0
  146. package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js +3 -0
  147. package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js.map +1 -0
  148. package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js +3 -0
  149. package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js.map +1 -0
  150. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +1 -1
  151. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +1 -1
  152. package/.next/server/chunks/ssr/_0nne7mz._.js +3 -0
  153. package/.next/server/chunks/ssr/_0nne7mz._.js.map +1 -0
  154. package/.next/server/chunks/ssr/app_0.zbtn5._.js +9 -0
  155. package/.next/server/chunks/ssr/app_0.zbtn5._.js.map +1 -0
  156. package/.next/server/chunks/ssr/app_01y-~mp._.js +3 -0
  157. package/.next/server/chunks/ssr/app_01y-~mp._.js.map +1 -0
  158. package/.next/server/chunks/ssr/app_04n78pr._.js +3 -0
  159. package/.next/server/chunks/ssr/app_04n78pr._.js.map +1 -0
  160. package/.next/server/chunks/ssr/app_0_agk0r._.js +3 -0
  161. package/.next/server/chunks/ssr/app_0_agk0r._.js.map +1 -0
  162. package/.next/server/chunks/ssr/app_0aenxef._.js +3 -0
  163. package/.next/server/chunks/ssr/app_0aenxef._.js.map +1 -0
  164. package/.next/server/chunks/ssr/app_0aum_g5._.js +3 -0
  165. package/.next/server/chunks/ssr/app_0aum_g5._.js.map +1 -0
  166. package/.next/server/chunks/ssr/app_0emvkn2._.js +3 -0
  167. package/.next/server/chunks/ssr/app_0emvkn2._.js.map +1 -0
  168. package/.next/server/chunks/ssr/app_0esuljp._.js +3 -0
  169. package/.next/server/chunks/ssr/app_0esuljp._.js.map +1 -0
  170. package/.next/server/chunks/ssr/app_0izi28j._.js +1 -1
  171. package/.next/server/chunks/ssr/app_0izi28j._.js.map +1 -1
  172. package/.next/server/chunks/ssr/app_components_08ys5y3._.js +5 -5
  173. package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +1 -1
  174. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js +1 -1
  175. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js.map +1 -1
  176. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
  177. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
  178. package/.next/server/middleware-build-manifest.js +3 -3
  179. package/.next/server/pages/404.html +4 -4
  180. package/.next/server/pages/500.html +1 -1
  181. package/.next/server/server-reference-manifest.js +1 -1
  182. package/.next/server/server-reference-manifest.json +1 -1
  183. package/.next/static/chunks/06uku.~mtk9zz.js +1 -0
  184. package/.next/static/chunks/07zsecr-~j5xa.js +1 -0
  185. package/.next/static/chunks/08i3fgrjz9_yj.css +2 -0
  186. package/.next/static/chunks/0av2~0.96~d~g.js +1 -0
  187. package/.next/static/chunks/0b64c5k_6b_n8.js +1 -0
  188. package/.next/static/chunks/{07okhs5561y5y.js → 0c1bp~g7.8ru..js} +1 -1
  189. package/.next/static/chunks/0dj5~edkdwh1n.js +1 -0
  190. package/.next/static/chunks/0geyh7e8y2s5f.js +1 -0
  191. package/.next/static/chunks/0gn0dhq_g343f.js +1 -0
  192. package/.next/static/chunks/0gtbwcilfu6b7.js +1 -0
  193. package/.next/static/chunks/0ir50f-dedv4s.js +1 -0
  194. package/.next/static/chunks/0rk5zmyp19fv8.js +7 -0
  195. package/.next/static/chunks/0ve696rkee.95.js +7 -0
  196. package/.next/static/chunks/0why2e264gmro.js +1 -0
  197. package/.next/static/chunks/0yw01eo779t1c.js +1 -0
  198. package/.next/static/chunks/11qk8zcquptgr.js +1 -0
  199. package/.next/static/chunks/158g10ijxldbh.js +1 -0
  200. package/.next/trace +2 -2
  201. package/.next/trace-build +1 -1
  202. package/app/admin/page.tsx +97 -27
  203. package/app/components/AgentCard.tsx +19 -5
  204. package/app/components/BroadcastBar.tsx +22 -4
  205. package/app/components/DispatchPanel.tsx +9 -5
  206. package/app/components/EmptyState.tsx +77 -4
  207. package/app/components/HealthBanner.tsx +11 -2
  208. package/app/components/HelpOverlay.tsx +50 -10
  209. package/app/components/InboxPanel.tsx +7 -4
  210. package/app/components/Sidebar.tsx +49 -41
  211. package/app/components/StatsBar.tsx +59 -36
  212. package/app/components/TaskChatPanel.tsx +8 -10
  213. package/app/components/TaskDrawer.tsx +72 -28
  214. package/app/components/TopoGraph.tsx +183 -31
  215. package/app/components/utils.ts +13 -0
  216. package/app/globals.css +90 -0
  217. package/app/lib/status.ts +85 -0
  218. package/app/lib/version.ts +7 -0
  219. package/app/login/page.tsx +40 -7
  220. package/app/logs/page.tsx +70 -21
  221. package/app/messages/page.tsx +34 -9
  222. package/app/node/page.tsx +52 -27
  223. package/app/nodes/page.tsx +45 -21
  224. package/app/page.tsx +99 -31
  225. package/app/server-logs/page.tsx +104 -28
  226. package/app/settings/networks/page.tsx +40 -16
  227. package/app/settings/page.tsx +39 -11
  228. package/app/tasks/[id]/page.tsx +2 -13
  229. package/app/tasks/page.tsx +66 -73
  230. package/package.json +1 -1
  231. package/scripts/fold-only.mjs +47 -0
  232. package/scripts/loop-review.mjs +270 -0
  233. package/scripts/topo-shot.mjs +25 -0
  234. package/.next/server/chunks/ssr/[root-of-the-server]__06w_u7t._.js +0 -3
  235. package/.next/server/chunks/ssr/[root-of-the-server]__06w_u7t._.js.map +0 -1
  236. package/.next/server/chunks/ssr/[root-of-the-server]__0ehu0jb._.js +0 -3
  237. package/.next/server/chunks/ssr/[root-of-the-server]__0ehu0jb._.js.map +0 -1
  238. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js +0 -3
  239. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js.map +0 -1
  240. package/.next/server/chunks/ssr/_13_v7-3._.js +0 -3
  241. package/.next/server/chunks/ssr/_13_v7-3._.js.map +0 -1
  242. package/.next/server/chunks/ssr/app_0.0wpa~._.js +0 -3
  243. package/.next/server/chunks/ssr/app_0.0wpa~._.js.map +0 -1
  244. package/.next/server/chunks/ssr/app_0ap_oiq._.js +0 -3
  245. package/.next/server/chunks/ssr/app_0ap_oiq._.js.map +0 -1
  246. package/.next/server/chunks/ssr/app_0dk5wao._.js +0 -3
  247. package/.next/server/chunks/ssr/app_0dk5wao._.js.map +0 -1
  248. package/.next/server/chunks/ssr/app_0et5yua._.js +0 -3
  249. package/.next/server/chunks/ssr/app_0et5yua._.js.map +0 -1
  250. package/.next/server/chunks/ssr/app_10b5blc._.js +0 -9
  251. package/.next/server/chunks/ssr/app_10b5blc._.js.map +0 -1
  252. package/.next/server/chunks/ssr/app_11vl967._.js +0 -3
  253. package/.next/server/chunks/ssr/app_11vl967._.js.map +0 -1
  254. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js +0 -3
  255. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js.map +0 -1
  256. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +0 -3
  257. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +0 -1
  258. package/.next/static/chunks/02afyxgpty3uf.js +0 -1
  259. package/.next/static/chunks/02flrewxlf_m_.js +0 -1
  260. package/.next/static/chunks/051gslfjka1kj.js +0 -1
  261. package/.next/static/chunks/07oobuohrpigy.js +0 -1
  262. package/.next/static/chunks/09al3qm76q7nd.js +0 -1
  263. package/.next/static/chunks/0e7c40p~wq665.js +0 -1
  264. package/.next/static/chunks/0g7m7zk.v7g6h.js +0 -1
  265. package/.next/static/chunks/0h.ajsk_uxigl.js +0 -1
  266. package/.next/static/chunks/0msa~bub38ytf.css +0 -2
  267. package/.next/static/chunks/0ogeals5ufhho.js +0 -1
  268. package/.next/static/chunks/0sgvs~8hw4df0.js +0 -1
  269. package/.next/static/chunks/10ir.g.n~hre0.js +0 -7
  270. package/.next/static/chunks/10njj7kn9ylkf.js +0 -1
  271. package/.next/static/chunks/12dcb86elsl-e.js +0 -7
  272. package/.next/static/chunks/13.dst7liziwo.js +0 -1
  273. package/.next/static/chunks/132e__998_q9r.js +0 -1
  274. package/public/file.svg +0 -1
  275. package/public/globe.svg +0 -1
  276. package/public/next.svg +0 -1
  277. package/public/vercel.svg +0 -1
  278. package/public/window.svg +0 -1
  279. /package/.next/static/{LQ5MjOWd5QM3a72laNr_D → 5EPRA3x3-w6LIGTySDUkk}/_buildManifest.js +0 -0
  280. /package/.next/static/{LQ5MjOWd5QM3a72laNr_D → 5EPRA3x3-w6LIGTySDUkk}/_clientMiddlewareManifest.js +0 -0
  281. /package/.next/static/{LQ5MjOWd5QM3a72laNr_D → 5EPRA3x3-w6LIGTySDUkk}/_ssgManifest.js +0 -0
package/app/logs/page.tsx CHANGED
@@ -5,6 +5,7 @@ import Link from 'next/link';
5
5
  import { timeAgo } from '../components/utils';
6
6
  import { useNetworkId } from '../lib/network-context';
7
7
  import { EmptyState } from '../components/EmptyState';
8
+ import { AliasAvatar } from '../components/AliasAvatar';
8
9
 
9
10
  interface AuditLog {
10
11
  id: string;
@@ -26,6 +27,18 @@ const ACTION_COLORS: Record<string, string> = {
26
27
  send_task: 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20',
27
28
  };
28
29
 
30
+ /** 2px left-rail stripe color per audit action — reuses the r32 server-logs
31
+ * pattern so failed logins / token rotations spike out of a wall of
32
+ * registers and logins without users reading every chip. Inline hex
33
+ * prevents Tailwind v4 from purging dynamic class names. */
34
+ const ACTION_STRIPE: Record<string, string> = {
35
+ register: '#4ade80',
36
+ login: '#60a5fa',
37
+ login_failed: '#ef4444',
38
+ create_network: '#a78bfa',
39
+ send_task: '#22d3ee',
40
+ };
41
+
29
42
  export default function LogsPage() {
30
43
  const { networkId } = useNetworkId();
31
44
  const [logs, setLogs] = useState<AuditLog[]>([]);
@@ -64,25 +77,50 @@ export default function LogsPage() {
64
77
  return (
65
78
  <div className="min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono">
66
79
  <div className="flex items-center gap-4 mb-6">
67
- <h1 className="text-2xl font-bold text-white lg:ml-0 ml-10">Logs</h1>
68
- <span className="text-xs bg-gray-900/30 text-gray-400 px-2 py-0.5 rounded-full border border-gray-800/30">
69
- Audit Log
70
- </span>
80
+ <h1 className="text-2xl font-bold text-white lg:ml-0 ml-10">Audit Log</h1>
81
+ {/* Round 87: dropped {logs.length} header chip — r43 added
82
+ `All <count>` to the action-filter strip below, which
83
+ duplicated this value 40px away. Matches r86 /server-logs. */}
71
84
  </div>
72
85
 
73
- <div className="flex flex-wrap gap-3 mb-6">
74
- <select
75
- value={filterAction}
76
- onChange={e => setFilterAction(e.target.value)}
77
- className="bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-sm text-white focus:border-blue-500/50 focus:outline-none"
78
- >
79
- <option value="">All actions</option>
80
- <option value="register">register</option>
81
- <option value="login">login</option>
82
- <option value="login_failed">login_failed</option>
83
- <option value="create_network">create_network</option>
84
- <option value="send_task">send_task</option>
85
- </select>
86
+ {/* Action filter chips (round 43) — same chip pattern as Tasks
87
+ status tabs / Overview agent filter. Each chip carries its
88
+ ACTION_STRIPE dot so the visual key matches the per-row left
89
+ rail. Counts come from the currently loaded log set. */}
90
+ <div className="mb-6 flex flex-wrap items-center gap-1 anet-tabstrip-wrap overflow-x-auto sm:overflow-x-visible">
91
+ {(() => {
92
+ const counts: Record<string, number> = {};
93
+ logs.forEach(l => { counts[l.action] = (counts[l.action] || 0) + 1; });
94
+ const chips: { key: string; label: string; dot?: string }[] = [
95
+ { key: '', label: 'All' },
96
+ { key: 'login', label: 'login', dot: ACTION_STRIPE.login },
97
+ { key: 'login_failed', label: 'login_failed', dot: ACTION_STRIPE.login_failed },
98
+ { key: 'register', label: 'register', dot: ACTION_STRIPE.register },
99
+ { key: 'create_network', label: 'create_network', dot: ACTION_STRIPE.create_network },
100
+ { key: 'send_task', label: 'send_task', dot: ACTION_STRIPE.send_task },
101
+ ];
102
+ return chips.map(c => {
103
+ const count = c.key === '' ? logs.length : counts[c.key] || 0;
104
+ const isActive = filterAction === c.key;
105
+ return (
106
+ <button
107
+ key={c.key || 'all'}
108
+ type="button"
109
+ onClick={() => setFilterAction(c.key)}
110
+ disabled={count === 0 && c.key !== ''}
111
+ 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 shrink-0 whitespace-nowrap ${
112
+ isActive
113
+ ? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/30'
114
+ : 'text-gray-500 hover:text-gray-200 hover:bg-[#1a1a2a] border border-transparent'
115
+ }`}
116
+ >
117
+ {c.dot && <span aria-hidden className="inline-block w-1.5 h-1.5 rounded-full" style={{ backgroundColor: c.dot }} />}
118
+ <span>{c.label}</span>
119
+ <span className={`text-[10px] tabular-nums ${isActive ? 'text-cyan-400' : 'text-gray-600'}`}>{count}</span>
120
+ </button>
121
+ );
122
+ });
123
+ })()}
86
124
  </div>
87
125
 
88
126
  {error && (() => {
@@ -125,13 +163,23 @@ export default function LogsPage() {
125
163
  <EmptyState variant="logs" />
126
164
  ) : (
127
165
  <div className="space-y-2">
128
- {logs.map(log => (
129
- <div key={log.id} className="bg-[#111128] border border-[#2a2a4a] rounded-lg px-4 py-3 hover:border-[#3a3a5a] transition-colors">
166
+ {logs.map(log => {
167
+ const userName = log.username || log.user_id;
168
+ return (
169
+ <div key={log.id} className="relative bg-[#111128] border border-[#2a2a4a] rounded-lg pl-4 pr-4 py-3 hover:border-[#3a3a5a] transition-colors overflow-hidden">
170
+ {/* 2px left rail per action (round 33) — failed logins, token
171
+ rotations spike out of a wall of register/login rows. */}
172
+ <span
173
+ aria-hidden
174
+ className="absolute left-0 top-0 bottom-0 w-0.5"
175
+ style={{ backgroundColor: ACTION_STRIPE[log.action] || 'transparent' }}
176
+ />
130
177
  <div className="flex flex-wrap items-center gap-2 mb-1">
131
178
  <span className={`text-xs px-2 py-0.5 rounded-md border ${ACTION_COLORS[log.action] || 'bg-gray-500/10 text-gray-400 border-gray-500/20'}`}>
132
179
  {log.action}
133
180
  </span>
134
- <span className="text-sm text-blue-400 font-medium">{log.username || log.user_id}</span>
181
+ {userName && <AliasAvatar alias={userName} size={16} />}
182
+ <span className="text-sm text-gray-200 font-medium">{userName}</span>
135
183
  {log.target_type && (
136
184
  <>
137
185
  <span className="text-xs text-gray-600">&rarr;</span>
@@ -143,7 +191,8 @@ export default function LogsPage() {
143
191
  {log.detail && <div className="text-xs text-gray-500">{log.detail}</div>}
144
192
  {log.ip && log.ip !== 'unknown' && <div className="text-[10px] text-gray-700">IP: {log.ip}</div>}
145
193
  </div>
146
- ))}
194
+ );
195
+ })}
147
196
  </div>
148
197
  )}
149
198
  </div>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useMemo, useState } from 'react';
4
4
  import { useMessages } from '../lib/hooks';
5
- import { timeAgo } from '../components/utils';
5
+ import { timeAgo, previewContent } from '../components/utils';
6
6
  import { EmptyState } from '../components/EmptyState';
7
7
  import { AliasAvatar } from '../components/AliasAvatar';
8
8
 
@@ -107,8 +107,16 @@ export default function MessagesPage() {
107
107
  <div className="min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono">
108
108
  <div className="flex items-center gap-4 mb-6">
109
109
  <h1 className="text-2xl font-bold text-white lg:ml-0 ml-10">Messages</h1>
110
- <span className="text-xs bg-blue-900/30 text-blue-400 px-2 py-0.5 rounded-full border border-blue-800/30">
111
- {messages.length}
110
+ {/* Round 89: filter-aware chip. When search/type filter is active
111
+ the visible rows are filtered.length; the total loaded is
112
+ messages.length (capped at 200 by useMessages). Show
113
+ `filtered / total` so users notice how much the filter is
114
+ hiding. Same pattern as r88 /tasks chip. */}
115
+ <span
116
+ className="text-xs bg-blue-900/30 text-blue-400 px-2 py-0.5 rounded-full border border-blue-800/30 tabular-nums"
117
+ title={filtered.length < messages.length ? `Showing ${filtered.length} of ${messages.length} messages` : undefined}
118
+ >
119
+ {filtered.length < messages.length ? `${filtered.length} / ${messages.length}` : messages.length}
112
120
  </span>
113
121
  {filtered.length > 0 && (
114
122
  <button
@@ -129,6 +137,11 @@ export default function MessagesPage() {
129
137
  )}
130
138
  </div>
131
139
 
140
+ {/* Round 76: hide search + type filter + view toggle + Debug button
141
+ when there are no messages at all. Same r70-class fix carried
142
+ across /nodes (r74) and /tasks (r75). When at least one message
143
+ exists, the chrome reappears so users can still clear filters. */}
144
+ {messages.length > 0 && (
132
145
  <div className="flex flex-wrap gap-3 mb-3">
133
146
  <input
134
147
  type="text"
@@ -165,6 +178,7 @@ export default function MessagesPage() {
165
178
  {debug ? 'Debug ON' : 'Debug'}
166
179
  </button>
167
180
  </div>
181
+ )}
168
182
 
169
183
  {quickFromChips.length > 0 && (
170
184
  <div className="mb-6 flex flex-wrap gap-2">
@@ -206,17 +220,20 @@ export default function MessagesPage() {
206
220
  <div key={ci} className="bg-[#111128] border border-[#2a2a4a] rounded-xl overflow-hidden">
207
221
  <div className="px-4 py-2.5 border-b border-[#2a2a4a] flex items-center justify-between">
208
222
  <div className="flex items-center gap-2">
209
- <span className="text-sm text-cyan-300 font-medium">{conv.participants[0]}</span>
223
+ <AliasAvatar alias={conv.participants[0]} size={16} />
224
+ <span className="text-sm text-gray-200 font-medium">{conv.participants[0]}</span>
210
225
  <span className="text-gray-600 text-xs">↔</span>
211
- <span className="text-sm text-green-300 font-medium">{conv.participants[1]}</span>
226
+ <AliasAvatar alias={conv.participants[1]} size={16} />
227
+ <span className="text-sm text-gray-200 font-medium">{conv.participants[1]}</span>
212
228
  </div>
213
229
  <span className="text-[10px] text-gray-600">{conv.messages.length} messages</span>
214
230
  </div>
215
231
  <div className="px-4 py-2 space-y-2 max-h-64 overflow-y-auto">
216
232
  {conv.messages.map(m => (
217
233
  <div key={m.id} className="flex items-start gap-2 text-xs py-1">
218
- <span className={`shrink-0 font-medium ${m.from_alias === conv.participants[0] ? 'text-cyan-400' : 'text-green-400'}`}>{m.from_alias}</span>
219
- <span className="text-gray-400 flex-1">{m.content?.slice(0, 120) || '--'}</span>
234
+ {m.from_alias && <AliasAvatar alias={m.from_alias} size={14} />}
235
+ <span className="shrink-0 font-medium text-gray-200">{m.from_alias}</span>
236
+ <span className="text-gray-400 flex-1 truncate" title={m.content || ''}>{previewContent(m.content).slice(0, 120)}</span>
220
237
  <span className="text-[9px] text-gray-600 shrink-0">{timeAgo(m.created_at || '')}</span>
221
238
  </div>
222
239
  ))}
@@ -264,7 +281,11 @@ export default function MessagesPage() {
264
281
  {message.priority === 'high' && <span className="text-xs text-red-400">HIGH</span>}
265
282
  <span className="ml-auto text-xs text-gray-500">{message.created_at ? timeAgo(message.created_at) : '--'}</span>
266
283
  </div>
267
- <div className="whitespace-pre-wrap text-sm leading-relaxed text-gray-200">
284
+ {/* Round 79: break-words + overflow-wrap:anywhere so
285
+ long unbroken runs (URLs, ASCII rules like
286
+ `═══════════════`) wrap instead of pushing the
287
+ chat bubble past the mobile viewport. */}
288
+ <div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-relaxed text-gray-200">
268
289
  {renderHighlighted(message.content, search)}
269
290
  </div>
270
291
  </div>
@@ -295,7 +316,11 @@ export default function MessagesPage() {
295
316
  </div>
296
317
  )}
297
318
 
298
- <div className="whitespace-pre-wrap text-sm leading-relaxed text-gray-200">
319
+ {/* Round 79: break-words + overflow-wrap:anywhere so
320
+ long unbroken runs (URLs, ASCII rules like
321
+ `═══════════════`) wrap instead of pushing the
322
+ chat bubble past the mobile viewport. */}
323
+ <div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-relaxed text-gray-200">
299
324
  {renderHighlighted(message.content, search)}
300
325
  </div>
301
326
 
package/app/node/page.tsx CHANGED
@@ -6,6 +6,8 @@ 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';
10
+ import { SESSION_STATUS_TEXT_CLASS } from '../lib/status';
9
11
 
10
12
  interface SessionDetail {
11
13
  resume_id: string;
@@ -145,20 +147,29 @@ function NodeDetailContent() {
145
147
  );
146
148
  }
147
149
 
148
- const statusColor = session?.status === 'working' ? 'text-green-400' :
149
- session?.status === 'idle' ? 'text-blue-400' : 'text-gray-500';
150
+ /* Round 92: shared 5-state palette (was working / idle / other,
151
+ collapsed blocked + error into gray). */
152
+ const statusColor = SESSION_STATUS_TEXT_CLASS[session?.status || ''] || SESSION_STATUS_TEXT_CLASS.offline;
150
153
 
151
154
  return (
152
155
  <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>
156
+ {/* Header — round 40: avatar joins the alias for cross-page hue
157
+ consistency; agent type lives in a subtitle so users know what
158
+ kind of node they're looking at. */}
159
+ <div className="flex items-center gap-3 mb-6 lg:ml-0 ml-10">
160
+ <Link href="/nodes" className="text-gray-500 hover:text-gray-300 text-sm shrink-0">&larr; Nodes</Link>
161
+ <AliasAvatar alias={alias} size={32} />
162
+ <div className="min-w-0 flex-1">
163
+ <div className="flex items-center gap-2">
164
+ <h1 className="text-xl sm:text-2xl font-bold text-white truncate">{alias}</h1>
165
+ <span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${
166
+ sse > 0 ? (session?.status === 'working' ? 'bg-green-500 animate-pulse' : 'bg-emerald-400') : 'bg-gray-500'
167
+ }`} />
168
+ <span className={`text-[10px] uppercase tracking-wide font-semibold ${statusColor}`}>{session?.status || 'unknown'}</span>
169
+ </div>
170
+ {session?.agent && (
171
+ <div className="text-xs text-gray-500 truncate">{session.agent}{session.server ? <> <span className="text-gray-700 mx-1">·</span> {session.server}</> : null}</div>
172
+ )}
162
173
  </div>
163
174
  </div>
164
175
 
@@ -190,14 +201,22 @@ function NodeFullPanel({ alias, session, sse, sendMsg, setSendMsg, sending, send
190
201
 
191
202
  return (
192
203
  <div className="bg-[#0d0d1a] border border-[#2a2a4a] rounded-xl overflow-hidden h-[calc(100vh-140px)] flex flex-col">
193
- {/* Tab bar */}
204
+ {/* Tab bar — round 40: emoji icons (💬 📋 📊) replaced with stroke
205
+ SVG to drop the "AI generated" tell. */}
194
206
  <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'
207
+ {([
208
+ { 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' },
209
+ { 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' },
210
+ { id: 'info', label: 'Info', icon: 'M12 21a9 9 0 100-18 9 9 0 000 18z M12 8h.01 M11 12h1v4h1' },
211
+ ] as const).map(t => (
212
+ <button key={t.id} onClick={() => setTab(t.id)}
213
+ className={`flex items-center gap-2 px-5 py-3 text-sm font-medium border-b-2 transition-colors ${
214
+ tab === t.id ? 'border-cyan-400 text-cyan-300' : 'border-transparent text-gray-500 hover:text-gray-300'
199
215
  }`}>
200
- {t === 'chat' ? '💬 Chat' : t === 'events' ? '📋 Events' : '📊 Info'}
216
+ <svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
217
+ <path d={t.icon} />
218
+ </svg>
219
+ <span>{t.label}</span>
201
220
  </button>
202
221
  ))}
203
222
  </div>
@@ -299,13 +318,16 @@ function NodeFullPanel({ alias, session, sse, sendMsg, setSendMsg, sending, send
299
318
  );
300
319
  }
301
320
 
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: '📝' },
321
+ /** Round 59: emoji icons replaced with stroke SVG paths to drop the
322
+ * "AI generated" tell. Each path is sized to fit inside the 18px
323
+ * colored event dot. */
324
+ const EVENT_COLORS: Record<string, { dot: string; line: string; path: string }> = {
325
+ delivered: { dot: 'bg-blue-400', line: 'bg-blue-500/30', path: 'M12 5v14 M5 12l7 7 7-7' }, // down arrow
326
+ acked: { dot: 'bg-yellow-400', line: 'bg-yellow-500/30', path: 'M20 6 9 17l-5-5' }, // check
327
+ running: { dot: 'bg-green-400', line: 'bg-green-500/30', path: 'M13 2L4 14h6l-1 8 10-12h-6l1-8z' }, // bolt
328
+ 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
329
+ failed: { dot: 'bg-red-400', line: 'bg-red-500/30', path: 'M18 6 6 18 M6 6l12 12' }, // X
330
+ created: { dot: 'bg-gray-400', line: 'bg-gray-500/30', path: 'M12 5v14 M5 12h14' }, // plus
309
331
  };
310
332
 
311
333
  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 +356,12 @@ function EventsTimeline({ events, loading }: { events: Array<{ id: number; event
334
356
  const cfg = EVENT_COLORS[e.to_status] || EVENT_COLORS.created;
335
357
  return (
336
358
  <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 : ''}
359
+ {/* Dot on line — round 59: emoji icons replaced with
360
+ a 10px stroke SVG path matching event type. */}
361
+ <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]`}>
362
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#0a0a15" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
363
+ <path d={cfg.path} />
364
+ </svg>
340
365
  </div>
341
366
  {/* Content */}
342
367
  <div className="ml-2">
@@ -5,17 +5,18 @@ 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
+ import { SESSION_STATUS_CHIP_CLASS as STATUS_COLORS } from '../lib/status';
11
12
 
12
- const STATUS_COLORS: Record<string, string> = {
13
- working: 'bg-green-500/10 text-green-300 border-green-500/20',
14
- idle: 'bg-blue-500/10 text-blue-300 border-blue-500/20',
15
- blocked: 'bg-yellow-500/10 text-yellow-300 border-yellow-500/20',
16
- error: 'bg-red-500/10 text-red-300 border-red-500/20',
17
- offline: 'bg-gray-500/10 text-gray-500 border-gray-500/20',
18
- };
13
+ /** Round 81: shorten long server hostnames (Alibaba `iZ…oyZ` style)
14
+ * for table display. Returns the original string unchanged when ≤12
15
+ * chars. Full value should stay in `title=` for hover + screen-readers. */
16
+ function shortServer(server: string | null | undefined): string {
17
+ if (!server) return '—';
18
+ return server.length > 12 ? `${server.slice(0, 8)}…` : server;
19
+ }
19
20
 
20
21
  type ViewMode = 'list' | 'grid';
21
22
  type SessionRow = Session & { online: boolean };
@@ -121,6 +122,12 @@ export default function NodesPage() {
121
122
  );
122
123
  })()}
123
124
 
125
+ {/* Round 74: hide the filter+view chrome when there are no nodes
126
+ anywhere — these controls have nothing to act on, and they only
127
+ push the onboarding CTA further down. When at least one session
128
+ exists, the chrome is back even if the current filter happens
129
+ to hide everything (so users can clear filters). */}
130
+ {sessions.length > 0 && (
124
131
  <div className="flex flex-wrap gap-3 mb-6">
125
132
  <input
126
133
  type="text"
@@ -142,7 +149,11 @@ export default function NodesPage() {
142
149
  <option value="blocked">Blocked</option>
143
150
  <option value="error">Error</option>
144
151
  </select>
145
- <div className="ml-auto flex rounded-lg border border-[#2a2a4a] bg-[#111128] p-1 text-sm">
152
+ {/* Round 80: List/Grid toggle hidden on mobile — Grid uses
153
+ grid-cols-1 below `md` and List degrades to the same single
154
+ column, so the toggle has no visual effect under sm and only
155
+ steals a row of vertical space. */}
156
+ <div className="hidden sm:flex ml-auto rounded-lg border border-[#2a2a4a] bg-[#111128] p-1 text-sm">
146
157
  {(['list', 'grid'] as const).map(mode => (
147
158
  <button
148
159
  key={mode}
@@ -159,17 +170,27 @@ export default function NodesPage() {
159
170
  ))}
160
171
  </div>
161
172
  </div>
173
+ )}
162
174
 
163
175
  {loading ? (
164
176
  <div className="animate-pulse space-y-2">
165
177
  {[1,2,3,4].map(i => <div key={i} className="h-16 bg-gray-800/20 rounded-lg" />)}
166
178
  </div>
167
179
  ) : 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
- />
180
+ sessions.length === 0 ? (
181
+ /* Round 73: first-run case — no agents anywhere. Use the same
182
+ onboarding empty state as the Overview (NodesEmptyState
183
+ includes the `npx init` quickstart command). Previously this
184
+ was the "No nodes match your filters" copy, which was wrong
185
+ for new users who have no filters applied. */
186
+ <NodesEmptyState />
187
+ ) : (
188
+ <EmptyState
189
+ variant="nodes"
190
+ title="No nodes match your filters"
191
+ sub="Try clearing search or status filters, or wait for an agent to register."
192
+ />
193
+ )
173
194
  ) : viewMode === 'grid' ? (
174
195
  <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
175
196
  {filtered.map(s => {
@@ -199,7 +220,7 @@ export default function NodesPage() {
199
220
  </span>
200
221
  </div>
201
222
  <div className="mt-0.5 truncate text-xs text-gray-500">
202
- {(s.agent || '—')}<span className="text-gray-700 mx-1.5">·</span>{(s.server || '')}
223
+ {(s.agent || '—')}<span className="text-gray-700 mx-1.5">·</span><span title={s.server || ''}>{shortServer(s.server)}</span>
203
224
  </div>
204
225
  </Link>
205
226
  <div className="flex gap-1.5 shrink-0">
@@ -268,12 +289,14 @@ export default function NodesPage() {
268
289
  </div>
269
290
  ) : (
270
291
  <div className="space-y-2">
292
+ {/* Round 94: AGENT + SERVER merged into one `agent · server`
293
+ cell; the freed 2 columns expand CURRENT TASK so long task
294
+ content lines wrap less. Total still 12: 1+2+2+4+1+2. */}
271
295
  <div className="hidden sm:grid sm:grid-cols-12 gap-2 px-4 py-2 text-xs text-gray-600 uppercase">
272
296
  <div className="col-span-1">Status</div>
273
297
  <div className="col-span-2">Alias</div>
274
- <div className="col-span-2">Agent</div>
275
- <div className="col-span-2">Server</div>
276
- <div className="col-span-2">Current Task</div>
298
+ <div className="col-span-2">Agent · Server</div>
299
+ <div className="col-span-4">Current Task</div>
277
300
  <div className="col-span-1">Updated</div>
278
301
  <div className="col-span-2 text-right">Actions</div>
279
302
  </div>
@@ -298,9 +321,10 @@ export default function NodesPage() {
298
321
  <span className="truncate text-sm font-medium text-white">{s.alias}</span>
299
322
  </Link>
300
323
  </div>
301
- <div className="col-span-2 truncate text-xs text-gray-400">{s.agent || '--'}</div>
302
- <div className="col-span-2 truncate text-xs text-gray-400">{s.server || '--'}</div>
303
- <div className="col-span-2 truncate text-xs text-gray-500">{s.task || '--'}</div>
324
+ <div className="col-span-2 truncate text-xs text-gray-400" title={s.server || ''}>
325
+ {s.agent || '--'}<span className="text-gray-700 mx-1.5">·</span>{shortServer(s.server)}
326
+ </div>
327
+ <div className="col-span-4 truncate text-xs text-gray-500" title={s.task || ''}>{s.task || '--'}</div>
304
328
  <div className="col-span-1 text-xs text-gray-500">{timeAgo(s.last_seen_at || s.updated_at)}</div>
305
329
  <div className="col-span-2 flex justify-end gap-1.5">
306
330
  <button type="button" onClick={() => setChatAlias(s.alias)}