@sleep2agi/agent-network-dashboard 0.6.3-preview.14 → 0.6.3-preview.15

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 (220) 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 +73 -73
  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 +13 -13
  17. package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  18. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  19. package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
  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 +15 -15
  27. package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
  28. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  29. package/.next/server/app/admin.segments/_index.segment.rsc +8 -8
  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 +15 -15
  35. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  36. package/.next/server/app/index.segments/_full.segment.rsc +15 -15
  37. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  38. package/.next/server/app/index.segments/_index.segment.rsc +8 -8
  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 +15 -15
  44. package/.next/server/app/login.segments/_full.segment.rsc +15 -15
  45. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  46. package/.next/server/app/login.segments/_index.segment.rsc +8 -8
  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 +15 -15
  54. package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
  55. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  56. package/.next/server/app/logs.segments/_index.segment.rsc +8 -8
  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 +15 -15
  64. package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
  65. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  66. package/.next/server/app/messages.segments/_index.segment.rsc +8 -8
  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 +15 -15
  74. package/.next/server/app/node.segments/_full.segment.rsc +15 -15
  75. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  76. package/.next/server/app/node.segments/_index.segment.rsc +8 -8
  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 +15 -15
  84. package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
  85. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  86. package/.next/server/app/nodes.segments/_index.segment.rsc +8 -8
  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/providers/page.js.nft.json +1 -1
  93. package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/providers.html +2 -2
  95. package/.next/server/app/providers.rsc +15 -15
  96. package/.next/server/app/providers.segments/_full.segment.rsc +15 -15
  97. package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
  98. package/.next/server/app/providers.segments/_index.segment.rsc +8 -8
  99. package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
  100. package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +4 -4
  101. package/.next/server/app/providers.segments/providers.segment.rsc +3 -3
  102. package/.next/server/app/server-logs/page.js.nft.json +1 -1
  103. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/server-logs.html +2 -2
  105. package/.next/server/app/server-logs.rsc +15 -15
  106. package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
  107. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  108. package/.next/server/app/server-logs.segments/_index.segment.rsc +8 -8
  109. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  110. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  111. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  112. package/.next/server/app/servers/page.js.nft.json +1 -1
  113. package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
  114. package/.next/server/app/servers.html +2 -2
  115. package/.next/server/app/servers.rsc +15 -15
  116. package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
  117. package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
  118. package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
  119. package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
  120. package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
  121. package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
  122. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  123. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/settings/networks.html +2 -2
  125. package/.next/server/app/settings/networks.rsc +15 -15
  126. package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
  127. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  128. package/.next/server/app/settings/networks.segments/_index.segment.rsc +8 -8
  129. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  130. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  131. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  132. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  133. package/.next/server/app/settings/page.js.nft.json +1 -1
  134. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  135. package/.next/server/app/settings/tokens/page.js.nft.json +1 -1
  136. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  137. package/.next/server/app/settings/tokens.html +2 -2
  138. package/.next/server/app/settings/tokens.rsc +15 -15
  139. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
  140. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  141. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +8 -8
  142. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  143. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  144. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  145. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  146. package/.next/server/app/settings.html +2 -2
  147. package/.next/server/app/settings.rsc +15 -15
  148. package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  149. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  150. package/.next/server/app/settings.segments/_index.segment.rsc +8 -8
  151. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  152. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  153. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  154. package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  155. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  156. package/.next/server/app/tasks/page.js.nft.json +1 -1
  157. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  158. package/.next/server/app/tasks.html +2 -2
  159. package/.next/server/app/tasks.rsc +15 -15
  160. package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
  161. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  162. package/.next/server/app/tasks.segments/_index.segment.rsc +8 -8
  163. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  164. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  165. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  166. package/.next/server/chunks/ssr/[root-of-the-server]__03jniap._.js +3 -0
  167. package/.next/server/chunks/ssr/[root-of-the-server]__03jniap._.js.map +1 -0
  168. package/.next/server/chunks/ssr/[root-of-the-server]__091su60._.js +2 -2
  169. package/.next/server/chunks/ssr/[root-of-the-server]__091su60._.js.map +1 -1
  170. package/.next/server/chunks/ssr/[root-of-the-server]__0vc.3k7._.js +1 -1
  171. package/.next/server/chunks/ssr/[root-of-the-server]__0vc.3k7._.js.map +1 -1
  172. package/.next/server/chunks/ssr/dash-loop-wt_0rh8p9o._.js +1 -1
  173. package/.next/server/chunks/ssr/dash-loop-wt_0rh8p9o._.js.map +1 -1
  174. package/.next/server/chunks/ssr/dash-loop-wt_app_0mxr2l9._.js +2 -2
  175. package/.next/server/chunks/ssr/dash-loop-wt_app_0mxr2l9._.js.map +1 -1
  176. package/.next/server/chunks/ssr/dash-loop-wt_app_0~.1~ew._.js +1 -1
  177. package/.next/server/chunks/ssr/dash-loop-wt_app_0~.1~ew._.js.map +1 -1
  178. package/.next/server/chunks/ssr/dash-loop-wt_app_13k3-ai._.js +1 -1
  179. package/.next/server/chunks/ssr/dash-loop-wt_app_13k3-ai._.js.map +1 -1
  180. package/.next/server/chunks/ssr/dash-loop-wt_app_components_0cym4cd._.js +1 -1
  181. package/.next/server/chunks/ssr/dash-loop-wt_app_components_0cym4cd._.js.map +1 -1
  182. package/.next/server/chunks/ssr/dash-loop-wt_app_nodes_page_tsx_0ma3oqt._.js +1 -1
  183. package/.next/server/chunks/ssr/dash-loop-wt_app_nodes_page_tsx_0ma3oqt._.js.map +1 -1
  184. package/.next/server/middleware-build-manifest.js +3 -3
  185. package/.next/server/pages/404.html +2 -2
  186. package/.next/server/pages/500.html +1 -1
  187. package/.next/server/server-reference-manifest.js +1 -1
  188. package/.next/server/server-reference-manifest.json +1 -1
  189. package/.next/static/chunks/02w_tb38yr0xe.css +1 -0
  190. package/.next/static/chunks/{0v-2rx4h02~2h.js → 06qwjph2.xkng.js} +2 -2
  191. package/.next/static/chunks/{1566p5peao240.js → 0_87-ekkmdd5n.js} +1 -1
  192. package/.next/static/chunks/{0g23p135n0swi.js → 0a229b11_ks8h.js} +2 -2
  193. package/.next/static/chunks/{146p-0l764ojl.js → 0ck-p90gm54x_.js} +1 -1
  194. package/.next/static/chunks/0imomomvqzqmm.js +1 -0
  195. package/.next/static/chunks/0ljzzrki8sm14.js +4 -0
  196. package/.next/static/chunks/{0_4kwsiltvp~l.js → 0mh28v-4j9dng.js} +1 -1
  197. package/.next/static/chunks/{16_v-g.qr-.xa.js → 0qgybbsb9e6q4.js} +1 -1
  198. package/.next/static/chunks/11.o41gzk3vi4.js +1 -0
  199. package/.next/static/chunks/{00_m_epf7h806.js → 11q_-4rnr__r..js} +1 -1
  200. package/.next/trace +2 -2
  201. package/.next/trace-build +1 -1
  202. package/app/components/AgentCard.tsx +17 -3
  203. package/app/components/CommandCenter.tsx +9 -0
  204. package/app/components/MobileNav.tsx +14 -3
  205. package/app/components/TaskChatPanel.tsx +1 -24
  206. package/app/lib/chat-unread.ts +70 -41
  207. package/app/lib/time.ts +27 -0
  208. package/app/messages/page.tsx +5 -3
  209. package/app/nodes/page.tsx +17 -3
  210. package/app/page.tsx +4 -1
  211. package/package.json +1 -1
  212. package/.next/server/chunks/ssr/[root-of-the-server]__0gv51is._.js +0 -3
  213. package/.next/server/chunks/ssr/[root-of-the-server]__0gv51is._.js.map +0 -1
  214. package/.next/static/chunks/0vz2kzk6pucsq.js +0 -1
  215. package/.next/static/chunks/0wd05udmyiwvk.js +0 -4
  216. package/.next/static/chunks/14d2lfzm8pyqn.js +0 -1
  217. package/.next/static/chunks/180t32do_.c~c.css +0 -1
  218. /package/.next/static/{qpprKezd_zujs8ouGr3LY → R5eL5ZqEdEjOLN_5qjTqM}/_buildManifest.js +0 -0
  219. /package/.next/static/{qpprKezd_zujs8ouGr3LY → R5eL5ZqEdEjOLN_5qjTqM}/_clientMiddlewareManifest.js +0 -0
  220. /package/.next/static/{qpprKezd_zujs8ouGr3LY → R5eL5ZqEdEjOLN_5qjTqM}/_ssgManifest.js +0 -0
@@ -12,6 +12,10 @@ interface AgentCardProps {
12
12
  hasSse: boolean;
13
13
  sseCount: number;
14
14
  onChat?: (alias: string) => void;
15
+ /** R8 (Vincent: 微信式未读角标): unread count for this conversation.
16
+ * Passed as a prop (computed once page-level) so the R6 memo keeps
17
+ * protecting against fleet-wide re-render storms. */
18
+ unreadCount?: number;
15
19
  }
16
20
 
