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

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 (198) 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 +30 -30
  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.js.nft.json +1 -1
  14. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/_not-found.html +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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.js.nft.json +1 -1
  93. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/server-logs.html +2 -2
  95. package/.next/server/app/server-logs.rsc +14 -14
  96. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  97. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  98. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  99. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  100. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  101. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  102. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  103. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/networks.html +2 -2
  105. package/.next/server/app/settings/networks.rsc +14 -14
  106. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  107. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  108. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  109. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  110. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  111. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  112. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  113. package/.next/server/app/settings/page.js.nft.json +1 -1
  114. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  115. package/.next/server/app/settings/tokens/page.js.nft.json +1 -1
  116. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  117. package/.next/server/app/settings/tokens.html +2 -2
  118. package/.next/server/app/settings/tokens.rsc +14 -14
  119. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  120. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  121. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  122. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  123. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  124. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  125. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  126. package/.next/server/app/settings.html +2 -2
  127. package/.next/server/app/settings.rsc +14 -14
  128. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  129. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  130. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  131. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  132. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  133. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  134. package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  135. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  136. package/.next/server/app/tasks/page.js.nft.json +1 -1
  137. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  138. package/.next/server/app/tasks.html +2 -2
  139. package/.next/server/app/tasks.rsc +14 -14
  140. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  141. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  142. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  143. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  144. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  145. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  146. package/.next/server/chunks/ssr/[root-of-the-server]__0a0ffmv._.js +3 -0
  147. package/.next/server/chunks/ssr/[root-of-the-server]__0a0ffmv._.js.map +1 -0
  148. package/.next/server/chunks/ssr/app_components_08ys5y3._.js +9 -0
  149. package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +1 -0
  150. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +1 -1
  151. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +1 -1
  152. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
  153. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
  154. package/.next/server/middleware-build-manifest.js +3 -3
  155. package/.next/server/pages/404.html +2 -2
  156. package/.next/server/pages/500.html +1 -1
  157. package/.next/server/server-reference-manifest.js +1 -1
  158. package/.next/server/server-reference-manifest.json +1 -1
  159. package/.next/static/chunks/{0hyhubmh-__wg.js → 0b1ymo-btjfha.js} +1 -1
  160. package/.next/static/chunks/0qgc758gz3on4.js +7 -0
  161. package/.next/static/chunks/0xxk2ms76kkuz.css +2 -0
  162. package/.next/static/chunks/1031v6nw33ud3.js +1 -0
  163. package/.next/trace +2 -2
  164. package/.next/trace-build +1 -1
  165. package/app/components/AppShell.tsx +9 -1
  166. package/app/components/CommandPalette.tsx +358 -0
  167. package/app/components/HealthBanner.tsx +132 -0
  168. package/app/components/HelpOverlay.tsx +121 -0
  169. package/app/components/LoadingSkeleton.tsx +81 -25
  170. package/app/components/Sidebar.tsx +18 -0
  171. package/app/components/UserBar.tsx +16 -2
  172. package/app/globals.css +181 -0
  173. package/app/page.tsx +87 -14
  174. package/app/tasks/page.tsx +35 -5
  175. package/docs/loop-design-rounds.md +298 -3
  176. package/docs/screenshots/0.4.5-preview.1/cyber-admin.png +0 -0
  177. package/docs/screenshots/0.4.5-preview.1/cyber-cmdk-search.png +0 -0
  178. package/docs/screenshots/0.4.5-preview.1/cyber-cmdk.png +0 -0
  179. package/docs/screenshots/0.4.5-preview.1/cyber-login.png +0 -0
  180. package/docs/screenshots/0.4.5-preview.1/cyber-overview.png +0 -0
  181. package/docs/screenshots/0.4.5-preview.1/cyber-settings.png +0 -0
  182. package/docs/screenshots/0.4.5-preview.1/cyber-tasks.png +0 -0
  183. package/docs/screenshots/0.4.5-preview.1/light-admin.png +0 -0
  184. package/docs/screenshots/0.4.5-preview.1/light-cmdk-search.png +0 -0
  185. package/docs/screenshots/0.4.5-preview.1/light-cmdk.png +0 -0
  186. package/docs/screenshots/0.4.5-preview.1/light-login.png +0 -0
  187. package/docs/screenshots/0.4.5-preview.1/light-overview.png +0 -0
  188. package/docs/screenshots/0.4.5-preview.1/light-settings.png +0 -0
  189. package/docs/screenshots/0.4.5-preview.1/light-tasks.png +0 -0
  190. package/package.json +1 -1
  191. package/.next/server/chunks/ssr/[root-of-the-server]__013i9e9._.js +0 -9
  192. package/.next/server/chunks/ssr/[root-of-the-server]__013i9e9._.js.map +0 -1
  193. package/.next/static/chunks/0snd~q2ucn.h1.css +0 -2
  194. package/.next/static/chunks/0xn9.0ejo1s_a.js +0 -7
  195. package/.next/static/chunks/0y0lo-~k2c~00.js +0 -1
  196. /package/.next/static/{2m1yIfZCIYDWMYrrJEKSC → dvjBKNMRovte6RFP_k8Lq}/_buildManifest.js +0 -0
  197. /package/.next/static/{2m1yIfZCIYDWMYrrJEKSC → dvjBKNMRovte6RFP_k8Lq}/_clientMiddlewareManifest.js +0 -0
  198. /package/.next/static/{2m1yIfZCIYDWMYrrJEKSC → dvjBKNMRovte6RFP_k8Lq}/_ssgManifest.js +0 -0
