@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
@@ -3,6 +3,7 @@
3
3
  import { useState } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { useAnetConfig, useHealth, useLicense } from '../lib/hooks';
6
+ import { DASHBOARD_VERSION } from '../lib/version';
6
7
 
7
8
  export default function SettingsPage() {
8
9
  const { config } = useAnetConfig();
@@ -18,12 +19,31 @@ export default function SettingsPage() {
18
19
 
19
20
  return (
20
21
  <div className="min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono">
21
- <h1 className="text-2xl font-bold text-white mb-6 lg:ml-0 ml-10">Settings</h1>
22
+ <h1 className="text-2xl font-bold text-white mb-3 lg:ml-0 ml-10">Settings</h1>
22
23
 
23
- <div className="max-w-2xl space-y-8">
24
+ {/* Section anchor nav (round 28) — jump to a group instead of scrolling
25
+ through the whole page. Mirrors the section headers below; pages
26
+ with 7+ cards benefit from a visible table-of-contents. */}
27
+ <nav className="mb-8 flex flex-wrap gap-1 text-xs">
28
+ {[
29
+ { href: '#connection', label: 'Connection' },
30
+ { href: '#account', label: 'Account' },
31
+ { href: '#resources', label: 'Resources' },
32
+ ].map(a => (
33
+ <a key={a.href} href={a.href}
34
+ className="rounded-md px-2.5 py-1 text-gray-500 hover:text-cyan-300 hover:bg-cyan-500/10 transition-colors">
35
+ {a.label}
36
+ </a>
37
+ ))}
38
+ </nav>
39
+
40
+ <div className="max-w-2xl space-y-10">
24
41
  {/* ── Group: Connection ─────────────────────────────────── */}
25
- <div className="space-y-4">
26
- <div className="text-[10px] uppercase tracking-[0.12em] text-gray-600 px-1">Connection</div>
42
+ <div id="connection" className="space-y-4 scroll-mt-6">
43
+ <div className="flex items-center gap-2 px-1">
44
+ <div className="text-[11px] uppercase tracking-[0.14em] text-gray-400 font-semibold">Connection</div>
45
+ <div className="flex-1 h-px bg-[#2a2a4a]" />
46
+ </div>
27
47
 
28
48
  {/* CommHub Connection */}
29
49
  <section className="bg-[#111128] border border-[#2a2a4a] rounded-xl p-5">
@@ -72,7 +92,7 @@ export default function SettingsPage() {
72
92
  <span className={`text-gray-300 ${valueClass}`}>{health?.sessions ?? '--'}</span>
73
93
  </div>
74
94
  <div className={rowClass}>
75
- <span className="text-gray-500">SSE Connections</span>
95
+ <span className="text-gray-500">SSE streams <span className="text-gray-600">· server</span></span>
76
96
  <span className={`text-gray-300 ${valueClass}`}>{health?.sse_connections ?? '--'}</span>
77
97
  </div>
78
98
  <div className={rowClass}>
@@ -88,7 +108,9 @@ export default function SettingsPage() {
88
108
  <div className="space-y-3 text-sm">
89
109
  <div className={rowClass}>
90
110
  <span className="text-gray-500">Version</span>
91
- <span className={`text-gray-300 ${valueClass}`}>V2.1</span>
111
+ <span className={`text-gray-300 font-mono ${valueClass}`} title="From package.json @sleep2agi/agent-network-dashboard">
112
+ {DASHBOARD_VERSION}
113
+ </span>
92
114
  </div>
93
115
  <div className={rowClass}>
94
116
  <span className="text-gray-500">Data Layer</span>
@@ -96,15 +118,18 @@ export default function SettingsPage() {
96
118
  </div>
97
119
  <div className={rowClass}>
98
120
  <span className="text-gray-500">Pages</span>
99
- <span className={`text-gray-300 ${valueClass}`}>Overview, Tasks, Nodes, Messages, Settings</span>
121
+ <span className={`text-gray-300 ${valueClass}`}>Overview, Tasks, Nodes, Messages, Networks, Audit, Server Logs, Admin, Settings</span>
100
122
  </div>
101
123
  </div>
102
124
  </section>
103
125
  </div>
104
126
 
105
127
  {/* ── Group: Account ────────────────────────────────────── */}
106
- <div className="space-y-4">
107
- <div className="text-[10px] uppercase tracking-[0.12em] text-gray-600 px-1">Account</div>
128
+ <div id="account" className="space-y-4 scroll-mt-6">
129
+ <div className="flex items-center gap-2 px-1">
130
+ <div className="text-[11px] uppercase tracking-[0.14em] text-gray-400 font-semibold">Account</div>
131
+ <div className="flex-1 h-px bg-[#2a2a4a]" />
132
+ </div>
108
133
 
109
134
  {/* License */}
110
135
  <section className="bg-[#111128] border border-[#2a2a4a] rounded-xl p-5">
@@ -232,8 +257,11 @@ export default function SettingsPage() {
232
257
  </div>
233
258
 
234
259
  {/* ── Group: Resources ──────────────────────────────────── */}
235
- <div className="space-y-4">
236
- <div className="text-[10px] uppercase tracking-[0.12em] text-gray-600 px-1">Resources</div>
260
+ <div id="resources" className="space-y-4 scroll-mt-6">
261
+ <div className="flex items-center gap-2 px-1">
262
+ <div className="text-[11px] uppercase tracking-[0.14em] text-gray-400 font-semibold">Resources</div>
263
+ <div className="flex-1 h-px bg-[#2a2a4a]" />
264
+ </div>
237
265
 
238
266
  {/* API Tokens + Networks */}
239
267
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
@@ -4,6 +4,7 @@ import { use } from 'react';
4
4
  import Link from 'next/link';
5
5
  import useSWR from 'swr';
6
6
  import { timeAgo } from '../../components/utils';
7
+ import { STATUS_CHIP_CLASS } from '../../lib/status';
7
8
 
8
9
  const fetcher = async (url: string) => {
9
10
  const res = await fetch(url);
@@ -55,18 +56,6 @@ function Timeline({ steps }: { steps: TimelineStep[] }) {
55
56
  );
56
57
  }
57
58
 
58
- const STATUS_COLORS: Record<string, string> = {
59
- created: 'bg-gray-500/10 text-gray-400 border-gray-500/20',
60
- delivered: 'bg-blue-500/10 text-blue-300 border-blue-500/20',
61
- acked: 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20',
62
- running: 'bg-green-500/10 text-green-300 border-green-500/20',
63
- replied: 'bg-purple-500/10 text-purple-300 border-purple-500/20',
64
- closed: 'bg-gray-500/10 text-gray-500 border-gray-500/20',
65
- failed: 'bg-red-500/10 text-red-300 border-red-500/20',
66
- cancelled: 'bg-yellow-500/10 text-yellow-300 border-yellow-500/20',
67
- expired: 'bg-orange-500/10 text-orange-300 border-orange-500/20',
68
- };
69
-
70
59
  export default function TaskDetailPage({ params }: { params: Promise<{ id: string }> }) {
71
60
  const { id } = use(params);
72
61
  const { data, isLoading } = useSWR(
@@ -105,7 +94,7 @@ export default function TaskDetailPage({ params }: { params: Promise<{ id: strin
105
94
  );
106
95
  }
107
96
 
108
- const statusColor = STATUS_COLORS[task.status] || STATUS_COLORS.created;
97
+ const statusColor = STATUS_CHIP_CLASS[task.status] || STATUS_CHIP_CLASS.created;
109
98
  const steps: TimelineStep[] = [
110
99
  { label: 'Created', time: task.created_at, color: 'text-gray-400' },
111
100
  { label: 'Delivered', time: task.delivered_at, color: 'text-blue-400' },
@@ -6,6 +6,8 @@ import Link from 'next/link';
6
6
  import { useNetworkId } from '../lib/network-context';
7
7
  import { TaskDrawer } from '../components/TaskDrawer';
8
8
  import { EmptyState } from '../components/EmptyState';
9
+ import { AliasAvatar } from '../components/AliasAvatar';
10
+ import { TASK_STATUSES, STATUS_CHIP_CLASS, STATUS_DOT_HEX, STATUS_BAR_CLASS } from '../lib/status';
9
11
 
10
12
  interface Task {
11
13
  task_id: string;
@@ -23,48 +25,8 @@ interface Task {
23
25
  expires_at: string;
24
26
  }
25
27
 
26
- const STATUS_OPTIONS = [
27
- '',
28
- 'created',
29
- 'delivered',
30
- 'acked',
31
- 'running',
32
- 'replied',
33
- 'closed',
34
- 'failed',
35
- 'cancelled',
36
- 'expired',
37
- ];
38
-
39
- const STATUS_COLORS: Record<string, string> = {
40
- created: 'bg-gray-500/10 text-gray-400 border-gray-500/20',
41
- delivered: 'bg-blue-500/10 text-blue-300 border-blue-500/20',
42
- acked: 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20',
43
- running: 'bg-green-500/10 text-green-300 border-green-500/20',
44
- replied: 'bg-purple-500/10 text-purple-300 border-purple-500/20',
45
- closed: 'bg-gray-500/10 text-gray-500 border-gray-500/20',
46
- failed: 'bg-red-500/10 text-red-300 border-red-500/20',
47
- cancelled: 'bg-yellow-500/10 text-yellow-300 border-yellow-500/20',
48
- expired: 'bg-orange-500/10 text-orange-300 border-orange-500/20',
49
- };
50
-
51
- /** Small colored dot per status — visible on every tab (active + inactive)
52
- * so users can scan the row at a glance. Hex inlined to avoid Tailwind
53
- * purging dynamic `bg-${family}-400` class names. */
54
- const STATUS_DOTS: Record<string, string> = {
55
- created: '#9ca3af',
56
- delivered: '#60a5fa',
57
- acked: '#22d3ee',
58
- running: '#4ade80',
59
- replied: '#a78bfa',
60
- closed: '#6b7280',
61
- failed: '#f87171',
62
- cancelled: '#facc15',
63
- expired: '#fb923c',
64
- };
65
-
66
28
  function statusBadge(status: string) {
67
- const color = STATUS_COLORS[status] || 'bg-gray-500/10 text-gray-400 border-gray-500/20';
29
+ const color = STATUS_CHIP_CLASS[status] || 'bg-gray-500/10 text-gray-400 border-gray-500/20';
68
30
  return `text-xs px-2 py-0.5 rounded-md border ${color}`;
69
31
  }
70
32
 
@@ -149,6 +111,13 @@ function TasksContent() {
149
111
  </span>
150
112
  </div>
151
113
 
114
+ {/* Round 75: hide the status tab strip + From/To filter row when
115
+ there are no tasks at all. Each tab would show "0" and the
116
+ filter inputs would have nothing to act on — pure noise above
117
+ the empty-state CTA. When at least one task exists, the chrome
118
+ comes back even if the current filter happens to hide
119
+ everything (so users can clear filters). */}
120
+ {tasks.length > 0 && <>
152
121
  {/* Status Tabs — color-coded dots per status family so users can
153
122
  scan the row at a glance. Active tab gets full chip styling
154
123
  (bg + border + text color), inactive tabs only carry the dot
@@ -158,64 +127,96 @@ function TasksContent() {
158
127
  elements) hinting more content. */}
159
128
  <div className="anet-tabstrip-wrap mb-4">
160
129
  <div className="anet-tabstrip flex sm:flex-wrap gap-1 bg-[#111128] rounded-lg border border-[#2a2a4a] p-1 overflow-x-auto sm:overflow-x-visible scrollbar-thin">
161
- {['', ...STATUS_OPTIONS.filter(Boolean)].map(s => (
162
- <button key={s} onClick={() => setFilterStatus(s)}
163
- className={`px-3 py-1.5 rounded-md text-xs transition-colors flex items-center gap-1.5 shrink-0 whitespace-nowrap ${
164
- filterStatus === s
165
- ? `${STATUS_COLORS[s] || 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'} border`
166
- : 'text-gray-500 hover:text-gray-300 hover:bg-[#1a1a2a]/40'
167
- }`}>
168
- {s && (
169
- <span
170
- aria-hidden
171
- className="inline-block w-1.5 h-1.5 rounded-full shrink-0"
172
- style={{ backgroundColor: STATUS_DOTS[s] || '#6b7280' }}
173
- />
174
- )}
175
- <span>{s || 'All'}</span>
176
- </button>
177
- ))}
130
+ {(() => {
131
+ // Round 56: compute per-status counts so the tab strip doubles as a
132
+ // status distribution dashboard. Matches Audit Log (r43) + Overview
133
+ // agent filter (r34) chip-with-count pattern.
134
+ const counts: Record<string, number> = {};
135
+ tasks.forEach(t => { counts[t.status] = (counts[t.status] || 0) + 1; });
136
+ return ['', ...TASK_STATUSES].map(s => {
137
+ const count = s === '' ? tasks.length : counts[s] || 0;
138
+ const isActive = filterStatus === s;
139
+ return (
140
+ <button
141
+ key={s}
142
+ onClick={() => setFilterStatus(s)}
143
+ disabled={count === 0 && s !== '' && !isActive}
144
+ className={`px-3 py-1.5 rounded-md text-xs transition-colors flex items-center gap-1.5 shrink-0 whitespace-nowrap disabled:opacity-30 disabled:cursor-not-allowed ${
145
+ isActive
146
+ ? `${STATUS_CHIP_CLASS[s] || 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'} border`
147
+ : 'text-gray-500 hover:text-gray-300 hover:bg-[#1a1a2a]/40'
148
+ }`}
149
+ >
150
+ {s && (
151
+ <span
152
+ aria-hidden
153
+ className="inline-block w-1.5 h-1.5 rounded-full shrink-0"
154
+ style={{ backgroundColor: STATUS_DOT_HEX[s] || '#6b7280' }}
155
+ />
156
+ )}
157
+ <span>{s || 'All'}</span>
158
+ <span className={`text-[10px] tabular-nums ${isActive ? 'opacity-80' : 'text-gray-600'}`}>{count}</span>
159
+ </button>
160
+ );
161
+ });
162
+ })()}
178
163
  </div>
179
164
  </div>
180
165
 
181
- {/* From/To Filters */}
182
- <div className="flex flex-wrap gap-3 mb-6">
183
- <input
184
- type="text"
185
- value={filterFrom}
186
- onChange={e => setFilterFrom(e.target.value)}
187
- placeholder="From node..."
188
- className="bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-blue-500/50 focus:outline-none w-40"
189
- />
190
- <input
191
- type="text"
192
- value={filterTo}
193
- onChange={e => setFilterTo(e.target.value)}
194
- placeholder="To node..."
195
- className="bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-blue-500/50 focus:outline-none w-40"
196
- />
166
+ {/* From/To Filters — same visual block as the status tabs above. */}
167
+ <div className="flex flex-wrap items-center gap-2 mb-6">
168
+ <div className="flex items-center gap-1.5 rounded-lg border border-[#2a2a4a] bg-[#111128] px-2.5 py-1.5 focus-within:border-blue-500/40">
169
+ <span className="text-[10px] uppercase tracking-wide text-gray-600">From</span>
170
+ <input
171
+ type="text"
172
+ value={filterFrom}
173
+ onChange={e => setFilterFrom(e.target.value)}
174
+ placeholder="any node"
175
+ className="w-28 bg-transparent text-sm text-white placeholder-gray-700 focus:outline-none"
176
+ />
177
+ </div>
178
+ <div className="flex items-center gap-1.5 rounded-lg border border-[#2a2a4a] bg-[#111128] px-2.5 py-1.5 focus-within:border-blue-500/40">
179
+ <span className="text-[10px] uppercase tracking-wide text-gray-600">To</span>
180
+ <input
181
+ type="text"
182
+ value={filterTo}
183
+ onChange={e => setFilterTo(e.target.value)}
184
+ placeholder="any node"
185
+ className="w-28 bg-transparent text-sm text-white placeholder-gray-700 focus:outline-none"
186
+ />
187
+ </div>
188
+ {(filterStatus || filterFrom || filterTo) && (
189
+ <button
190
+ type="button"
191
+ onClick={() => { setFilterStatus(''); setFilterFrom(''); setFilterTo(''); }}
192
+ className="rounded-lg border border-gray-700 px-2.5 py-1.5 text-[11px] text-gray-500 hover:text-gray-200 hover:border-gray-600"
193
+ >
194
+ Clear filters
195
+ </button>
196
+ )}
197
197
  </div>
198
+ </>}
198
199
 
199
200
  {/* Status distribution */}
200
201
  {tasks.length > 0 && !filterStatus && (() => {
201
202
  const stats: Record<string, number> = {};
202
203
  tasks.forEach(t => { stats[t.status] = (stats[t.status] || 0) + 1; });
203
204
  const total = tasks.length || 1;
204
- const bars = [
205
- { key: 'running', color: 'bg-green-500', text: 'text-green-400' },
206
- { key: 'delivered', color: 'bg-blue-500', text: 'text-blue-400' },
207
- { key: 'replied', color: 'bg-purple-500', text: 'text-purple-400' },
208
- { key: 'failed', color: 'bg-red-500', text: 'text-red-400' },
209
- { key: 'expired', color: 'bg-orange-500', text: 'text-orange-400' },
210
- ].filter(b => stats[b.key]);
205
+ // Round 67: derive from shared TASK_STATUSES + STATUS_BAR_CLASS in
206
+ // app/lib/status.ts. Adding a new lifecycle status updates badge,
207
+ // tab dots, and bar in one edit.
208
+ const bars = TASK_STATUSES
209
+ .map(key => ({ key, color: STATUS_BAR_CLASS[key] }))
210
+ .filter(b => stats[b.key]);
211
211
  if (!bars.length) return null;
212
+ // Round 63: removed the per-status legend row beneath the bar —
213
+ // it duplicated the counts already shown in the tab chip strip
214
+ // since r56. The proportional bar stays because it adds a visual
215
+ // "shape of workload" that the chip numbers don't convey.
212
216
  return (
213
217
  <div className="mb-6">
214
218
  <div className="flex h-2 rounded-full overflow-hidden bg-gray-800">
215
- {bars.map(b => <div key={b.key} className={b.color} style={{ width: `${(stats[b.key]/total)*100}%` }} />)}
216
- </div>
217
- <div className="flex flex-wrap gap-3 mt-1.5 text-[10px] text-gray-500">
218
- {bars.map(b => <span key={b.key}><span className={`inline-block w-2 h-2 rounded-full ${b.color} mr-1`} />{stats[b.key]} {b.key}</span>)}
219
+ {bars.map(b => <div key={b.key} className={b.color} style={{ width: `${(stats[b.key]/total)*100}%` }} title={`${b.key}: ${stats[b.key]}`} />)}
219
220
  </div>
220
221
  </div>
221
222
  );
@@ -276,11 +277,13 @@ function TasksContent() {
276
277
  <div className="col-span-1">
277
278
  <span className={statusBadge(t.status)}>{t.status}</span>
278
279
  </div>
279
- <div className="col-span-2 text-sm text-blue-400 truncate" title={t.from_name}>
280
- {t.from_name || '--'}
280
+ <div className="col-span-2 flex items-center gap-1.5 min-w-0" title={t.from_name}>
281
+ {t.from_name && <AliasAvatar alias={t.from_name} size={18} />}
282
+ <span className="truncate text-sm text-gray-200">{t.from_name || '--'}</span>
281
283
  </div>
282
- <div className="col-span-2 text-sm text-cyan-400 truncate" title={t.to_name}>
283
- {t.to_name || '--'}
284
+ <div className="col-span-2 flex items-center gap-1.5 min-w-0" title={t.to_name}>
285
+ {t.to_name && <AliasAvatar alias={t.to_name} size={18} />}
286
+ <span className="truncate text-sm text-gray-200">{t.to_name || '--'}</span>
284
287
  </div>
285
288
  <div className="col-span-4 text-xs text-gray-400 truncate" title={t.content}>
286
289
  {t.content || '--'}
@@ -315,10 +318,12 @@ function TasksContent() {
315
318
  </svg>
316
319
  </div>
317
320
  </div>
318
- <div className="text-xs text-gray-500">
319
- <span className="text-blue-400">{t.from_name || '--'}</span>
320
- <span className="text-gray-600 mx-1">&rarr;</span>
321
- <span className="text-cyan-400">{t.to_name || '--'}</span>
321
+ <div className="flex items-center gap-1.5 text-xs text-gray-300 min-w-0">
322
+ {t.from_name && <AliasAvatar alias={t.from_name} size={16} />}
323
+ <span className="truncate max-w-[40%]">{t.from_name || '--'}</span>
324
+ <span className="text-gray-600">&rarr;</span>
325
+ {t.to_name && <AliasAvatar alias={t.to_name} size={16} />}
326
+ <span className="truncate max-w-[40%]">{t.to_name || '--'}</span>
322
327
  </div>
323
328
  <div className="text-xs text-gray-400 line-clamp-1">{t.content || '--'}</div>
324
329
  <div className="text-xs text-gray-600">{timeAgo(t.created_at)}</div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.4.5-preview.6",
3
+ "version": "0.4.5-preview.61",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,224 @@
1
+ /* Loop self-review — captures Overview in cyber+light × desktop+mobile,
2
+ * runs a few observable checks, prints a per-axis score. Not a regression
3
+ * test — the cron's "review and score then propose next round" output.
4
+ *
5
+ * Round 71: replaced the toy checks (overflow, sidebar visible, blur count)
6
+ * with checks that actually catch the bugs we keep shipping:
7
+ * - CTA-above-fold: does the primary "Spin up" / "Get started" / etc
8
+ * button live within the first viewport? (deduct 3)
9
+ * - Zero-card density: how many "0" stat cards are crammed in above the
10
+ * CTA? (deduct 1 per zero-card beyond the first 2)
11
+ * - Whitespace gap: any element-to-element vertical gap >200px in the
12
+ * top half? (deduct 1)
13
+ * - Misleading-copy: "All systems go" text rendered when nodes.total=0
14
+ * (deduct 2) — round-70-class bug guard
15
+ * - Old: theme, horizontal overflow, sidebar, glow tell — still in
16
+ *
17
+ * Score is 10 max; deductions accumulate. The pre-r70 design that scored
18
+ * 10/10 here should now score ~4–5.
19
+ */
20
+
21
+ import { chromium } from 'playwright';
22
+ import { mkdirSync } from 'node:fs';
23
+ import path from 'node:path';
24
+
25
+ const BASE = process.env.LOOP_REVIEW_BASE || 'http://127.0.0.1:3000';
26
+ const TOKEN = process.env.LOOP_REVIEW_TOKEN; // utok_...
27
+ const OUT = path.resolve(process.cwd(), 'test-results/loop-review');
28
+ mkdirSync(OUT, { recursive: true });
29
+
30
+ if (!TOKEN) {
31
+ console.error('LOOP_REVIEW_TOKEN env var required (utok_...)');
32
+ process.exit(1);
33
+ }
34
+
35
+ const viewports = [
36
+ { tag: 'desktop', width: 1440, height: 900 },
37
+ { tag: 'mobile', width: 390, height: 844 },
38
+ ];
39
+ const themes = ['cyber', 'light'];
40
+ const pages = [
41
+ { name: 'overview', path: '/' },
42
+ { name: 'nodes', path: '/nodes' },
43
+ { name: 'tasks', path: '/tasks' },
44
+ { name: 'messages', path: '/messages' },
45
+ { name: 'admin', path: '/admin' },
46
+ { name: 'settings', path: '/settings' },
47
+ { name: 'networks', path: '/settings/networks' },
48
+ { name: 'logs', path: '/logs' },
49
+ ];
50
+
51
+ const matrix = [];
52
+ for (const t of themes) for (const v of viewports) for (const p of pages) {
53
+ matrix.push({ theme: t, viewport: { width: v.width, height: v.height }, tag: v.tag, pageName: p.name, pagePath: p.path });
54
+ }
55
+
56
+ const browser = await chromium.launch({ headless: true });
57
+
58
+ const report = [];
59
+
60
+ for (const m of matrix) {
61
+ const ctx = await browser.newContext({
62
+ viewport: m.viewport,
63
+ deviceScaleFactor: 1,
64
+ });
65
+ await ctx.addCookies([{
66
+ name: 'anet_dashboard_session',
67
+ value: `v3:${TOKEN}`,
68
+ domain: '127.0.0.1',
69
+ path: '/',
70
+ }]);
71
+ await ctx.addInitScript((theme) => {
72
+ try {
73
+ localStorage.setItem('anet-theme', theme);
74
+ sessionStorage.setItem('anet_v3_auth', '1');
75
+ } catch {}
76
+ }, m.theme);
77
+
78
+ const page = await ctx.newPage();
79
+ await page.goto(`${BASE}${m.pagePath}`, { waitUntil: 'domcontentloaded', timeout: 15000 });
80
+ await page.waitForTimeout(1200);
81
+
82
+ await page.evaluate((t) => document.documentElement.setAttribute('data-theme', t), m.theme);
83
+ await page.waitForTimeout(300);
84
+
85
+ const file = path.join(OUT, `${m.pageName}-${m.theme}-${m.tag}.png`);
86
+ await page.screenshot({ path: file, fullPage: true });
87
+
88
+ const checks = await page.evaluate(() => {
89
+ const doc = document.documentElement;
90
+ const theme = doc.getAttribute('data-theme');
91
+ const body = document.body;
92
+ const bodyBg = getComputedStyle(body).backgroundColor;
93
+ const bodyFg = getComputedStyle(body).color;
94
+
95
+ const viewportH = window.innerHeight;
96
+ const viewportW = window.innerWidth;
97
+
98
+ // --- CTA-above-fold ---
99
+ // We treat any of these texts as the page's primary CTA:
100
+ // "Spin up your first agent" — empty Overview onboarding
101
+ // "Get started" — generic
102
+ // "Dispatch" (when not empty) — populated state primary action
103
+ const ctaTextRe = /Spin up your first agent|Get started/i;
104
+ const ctaCandidates = [...document.querySelectorAll('h1, h2, h3, p, div, button, a')]
105
+ .filter(el => ctaTextRe.test(el.textContent || ''));
106
+ let ctaTop = null;
107
+ if (ctaCandidates.length) {
108
+ // Pick the smallest (innermost) element matching to avoid catching a wrapping section
109
+ const sorted = ctaCandidates.sort((a, b) => (a.textContent?.length || 0) - (b.textContent?.length || 0));
110
+ const r = sorted[0].getBoundingClientRect();
111
+ ctaTop = r.top;
112
+ }
113
+
114
+ // --- Zero-card density ---
115
+ // Count visible elements whose trimmed text === "0". These are usually
116
+ // big stat-card values dominating prime real estate.
117
+ const zeroCards = [...document.querySelectorAll('div, span')]
118
+ .filter(el => {
119
+ const t = el.textContent?.trim();
120
+ if (t !== '0' && t !== '0/0') return false;
121
+ const r = el.getBoundingClientRect();
122
+ return r.top >= 0 && r.top < viewportH && r.width > 20 && r.height > 20;
123
+ });
124
+
125
+ // --- Whitespace gap ---
126
+ // Look for sibling-to-sibling top gaps >200px in the page content
127
+ // container. AppShell wraps page children in
128
+ // <main><HealthBanner/><div><div class="min-h-screen ...">{page}</div></div></main>,
129
+ // so we walk into that inner page wrapper to read real section gaps.
130
+ const main = document.querySelector('main');
131
+ const inner = main?.querySelector(':scope > div:last-of-type > div') || main || document.body;
132
+ const directKids = [...inner.children].filter(el => {
133
+ const r = el.getBoundingClientRect();
134
+ return r.height > 0 && r.width > 0;
135
+ });
136
+ let maxGap = 0;
137
+ for (let i = 0; i + 1 < directKids.length; i++) {
138
+ const a = directKids[i].getBoundingClientRect();
139
+ const b = directKids[i + 1].getBoundingClientRect();
140
+ if (a.bottom < 1600 && b.top - a.bottom > maxGap) {
141
+ maxGap = Math.round(b.top - a.bottom);
142
+ }
143
+ }
144
+
145
+ // --- Misleading copy guard ---
146
+ // "All systems go" must not appear anywhere when nodes.total === 0.
147
+ // We can't read state directly, but if the page also shows
148
+ // "Waiting for first agent" then we know fleet is empty.
149
+ const bodyText = document.body.innerText;
150
+ const fleetEmpty = /Waiting for first agent/i.test(bodyText);
151
+ const sayingAllSystemsGo = /All systems go/i.test(bodyText);
152
+ const misleadingCopy = fleetEmpty && sayingAllSystemsGo;
153
+
154
+ // --- Carryover ---
155
+ const docWidth = doc.scrollWidth;
156
+ const horizontalOverflow = docWidth > viewportW + 2;
157
+ let glowyCount = 0;
158
+ for (const el of document.querySelectorAll('body *')) {
159
+ const cs = getComputedStyle(el);
160
+ if (cs.filter && cs.filter.includes('blur(') && !cs.filter.includes('blur(0px)')) glowyCount++;
161
+ }
162
+ const sidebar = document.querySelector('aside, [data-sidebar], nav');
163
+ const sidebarRect = sidebar?.getBoundingClientRect();
164
+ const sidebarVisible = !!(sidebarRect && sidebarRect.width > 0 && sidebarRect.height > 0);
165
+
166
+ return {
167
+ theme, bodyBg, bodyFg,
168
+ viewportH, viewportW,
169
+ ctaTop, ctaPresent: ctaCandidates.length > 0,
170
+ zeroCardCount: zeroCards.length,
171
+ maxGap,
172
+ fleetEmpty, sayingAllSystemsGo, misleadingCopy,
173
+ docWidth, horizontalOverflow,
174
+ glowyCount, sidebarVisible,
175
+ };
176
+ });
177
+
178
+ let score = 10;
179
+ const notes = [];
180
+ if (checks.theme !== m.theme) { score -= 3; notes.push(`theme=${checks.theme}`); }
181
+ if (checks.horizontalOverflow) { score -= 2; notes.push(`horiz overflow ${checks.docWidth}/${checks.viewportW}`); }
182
+ if (m.tag === 'desktop' && !checks.sidebarVisible) { score -= 1; notes.push('sidebar missing on desktop'); }
183
+ if (checks.glowyCount > 8) { score -= 1; notes.push(`${checks.glowyCount} blur-filter elements (glow tell)`); }
184
+
185
+ // New checks — the ones that would have caught r70's bug
186
+ if (checks.ctaPresent && checks.ctaTop !== null) {
187
+ if (checks.ctaTop > checks.viewportH) {
188
+ score -= 3;
189
+ notes.push(`CTA below fold (top=${Math.round(checks.ctaTop)} > vh=${checks.viewportH})`);
190
+ } else if (checks.ctaTop > checks.viewportH * 0.7) {
191
+ score -= 1;
192
+ notes.push(`CTA low in fold (top=${Math.round(checks.ctaTop)})`);
193
+ }
194
+ }
195
+ if (checks.zeroCardCount > 4) {
196
+ const extra = Math.min(checks.zeroCardCount - 4, 3);
197
+ score -= extra;
198
+ notes.push(`${checks.zeroCardCount} zero-value cards above fold`);
199
+ }
200
+ if (checks.maxGap > 200) {
201
+ score -= 1;
202
+ notes.push(`whitespace gap ${checks.maxGap}px`);
203
+ }
204
+ if (checks.misleadingCopy) {
205
+ score -= 2;
206
+ notes.push(`'All systems go' shown while fleet empty`);
207
+ }
208
+
209
+ report.push({ ...m, file, checks, score: Math.max(score, 0), notes });
210
+
211
+ await page.close();
212
+ await ctx.close();
213
+ }
214
+
215
+ await browser.close();
216
+
217
+ console.log('\n=== Loop self-review (harness r73) ===\n');
218
+ for (const r of report) {
219
+ console.log(`${r.pageName.padEnd(8)} ${r.theme}/${r.tag.padEnd(7)} score=${r.score}/10`);
220
+ console.log(` cta_top=${r.checks.ctaTop !== null ? Math.round(r.checks.ctaTop) : '--'} zero_cards=${r.checks.zeroCardCount} max_gap=${r.checks.maxGap}px fleet_empty=${r.checks.fleetEmpty}`);
221
+ if (r.notes.length) console.log(` notes: ${r.notes.join(' · ')}`);
222
+ }
223
+ const avg = report.reduce((s, r) => s + r.score, 0) / report.length;
224
+ console.log(`\noverall avg = ${avg.toFixed(1)}/10 across ${report.length} captures`);