17
21
  const STATUS_CONFIG: Record<string, { bg: string; text: string; dot: string; glow: string }> = {
@@ -23,7 +27,7 @@ const STATUS_CONFIG: Record<string, { bg: string; text: string; dot: string; glo
23
27
 
24
28
  const DEFAULT_STATUS = { bg: 'bg-gray-800/50 border-gray-700/30', text: 'text-gray-500', dot: 'bg-gray-500', glow: '' };
25
29
 
26
- function AgentCardInner({ session: s, hasSse, sseCount, onChat }: AgentCardProps) {
30
+ function AgentCardInner({ session: s, hasSse, sseCount, onChat, unreadCount = 0 }: AgentCardProps) {
27
31
  const cfg = hasSse ? (STATUS_CONFIG[s.status] || DEFAULT_STATUS) : DEFAULT_STATUS;
28
32
  // #260: per-node settings panel (UI-only stub). Local open state — the
29
33
  // panel itself is presentational and issues no backend calls.
@@ -65,7 +69,17 @@ function AgentCardInner({ session: s, hasSse, sseCount, onChat }: AgentCardProps
65
69
  tightens on narrow viewports. */}
66
70
  <div className="flex items-center justify-between mb-2 sm:mb-3">
67
71
  <div className="flex items-center gap-2 min-w-0">
68
- <AliasAvatar alias={s.alias} size={22} />
72
+ <span className="relative inline-flex shrink-0">
73
+ <AliasAvatar alias={s.alias} size={22} />
74
+ {unreadCount > 0 && (
75
+ <span
76
+ aria-label={`${unreadCount} 条未读`}
77
+ className="absolute -top-1.5 -right-2 min-w-[16px] h-4 px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-4 text-center ring-2 ring-[#161618]"
78
+ >
79
+ {unreadCount > 99 ? '99+' : unreadCount}
80
+ </span>
81
+ )}
82
+ </span>
69
83
  <span className="font-semibold text-white truncate text-sm" title={s.alias}>{s.alias}</span>
70
84
  <span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${cfg.dot} ${hasSse && s.status === 'working' ? 'animate-pulse' : ''}`} />
71
85
  </div>
@@ -194,7 +208,7 @@ const CARD_FIELDS = [
194
208
  ] as const;
195
209
 
196
210
  export const AgentCard = memo(AgentCardInner, (prev, next) => {
197
- if (prev.hasSse !== next.hasSse || prev.sseCount !== next.sseCount || prev.onChat !== next.onChat) return false;
211
+ if (prev.hasSse !== next.hasSse || prev.sseCount !== next.sseCount || prev.onChat !== next.onChat || prev.unreadCount !== next.unreadCount) return false;
198
212
  const a = prev.session as unknown as Record<string, unknown>, b = next.session as unknown as Record<string, unknown>;
199
213
  for (const k of CARD_FIELDS) if (a[k] !== b[k]) return false;
200
214
  return true;
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useCallback } from 'react';
4
+ import { useChatUnread } from '../lib/chat-unread';
4
5
  import { TaskChatPanel } from './TaskChatPanel';
5
6
 
6
7
  interface CommandCenterProps {
@@ -18,6 +19,7 @@ interface CommandCenterProps {
18
19
  * Wraps TaskChatPanel with a tab bar for switching between agents.
19
20
  */
20
21
  export function CommandCenter({ tabs, activeTab, onOpenTab, onCloseTab, onSetActive, onClose }: CommandCenterProps) {
22
+ const { unreadCount } = useChatUnread();
21
23
  if (tabs.length === 0) return null;
22
24
 
23
25
  return (
@@ -42,6 +44,13 @@ export function CommandCenter({ tabs, activeTab, onOpenTab, onCloseTab, onSetAct
42
44
  >
43
45
  <div className={`w-2 h-2 rounded-full ${activeTab === alias ? 'bg-cyan-400' : 'bg-gray-600'}`} />
44
46
  <span className="max-w-[80px] truncate">{alias}</span>
47
+ {/* R8: WeChat unread badge on BACKGROUND tabs (the active
48
+ tab is being read — no badge). */}
49
+ {activeTab !== alias && unreadCount(alias) > 0 && (
50
+ <span className="min-w-[15px] h-[15px] px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-[15px] text-center">
51
+ {unreadCount(alias) > 99 ? '99+' : unreadCount(alias)}
52
+ </span>
53
+ )}
45
54
  {/* R21 of #190 mobile a11y: nested close had no aria-label
46
55
  (screen-readers spoke just "button") and an ~8 px
47
56
  tap target (p-0.5 + text "×"). Both fixed; nested
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import Link from 'next/link';
4
+ import { useChatUnread, badgeLabel } from '../lib/chat-unread';
4
5
  import { usePathname } from 'next/navigation';
5
6
 
6
7
  // #209 R25 (Vincent msg 529 "command 留着干嘛" / 530 "Agents 放到最前面?"
@@ -25,6 +26,9 @@ const MOBILE_NAV_ITEMS = [
25
26
 
26
27
  export function MobileNav() {
27
28
  const pathname = usePathname();
29
+ // R8: WeChat-style total unread on the Agents tab (a glance answers
30
+ // "有没有人找我" from anywhere in the app).
31
+ const { totalUnread } = useChatUnread();
28
32
  // Match Sidebar.tsx isActive — /settings must be exact-match because
29
33
  // Settings subpages (/settings/tokens, /settings/networks) have their
30
34
  // own headers; startsWith would keep the bottom-nav highlighted there
@@ -52,9 +56,16 @@ export function MobileNav() {
52
56
  : 'text-gray-500 active:bg-[#232327] active:text-gray-200'
53
57
  }`}
54
58
  >
55
- <svg className="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.6}>
56
- <path strokeLinecap="round" strokeLinejoin="round" d={item.icon} />
57
- </svg>
59
+ <span className="relative inline-flex">
60
+ <svg className="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.6}>
61
+ <path strokeLinecap="round" strokeLinejoin="round" d={item.icon} />
62
+ </svg>
63
+ {item.href === '/nodes' && totalUnread > 0 && (
64
+ <span className="absolute -top-1 -right-2.5 min-w-[15px] h-[15px] px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-[15px] text-center">
65
+ {badgeLabel(totalUnread)}
66
+ </span>
67
+ )}
68
+ </span>
58
69
  <span className="max-w-full truncate">{item.label}</span>
59
70
  </Link>
60
71
  );
@@ -29,30 +29,6 @@ type ChatEvent =
29
29
  title tooltip. */
30
30
  const TIME_GROUP_GAP_MS = 5 * 60 * 1000;
31
31
 
32
- // R4 polish: locale-aware (R3 hardcoded Chinese weekdays — English users saw
33
- // 星期二). zh keeps the native WeChat wording; other locales get Intl output.
34
- function formatWeChatTime(value: string): string {
35
- const t = eventTime(value);
36
- if (!t) return '';
37
- const d = new Date(t);
38
- const now = new Date();
39
- const zh = typeof navigator !== 'undefined' && (navigator.language || '').toLowerCase().startsWith('zh');
40
- const hm = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
41
- const startOfDay = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
42
- const dayDiff = Math.round((startOfDay(now) - startOfDay(d)) / 86400000);
43
- if (dayDiff === 0) return hm;
44
- if (dayDiff === 1) return zh ? `昨天 ${hm}` : `Yesterday ${hm}`;
45
- if (dayDiff > 1 && dayDiff < 7) {
46
- const wd = new Intl.DateTimeFormat(zh ? 'zh-CN' : undefined, { weekday: zh ? 'long' : 'short' }).format(d);
47
- return `${wd} ${hm}`;
48
- }
49
- const md = new Intl.DateTimeFormat(zh ? 'zh-CN' : undefined, {
50
- month: zh ? 'long' : 'short', day: 'numeric',
51
- ...(d.getFullYear() !== now.getFullYear() ? { year: 'numeric' } : {}),
52
- }).format(d);
53
- return `${md} ${hm}`;
54
- }
55
-
56
32
  function eventTime(value?: string) {
57
33
  if (!value) return 0;
58
34
  return new Date(value.replace(' ', 'T') + (value.includes('T') ? '' : 'Z')).getTime() || 0;
@@ -171,6 +147,7 @@ import remarkGfm from 'remark-gfm';
171
147
  import { useSSE } from '../lib/useSSE';
172
148
  import { useNetworkId } from '../lib/network-context';
173
149
  import { markChatRead } from '../lib/chat-unread';
150
+ import { formatWeChatTime } from '../lib/time';
174
151
 
175
152
  function MarkdownContent({ text }: { text: string }) {
176
153
  if (!text) return <span className="text-[var(--fg-dim)] italic">No content</span>;
@@ -9,17 +9,14 @@ const UNREAD_EVENT = 'anet-chat-read-updated';
9
9
 
10
10
  interface ChatMessage {
11
11
  from_alias?: string | null;
12
+ to_alias?: string | null;
12
13
  created_at?: string | null;
13
14
  }
14
15
 
15
- interface ChatTask {
16
- to_name?: string | null;
17
- status?: string | null;
18
- result?: string | null;
19
- created_at?: string | null;
20
- completed_at?: string | null;
21
- delivered_at?: string | null;
22
- }
16
+ /** Senders/recipients that mean "the dashboard user" in hub data. The chat
17
+ * panel uses the same set to decide which bubbles are "You". */
18
+ const USERISH = new Set(['', 'dashboard', 'admin', 'api', 'hub']);
19
+ const isUserish = (v?: string | null) => USERISH.has((v || '').trim().toLowerCase());
23
20
 
24
21
  interface ReadMap {
25
22
  [alias: string]: string;
@@ -37,13 +34,32 @@ function parseHubTime(value?: string | null): number {
37
34
  return Number.isFinite(ts) ? ts : 0;
38
35
  }
39
36
 
37
+ // Loop R8 (Vincent: 未读数字角标): last-read moved sessionStorage →
38
+ // localStorage so read state survives refresh/new tabs/restart (微信预期).
39
+ // One-time migration pulls any old session value forward.
40
+ const BASELINE_KEY = '__baseline';
41
+
40
42
  function readStoredMap(networkId: string): ReadMap {
41
43
  if (typeof window === 'undefined') return {};
42
44
  try {
43
- const raw = window.sessionStorage.getItem(storageKey(networkId));
44
- if (!raw) return {};
45
- const parsed = JSON.parse(raw);
46
- return parsed && typeof parsed === 'object' ? parsed : {};
45
+ let raw = window.localStorage.getItem(storageKey(networkId));
46
+ if (!raw) {
47
+ raw = window.sessionStorage.getItem(storageKey(networkId));
48
+ if (raw) window.localStorage.setItem(storageKey(networkId), raw);
49
+ }
50
+ const parsed = raw ? JSON.parse(raw) : {};
51
+ const map: ReadMap = parsed && typeof parsed === 'object' ? parsed : {};
52
+ // R8 first-visit floor, written SYNCHRONOUSLY on first read: an async
53
+ // (useEffect) baseline lost the race against the first SWR render and
54
+ // let a badge storm flash — and stick, since nothing re-triggered the
55
+ // count. Ensuring it here means no compute can ever see a missing
56
+ // baseline. No event dispatch: every hook instance does the same
57
+ // idempotent write.
58
+ if (!map[BASELINE_KEY]) {
59
+ map[BASELINE_KEY] = new Date().toISOString();
60
+ window.localStorage.setItem(storageKey(networkId), JSON.stringify(map));
61
+ }
62
+ return map;
47
63
  } catch {
48
64
  return {};
49
65
  }
@@ -52,7 +68,7 @@ function readStoredMap(networkId: string): ReadMap {
52
68
  function writeStoredMap(networkId: string, next: ReadMap) {
53
69
  if (typeof window === 'undefined') return;
54
70
  try {
55
- window.sessionStorage.setItem(storageKey(networkId), JSON.stringify(next));
71
+ window.localStorage.setItem(storageKey(networkId), JSON.stringify(next));
56
72
  window.dispatchEvent(new CustomEvent(UNREAD_EVENT, { detail: { networkId } }));
57
73
  } catch {}
58
74
  }
@@ -83,16 +99,15 @@ export function markChatRead(alias: string, networkId: string) {
83
99
 
84
100
  export function useChatUnread() {
85
101
  const { networkId } = useNetworkId();
102
+ // Messages are the ONLY count source: the hub mirrors every task reply as
103
+ // a message to the originator (verified live: replied task + same-second
104
+ // message, both present), so counting tasks too double-counted every
105
+ // reply (E2E showed 4 where 2 was right).
86
106
  const { data: messagesData } = useSWR<{ messages?: ChatMessage[] }>(
87
107
  withNetwork('/api/hub/messages?limit=200', networkId),
88
108
  fetcher,
89
109
  { refreshInterval: 5000, dedupingInterval: 3000 },
90
110
  );
91
- const { data: tasksData } = useSWR<{ tasks?: ChatTask[] }>(
92
- withNetwork('/api/hub/tasks?limit=200', networkId),
93
- fetcher,
94
- { refreshInterval: 5000, dedupingInterval: 3000 },
95
- );
96
111
  const [readVersion, setReadVersion] = useState(0);
97
112
 
98
113
  useEffect(() => {
@@ -119,40 +134,54 @@ export function useChatUnread() {
119
134
  }, [networkId, readVersion]);
120
135
 
121
136
  const unreadMap = useMemo(() => {
122
- const latestByAlias = new Map<string, number>();
137
+ // R8: count EVERY unread item per alias (Vincent: "有几条消息显示数字"),
138
+ // not just a latest-activity boolean. A message from the alias counts;
139
+ // a task's REPLY counts once when it lands after last-read. Self-sent
140
+ // items never count (they're keyed by the counterpart alias).
141
+ const counts = new Map<string, number>();
142
+ const baseline = parseHubTime(readMap[BASELINE_KEY]);
143
+ const bump = (alias: string, at: number) => {
144
+ const readAt = Math.max(parseHubTime(readMap[alias]), baseline);
145
+ if (at > readAt) counts.set(alias, (counts.get(alias) || 0) + 1);
146
+ };
147
+ // Scope: only the USER'S conversation — a badge on X means "X has
148
+ // something FOR YOU". Fleet chatter (X's messages to other agents)
149
+ // must NOT inflate it.
123
150
  for (const message of messagesData?.messages || []) {
124
151
  const alias = message.from_alias?.trim();
125
- if (!alias) continue;
152
+ if (!alias || isUserish(alias)) continue; // own sends never count
153
+ if (!isUserish(message.to_alias)) continue; // must be addressed to the user
126
154
  const at = parseHubTime(message.created_at);
127
- if (!at) continue;
128
- latestByAlias.set(alias, Math.max(latestByAlias.get(alias) || 0, at));
155
+ if (at) bump(alias, at);
129
156
  }
130
- for (const task of tasksData?.tasks || []) {
131
- const alias = task.to_name?.trim();
132
- if (!alias) continue;
133
- const hasReply = Boolean(task.result) || ['replied', 'failed', 'closed', 'expired', 'cancelled'].includes(task.status || '');
134
- if (!hasReply) continue;
135
- const at = parseHubTime(task.completed_at) || parseHubTime(task.delivered_at) || parseHubTime(task.created_at);
136
- if (!at) continue;
137
- latestByAlias.set(alias, Math.max(latestByAlias.get(alias) || 0, at));
138
- }
139
-
140
- const next = new Map<string, boolean>();
141
- for (const [alias, latestAt] of latestByAlias) {
142
- const readAt = parseHubTime(readMap[alias]);
143
- if (latestAt > readAt) next.set(alias, true);
144
- }
145
- return next;
146
- }, [messagesData, readMap, tasksData]);
157
+ return counts;
158
+ }, [messagesData, readMap]);
147
159
 
148
160
  const hasUnread = useCallback((alias?: string | null) => {
149
161
  if (!alias) return false;
150
- return unreadMap.get(alias) === true;
162
+ return (unreadMap.get(alias) || 0) > 0;
163
+ }, [unreadMap]);
164
+
165
+ const unreadCount = useCallback((alias?: string | null) => {
166
+ if (!alias) return 0;
167
+ return unreadMap.get(alias) || 0;
168
+ }, [unreadMap]);
169
+
170
+ const totalUnread = useMemo(() => {
171
+ let t = 0;
172
+ for (const n of unreadMap.values()) t += n;
173
+ return t;
151
174
  }, [unreadMap]);
152
175
 
153
176
  const clearUnread = useCallback((alias: string) => {
154
177
  markChatRead(alias, networkId);
155
178
  }, [networkId]);
156
179
 
157
- return { unreadMap, hasUnread, clearUnread };
180
+ return { unreadMap, hasUnread, unreadCount, totalUnread, clearUnread };
181
+ }
182
+
183
+ /** WeChat badge label: 1-99 as-is, above → "99+"; 0/negative → '' (hide). */
184
+ export function badgeLabel(n: number): string {
185
+ if (n <= 0) return '';
186
+ return n > 99 ? '99+' : String(n);
158
187
  }
package/app/lib/time.ts CHANGED
@@ -34,3 +34,30 @@ export function relativeAgo(dateStr: string | null | undefined): string | null {
34
34
  if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
35
35
  return `${Math.floor(s / 86400)}d ago`;
36
36
  }
37
+
38
+ /** Loop R7 (跨面一致性): WeChat-style contextual timestamp shared by the
39
+ * chat panel (R3) and /messages dividers. zh locales keep the native
40
+ * wording (昨天/星期X); others get Intl output ("Yesterday"/"Tue").
41
+ * Today → "14:32" · yesterday → "昨天 14:32" · <7d → "星期二 14:32" ·
42
+ * this year → "7月16日 14:32" · older adds the year. */
43
+ export function formatWeChatTime(value: string | null | undefined): string {
44
+ const t = parseHubTime(value);
45
+ if (t === null) return '';
46
+ const d = new Date(t);
47
+ const now = new Date();
48
+ const zh = typeof navigator !== 'undefined' && (navigator.language || '').toLowerCase().startsWith('zh');
49
+ const hm = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
50
+ const startOfDay = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
51
+ const dayDiff = Math.round((startOfDay(now) - startOfDay(d)) / 86400000);
52
+ if (dayDiff === 0) return hm;
53
+ if (dayDiff === 1) return zh ? `昨天 ${hm}` : `Yesterday ${hm}`;
54
+ if (dayDiff > 1 && dayDiff < 7) {
55
+ const wd = new Intl.DateTimeFormat(zh ? 'zh-CN' : undefined, { weekday: zh ? 'long' : 'short' }).format(d);
56
+ return `${wd} ${hm}`;
57
+ }
58
+ const md = new Intl.DateTimeFormat(zh ? 'zh-CN' : undefined, {
59
+ month: zh ? 'long' : 'short', day: 'numeric',
60
+ ...(d.getFullYear() !== now.getFullYear() ? { year: 'numeric' } : {}),
61
+ }).format(d);
62
+ return `${md} ${hm}`;
63
+ }
@@ -3,6 +3,7 @@
3
3
  import { useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { useMessages } from '../lib/hooks';
5
5
  import { timeAgo, previewContent } from '../components/utils';
6
+ import { formatWeChatTime } from '../lib/time';
6
7
  import { EmptyState } from '../components/EmptyState';
7
8
  import { AliasAvatar } from '../components/AliasAvatar';
8
9
  import { useCollapsibleSearch } from '../components/CollapsibleSearch';
@@ -30,10 +31,11 @@ function normalizeDate(value?: string) {
30
31
  return new Date(value.replace(' ', 'T') + 'Z').getTime();
31
32
  }
32
33
 
34
+ // Loop R7: divider label now shares the chat panel's WeChat-style
35
+ // contextual formatter (was a raw toLocaleString() dump) — one time
36
+ // language across every conversation surface.
33
37
  function formatDividerLabel(value?: string) {
34
- if (!value) return 'Time gap';
35
- const date = new Date(value.replace(' ', 'T') + 'Z');
36
- return date.toLocaleString();
38
+ return formatWeChatTime(value) || 'Time gap';
37
39
  }
38
40
 
39
41
  function bubbleVariant(message: MessageItem) {
@@ -29,7 +29,7 @@ export default function NodesPage() {
29
29
  const { sessions, isLoading: loading } = useSessions();
30
30
  const { health } = useHealth();
31
31
  const { lifecycleByAlias, nodeIdByAlias } = useNodeLifecycle();
32
- const { hasUnread } = useChatUnread();
32
+ const { hasUnread, unreadCount } = useChatUnread();
33
33
  const { mutate } = useSWRConfig();
34
34
  // RFC-027 PR2 — after a stop/delete/restart dispatch lands, refresh
35
35
  // /api/hub/nodes (lifecycle_state) AND /api/hub/status (sessions).
@@ -210,7 +210,14 @@ export default function NodesPage() {
210
210
  )}
211
211
  <div className="flex items-start gap-3">
212
212
  <div className="relative shrink-0">
213
- <AliasAvatar alias={s.alias} size={36} />
213
+ <span className="relative inline-flex shrink-0">
214
+ <AliasAvatar alias={s.alias} size={36} />
215
+ {unreadCount(s.alias) > 0 && (
216
+ <span aria-label={`${unreadCount(s.alias)} 条未读`} className="absolute -top-1 -right-1.5 min-w-[16px] h-4 px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-4 text-center ring-2 ring-[#161618]">
217
+ {unreadCount(s.alias) > 99 ? '99+' : unreadCount(s.alias)}
218
+ </span>
219
+ )}
220
+ </span>
214
221
  {unread && <span className="absolute -right-1 -top-1 h-3 w-3 rounded-full border-2 border-[#161618] bg-red-500" />}
215
222
  </div>
216
223
  <div className="min-w-0 flex-1">
@@ -292,7 +299,14 @@ export default function NodesPage() {
292
299
  <div className="col-span-2 min-w-0">
293
300
  <div className="flex items-center gap-2 min-w-0">
294
301
  <div className="relative shrink-0">
295
- <AliasAvatar alias={s.alias} size={20} />
302
+ <span className="relative inline-flex shrink-0">
303
+ <AliasAvatar alias={s.alias} size={20} />
304
+ {unreadCount(s.alias) > 0 && (
305
+ <span aria-label={`${unreadCount(s.alias)} 条未读`} className="absolute -top-1.5 -right-2 min-w-[15px] h-[15px] px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-[15px] text-center">
306
+ {unreadCount(s.alias) > 99 ? '99+' : unreadCount(s.alias)}
307
+ </span>
308
+ )}
309
+ </span>
296
310
  {unread && <span className="absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full border border-[#161618] bg-red-500" />}
297
311
  </div>
298
312
  <span className="truncate text-sm font-medium text-white">{s.alias}</span>
package/app/page.tsx CHANGED
@@ -9,6 +9,7 @@ import { LoadingSkeleton } from './components/LoadingSkeleton';
9
9
  import { NodesEmptyState as EmptyState } from './components/EmptyState';
10
10
  import { CommandCenter, useCommandCenter } from './components/CommandCenter';
11
11
  import { useSessions, useHealth, useTasks, useStats } from './lib/hooks';
12
+ import { useChatUnread } from './lib/chat-unread';
12
13
  import { useSSE } from './lib/useSSE';
13
14
  import { useSWRConfig } from 'swr';
14
15
 
@@ -28,6 +29,8 @@ export default function Dashboard() {
28
29
  const { stats } = useStats();
29
30
  const [showTopo, setShowTopo] = useState(typeof window !== 'undefined' && window.innerWidth >= 1024);
30
31
  const cmd = useCommandCenter();
32
+ // R8: one page-level unread computation; counts flow to memo'd cards as props.
33
+ const { unreadCount } = useChatUnread();
31
34
  const [agentFilter, setAgentFilter] = useState<'all' | 'working' | 'idle' | 'offline'>('all');
32
35
  // #84: last node.renamed event — passed to TopoGraph so an open chat
33
36
  // popover follows the rename instead of pointing at a dead alias. `ts`
@@ -232,7 +235,7 @@ export default function Dashboard() {
232
235
  xl breakpoint auto-inherits lg=3 cols; 2xl bumps to 4. */}
233
236
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-3 sm:gap-4">
234
237
  {filtered.map(s => (
235
- <AgentCard key={s.alias} session={s} hasSse={isOnline(s)} sseCount={sseLookup(s) || 0} onChat={cmd.openTab} />
238
+ <AgentCard key={s.alias} session={s} hasSse={isOnline(s)} sseCount={sseLookup(s) || 0} onChat={cmd.openTab} unreadCount={unreadCount(s.alias)} />
236
239
  ))}
237
240
  </div>
238
241
  {filtered.length === 0 && (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.6.3-preview.14",
3
+ "version": "0.6.3-preview.15",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "main": "apps/desktop/electron/main.cjs",
6
6
  "scripts": {
@@ -1,3 +0,0 @@
1
- module.exports=[18622,(a,b,c)=>{b.exports=a.x("next/dist/compiled/next-server/app-page-turbo.runtime.prod.js",()=>require("next/dist/compiled/next-server/app-page-turbo.runtime.prod.js"))},56704,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-async-storage.external.js",()=>require("next/dist/server/app-render/work-async-storage.external.js"))},32319,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-unit-async-storage.external.js",()=>require("next/dist/server/app-render/work-unit-async-storage.external.js"))},24725,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/after-task-async-storage.external.js",()=>require("next/dist/server/app-render/after-task-async-storage.external.js"))},20635,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/action-async-storage.external.js",()=>require("next/dist/server/app-render/action-async-storage.external.js"))},43285,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/dynamic-access-async-storage.external.js",()=>require("next/dist/server/app-render/dynamic-access-async-storage.external.js"))},1514,(a,b,c)=>{"use strict";b.exports=a.r(18622)},91339,(a,b,c)=>{"use strict";b.exports=a.r(1514).vendored["react-ssr"].ReactJsxRuntime},91921,(a,b,c)=>{"use strict";b.exports=a.r(1514).vendored["react-ssr"].React},43663,(a,b,c)=>{"use strict";b.exports=a.r(1514).vendored.contexts.AppRouterContext},14883,(a,b,c)=>{"use strict";b.exports=a.r(1514).vendored["react-ssr"].ReactServerDOMTurbopackClient},77924,(a,b,c)=>{"use strict";b.exports=a.r(1514).vendored.contexts.HooksClientContext},40013,(a,b,c)=>{"use strict";b.exports=a.r(1514).vendored.contexts.ServerInsertedHtml},18945,a=>{"use strict";function b(a){if(!a)return null;let b=Date.parse(a.includes("T")?a:a.replace(" ","T")+"Z");return Number.isFinite(b)?b:null}a.s(["parseHubTime",0,b,"relativeAgo",0,function(a){let c=b(a);if(null===c)return null;let d=Math.floor((Date.now()-c)/1e3);return d<0?"just now":d<60?`${d}s ago`:d<3600?`${Math.floor(d/60)}m ago`:d<86400?`${Math.floor(d/3600)}h ago`:`${Math.floor(d/86400)}d ago`}])},33617,a=>{"use strict";var b=a.i(91339),c=a.i(91921);let d=(0,c.createContext)({networkId:"",setNetworkId:()=>{}});a.s(["NetworkProvider",0,function({children:a}){let[e,f]=(0,c.useState)("");return(0,c.useEffect)(()=>{let a=sessionStorage.getItem("anet_network_id");a&&f(a)},[]),(0,b.jsx)(d.Provider,{value:{networkId:e,setNetworkId:a=>{f(a),sessionStorage.setItem("anet_network_id",a)}},children:a})},"useNetworkId",0,function(){return(0,c.useContext)(d)}])},66905,a=>{"use strict";var b=a.i(91339),c=a.i(91921);let d=new Set(["cyber","light","slack"]);function e(a){if("u"<typeof document)return;let b=document.documentElement;"slack"===a?(b.setAttribute("data-theme","light"),b.setAttribute("data-skin","slack")):(b.setAttribute("data-theme",a),b.removeAttribute("data-skin"))}a.s(["THEMES",0,d,"ThemeProvider",0,function({children:a}){return(0,c.useEffect)(()=>{if("u"<typeof document)return;let a="cyber";try{let b=localStorage.getItem("anet-theme");b&&d.has(b)&&(a=b)}catch{}e(a)},[]),(0,b.jsx)(b.Fragment,{children:a})},"applyTheme",0,e])},52745,a=>{"use strict";var b=a.i(91339),c=a.i(91921),d=a.i(80704);let e=async a=>{let b=await fetch(a);if(401===b.status)throw Error("unauthorized");if(!b.ok)throw Error(`hub ${b.status}`);return b.json()};function f(a){return a>=85?{fill:"#ef4444",track:"rgb(239 68 68 / 0.16)"}:a>=60?{fill:"#f59e0b",track:"rgb(245 158 11 / 0.16)"}:{fill:"#10b981",track:"rgb(16 185 129 / 0.16)"}}function g({pct:a,label:c}){let d=f(a),e=Math.max(0,Math.min(100,a));return(0,b.jsxs)("div",{className:"space-y-0.5",children:[(0,b.jsxs)("div",{className:"flex justify-between text-[10px] text-[var(--fg-muted)] font-mono",children:[(0,b.jsx)("span",{children:c}),(0,b.jsxs)("span",{className:"tabular-nums",children:[Math.round(e),"%"]})]}),(0,b.jsx)("div",{className:"h-1.5 rounded-full overflow-hidden",style:{background:d.track},children:(0,b.jsx)("div",{className:"h-full transition-[width] duration-300 ease-out",style:{width:`${e}%`,background:d.fill}})})]})}function h({values:a,tint:c,label:d}){if(!a||a.length<2)return null;let e=100/Math.max(1,a.length-1),f=a.map((a,b)=>{let c=18-Math.max(0,Math.min(100,a))/100*18;return`${(b*e).toFixed(1)},${c.toFixed(1)}`}),g=f.join(" "),i=`M0,18 L${f.join(" L")} L100,18 Z`;return(0,b.jsxs)("svg",{width:"100%",height:18,viewBox:"0 0 100 18",preserveAspectRatio:"none","aria-label":`${d} 5-min history`,"data-server-sparkline":d,children:[(0,b.jsx)("path",{d:i,fill:c,opacity:"0.18"}),(0,b.jsx)("polyline",{points:g,fill:"none",stroke:c,strokeWidth:"1.2"})]})}function i({cpu:a,mem:c,disk:d}){let e=[a,c,d].filter(a=>"number"==typeof a);if(0===e.length)return null;let g=Math.max(...e),h=f(g);return(0,b.jsx)("span",{className:"inline-block w-2 h-2 rounded-full shrink-0",style:{background:h.fill,boxShadow:`0 0 0 2px ${h.track}`},"data-server-health-badge":g>=85?"red":g>=60?"amber":"green",title:`worst: ${Math.round(g)}%`})}function j({agents:a}){return a&&0!==a.length?(0,b.jsx)("ul",{className:"space-y-1","data-server-agent-list":!0,children:a.map(a=>{let c="working"===a.status?"#10b981":"idle"===a.status?"#06b6d4":"#6b7280";return(0,b.jsxs)("li",{className:"flex items-center gap-1.5 text-[10px] font-mono","data-server-agent":a.alias,children:[(0,b.jsx)("span",{className:"w-1.5 h-1.5 rounded-full shrink-0",style:{background:c}}),(0,b.jsx)("span",{className:"font-semibold truncate",style:{color:"var(--fg)"},children:a.alias}),a.runtime&&(0,b.jsxs)("span",{className:"text-[9px] text-[var(--fg-dim)] shrink-0",children:["· ",a.runtime]}),"number"==typeof a.progress&&a.progress>0&&(0,b.jsxs)("span",{className:"ml-auto text-[9px] tabular-nums text-[var(--fg-muted)]",children:[Math.round(100*a.progress),"%"]})]},a.alias)})}):(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-dim)] font-mono italic px-1 py-1","data-server-agents-missing":"true",children:"agent rollup not reported by hub"})}function k(a=!0){let{data:b,error:c}=(0,d.default)(a?"/api/hub/servers":null,e,{refreshInterval:5e3,dedupingInterval:3e3}),f=b?.servers??[],g=b?.unavailable===!0,h=f.filter(a=>"online"===a.status).length,i=a&&!b&&!c;return{data:b,error:c,servers:f,unavailable:g,onlineCount:h,loading:i}}function l({enabled:a=!0,className:d=""}){let[e,f]=(0,c.useState)(new Set);(0,c.useEffect)(()=>{try{let a=localStorage.getItem("anet-servers-drawer-expanded");a&&f(new Set(JSON.parse(a)))}catch{}},[]);let{data:m,error:n,servers:o,unavailable:p,loading:q}=k(a);return(0,b.jsxs)("div",{className:`min-h-0 overflow-y-auto px-2 py-2 space-y-2 ${d}`,"data-servers-body":!0,children:[q&&(0,b.jsx)("div",{className:"text-[10px] text-[var(--fg-muted)] font-mono text-center py-4",children:"loading servers…"}),n&&!m&&(0,b.jsx)("div",{className:"rounded-md border px-2.5 py-2 text-[10px] font-mono",style:{background:"rgb(239 68 68 / 0.06)",borderColor:"rgb(239 68 68 / 0.25)",color:"#ef4444"},children:"hub unreachable · retrying every 5s"}),p&&(0,b.jsxs)("div",{className:"text-[10px] text-[var(--fg-muted)] font-mono text-center py-3 leading-relaxed",children:["host telemetry not available",(0,b.jsx)("br",{}),(0,b.jsx)("span",{className:"text-[var(--fg-dim)]",children:"upgrade commhub-server ≥ 0.8.1-preview.2"})]}),!q&&!n&&!p&&0===o.length&&(0,b.jsx)("div",{className:"text-[10px] text-[var(--fg-muted)] font-mono text-center py-4",children:"no servers reporting yet"}),o.map(a=>{let c="offline"===a.status,d=null!=a.cpu_load_1min&&a.cpu_cores>0?a.cpu_load_1min/a.cpu_cores*100:null,k=null!=a.mem_used_gb&&null!=a.mem_total_gb&&a.mem_total_gb>0?a.mem_used_gb/a.mem_total_gb*100:null,l=null!=a.disk_used_gb&&null!=a.disk_total_gb&&a.disk_total_gb>0?a.disk_used_gb/a.disk_total_gb*100:null,m=e.has(a.hostname);return(0,b.jsxs)("div",{className:"rounded-lg border px-2.5 py-2 space-y-1.5",style:{background:"var(--bg)",borderColor:"var(--border)",opacity:c?.55:1},"data-server-card":a.hostname,"data-server-expanded":m?"true":"false",title:a.ip?`${a.hostname} \xb7 ${a.ip}`:a.hostname,children:[(0,b.jsxs)("button",{type:"button",onClick:()=>{let b;return b=a.hostname,f(a=>{let c=new Set(a);c.has(b)?c.delete(b):c.add(b);try{localStorage.setItem("anet-servers-drawer-expanded",JSON.stringify([...c]))}catch{}return c})},"aria-expanded":m,className:"w-full flex items-center justify-between gap-2 text-left","data-server-card-toggle":a.hostname,children:[(0,b.jsxs)("div",{className:"min-w-0 flex items-center gap-1.5",children:[!c&&(0,b.jsx)(i,{cpu:d,mem:k,disk:l}),(0,b.jsx)("span",{className:"font-mono text-[12px] font-semibold truncate",style:{color:"var(--fg)"},children:a.hostname}),a.note&&(0,b.jsxs)("span",{className:"text-[9px] text-[var(--fg-dim)]",children:["(",a.note,")"]})]}),(0,b.jsxs)("span",{className:"flex items-center gap-1 shrink-0",children:[(0,b.jsxs)("span",{className:"text-[10px] text-[var(--fg-muted)] font-mono tabular-nums",children:[a.agent_count," agent",1===a.agent_count?"":"s"]}),(0,b.jsx)("svg",{width:"10",height:"10",viewBox:"0 0 10 10",style:{transform:m?"rotate(90deg)":"rotate(0deg)",transition:"transform 150ms ease-out"},"aria-hidden":!0,children:(0,b.jsx)("path",{d:"M3 1.5L7 5L3 8.5",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})]})]}),!c&&null!=d&&(0,b.jsx)(g,{pct:d,label:`CPU ${a.cpu_load_1min.toFixed(2)}/${a.cpu_cores}`}),!c&&null!=k&&(0,b.jsx)(g,{pct:k,label:`RAM ${a.mem_used_gb.toFixed(1)}/${a.mem_total_gb.toFixed(1)}G`}),c&&(0,b.jsx)("div",{className:"text-[10px] text-[var(--fg-dim)] font-mono italic",children:"CPU n/a · RAM n/a · offline"}),m&&!c&&(0,b.jsxs)("div",{className:"pt-1 mt-1 border-t space-y-1.5",style:{borderColor:"var(--border)"},children:[null!=l?(0,b.jsx)(g,{pct:l,label:`DISK ${a.disk_used_gb.toFixed(1)}/${a.disk_total_gb.toFixed(1)}G`}):(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-dim)] font-mono italic","data-server-disk-missing":"true",children:"disk metric not reported by hub"}),a.cpu_history&&a.cpu_history.length>=2&&(0,b.jsxs)("div",{className:"space-y-0.5",children:[(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-muted)] font-mono",children:"CPU · 5-min"}),(0,b.jsx)(h,{values:a.cpu_history,tint:"#10b981",label:"CPU"})]}),a.mem_history&&a.mem_history.length>=2&&(0,b.jsxs)("div",{className:"space-y-0.5",children:[(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-muted)] font-mono",children:"RAM · 5-min"}),(0,b.jsx)(h,{values:a.mem_history,tint:"#06b6d4",label:"MEM"})]}),a.disk_history&&a.disk_history.length>=2&&(0,b.jsxs)("div",{className:"space-y-0.5",children:[(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-muted)] font-mono",children:"DISK · 5-min"}),(0,b.jsx)(h,{values:a.disk_history,tint:"#f59e0b",label:"DISK"})]}),(0,b.jsxs)("div",{className:"pt-1",children:[(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-muted)] font-mono mb-0.5",children:"agents"}),(0,b.jsx)(j,{agents:a.agents})]})]})]},a.hostname)}),o.length>0&&(0,b.jsx)("div",{className:"pt-1 text-[9px] text-[var(--fg-dim)] font-mono text-center",children:"live · refreshing every 5s"})]})}a.s(["ServersDrawer",0,function(){let[a,d]=(0,c.useState)(!1);(0,c.useEffect)(()=>{try{"1"===localStorage.getItem("anet-servers-drawer")&&d(!0)}catch{}},[]);let{servers:e,onlineCount:f}=k(a);return(0,b.jsxs)("aside",{className:"fixed right-0 top-20 z-40 hidden lg:flex flex-col rounded-l-xl border border-r-0 shadow-xl shadow-black/30 anet-fade-in",style:{background:"var(--bg-secondary)",borderColor:"var(--border)",width:a?296:40,maxHeight:"calc(100vh - 6rem)",transition:"width 200ms ease-out"},"aria-label":"Servers panel",children:[(0,b.jsxs)("button",{onClick:()=>d(a=>{let b=!a;try{localStorage.setItem("anet-servers-drawer",b?"1":"0")}catch{}return b}),"aria-expanded":a,"aria-label":a?"Collapse servers panel":"Expand servers panel",className:"flex items-center gap-2 px-2.5 py-2 border-b text-[11px] font-semibold tracking-wide select-none",style:{color:"var(--fg)",borderColor:"var(--border)"},children:[(0,b.jsxs)("svg",{className:"w-4 h-4 shrink-0",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.8",strokeLinecap:"round",strokeLinejoin:"round",children:[(0,b.jsx)("rect",{x:"3",y:"4",width:"18",height:"6",rx:"1.5"}),(0,b.jsx)("rect",{x:"3",y:"14",width:"18",height:"6",rx:"1.5"}),(0,b.jsx)("circle",{cx:"7",cy:"7",r:"0.7",fill:"currentColor"}),(0,b.jsx)("circle",{cx:"7",cy:"17",r:"0.7",fill:"currentColor"})]}),a&&(0,b.jsxs)(b.Fragment,{children:[(0,b.jsx)("span",{className:"flex-1 text-left",children:"Servers"}),(0,b.jsxs)("span",{className:"text-[10px] text-[var(--fg-muted)] font-mono",children:[f,"/",e.length]})]})]}),a&&(0,b.jsx)(l,{enabled:a,className:"flex-1"})]})},"ServersPanel",0,l])}];
2
-
3
- //# sourceMappingURL=%5Broot-of-the-server%5D__0gv51is._.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../dash-loop-wt/node_modules/next/src/server/route-modules/app-page/module.compiled.js","../../../../../dash-loop-wt/node_modules/next/src/server/route-modules/app-page/vendored/ssr/react-jsx-runtime.ts","../../../../../dash-loop-wt/node_modules/next/src/server/route-modules/app-page/vendored/ssr/react.ts","../../../../../dash-loop-wt/node_modules/next/src/server/route-modules/app-page/vendored/contexts/app-router-context.ts","../../../../../dash-loop-wt/node_modules/next/src/server/route-modules/app-page/vendored/ssr/react-server-dom-turbopack-client.ts","../../../../../dash-loop-wt/node_modules/next/src/server/route-modules/app-page/vendored/contexts/hooks-client-context.ts","../../../../../dash-loop-wt/node_modules/next/src/server/route-modules/app-page/vendored/contexts/server-inserted-html.ts","../../../../../dash-loop-wt/app/lib/time.ts","../../../../../dash-loop-wt/app/lib/network-context.tsx","../../../../../dash-loop-wt/app/components/ThemeSwitcher.tsx","../../../../../dash-loop-wt/app/components/ServersDrawer.tsx"],"sourcesContent":["if (process.env.NEXT_RUNTIME === 'edge') {\n module.exports = require('next/dist/server/route-modules/app-page/module.js')\n} else {\n if (process.env.__NEXT_EXPERIMENTAL_REACT) {\n if (process.env.NODE_ENV === 'development') {\n if (process.env.TURBOPACK) {\n module.exports = require('next/dist/compiled/next-server/app-page-turbo-experimental.runtime.dev.js')\n } else {\n module.exports = require('next/dist/compiled/next-server/app-page-experimental.runtime.dev.js')\n }\n } else {\n if (process.env.TURBOPACK) {\n module.exports = require('next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js')\n } else {\n module.exports = require('next/dist/compiled/next-server/app-page-experimental.runtime.prod.js')\n }\n }\n } else {\n if (process.env.NODE_ENV === 'development') {\n if (process.env.TURBOPACK) {\n module.exports = require('next/dist/compiled/next-server/app-page-turbo.runtime.dev.js')\n } else {\n module.exports = require('next/dist/compiled/next-server/app-page.runtime.dev.js')\n }\n } else {\n if (process.env.TURBOPACK) {\n module.exports = require('next/dist/compiled/next-server/app-page-turbo.runtime.prod.js')\n } else {\n module.exports = require('next/dist/compiled/next-server/app-page.runtime.prod.js')\n }\n }\n }\n}\n","module.exports = (\n require('../../module.compiled') as typeof import('../../module.compiled')\n).vendored['react-ssr']!.ReactJsxRuntime\n","module.exports = (\n require('../../module.compiled') as typeof import('../../module.compiled')\n).vendored['react-ssr']!.React\n","module.exports = (\n require('../../module.compiled') as typeof import('../../module.compiled')\n).vendored['contexts'].AppRouterContext\n","module.exports = (\n require('../../module.compiled') as typeof import('../../module.compiled')\n).vendored['react-ssr']!.ReactServerDOMTurbopackClient\n","module.exports = (\n require('../../module.compiled') as typeof import('../../module.compiled')\n).vendored['contexts'].HooksClientContext\n","module.exports = (\n require('../../module.compiled') as typeof import('../../module.compiled')\n).vendored['contexts'].ServerInsertedHtml\n","/** Shared timestamp helpers for hub-sourced data.\n *\n * Background — the CommHub serialises timestamps SQL-style without a\n * zone (\"2026-05-15 06:00:28\"), but some test data uses full ISO\n * (\"…T06:00:28Z\"). `Date.parse` on bare SQL is **local-time** in every\n * browser the dashboard supports, so a UTC+8 operator silently sees\n * every timestamp 8 h older than reality (Round 35 dug this out via\n * the `isGhost` filter mis-fire).\n *\n * Round 38 / Loop consolidates parsing — TopoGraph (R35), ChatPopover\n * (R37) and utils.ts::timeAgo all mirrored the same SQL→UTC rewrite;\n * one source of truth here. */\n\n/** Parse a hub timestamp to ms-since-epoch, treating SQL-style strings\n * as UTC. ISO strings (with `T`) bypass the rewrite and are parsed as-is.\n * Returns null for empty / unparseable input. */\nexport function parseHubTime(dateStr: string | null | undefined): number | null {\n if (!dateStr) return null;\n const iso = dateStr.includes('T') ? dateStr : dateStr.replace(' ', 'T') + 'Z';\n const t = Date.parse(iso);\n return Number.isFinite(t) ? t : null;\n}\n\n/** Format `dateStr` as a short relative-time string (\"6m ago\" / \"2h ago\").\n * Future timestamps (clock skew) collapse to \"just now\". Returns null when\n * the input doesn't parse — callers can decide on a fallback. */\nexport function relativeAgo(dateStr: string | null | undefined): string | null {\n const t = parseHubTime(dateStr);\n if (t === null) return null;\n const s = Math.floor((Date.now() - t) / 1000);\n if (s < 0) return 'just now';\n if (s < 60) return `${s}s ago`;\n if (s < 3600) return `${Math.floor(s / 60)}m ago`;\n if (s < 86400) return `${Math.floor(s / 3600)}h ago`;\n return `${Math.floor(s / 86400)}d ago`;\n}\n","'use client';\n\nimport { createContext, useContext, useState, useEffect, ReactNode } from 'react';\n\ninterface NetworkContextType {\n networkId: string;\n setNetworkId: (id: string) => void;\n}\n\nconst NetworkContext = createContext<NetworkContextType>({ networkId: '', setNetworkId: () => {} });\n\nexport function NetworkProvider({ children }: { children: ReactNode }) {\n const [networkId, setNetworkId] = useState('');\n\n useEffect(() => {\n const saved = sessionStorage.getItem('anet_network_id');\n if (saved) setNetworkId(saved);\n }, []);\n\n const setAndPersist = (id: string) => {\n setNetworkId(id);\n sessionStorage.setItem('anet_network_id', id);\n };\n\n return (\n <NetworkContext.Provider value={{ networkId, setNetworkId: setAndPersist }}>\n {children}\n </NetworkContext.Provider>\n );\n}\n\nexport function useNetworkId() {\n return useContext(NetworkContext);\n}\n","'use client';\n\nimport { useEffect } from 'react';\n\n// #217 S6 — verified themes: cyber (dark), light (white), and slack\n// (Vincent 2026-07-16 via api channel: \"其他的就搞成那种 Slack 方式吧…快速搞一个\"\n// — Slack-style is now the DEFAULT look; the network/topology page keeps its\n// showcase visuals, see TopoGraph's data-skin exemption).\n//\n// Mechanism: `slack` is implemented as a SKIN on top of the light theme —\n// applyTheme sets `data-theme=\"light\"` (so every existing light compat-shim\n// in globals.css applies unchanged) plus `data-skin=\"slack\"` which the\n// slack-only overrides key on (aubergine rail, Slack-blue accent, de-noise).\n// This keeps the fast first version to globals.css + three tiny component\n// touches instead of a full hex→token sweep (that sweep stays scheduled as\n// redesign Phase 0 next version).\n\nexport const THEMES = new Set(['cyber', 'light', 'slack']);\n\n/** Apply a theme choice to <html>. Single owner of the data-theme/data-skin\n * contract — Settings and Cmd+K call this too, so the skin attribute can\n * never desync from the theme attribute. */\nexport function applyTheme(t: string) {\n if (typeof document === 'undefined') return;\n const el = document.documentElement;\n if (t === 'slack') {\n el.setAttribute('data-theme', 'light');\n el.setAttribute('data-skin', 'slack');\n } else {\n el.setAttribute('data-theme', t);\n el.removeAttribute('data-skin');\n }\n}\n\nexport function ThemeProvider({ children }: { children: React.ReactNode }) {\n useEffect(() => {\n if (typeof document === 'undefined') return;\n // ROLLBACK (Vincent 07-20): the classic dark theme is the default again\n // — he prefers the original look and doesn't want a Slack copy as the\n // face of the product. 'slack' stays available as an explicit Settings\n // choice until the original agent-network theme replaces it. Users who\n // explicitly picked a theme keep their pick; everyone else returns to\n // cyber automatically (the slack-default window never wrote storage).\n let t = 'cyber';\n try {\n const saved = localStorage.getItem('anet-theme');\n if (saved && THEMES.has(saved)) t = saved;\n } catch {}\n applyTheme(t);\n }, []);\n return <>{children}</>;\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\nimport useSWR from 'swr';\n\n/** Issue #119 — server-side panel listing the hub's hosts + CPU/RAM bars.\n *\n * Steps 1+2 (SDK马 host telemetry + CommHub aggregation) shipped with\n * commhub-server v0.8.1-preview.2. Step 3 (this file, Round 20 of the\n * dashboard /loop) swaps the original mock seed for a SWR fetch of\n * `/api/hub/servers` (which proxies to the hub's `/api/servers`).\n *\n * Collapsed by default to a thin vertical icon strip so it never competes\n * with the topology graph for horizontal real-estate. State persists to\n * localStorage just like the other dashboard drawers.\n *\n * Backward-compat: if the upstream hub predates v0.8.1-preview.2 the\n * proxy returns `{ servers: [], unavailable: true }`. The drawer\n * surfaces a friendly \"not yet available\" hint instead of an empty list,\n * so old hubs don't read as \"something broke\".\n */\n\ninterface ServerAgent {\n alias: string;\n runtime: string | null;\n status: 'working' | 'idle' | 'offline';\n last_seen_at?: string | null;\n progress?: number | null; // 0..1\n}\n\ninterface ServerCard {\n hostname: string;\n ip?: string | null;\n cpu_load_1min: number | null;\n cpu_cores: number;\n mem_used_gb: number | null;\n mem_total_gb: number | null;\n /** Hero 1 v0.10.0: disk usage. Optional — older hubs don't report. */\n disk_used_gb?: number | null;\n disk_total_gb?: number | null;\n /** Hero 1 v0.10.0: short history for sparklines (5-min sliding window).\n * Each is the rolling 1-min average sampled at ~30s cadence (so ~10\n * data points for 5-min). Optional — older hubs don't report. */\n cpu_history?: number[];\n mem_history?: number[];\n /** v0.10.2 RFC-014 — disk usage 5-min rolling history. Optional;\n * older agent-node (< 2.4.1-preview.0) doesn't ship it. */\n disk_history?: number[];\n /** Hero 2 v0.10.0: per-server agent rollup. Optional — older hubs\n * send only aggregate agent_count. */\n agents?: ServerAgent[];\n agent_count: number;\n status: 'online' | 'offline';\n note?: string;\n}\n\ninterface ServersResponse {\n servers: ServerCard[];\n unavailable?: boolean;\n}\n\nconst fetcher = async (url: string): Promise<ServersResponse> => {\n const res = await fetch(url);\n if (res.status === 401) {\n if (typeof window !== 'undefined') window.location.assign('/login');\n throw new Error('unauthorized');\n }\n if (!res.ok) throw new Error(`hub ${res.status}`);\n return res.json();\n};\n\n// Bar tint by usage — green ≤60%, amber 60–85%, red >85%. Keeps the eye\n// going to whichever box is actually about to tip over.\nfunction barTint(pct: number) {\n if (pct >= 85) return { fill: '#ef4444', track: 'rgb(239 68 68 / 0.16)' };\n if (pct >= 60) return { fill: '#f59e0b', track: 'rgb(245 158 11 / 0.16)' };\n return { fill: '#10b981', track: 'rgb(16 185 129 / 0.16)' };\n}\n\nfunction Bar({ pct, label }: { pct: number; label: string }) {\n const t = barTint(pct);\n const clamped = Math.max(0, Math.min(100, pct));\n return (\n <div className=\"space-y-0.5\">\n <div className=\"flex justify-between text-[10px] text-[var(--fg-muted)] font-mono\">\n <span>{label}</span>\n <span className=\"tabular-nums\">{Math.round(clamped)}%</span>\n </div>\n <div className=\"h-1.5 rounded-full overflow-hidden\" style={{ background: t.track }}>\n <div\n className=\"h-full transition-[width] duration-300 ease-out\"\n style={{ width: `${clamped}%`, background: t.fill }}\n />\n </div>\n </div>\n );\n}\n\n/** Hero 1 v0.10.0 — inline SVG sparkline for 5-min rolling history.\n * Zero-dep (no recharts). Renders a stroked polyline + filled area.\n * Values are percentages (0..100); clamps inside. */\nfunction Sparkline({ values, tint, label }: { values: number[]; tint: string; label: string }) {\n if (!values || values.length < 2) return null;\n const w = 100, h = 18;\n const max = 100;\n const stride = w / Math.max(1, values.length - 1);\n const pts = values.map((v, i) => {\n const x = i * stride;\n const y = h - (Math.max(0, Math.min(max, v)) / max) * h;\n return `${x.toFixed(1)},${y.toFixed(1)}`;\n });\n const polyline = pts.join(' ');\n const area = `M0,${h} L${pts.join(' L')} L${w},${h} Z`;\n return (\n <svg\n width=\"100%\" height={h} viewBox={`0 0 ${w} ${h}`}\n preserveAspectRatio=\"none\" aria-label={`${label} 5-min history`}\n data-server-sparkline={label}\n >\n <path d={area} fill={tint} opacity=\"0.18\" />\n <polyline points={polyline} fill=\"none\" stroke={tint} strokeWidth=\"1.2\" />\n </svg>\n );\n}\n\n/** Hero 1 v0.10.0 — single health badge derived from the worst of\n * CPU / Mem / Disk percentages. Green ≤60%, amber 60-85%, red >85%\n * (same thresholds as Bar). Returns null when no percentages available\n * (e.g. offline server) so the card layout reads \"n/a · offline\" instead. */\nfunction HealthBadge({ cpu, mem, disk }: { cpu: number | null; mem: number | null; disk: number | null }) {\n const vals = [cpu, mem, disk].filter((v): v is number => typeof v === 'number');\n if (vals.length === 0) return null;\n const worst = Math.max(...vals);\n const t = barTint(worst);\n const status = worst >= 85 ? 'red' : worst >= 60 ? 'amber' : 'green';\n return (\n <span\n className=\"inline-block w-2 h-2 rounded-full shrink-0\"\n style={{ background: t.fill, boxShadow: `0 0 0 2px ${t.track}` }}\n data-server-health-badge={status}\n title={`worst: ${Math.round(worst)}%`}\n />\n );\n}\n\n/** Hero 2 v0.10.0 — per-server expandable agent list. Renders compact\n * rows: alias · runtime · status chip · last_seen relative. progress\n * bar appears when an agent reports progress (0..1). When the upstream\n * hub doesn't report agents[], the row falls through to a small\n * \"agent list pending hub upgrade\" hint. */\nfunction AgentList({ agents }: { agents?: ServerAgent[] }) {\n if (!agents || agents.length === 0) {\n return (\n <div className=\"text-[9px] text-[var(--fg-dim)] font-mono italic px-1 py-1\" data-server-agents-missing=\"true\">\n {/* #157 fix — copy update. Pre-#157 the placeholder read\n \"agent rollup pending hub ≥ 0.8.2-preview\". commhub-server@\n 0.8.2 is LIVE in prod (Vincent screenshot 5560 verified) but\n still doesn't ship `agents[]`. Version-specific text was\n misleading — implied upgrade-needed when hub already\n crossed the threshold. New copy drops the version pinning\n and just states the data-shape: hub hasn't reported the\n agent rollup for this server (could be hub-side feature\n gap or session-source gap). data-server-agents-missing\n attr surfaces the gate for tests. */}\n agent rollup not reported by hub\n </div>\n );\n }\n return (\n <ul className=\"space-y-1\" data-server-agent-list>\n {agents.map(a => {\n const statusColor =\n a.status === 'working' ? '#10b981' :\n a.status === 'idle' ? '#06b6d4' : '#6b7280';\n return (\n <li\n key={a.alias}\n className=\"flex items-center gap-1.5 text-[10px] font-mono\"\n data-server-agent={a.alias}\n >\n <span className=\"w-1.5 h-1.5 rounded-full shrink-0\" style={{ background: statusColor }} />\n <span className=\"font-semibold truncate\" style={{ color: 'var(--fg)' }}>{a.alias}</span>\n {a.runtime && (\n <span className=\"text-[9px] text-[var(--fg-dim)] shrink-0\">· {a.runtime}</span>\n )}\n {typeof a.progress === 'number' && a.progress > 0 && (\n <span className=\"ml-auto text-[9px] tabular-nums text-[var(--fg-muted)]\">\n {Math.round(a.progress * 100)}%\n </span>\n )}\n </li>\n );\n })}\n </ul>\n );\n}\n\nfunction useServers(enabled = true) {\n const { data, error } = useSWR<ServersResponse>(\n enabled ? '/api/hub/servers' : null,\n fetcher,\n { refreshInterval: 5000, dedupingInterval: 3000 },\n );\n const servers = data?.servers ?? [];\n const unavailable = data?.unavailable === true;\n const onlineCount = servers.filter(s => s.status === 'online').length;\n const loading = enabled && !data && !error;\n return { data, error, servers, unavailable, onlineCount, loading };\n}\n\nexport function ServersPanel({ enabled = true, className = '' }: { enabled?: boolean; className?: string }) {\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n useEffect(() => {\n try {\n const raw = localStorage.getItem('anet-servers-drawer-expanded');\n if (raw) setExpanded(new Set(JSON.parse(raw)));\n } catch {}\n }, []);\n const toggleExpanded = (hostname: string) => setExpanded(prev => {\n const next = new Set(prev);\n if (next.has(hostname)) next.delete(hostname); else next.add(hostname);\n try { localStorage.setItem('anet-servers-drawer-expanded', JSON.stringify([...next])); } catch {}\n return next;\n });\n\n const { data, error, servers, unavailable, loading } = useServers(enabled);\n\n return (\n <div className={`min-h-0 overflow-y-auto px-2 py-2 space-y-2 ${className}`} data-servers-body>\n {loading && (\n <div className=\"text-[10px] text-[var(--fg-muted)] font-mono text-center py-4\">\n loading servers…\n </div>\n )}\n {error && !data && (\n <div className=\"rounded-md border px-2.5 py-2 text-[10px] font-mono\"\n style={{ background: 'rgb(239 68 68 / 0.06)', borderColor: 'rgb(239 68 68 / 0.25)', color: '#ef4444' }}>\n hub unreachable · retrying every 5s\n </div>\n )}\n {unavailable && (\n <div className=\"text-[10px] text-[var(--fg-muted)] font-mono text-center py-3 leading-relaxed\">\n host telemetry not available<br/>\n <span className=\"text-[var(--fg-dim)]\">upgrade commhub-server ≥ 0.8.1-preview.2</span>\n </div>\n )}\n {!loading && !error && !unavailable && servers.length === 0 && (\n <div className=\"text-[10px] text-[var(--fg-muted)] font-mono text-center py-4\">\n no servers reporting yet\n </div>\n )}\n {servers.map(s => {\n const offline = s.status === 'offline';\n const cpuPct = s.cpu_load_1min != null && s.cpu_cores > 0 ? (s.cpu_load_1min / s.cpu_cores) * 100 : null;\n const memPct = s.mem_used_gb != null && s.mem_total_gb != null && s.mem_total_gb > 0 ? (s.mem_used_gb / s.mem_total_gb) * 100 : null;\n const diskPct = s.disk_used_gb != null && s.disk_total_gb != null && s.disk_total_gb > 0 ? (s.disk_used_gb / s.disk_total_gb) * 100 : null;\n const isExpanded = expanded.has(s.hostname);\n return (\n <div\n key={s.hostname}\n className=\"rounded-lg border px-2.5 py-2 space-y-1.5\"\n style={{\n background: 'var(--bg)',\n borderColor: 'var(--border)',\n opacity: offline ? 0.55 : 1,\n }}\n data-server-card={s.hostname}\n data-server-expanded={isExpanded ? 'true' : 'false'}\n title={s.ip ? `${s.hostname} · ${s.ip}` : s.hostname}\n >\n {/* v0.10.0 Hero 1+2: header row — click toggles expanded\n detail view (sparklines + disk + agent rollup). */}\n <button\n type=\"button\"\n onClick={() => toggleExpanded(s.hostname)}\n aria-expanded={isExpanded}\n className=\"w-full flex items-center justify-between gap-2 text-left\"\n data-server-card-toggle={s.hostname}\n >\n <div className=\"min-w-0 flex items-center gap-1.5\">\n {/* Hero 1 health badge — worst-of CPU/Mem/Disk */}\n {!offline && <HealthBadge cpu={cpuPct} mem={memPct} disk={diskPct} />}\n <span className=\"font-mono text-[12px] font-semibold truncate\" style={{ color: 'var(--fg)' }}>{s.hostname}</span>\n {s.note && <span className=\"text-[9px] text-[var(--fg-dim)]\">({s.note})</span>}\n </div>\n <span className=\"flex items-center gap-1 shrink-0\">\n <span className=\"text-[10px] text-[var(--fg-muted)] font-mono tabular-nums\">\n {s.agent_count}&nbsp;agent{s.agent_count === 1 ? '' : 's'}\n </span>\n {/* Chevron — rotates 90° on expanded */}\n <svg\n width=\"10\" height=\"10\" viewBox=\"0 0 10 10\"\n style={{ transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 150ms ease-out' }}\n aria-hidden\n >\n <path d=\"M3 1.5L7 5L3 8.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </span>\n </button>\n {!offline && cpuPct != null && (\n <Bar pct={cpuPct} label={`CPU ${s.cpu_load_1min!.toFixed(2)}/${s.cpu_cores}`} />\n )}\n {!offline && memPct != null && (\n <Bar pct={memPct} label={`RAM ${s.mem_used_gb!.toFixed(1)}/${s.mem_total_gb!.toFixed(1)}G`} />\n )}\n {offline && (\n <div className=\"text-[10px] text-[var(--fg-dim)] font-mono italic\">CPU n/a · RAM n/a · offline</div>\n )}\n {/* v0.10.0 Hero 1+2: expanded detail — disk bar +\n 5-min sparklines + agent rollup. Visible only when\n the user clicks the card. Renders gracefully when\n upstream hub hasn't shipped optional fields. */}\n {isExpanded && !offline && (\n <div className=\"pt-1 mt-1 border-t space-y-1.5\" style={{ borderColor: 'var(--border)' }}>\n {diskPct != null ? (\n <Bar pct={diskPct} label={`DISK ${s.disk_used_gb!.toFixed(1)}/${s.disk_total_gb!.toFixed(1)}G`} />\n ) : (\n /* #157 sibling fix — same misleading version-pin copy\n dropped at the disk-metric placeholder. Same\n rationale as the agent-rollup copy above. */\n <div className=\"text-[9px] text-[var(--fg-dim)] font-mono italic\" data-server-disk-missing=\"true\">disk metric not reported by hub</div>\n )}\n {s.cpu_history && s.cpu_history.length >= 2 && (\n <div className=\"space-y-0.5\">\n <div className=\"text-[9px] text-[var(--fg-muted)] font-mono\">CPU · 5-min</div>\n <Sparkline values={s.cpu_history} tint=\"#10b981\" label=\"CPU\" />\n </div>\n )}\n {s.mem_history && s.mem_history.length >= 2 && (\n <div className=\"space-y-0.5\">\n <div className=\"text-[9px] text-[var(--fg-muted)] font-mono\">RAM · 5-min</div>\n <Sparkline values={s.mem_history} tint=\"#06b6d4\" label=\"MEM\" />\n </div>\n )}\n {/* v0.10.2 RFC-014 §7 close gate #3 — disk usage\n 5-min curve. Amber tint matches the disk bar\n tier convention (DISK > CPU/Mem in alert-\n priority hierarchy since disk-full is a hard\n failure mode). Render only when agent-node\n 2.4.1-preview.0+ has shipped disk_history;\n backward-compat handles older agents silently\n (no sparkline, no broken state). */}\n {s.disk_history && s.disk_history.length >= 2 && (\n <div className=\"space-y-0.5\">\n <div className=\"text-[9px] text-[var(--fg-muted)] font-mono\">DISK · 5-min</div>\n <Sparkline values={s.disk_history} tint=\"#f59e0b\" label=\"DISK\" />\n </div>\n )}\n <div className=\"pt-1\">\n <div className=\"text-[9px] text-[var(--fg-muted)] font-mono mb-0.5\">agents</div>\n <AgentList agents={s.agents} />\n </div>\n </div>\n )}\n </div>\n );\n })}\n {servers.length > 0 && (\n <div className=\"pt-1 text-[9px] text-[var(--fg-dim)] font-mono text-center\">\n live · refreshing every 5s\n </div>\n )}\n </div>\n );\n}\n\nexport function ServersDrawer() {\n const [open, setOpen] = useState(false);\n // Drawer state is per-user-machine — persist like the other dashboard\n // sticky toggles (`anet-topo-layout`, `anet-topo-view`, etc.).\n useEffect(() => {\n try { if (localStorage.getItem('anet-servers-drawer') === '1') setOpen(true); } catch {}\n }, []);\n const toggle = () => setOpen(prev => {\n const next = !prev;\n try { localStorage.setItem('anet-servers-drawer', next ? '1' : '0'); } catch {}\n return next;\n });\n\n // Round 20 / #119 step 3 final delivery — real SWR fetch. 5s refresh\n // matches the other live drawers (HealthBanner, Sidebar) so an operator\n // sees host telemetry update in roughly the same beat as session state.\n // Only poll while expanded — collapsed icon strip doesn't need fresh data.\n const { servers, onlineCount } = useServers(open);\n\n return (\n <aside\n className=\"fixed right-0 top-20 z-40 hidden lg:flex flex-col rounded-l-xl border border-r-0 shadow-xl shadow-black/30 anet-fade-in\"\n style={{\n background: 'var(--bg-secondary)',\n borderColor: 'var(--border)',\n width: open ? 296 : 40,\n maxHeight: 'calc(100vh - 6rem)',\n transition: 'width 200ms ease-out',\n }}\n aria-label=\"Servers panel\"\n >\n <button\n onClick={toggle}\n aria-expanded={open}\n aria-label={open ? 'Collapse servers panel' : 'Expand servers panel'}\n className=\"flex items-center gap-2 px-2.5 py-2 border-b text-[11px] font-semibold tracking-wide select-none\"\n style={{ color: 'var(--fg)', borderColor: 'var(--border)' }}\n >\n <svg className=\"w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"6\" rx=\"1.5\" />\n <rect x=\"3\" y=\"14\" width=\"18\" height=\"6\" rx=\"1.5\" />\n <circle cx=\"7\" cy=\"7\" r=\"0.7\" fill=\"currentColor\" />\n <circle cx=\"7\" cy=\"17\" r=\"0.7\" fill=\"currentColor\" />\n </svg>\n {open && (\n <>\n <span className=\"flex-1 text-left\">Servers</span>\n <span className=\"text-[10px] text-[var(--fg-muted)] font-mono\">{onlineCount}/{servers.length}</span>\n </>\n )}\n </button>\n\n {open && (\n <ServersPanel enabled={open} className=\"flex-1\" />\n )}\n </aside>\n );\n}\n"],"names":["process","env","NEXT_RUNTIME","module","exports","require","__NEXT_EXPERIMENTAL_REACT","NODE_ENV","TURBOPACK","vendored","ReactJsxRuntime","React","AppRouterContext","ReactServerDOMTurbopackClient","HooksClientContext","ServerInsertedHtml","parseHubTime","dateStr","iso","includes","replace","t","Date","parse","Number","isFinite","relativeAgo","s","Math","floor","now","NetworkContext","networkId","setNetworkId","NetworkProvider","children","saved","sessionStorage","getItem","setAndPersist","id","setItem","Provider","value","useNetworkId","THEMES","Set","applyTheme","document","el","documentElement","setAttribute","removeAttribute","ThemeProvider","localStorage","has","fetcher","url","res","fetch","status","Error","ok","json","barTint","pct","fill","track","Bar","label","clamped","max","min","className","round","style","background","width","Sparkline","values","tint","length","w","h","stride","pts","map","v","i","x","y","toFixed","polyline","join","area","height","viewBox","preserveAspectRatio","d","opacity","points","stroke","strokeWidth","HealthBadge","cpu","mem","disk","vals","filter","worst","boxShadow","title","AgentList","agents","a","statusColor","alias","color","runtime","progress","useServers","enabled","data","error","refreshInterval","dedupingInterval","servers","unavailable","onlineCount","loading","ServersPanel","expanded","setExpanded","raw","JSON","toggleExpanded","hostname","prev","next","delete","add","stringify","borderColor","offline","cpuPct","cpu_load_1min","cpu_cores","memPct","mem_used_gb","mem_total_gb","diskPct","disk_used_gb","disk_total_gb","isExpanded","ip","type","onClick","note","agent_count","transform","transition","strokeLinecap","strokeLinejoin","cpu_history","mem_history","disk_history","ServersDrawer","open","setOpen","toggle","maxHeight","rx","cx","cy","r"],"mappings":"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","ignoreList":[0,1,2,3,4,5,6]}