@@ -1,42 +1,86 @@
1
1
  'use client';
2
2
 
3
+ /**
4
+ * Loading skeleton — mirrors the actual Overview layout structure so the
5
+ * page doesn't appear to "shift" once data arrives. Uses the same
6
+ * `anet-skeleton-pulse` rhythm as the brand pulse (1.6s opacity drift,
7
+ * no scale, no blur, no glow) so the loading animation feels native to
8
+ * the rest of the dashboard rather than a generic spinner.
9
+ *
10
+ * Bars are theme-aware: anet-skeleton-bar (existing CSS shim resolves this to
11
+ * var(--bg-elevated) on light/mint), so light theme shows soft grey blocks
12
+ * on white cards and dark theme shows lighter blocks on navy cards.
13
+ */
3
14
  export function LoadingSkeleton() {
4
15
  return (
5
- <div className="min-h-screen bg-[#0a0a1a] text-gray-100 p-6 font-mono animate-pulse">
6
- {/* Header skeleton */}
7
- <div className="flex items-center justify-between mb-8">
8
- <div>
9
- <div className="h-7 w-48 bg-gray-800 rounded mb-2" />
10
- <div className="h-4 w-64 bg-gray-800/60 rounded" />
11
- </div>
12
- <div className="flex gap-3">
13
- {[1, 2, 3].map(i => (
14
- <div key={i} className="w-[72px] h-[60px] bg-gray-800/40 rounded-lg border border-gray-800" />
15
- ))}
16
+ <div className="min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono">
17
+ {/* KPI top strip — 4 cards matching StatsBar */}
18
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8 anet-skeleton-pulse">
19
+ {[1, 2, 3, 4].map(i => (
20
+ <div key={i} className="rounded-xl border border-[#2a2a4a] bg-[#111128] px-4 py-3">
21
+ <Bar w="2.5rem" h="1.75rem" />
22
+ <Bar w="3.5rem" h="0.75rem" className="mt-2" />
23
+ <Bar w="5rem" h="0.625rem" className="mt-1" />
24
+ </div>
25
+ ))}
26
+ </div>
27
+
28
+ {/* Dispatch + UserBar row */}
29
+ <div className="flex items-center gap-3 mb-3 anet-skeleton-pulse">
30
+ <Bar w="6rem" h="2.5rem" rounded="0.75rem" />
31
+ <div className="flex-1 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-2.5 flex items-center gap-3">
32
+ <div className="w-8 h-8 rounded-full anet-skeleton-bar" />
33
+ <div className="flex-1">
34
+ <Bar w="6rem" h="0.875rem" />
35
+ <Bar w="10rem" h="0.625rem" className="mt-1" />
36
+ </div>
16
37
  </div>
17
38
  </div>
18
39
 
19
- {/* Broadcast bar skeleton */}
20
- <div className="mb-6 flex gap-2">
21
- <div className="flex-1 h-10 bg-gray-800/40 rounded-lg" />
22
- <div className="w-28 h-10 bg-gray-800/40 rounded-lg" />
40
+ {/* Config bar */}
41
+ <div className="mb-6 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-3 anet-skeleton-pulse">
42
+ <Bar w="14rem" h="0.875rem" />
43
+ </div>
44
+
45
+ {/* Stat strip 3 cards */}
46
+ <div className="grid grid-cols-3 gap-2 sm:gap-3 mb-3 anet-skeleton-pulse">
47
+ {[1, 2, 3].map(i => (
48
+ <div key={i} className="rounded-xl border border-[#2a2a4a] bg-[#111128] px-3 py-3">
49
+ <Bar w="2rem" h="1.25rem" />
50
+ <Bar w="2.5rem" h="0.75rem" className="mt-1" />
51
+ <Bar w="3.5rem" h="0.625rem" className="mt-px" />
52
+ </div>
53
+ ))}
54
+ </div>
55
+
56
+ {/* Nav rail 3 cards */}
57
+ <div className="grid grid-cols-3 gap-2 sm:gap-3 mb-6 anet-skeleton-pulse">
58
+ {[1, 2, 3].map(i => (
59
+ <div key={i} className="rounded-xl border border-[#2a2a4a] bg-[#111128] px-3 py-2.5 flex items-center justify-center gap-2">
60
+ <div className="w-4 h-4 rounded anet-skeleton-bar" />
61
+ <Bar w="4rem" h="0.75rem" />
62
+ </div>
63
+ ))}
23
64
  </div>
24
65
 
25
- {/* Topology skeleton */}
26
- <div className="w-full max-w-4xl mx-auto mb-8 h-64 bg-gray-800/20 rounded-xl" />
66
+ {/* Broadcast bar */}
67
+ <div className="mb-6 flex gap-2 anet-skeleton-pulse">
68
+ <div className="flex-1 h-10 rounded-lg border border-[#2a2a4a] bg-[#111128]" />
69
+ <div className="w-28 h-10 rounded-lg anet-skeleton-bar" />
70
+ </div>
27
71
 
28
- {/* Card grid skeleton */}
29
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
72
+ {/* Agent card grid */}
73
+ <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 anet-skeleton-pulse">
30
74
  {[1, 2, 3, 4].map(i => (
31
- <div key={i} className="rounded-xl border border-gray-800/40 p-4 bg-gray-800/20">
75
+ <div key={i} className="rounded-xl border border-[#2a2a4a] bg-[#111128] p-4">
32
76
  <div className="flex items-center gap-2 mb-3">
33
- <div className="w-2.5 h-2.5 rounded-full bg-gray-700" />
34
- <div className="h-4 w-24 bg-gray-700 rounded" />
77
+ <div className="w-2 h-2 rounded-full anet-skeleton-bar" />
78
+ <Bar w="6rem" h="0.875rem" />
35
79
  </div>
36
80
  <div className="space-y-2">
37
- <div className="h-3 w-full bg-gray-800/60 rounded" />
38
- <div className="h-3 w-3/4 bg-gray-800/60 rounded" />
39
- <div className="h-3 w-1/2 bg-gray-800/60 rounded" />
81
+ <Bar w="100%" h="0.625rem" />
82
+ <Bar w="75%" h="0.625rem" />
83
+ <Bar w="55%" h="0.625rem" />
40
84
  </div>
41
85
  </div>
42
86
  ))}
@@ -45,6 +89,18 @@ export function LoadingSkeleton() {
45
89
  );
46
90
  }
47
91
 
92
+ /** Single shimmer bar — uses `anet-skeleton-bar` so theme-specific bar
93
+ * color (dark navy on dark themes, mid-grey on light) overrides the
94
+ * default. */
95
+ function Bar({ w, h, rounded, className }: { w: string; h: string; rounded?: string; className?: string }) {
96
+ return (
97
+ <div
98
+ className={`anet-skeleton-bar ${className || ''}`}
99
+ style={{ width: w, height: h, borderRadius: rounded || '0.375rem' }}
100
+ />
101
+ );
102
+ }
103
+
48
104
  // EmptyState export removed in 0.4.5 — replaced by EmptyState.tsx with
49
105
  // per-variant glyphs and a NodesEmptyState wrapper for the Overview
50
106
  // hint-aware behavior. See app/components/EmptyState.tsx.
@@ -151,6 +151,24 @@ export function Sidebar() {
151
151
  Sign out
152
152
  </button>
153
153
  )}
154
+ {!collapsed && (
155
+ <button
156
+ onClick={() => {
157
+ // Dispatch a synthetic keypress so the global Cmd+K listener fires
158
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, ctrlKey: true }));
159
+ }}
160
+ className="w-full flex items-center justify-between gap-2 px-5 py-2 text-[11px] text-gray-600 hover:text-gray-400 hover:bg-[#1a1a2a] transition-colors"
161
+ aria-label="Open command palette"
162
+ >
163
+ <span className="flex items-center gap-2">
164
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
165
+ <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
166
+ </svg>
167
+ Quick search
168
+ </span>
169
+ <kbd className="text-[10px] border border-current rounded px-1 py-0.5 opacity-60 font-mono">⌘K</kbd>
170
+ </button>
171
+ )}
154
172
  <div className={`flex items-center gap-2 px-3 py-2 ${collapsed ? 'justify-center' : 'justify-between'}`}>
155
173
  <ThemeSwitcher compact={collapsed} />
156
174
  <button
@@ -140,8 +140,22 @@ export function UserBar() {
140
140
  ))}
141
141
  </select>
142
142
  )}
143
- <button onClick={() => { setEditName(auth.user?.display_name || ''); setEditEmail(''); setEditing(!editing); }} className="text-xs text-gray-500 hover:text-gray-300">Edit</button>
144
- <button onClick={logout} className="text-xs text-gray-500 hover:text-gray-300">Sign out</button>
143
+ <button onClick={() => { setEditName(auth.user?.display_name || ''); setEditEmail(''); setEditing(!editing); }}
144
+ className="text-xs text-gray-500 hover:text-gray-300 whitespace-nowrap"
145
+ aria-label="Edit profile">
146
+ <span className="hidden sm:inline">Edit</span>
147
+ <svg className="sm:hidden w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
148
+ <path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.862 4.487z" />
149
+ </svg>
150
+ </button>
151
+ <button onClick={logout}
152
+ className="text-xs text-gray-500 hover:text-gray-300 whitespace-nowrap"
153
+ aria-label="Sign out">
154
+ <span className="hidden sm:inline">Sign out</span>
155
+ <svg className="sm:hidden w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
156
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
157
+ </svg>
158
+ </button>
145
159
  </div>
146
160
  {editing && (
147
161
  <div className="flex flex-wrap items-center gap-2 mt-2 pt-2 border-t border-[#2a2a4a]">
package/app/globals.css CHANGED
@@ -309,6 +309,66 @@ body {
309
309
  [data-theme="light"] .text-indigo-400 {
310
310
  color: var(--accent) !important;
311
311
  }
312
+
313
+ /* Light theme: every "-300/400" accent text color needs a darker mid-tone
314
+ variant. The default Tailwind `-300/-400` shades are designed for dark
315
+ backgrounds — putting them on white gives "barely visible" text (the
316
+ problem Vincent flagged: "浅色下很多字体看不见"). Map each family to its
317
+ `-700` equivalent so contrast against white card is ≥ AA. */
318
+ [data-theme="light"] .text-green-400,
319
+ [data-theme="light"] .text-green-300,
320
+ [data-theme="mint"] .text-green-400,
321
+ [data-theme="mint"] .text-green-300 { color: #047857 !important; }
322
+
323
+ [data-theme="light"] .text-yellow-400,
324
+ [data-theme="light"] .text-yellow-300,
325
+ [data-theme="mint"] .text-yellow-400,
326
+ [data-theme="mint"] .text-yellow-300 { color: #92400e !important; }
327
+
328
+ [data-theme="light"] .text-amber-400,
329
+ [data-theme="light"] .text-amber-300,
330
+ [data-theme="mint"] .text-amber-400,
331
+ [data-theme="mint"] .text-amber-300 { color: #92400e !important; }
332
+
333
+ [data-theme="light"] .text-red-400,
334
+ [data-theme="light"] .text-red-300,
335
+ [data-theme="mint"] .text-red-400,
336
+ [data-theme="mint"] .text-red-300 { color: #b91c1c !important; }
337
+
338
+ [data-theme="light"] .text-purple-400,
339
+ [data-theme="light"] .text-purple-300,
340
+ [data-theme="mint"] .text-purple-400,
341
+ [data-theme="mint"] .text-purple-300 { color: #6d28d9 !important; }
342
+
343
+ [data-theme="light"] .text-orange-400,
344
+ [data-theme="light"] .text-orange-300,
345
+ [data-theme="mint"] .text-orange-400,
346
+ [data-theme="mint"] .text-orange-300 { color: #c2410c !important; }
347
+
348
+ [data-theme="light"] .text-blue-400,
349
+ [data-theme="light"] .text-blue-300,
350
+ [data-theme="mint"] .text-blue-400,
351
+ [data-theme="mint"] .text-blue-300 { color: #1d4ed8 !important; }
352
+
353
+ [data-theme="light"] .text-pink-400,
354
+ [data-theme="light"] .text-pink-300,
355
+ [data-theme="mint"] .text-pink-400,
356
+ [data-theme="mint"] .text-pink-300 { color: #be185d !important; }
357
+
358
+ [data-theme="light"] .text-violet-400,
359
+ [data-theme="light"] .text-violet-300,
360
+ [data-theme="mint"] .text-violet-400,
361
+ [data-theme="mint"] .text-violet-300 { color: #6d28d9 !important; }
362
+
363
+ [data-theme="light"] .text-teal-400,
364
+ [data-theme="light"] .text-teal-300,
365
+ [data-theme="mint"] .text-teal-400,
366
+ [data-theme="mint"] .text-teal-300 { color: #0f766e !important; }
367
+
368
+ /* Light theme: text-gray-700 (used in footers / muted body) is too pale
369
+ when sitting on slightly-grey page bg. Bump to fg-muted. */
370
+ [data-theme="light"] .text-gray-700,
371
+ [data-theme="mint"] .text-gray-700 { color: var(--fg-muted) !important; }
312
372
  [data-theme="light"] .bg-emerald-500\/10,
313
373
  [data-theme="light"] .bg-emerald-500\/20,
314
374
  [data-theme="light"] .bg-cyan-500\/10,
@@ -539,6 +599,29 @@ body {
539
599
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), 0 0 0 1px var(--border);
540
600
  }
541
601
 
602
+ /* Health banner — sticky thin strip above content, with theme-aware
603
+ green/amber/red tints. Dark themes use the lower-opacity bg+border
604
+ that come from the component classes directly. Light/mint need
605
+ slightly more saturation so the tint reads on a white surface. */
606
+ [data-theme="light"] .anet-health-banner.bg-emerald-500\/6,
607
+ [data-theme="mint"] .anet-health-banner.bg-emerald-500\/6 {
608
+ background-color: rgba(16, 185, 129, 0.08) !important;
609
+ color: #047857 !important;
610
+ border-color: rgba(16, 185, 129, 0.25) !important;
611
+ }
612
+ [data-theme="light"] .anet-health-banner.bg-amber-500\/8,
613
+ [data-theme="mint"] .anet-health-banner.bg-amber-500\/8 {
614
+ background-color: rgba(245, 158, 11, 0.12) !important;
615
+ color: #92400e !important;
616
+ border-color: rgba(245, 158, 11, 0.30) !important;
617
+ }
618
+ [data-theme="light"] .anet-health-banner.bg-red-500\/8,
619
+ [data-theme="mint"] .anet-health-banner.bg-red-500\/8 {
620
+ background-color: rgba(220, 38, 38, 0.10) !important;
621
+ color: #b91c1c !important;
622
+ border-color: rgba(220, 38, 38, 0.30) !important;
623
+ }
624
+
542
625
  /* Sidebar brand "online" pulse — slow, restrained, NO glow.
543
626
  1.6s opacity drift between 1.0 and 0.5; nothing scales, nothing blurs. */
544
627
  @keyframes anet-brand-pulse-kf {
@@ -552,6 +635,104 @@ body {
552
635
  .anet-brand-pulse { animation: none; }
553
636
  }
554
637
 
638
+ /* Skeleton pulse — same 1.6s rhythm as the brand pulse but applied to a
639
+ container so all bars inside breathe together. Drifts between full
640
+ token value and 60% so light themes show a calm grey-to-bg-elevated
641
+ fade and dark themes show a calm navy-to-elevated fade. */
642
+ @keyframes anet-skeleton-kf {
643
+ 0%, 100% { opacity: 1; }
644
+ 50% { opacity: 0.55; }
645
+ }
646
+ .anet-skeleton-pulse {
647
+ animation: anet-skeleton-kf 1.6s ease-in-out infinite;
648
+ }
649
+ @media (prefers-reduced-motion: reduce) {
650
+ .anet-skeleton-pulse { animation: none; }
651
+ }
652
+
653
+ /* Skeleton bar fill — different per theme so it's visible against the
654
+ card bg without being loud. Dark: lighter navy block on dark card.
655
+ Light: mid-grey on white card (not too light or it disappears). */
656
+ .anet-skeleton-bar {
657
+ background-color: #1a1a2a;
658
+ }
659
+ [data-theme="light"] .anet-skeleton-bar,
660
+ [data-theme="mint"] .anet-skeleton-bar {
661
+ background-color: #d4d8df;
662
+ }
663
+
664
+ /* Command palette modal surface — bumped depth so it sits clearly above
665
+ the page content in both themes. Light/mint: white shell with a real
666
+ shadow (not the dark navy that the CSS shim would normally translate
667
+ `bg-[#0d0d1a]` into). */
668
+ .anet-cmdk kbd {
669
+ font-size: 9px;
670
+ line-height: 1;
671
+ padding: 1px 4px;
672
+ border: 1px solid currentColor;
673
+ border-radius: 3px;
674
+ opacity: 0.6;
675
+ }
676
+ [data-theme="light"] .anet-cmdk > div:not([aria-hidden]),
677
+ [data-theme="mint"] .anet-cmdk > div:not([aria-hidden]) {
678
+ background-color: var(--bg-secondary) !important;
679
+ box-shadow: 0 10px 30px -8px rgba(15, 23, 42, 0.18),
680
+ 0 2px 6px -2px rgba(15, 23, 42, 0.10);
681
+ }
682
+ [data-theme="light"] .anet-cmdk-row.bg-cyan-500\/10,
683
+ [data-theme="mint"] .anet-cmdk-row.bg-cyan-500\/10 {
684
+ background-color: rgba(0, 158, 126, 0.08) !important;
685
+ }
686
+
687
+ /* Help overlay — same white-card-with-shadow treatment as cmdk on light */
688
+ [data-theme="light"] .anet-help > div:not([aria-hidden]),
689
+ [data-theme="mint"] .anet-help > div:not([aria-hidden]) {
690
+ background-color: var(--bg-secondary) !important;
691
+ box-shadow: 0 10px 30px -8px rgba(15, 23, 42, 0.18),
692
+ 0 2px 6px -2px rgba(15, 23, 42, 0.10);
693
+ }
694
+
695
+ /* Tasks tab strip horizontal scroll fade — gradient overlays at left/right
696
+ edges hint that there's more content when the row overflows. Only
697
+ applied on <sm (where the strip actually scrolls). The wrapper is
698
+ `position: relative` and the fades are absolute siblings. */
699
+ .anet-tabstrip-wrap {
700
+ position: relative;
701
+ }
702
+ .anet-tabstrip-wrap::before,
703
+ .anet-tabstrip-wrap::after {
704
+ content: '';
705
+ position: absolute;
706
+ top: 4px;
707
+ bottom: 4px;
708
+ width: 20px;
709
+ pointer-events: none;
710
+ z-index: 1;
711
+ }
712
+ .anet-tabstrip-wrap::before {
713
+ left: 0;
714
+ background: linear-gradient(to right, #111128, transparent);
715
+ border-radius: 8px 0 0 8px;
716
+ }
717
+ .anet-tabstrip-wrap::after {
718
+ right: 0;
719
+ background: linear-gradient(to left, #111128, transparent);
720
+ border-radius: 0 8px 8px 0;
721
+ }
722
+ @media (min-width: 640px) {
723
+ /* sm: stops scrolling, hide the fades */
724
+ .anet-tabstrip-wrap::before,
725
+ .anet-tabstrip-wrap::after { display: none; }
726
+ }
727
+ [data-theme="light"] .anet-tabstrip-wrap::before,
728
+ [data-theme="mint"] .anet-tabstrip-wrap::before {
729
+ background: linear-gradient(to right, var(--bg-secondary), transparent);
730
+ }
731
+ [data-theme="light"] .anet-tabstrip-wrap::after,
732
+ [data-theme="mint"] .anet-tabstrip-wrap::after {
733
+ background: linear-gradient(to left, var(--bg-secondary), transparent);
734
+ }
735
+
555
736
  /* P0-3 baseline: error / warning chip colors for light themes.
556
737
  The component-level copy (replacing raw JSON with human text) is fixed
557
738
  in the Logs / Server-Logs page components separately. */
package/app/page.tsx CHANGED
@@ -193,20 +193,93 @@ export default function Dashboard() {
193
193
  ("0/0", "--", "0") with placeholder arrows ("→ Messages").
194
194
  Users couldn't tell which were stats vs which were navigation. */}
195
195
  <section className="mb-3 grid grid-cols-3 gap-2 sm:gap-3">
196
- {[
197
- { href: '/nodes', label: 'Nodes', value: `${online}/${total}`, sub: `${online > 0 ? Math.round((online/total)*100) : 0}% online`, color: 'text-green-400 border-green-500/20' },
198
- { href: '/tasks', label: 'Tasks', value: String(Object.values(taskStats).reduce((a, b) => a + b, 0) || 0), sub: 'all-time', color: 'text-cyan-400 border-cyan-500/20' },
199
- { href: '/tasks?status=failed', label: 'Failed', value: String(taskStats['failed'] || 0), sub: taskStats['failed'] ? 'needs review' : 'none', color: taskStats['failed'] ? 'text-red-400 border-red-500/25' : 'text-gray-500 border-gray-700/30' },
200
- ].map(a => (
201
- <Link key={a.href} href={a.href} prefetch={false} className={`anet-stat-link group relative rounded-xl border ${a.color} bg-[#111128] px-3 py-3 transition-all hover:-translate-y-px`}>
202
- <div className="flex items-baseline justify-between">
203
- <div className={`text-xl font-semibold tabular-nums ${a.color.split(' ')[0]}`}>{a.value}</div>
204
- <div className="text-[10px] text-gray-600 group-hover:text-gray-400 transition-colors">View →</div>
205
- </div>
206
- <div className="text-[11px] text-gray-400 mt-0.5">{a.label}</div>
207
- <div className="text-[10px] text-gray-600 mt-px">{a.sub}</div>
208
- </Link>
209
- ))}
196
+ {(() => {
197
+ // Build breakdown popover content per card. Pure data pure CSS
198
+ // popover (no JS state) wires the hover-show transition below.
199
+ const idleCount = sessions.filter(s => isOnline(s) && s.status !== 'working').length;
200
+ const offlineCount = total - online;
201
+ const orderedStatuses = ['running', 'replied', 'failed', 'cancelled', 'expired', 'closed', 'created', 'delivered', 'acked'];
202
+ const failedRecent = tasks.filter((t: { status: string }) => t.status === 'failed').length;
203
+
204
+ const cards = [
205
+ {
206
+ href: '/nodes', label: 'Nodes',
207
+ value: `${online}/${total}`,
208
+ sub: `${total > 0 ? Math.round((online / total) * 100) : 0}% online`,
209
+ color: 'text-green-400 border-green-500/20',
210
+ popover: total === 0 ? null : [
211
+ { k: 'working', v: working, dot: '', color: '#4ade80' },
212
+ { k: 'idle', v: idleCount, dot: '', color: '#22d3ee' },
213
+ { k: 'offline', v: offlineCount, dot: '', color: '#9ca3af' },
214
+ ],
215
+ },
216
+ {
217
+ href: '/tasks', label: 'Tasks',
218
+ value: String(Object.values(taskStats).reduce((a, b) => a + b, 0) || 0),
219
+ sub: 'all-time',
220
+ color: 'text-cyan-400 border-cyan-500/20',
221
+ popover: Object.keys(taskStats).length === 0 ? null
222
+ : orderedStatuses
223
+ .filter(s => taskStats[s])
224
+ .map(s => {
225
+ // Inline hex avoids Tailwind purging dynamic
226
+ // `bg-${color}-400` class names.
227
+ const hex = ({
228
+ running: '#4ade80', replied: '#a78bfa', failed: '#f87171',
229
+ cancelled: '#facc15', expired: '#fb923c', closed: '#9ca3af',
230
+ created: '#9ca3af', delivered: '#60a5fa', acked: '#22d3ee',
231
+ } as Record<string, string>)[s] || '#9ca3af';
232
+ return { k: s, v: taskStats[s], dot: '', color: hex };
233
+ }),
234
+ },
235
+ {
236
+ href: '/tasks?status=failed', label: 'Failed',
237
+ value: String(taskStats['failed'] || 0),
238
+ sub: taskStats['failed'] ? 'needs review' : 'none',
239
+ color: taskStats['failed'] ? 'text-red-400 border-red-500/25' : 'text-gray-500 border-gray-700/30',
240
+ popover: !failedRecent ? [{ k: 'no failures yet', v: '', dot: '', color: '#4b5563' }]
241
+ : [{ k: `${failedRecent} in current view`, v: '', dot: '', color: '#f87171' }],
242
+ },
243
+ ];
244
+
245
+ return cards.map(a => (
246
+ <Link
247
+ key={a.href}
248
+ href={a.href}
249
+ prefetch={false}
250
+ className={`anet-stat-link group relative rounded-xl border ${a.color} bg-[#111128] px-3 py-3 transition-all hover:-translate-y-px`}
251
+ >
252
+ <div className="flex items-baseline justify-between">
253
+ <div className={`text-xl font-semibold tabular-nums ${a.color.split(' ')[0]}`}>{a.value}</div>
254
+ <div className="hidden sm:block text-[10px] text-gray-600 group-hover:text-gray-400 transition-colors">View →</div>
255
+ </div>
256
+ <div className="text-[11px] text-gray-400 mt-0.5">{a.label}</div>
257
+ <div className="text-[10px] text-gray-600 mt-px">{a.sub}</div>
258
+
259
+ {/* Hover popover — CSS-only, restrained. Hidden on touch
260
+ (no :hover) so mobile is unaffected. Positioned just
261
+ below the card so it doesn't fight nav rail. */}
262
+ {a.popover && a.popover.length > 0 && (
263
+ <div className="anet-stat-popover hidden md:block pointer-events-none absolute left-2 right-2 top-full mt-1 z-20 rounded-lg border border-[#2a2a4a] bg-[#0d0d1a] shadow-lg shadow-black/30 px-3 py-2 opacity-0 translate-y-[-2px] group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-150 delay-100">
264
+ <div className="text-[10px] text-gray-600 uppercase tracking-wider mb-1.5">Breakdown</div>
265
+ <ul className="space-y-1">
266
+ {a.popover.map(row => (
267
+ <li key={row.k} className="flex items-center gap-2 text-[11px]">
268
+ <span
269
+ className="inline-block w-1.5 h-1.5 rounded-full shrink-0"
270
+ style={{ backgroundColor: row.color }}
271
+ aria-hidden
272
+ />
273
+ <span className="text-gray-400 capitalize">{row.k}</span>
274
+ {row.v !== '' && <span className="ml-auto text-gray-300 tabular-nums font-medium">{row.v}</span>}
275
+ </li>
276
+ ))}
277
+ </ul>
278
+ </div>
279
+ )}
280
+ </Link>
281
+ ));
282
+ })()}
210
283
  </section>
211
284
 
212
285
  {/* Nav rail — pure navigation, icon + label, no data */}
@@ -48,6 +48,21 @@ const STATUS_COLORS: Record<string, string> = {
48
48
  expired: 'bg-orange-500/10 text-orange-300 border-orange-500/20',
49
49
  };
50
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
+
51
66
  function statusBadge(status: string) {
52
67
  const color = STATUS_COLORS[status] || 'bg-gray-500/10 text-gray-400 border-gray-500/20';
53
68
  return `text-xs px-2 py-0.5 rounded-md border ${color}`;
@@ -134,19 +149,34 @@ function TasksContent() {
134
149
  </span>
135
150
  </div>
136
151
 
137
- {/* Status Tabs */}
138
- <div className="flex flex-wrap gap-1 mb-4 bg-[#111128] rounded-lg border border-[#2a2a4a] p-1">
152
+ {/* Status Tabs — color-coded dots per status family so users can
153
+ scan the row at a glance. Active tab gets full chip styling
154
+ (bg + border + text color), inactive tabs only carry the dot
155
+ + neutral label so the active state remains the strong cue.
156
+ On narrow viewports (<sm) the row scrolls horizontally with
157
+ subtle gradient fade-edges (via .anet-tabstrip-wrap pseudo
158
+ elements) hinting more content. */}
159
+ <div className="anet-tabstrip-wrap mb-4">
160
+ <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">
139
161
  {['', ...STATUS_OPTIONS.filter(Boolean)].map(s => (
140
162
  <button key={s} onClick={() => setFilterStatus(s)}
141
- className={`px-3 py-1.5 rounded-md text-xs transition-colors ${
163
+ className={`px-3 py-1.5 rounded-md text-xs transition-colors flex items-center gap-1.5 shrink-0 whitespace-nowrap ${
142
164
  filterStatus === s
143
165
  ? `${STATUS_COLORS[s] || 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'} border`
144
- : 'text-gray-500 hover:text-gray-300'
166
+ : 'text-gray-500 hover:text-gray-300 hover:bg-[#1a1a2a]/40'
145
167
  }`}>
146
- {s || 'All'}
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>
147
176
  </button>
148
177
  ))}
149
178
  </div>
179
+ </div>
150
180
 
151
181
  {/* From/To Filters */}
152
182
  <div className="flex flex-wrap gap-3 mb-6">