@yeaft/webchat-agent 1.0.188 → 1.0.189

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 (121) hide show
  1. package/cli.js +12 -0
  2. package/index.js +10 -2
  3. package/local-run.js +218 -0
  4. package/local-runtime/server/.env.example +54 -0
  5. package/local-runtime/server/api.js +111 -0
  6. package/local-runtime/server/auth/aad.js +235 -0
  7. package/local-runtime/server/auth/login.js +156 -0
  8. package/local-runtime/server/auth/oauth-flow.js +277 -0
  9. package/local-runtime/server/auth/password-reset.js +134 -0
  10. package/local-runtime/server/auth/providers/alipay.js +125 -0
  11. package/local-runtime/server/auth/providers/github.js +82 -0
  12. package/local-runtime/server/auth/providers/google.js +60 -0
  13. package/local-runtime/server/auth/providers/microsoft.js +71 -0
  14. package/local-runtime/server/auth/providers/types.js +57 -0
  15. package/local-runtime/server/auth/providers/wechat.js +68 -0
  16. package/local-runtime/server/auth/register.js +91 -0
  17. package/local-runtime/server/auth/session-store.js +57 -0
  18. package/local-runtime/server/auth/token.js +85 -0
  19. package/local-runtime/server/auth/totp-auth.js +133 -0
  20. package/local-runtime/server/auth/utils.js +42 -0
  21. package/local-runtime/server/auth.js +8 -0
  22. package/local-runtime/server/check-node-version.js +74 -0
  23. package/local-runtime/server/config.js +298 -0
  24. package/local-runtime/server/context.js +140 -0
  25. package/local-runtime/server/create-user.js +59 -0
  26. package/local-runtime/server/database.js +12 -0
  27. package/local-runtime/server/db/connection.js +963 -0
  28. package/local-runtime/server/db/expert-db.js +171 -0
  29. package/local-runtime/server/db/identity-db.js +92 -0
  30. package/local-runtime/server/db/invitation-db.js +38 -0
  31. package/local-runtime/server/db/message-db.js +257 -0
  32. package/local-runtime/server/db/session-db.js +118 -0
  33. package/local-runtime/server/db/user-db.js +165 -0
  34. package/local-runtime/server/db/user-stats-db.js +185 -0
  35. package/local-runtime/server/db/yeaft-session-db.js +258 -0
  36. package/local-runtime/server/email.js +96 -0
  37. package/local-runtime/server/encryption.js +105 -0
  38. package/local-runtime/server/handlers/agent-conversation.js +347 -0
  39. package/local-runtime/server/handlers/agent-file-terminal.js +99 -0
  40. package/local-runtime/server/handlers/agent-output.js +854 -0
  41. package/local-runtime/server/handlers/agent-sync.js +399 -0
  42. package/local-runtime/server/handlers/agent-work-center.js +27 -0
  43. package/local-runtime/server/handlers/client-conversation.js +1182 -0
  44. package/local-runtime/server/handlers/client-misc.js +254 -0
  45. package/local-runtime/server/handlers/client-work-center.js +269 -0
  46. package/local-runtime/server/handlers/client-workbench.js +146 -0
  47. package/local-runtime/server/handlers/session-pin-router.js +61 -0
  48. package/local-runtime/server/heartbeat-policy.js +46 -0
  49. package/local-runtime/server/index.js +275 -0
  50. package/local-runtime/server/package.json +55 -0
  51. package/local-runtime/server/perf-trace.js +154 -0
  52. package/local-runtime/server/proxy.js +273 -0
  53. package/local-runtime/server/routes/admin-routes.js +207 -0
  54. package/local-runtime/server/routes/auth-routes.js +322 -0
  55. package/local-runtime/server/routes/expert-routes.js +117 -0
  56. package/local-runtime/server/routes/invitation-routes.js +60 -0
  57. package/local-runtime/server/routes/session-routes.js +112 -0
  58. package/local-runtime/server/routes/upload-routes.js +109 -0
  59. package/local-runtime/server/routes/user-routes.js +241 -0
  60. package/local-runtime/server/totp.js +74 -0
  61. package/local-runtime/server/work-item-attachment-policy.js +56 -0
  62. package/local-runtime/server/ws-agent.js +319 -0
  63. package/local-runtime/server/ws-client.js +214 -0
  64. package/local-runtime/server/ws-utils.js +394 -0
  65. package/local-runtime/server/yeaft-asset-store.js +339 -0
  66. package/local-runtime/version.json +1 -0
  67. package/local-runtime/web/app.bundle.js +7673 -0
  68. package/local-runtime/web/app.bundle.js.gz +0 -0
  69. package/local-runtime/web/assets/avatars/README.md +34 -0
  70. package/local-runtime/web/assets/avatars/ada.svg +1 -0
  71. package/local-runtime/web/assets/avatars/alan.svg +1 -0
  72. package/local-runtime/web/assets/avatars/alice.svg +1 -0
  73. package/local-runtime/web/assets/avatars/anders.svg +1 -0
  74. package/local-runtime/web/assets/avatars/bezos.svg +1 -0
  75. package/local-runtime/web/assets/avatars/borges.svg +1 -0
  76. package/local-runtime/web/assets/avatars/buffett.svg +1 -0
  77. package/local-runtime/web/assets/avatars/clausewitz.svg +1 -0
  78. package/local-runtime/web/assets/avatars/dalio.svg +1 -0
  79. package/local-runtime/web/assets/avatars/dieter.svg +1 -0
  80. package/local-runtime/web/assets/avatars/drucker.svg +1 -0
  81. package/local-runtime/web/assets/avatars/einstein.svg +1 -0
  82. package/local-runtime/web/assets/avatars/grace.svg +1 -0
  83. package/local-runtime/web/assets/avatars/harari.svg +1 -0
  84. package/local-runtime/web/assets/avatars/jung.svg +1 -0
  85. package/local-runtime/web/assets/avatars/kahneman.svg +1 -0
  86. package/local-runtime/web/assets/avatars/ken.svg +1 -0
  87. package/local-runtime/web/assets/avatars/kongzi.svg +1 -0
  88. package/local-runtime/web/assets/avatars/kubrick.svg +1 -0
  89. package/local-runtime/web/assets/avatars/linus.svg +1 -0
  90. package/local-runtime/web/assets/avatars/luxun.svg +1 -0
  91. package/local-runtime/web/assets/avatars/margaret.svg +1 -0
  92. package/local-runtime/web/assets/avatars/martin.svg +1 -0
  93. package/local-runtime/web/assets/avatars/miyazaki.svg +1 -0
  94. package/local-runtime/web/assets/avatars/munger.svg +1 -0
  95. package/local-runtime/web/assets/avatars/nietzsche.svg +1 -0
  96. package/local-runtime/web/assets/avatars/norman.svg +1 -0
  97. package/local-runtime/web/assets/avatars/shannon.svg +1 -0
  98. package/local-runtime/web/assets/avatars/simaqian.svg +1 -0
  99. package/local-runtime/web/assets/avatars/socrates.svg +1 -0
  100. package/local-runtime/web/assets/avatars/steve.svg +1 -0
  101. package/local-runtime/web/assets/avatars/sudongpo.svg +1 -0
  102. package/local-runtime/web/assets/avatars/sunzi.svg +1 -0
  103. package/local-runtime/web/docx-preview.min.js +2 -0
  104. package/local-runtime/web/docx-preview.min.js.gz +0 -0
  105. package/local-runtime/web/html-to-image.min.js +2 -0
  106. package/local-runtime/web/html-to-image.min.js.gz +0 -0
  107. package/local-runtime/web/index.html +21 -0
  108. package/local-runtime/web/jszip.min.js +13 -0
  109. package/local-runtime/web/jszip.min.js.gz +0 -0
  110. package/local-runtime/web/mermaid.min.js +2843 -0
  111. package/local-runtime/web/mermaid.min.js.gz +0 -0
  112. package/local-runtime/web/msal-browser.min.js +69 -0
  113. package/local-runtime/web/msal-browser.min.js.gz +0 -0
  114. package/local-runtime/web/style.bundle.css +10 -0
  115. package/local-runtime/web/style.bundle.css.gz +0 -0
  116. package/local-runtime/web/vendor.bundle.js +1522 -0
  117. package/local-runtime/web/vendor.bundle.js.gz +0 -0
  118. package/local-runtime/web/xlsx.min.js +24 -0
  119. package/local-runtime/web/xlsx.min.js.gz +0 -0
  120. package/package.json +13 -3
  121. package/scripts/prepare-local-runtime.js +25 -0
@@ -0,0 +1,273 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { WebSocket, WebSocketServer } from 'ws';
3
+ import { agents, pendingProxyRequests, proxyWsConnections } from './context.js';
4
+ import { sendToAgent, findAgentByName } from './ws-utils.js';
5
+
6
+ export function registerProxyRoutes(app) {
7
+ app.all('/agent/:agentName/:port/*', handleProxyRequest);
8
+ app.all('/agent/:agentName/:port', handleProxyRequest);
9
+ }
10
+
11
+ async function handleProxyRequest(req, res) {
12
+ // No JWT authentication for proxy routes — external services need direct access.
13
+ // Access control is enforced by the port enable/disable mechanism in ProxyTab.
14
+
15
+ const { agentName, port } = req.params;
16
+ const portNum = parseInt(port);
17
+ if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
18
+ return res.status(400).send('Invalid port');
19
+ }
20
+
21
+ const found = findAgentByName(agentName);
22
+ if (!found || found.agent.ws.readyState !== WebSocket.OPEN) {
23
+ return res.status(502).send('Agent not connected');
24
+ }
25
+
26
+ // Check if this port is enabled for proxy
27
+ const proxyPorts = found.agent.proxyPorts || [];
28
+ const portConfig = proxyPorts.find(p => p.port === portNum);
29
+ if (!portConfig || !portConfig.enabled) {
30
+ return res.status(403).send('Port not enabled for proxy');
31
+ }
32
+
33
+ // Handle CORS preflight — respond immediately without forwarding to agent
34
+ if (req.method === 'OPTIONS') {
35
+ const origin = req.headers.origin || '*';
36
+ res.setHeader('Access-Control-Allow-Origin', origin);
37
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
38
+ res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers'] || 'Content-Type, Authorization');
39
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
40
+ res.setHeader('Access-Control-Max-Age', '86400');
41
+ return res.status(204).end();
42
+ }
43
+
44
+ const requestId = randomUUID();
45
+ const requestOrigin = req.headers.origin || null;
46
+
47
+ // Collect raw body
48
+ const bodyChunks = [];
49
+ req.on('data', chunk => bodyChunks.push(chunk));
50
+ req.on('end', async () => {
51
+ const body = Buffer.concat(bodyChunks);
52
+
53
+ // Build proxy path: strip /agent/:name/:port prefix
54
+ const proxyPath = req.params[0] ? ('/' + req.params[0]) : '/';
55
+ const queryString = req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : '';
56
+
57
+ // Clean headers for forwarding
58
+ const fwdHeaders = { ...req.headers };
59
+ delete fwdHeaders['host'];
60
+ const proxyHost = portConfig.host || 'localhost';
61
+ fwdHeaders['host'] = `${proxyHost}:${portNum}`;
62
+ delete fwdHeaders['connection'];
63
+
64
+ // Build publicOrigin so agent can rewrite localhost URLs in responses
65
+ const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'https';
66
+ const publicOrigin = `${protocol}://${req.headers.host}`;
67
+
68
+ await sendToAgent(found.agent, {
69
+ type: 'proxy_request',
70
+ requestId,
71
+ port: portNum,
72
+ host: portConfig.host || 'localhost',
73
+ scheme: portConfig.scheme || 'http',
74
+ basePath: `/agent/${agentName}/${portNum}`,
75
+ publicOrigin,
76
+ method: req.method,
77
+ path: proxyPath + queryString,
78
+ headers: fwdHeaders,
79
+ body: body.length > 0 ? body.toString('base64') : null
80
+ });
81
+
82
+ const timeout = setTimeout(() => {
83
+ pendingProxyRequests.delete(requestId);
84
+ if (!res.headersSent) res.status(504).send('Proxy timeout');
85
+ }, 60000);
86
+
87
+ pendingProxyRequests.set(requestId, { res, timeout, streaming: false, origin: requestOrigin });
88
+ });
89
+
90
+ req.on('error', () => {
91
+ pendingProxyRequests.delete(requestId);
92
+ });
93
+ }
94
+
95
+ // Overwrite CORS headers so browser accepts proxied responses
96
+ function applyCorsHeaders(headers, origin) {
97
+ if (origin) {
98
+ headers['access-control-allow-origin'] = origin;
99
+ headers['access-control-allow-credentials'] = 'true';
100
+ }
101
+ }
102
+
103
+ // Handle proxy response messages from agent
104
+ export function handleProxyResponse(msg) {
105
+ const pending = pendingProxyRequests.get(msg.requestId);
106
+ if (!pending) return;
107
+ clearTimeout(pending.timeout);
108
+ pendingProxyRequests.delete(msg.requestId);
109
+
110
+ const headers = msg.headers || {};
111
+ delete headers['transfer-encoding'];
112
+ delete headers['connection'];
113
+ applyCorsHeaders(headers, pending.origin);
114
+
115
+ try {
116
+ pending.res.status(msg.statusCode || 500);
117
+ for (const [k, v] of Object.entries(headers)) {
118
+ try { pending.res.setHeader(k, v); } catch(e) {}
119
+ }
120
+ if (msg.body) {
121
+ pending.res.end(Buffer.from(msg.body, 'base64'));
122
+ } else {
123
+ pending.res.end();
124
+ }
125
+ } catch(e) {
126
+ console.error('[Proxy] Error sending response:', e.message);
127
+ }
128
+ }
129
+
130
+ export function handleProxyResponseChunk(msg) {
131
+ const pending = pendingProxyRequests.get(msg.requestId);
132
+ if (!pending) return;
133
+
134
+ try {
135
+ if (!pending.streaming) {
136
+ pending.streaming = true;
137
+ clearTimeout(pending.timeout);
138
+ // Set a longer timeout for streaming responses
139
+ pending.timeout = setTimeout(() => {
140
+ pendingProxyRequests.delete(msg.requestId);
141
+ try { pending.res.end(); } catch(e) {}
142
+ }, 300000); // 5 minutes for streaming
143
+
144
+ pending.res.status(msg.statusCode || 200);
145
+ const headers = msg.headers || {};
146
+ delete headers['transfer-encoding'];
147
+ delete headers['connection'];
148
+ applyCorsHeaders(headers, pending.origin);
149
+ for (const [k, v] of Object.entries(headers)) {
150
+ try { pending.res.setHeader(k, v); } catch(e) {}
151
+ }
152
+ pending.res.setHeader('X-Accel-Buffering', 'no');
153
+ pending.res.flushHeaders();
154
+ }
155
+
156
+ if (msg.chunk) {
157
+ pending.res.write(Buffer.from(msg.chunk, 'base64'));
158
+ }
159
+ } catch(e) {
160
+ console.error('[Proxy] Error writing chunk:', e.message);
161
+ pendingProxyRequests.delete(msg.requestId);
162
+ }
163
+ }
164
+
165
+ export function handleProxyResponseEnd(msg) {
166
+ const pending = pendingProxyRequests.get(msg.requestId);
167
+ if (!pending) return;
168
+ clearTimeout(pending.timeout);
169
+ pendingProxyRequests.delete(msg.requestId);
170
+ try { pending.res.end(); } catch(e) {}
171
+ }
172
+
173
+ // Handle proxy WebSocket messages from agent to browser
174
+ export function handleProxyWsAgentMessage(msg) {
175
+ const conn = proxyWsConnections.get(msg.proxyWsId);
176
+ if (!conn || !conn.browserWs) return;
177
+
178
+ switch (msg.type) {
179
+ case 'proxy_ws_opened':
180
+ break;
181
+ case 'proxy_ws_message':
182
+ try {
183
+ if (msg.isBinary) {
184
+ conn.browserWs.send(Buffer.from(msg.data, 'base64'));
185
+ } else {
186
+ conn.browserWs.send(msg.data);
187
+ }
188
+ } catch(e) {}
189
+ break;
190
+ case 'proxy_ws_closed':
191
+ try { conn.browserWs.close(msg.code || 1000); } catch(e) {}
192
+ proxyWsConnections.delete(msg.proxyWsId);
193
+ break;
194
+ case 'proxy_ws_error':
195
+ try { conn.browserWs.close(1011, msg.error); } catch(e) {}
196
+ proxyWsConnections.delete(msg.proxyWsId);
197
+ break;
198
+ }
199
+ }
200
+
201
+ // Handle WebSocket upgrade for proxy connections
202
+ export async function handleProxyWebSocketUpgrade(req, socket, head, match) {
203
+ const agentName = match[1];
204
+ const port = parseInt(match[2]);
205
+ const path = match[3] || '/';
206
+
207
+ const found = findAgentByName(agentName);
208
+ if (!found || found.agent.ws.readyState !== WebSocket.OPEN) {
209
+ socket.destroy();
210
+ return;
211
+ }
212
+
213
+ // Check if port is enabled
214
+ const proxyPorts = found.agent.proxyPorts || [];
215
+ const portConfig = proxyPorts.find(p => p.port === port);
216
+ if (!portConfig || !portConfig.enabled) {
217
+ socket.destroy();
218
+ return;
219
+ }
220
+
221
+ // Create a temporary WSS for this upgrade
222
+ const proxyWss = new WebSocketServer({ noServer: true });
223
+ proxyWss.handleUpgrade(req, socket, head, async (browserWs) => {
224
+ const proxyWsId = randomUUID();
225
+
226
+ proxyWsConnections.set(proxyWsId, {
227
+ browserWs,
228
+ agentId: found.id
229
+ });
230
+
231
+ // Forward browser messages to agent
232
+ browserWs.on('message', async (data, isBinary) => {
233
+ await sendToAgent(found.agent, {
234
+ type: 'proxy_ws_message',
235
+ proxyWsId,
236
+ data: isBinary ? data.toString('base64') : data.toString(),
237
+ isBinary
238
+ });
239
+ });
240
+
241
+ browserWs.on('close', async (code) => {
242
+ proxyWsConnections.delete(proxyWsId);
243
+ await sendToAgent(found.agent, {
244
+ type: 'proxy_ws_close',
245
+ proxyWsId,
246
+ code
247
+ });
248
+ });
249
+
250
+ browserWs.on('error', () => {
251
+ proxyWsConnections.delete(proxyWsId);
252
+ });
253
+
254
+ // Tell agent to open WS connection to localhost:port
255
+ const fwdHeaders = { ...req.headers };
256
+ delete fwdHeaders['host'];
257
+ delete fwdHeaders['upgrade'];
258
+ delete fwdHeaders['connection'];
259
+ delete fwdHeaders['sec-websocket-key'];
260
+ delete fwdHeaders['sec-websocket-version'];
261
+ delete fwdHeaders['sec-websocket-extensions'];
262
+
263
+ await sendToAgent(found.agent, {
264
+ type: 'proxy_ws_open',
265
+ proxyWsId,
266
+ port,
267
+ host: portConfig.host || 'localhost',
268
+ scheme: portConfig.scheme || 'http',
269
+ path,
270
+ headers: fwdHeaders
271
+ });
272
+ });
273
+ }
@@ -0,0 +1,207 @@
1
+ import { WebSocket } from 'ws';
2
+ import { userStatsDb } from '../database.js';
3
+ import { agents, webClients, userStatsDeltas } from '../context.js';
4
+
5
+ const toNumber = (value) => Number(value) || 0;
6
+
7
+ function emptyAgentMetrics() {
8
+ return {
9
+ totalTurns: 0,
10
+ chatTurns: 0,
11
+ yeaftTurns: 0,
12
+ sessionsCreated: 0,
13
+ inputTokens: 0,
14
+ outputTokens: 0,
15
+ cacheReadTokens: 0,
16
+ cacheWriteTokens: 0,
17
+ totalTokens: 0,
18
+ lastUpdatedAt: null,
19
+ };
20
+ }
21
+
22
+ function safeAgentMetrics(metrics = {}) {
23
+ const out = emptyAgentMetrics();
24
+ for (const key of Object.keys(out)) {
25
+ if (key === 'lastUpdatedAt') continue;
26
+ out[key] = toNumber(metrics[key]);
27
+ }
28
+ out.lastUpdatedAt = metrics.lastUpdatedAt || null;
29
+ if (!out.totalTurns) out.totalTurns = out.chatTurns + out.yeaftTurns;
30
+ if (!out.totalTokens) out.totalTokens = out.inputTokens + out.outputTokens + out.cacheReadTokens + out.cacheWriteTokens;
31
+ return out;
32
+ }
33
+
34
+ function sumAgentMetrics() {
35
+ const totals = emptyAgentMetrics();
36
+ for (const [, agent] of agents) {
37
+ const metrics = safeAgentMetrics(agent.metrics || {});
38
+ totals.totalTurns += metrics.totalTurns;
39
+ totals.chatTurns += metrics.chatTurns;
40
+ totals.yeaftTurns += metrics.yeaftTurns;
41
+ totals.sessionsCreated += metrics.sessionsCreated;
42
+ totals.inputTokens += metrics.inputTokens;
43
+ totals.outputTokens += metrics.outputTokens;
44
+ totals.cacheReadTokens += metrics.cacheReadTokens;
45
+ totals.cacheWriteTokens += metrics.cacheWriteTokens;
46
+ totals.totalTokens += metrics.totalTokens;
47
+ if (metrics.lastUpdatedAt && (!totals.lastUpdatedAt || metrics.lastUpdatedAt > totals.lastUpdatedAt)) {
48
+ totals.lastUpdatedAt = metrics.lastUpdatedAt;
49
+ }
50
+ }
51
+ return totals;
52
+ }
53
+
54
+ function mergePendingUserStats(stats) {
55
+ const byUser = new Map(stats.map(row => [row.user_id, { ...row }]));
56
+ for (const [userId, delta] of userStatsDeltas) {
57
+ const row = byUser.get(userId);
58
+ if (!row) {
59
+ byUser.set(userId, {
60
+ user_id: userId,
61
+ username: userId,
62
+ display_name: userId,
63
+ role: 'pro',
64
+ last_login_at: null,
65
+ updated_at: Date.now(),
66
+ message_count: toNumber(delta.messages),
67
+ session_count: toNumber(delta.sessions),
68
+ request_count: toNumber(delta.requests),
69
+ bytes_sent: toNumber(delta.bytesSent),
70
+ bytes_received: toNumber(delta.bytesReceived),
71
+ });
72
+ continue;
73
+ }
74
+ row.message_count = toNumber(row.message_count) + toNumber(delta.messages);
75
+ row.session_count = toNumber(row.session_count) + toNumber(delta.sessions);
76
+ row.request_count = toNumber(row.request_count) + toNumber(delta.requests);
77
+ row.bytes_sent = toNumber(row.bytes_sent) + toNumber(delta.bytesSent);
78
+ row.bytes_received = toNumber(row.bytes_received) + toNumber(delta.bytesReceived);
79
+ }
80
+ return Array.from(byUser.values());
81
+ }
82
+
83
+ function pendingTodayMessages() {
84
+ let count = 0;
85
+ for (const delta of userStatsDeltas.values()) count += toNumber(delta.messages);
86
+ return count;
87
+ }
88
+
89
+ /**
90
+ * Register admin-only REST API routes for the dashboard.
91
+ */
92
+ export function registerAdminRoutes(app, { requireAuth, requireAdmin }) {
93
+ // GET /api/admin/dashboard — aggregated overview
94
+ app.get('/api/admin/dashboard', requireAuth, requireAdmin, (req, res) => {
95
+ try {
96
+ const totals = userStatsDb.getDashboardTotals();
97
+ const onlineUsers = new Set();
98
+ for (const [, client] of webClients) {
99
+ if (client.authenticated && client.userId) {
100
+ onlineUsers.add(client.userId);
101
+ }
102
+ }
103
+ let onlineAgents = 0;
104
+ for (const [, agent] of agents) {
105
+ if (agent.ws.readyState === WebSocket.OPEN) onlineAgents++;
106
+ }
107
+
108
+ const agentMetrics = sumAgentMetrics();
109
+ const tokenTotals = userStatsDb.getDashboardTokenTotals();
110
+ res.json({
111
+ totalUsers: totals.total_users,
112
+ totalSessions: totals.total_sessions,
113
+ totalMessages: totals.total_messages,
114
+ onlineUsers: onlineUsers.size,
115
+ onlineAgents,
116
+ todayActiveUsers: userStatsDb.getTodayActiveUsers(),
117
+ todayMessages: userStatsDb.getTodayMessages() + pendingTodayMessages(),
118
+ agentMetrics,
119
+ totalAgentTurns: agentMetrics.totalTurns,
120
+ totalTokens: toNumber(tokenTotals.total_tokens)
121
+ });
122
+ } catch (e) {
123
+ console.error('[Admin] Dashboard error:', e.message);
124
+ res.status(500).json({ error: 'Internal server error' });
125
+ }
126
+ });
127
+
128
+ // GET /api/admin/user-stats — per-user stats list (supports ?period=today|week|month|all)
129
+ app.get('/api/admin/user-stats', requireAuth, requireAdmin, (req, res) => {
130
+ try {
131
+ const period = req.query.period || 'all';
132
+ const validPeriods = ['today', 'week', 'month', 'all'];
133
+ const stats = mergePendingUserStats(userStatsDb.getByPeriod(validPeriods.includes(period) ? period : 'all'));
134
+ res.json(stats.map(s => ({
135
+ userId: s.user_id,
136
+ username: s.username,
137
+ displayName: s.display_name,
138
+ role: s.role,
139
+ messageCount: s.message_count,
140
+ sessionCount: s.session_count,
141
+ requestCount: s.request_count,
142
+ bytesSent: s.bytes_sent,
143
+ bytesReceived: s.bytes_received,
144
+ inputTokens: s.input_tokens,
145
+ outputTokens: s.output_tokens,
146
+ cacheReadTokens: s.cache_read_tokens,
147
+ cacheWriteTokens: s.cache_write_tokens,
148
+ totalTokens: s.total_tokens,
149
+ lastLoginAt: s.last_login_at,
150
+ updatedAt: s.updated_at
151
+ })));
152
+ } catch (e) {
153
+ console.error('[Admin] User stats error:', e.message);
154
+ res.status(500).json({ error: 'Internal server error' });
155
+ }
156
+ });
157
+
158
+ // GET /api/admin/agents — full agent list (no owner filtering)
159
+ app.get('/api/admin/agents', requireAuth, requireAdmin, (req, res) => {
160
+ try {
161
+ const agentList = Array.from(agents.entries()).map(([id, agent]) => ({
162
+ id,
163
+ name: agent.name,
164
+ workDir: agent.workDir,
165
+ online: agent.ws.readyState === WebSocket.OPEN,
166
+ status: agent.status || 'ready',
167
+ latency: agent.latency || null,
168
+ version: agent.version || null,
169
+ ownerId: agent.ownerId || null,
170
+ ownerUsername: agent.ownerUsername || null,
171
+ capabilities: agent.capabilities || [],
172
+ conversationCount: agent.conversations?.size || 0,
173
+ metrics: safeAgentMetrics(agent.metrics || {}),
174
+ metricsUpdatedAt: agent.metricsUpdatedAt || null
175
+ }));
176
+ res.json(agentList);
177
+ } catch (e) {
178
+ console.error('[Admin] Agents error:', e.message);
179
+ res.status(500).json({ error: 'Internal server error' });
180
+ }
181
+ });
182
+
183
+ // GET /api/admin/online-users — currently connected web clients
184
+ app.get('/api/admin/online-users', requireAuth, requireAdmin, (req, res) => {
185
+ try {
186
+ // Deduplicate by userId, keep the most active connection
187
+ const userMap = new Map();
188
+ for (const [, client] of webClients) {
189
+ if (!client.authenticated || !client.userId) continue;
190
+ const existing = userMap.get(client.userId);
191
+ if (!existing || client.currentAgent) {
192
+ userMap.set(client.userId, {
193
+ userId: client.userId,
194
+ username: client.username,
195
+ role: client.role,
196
+ currentAgent: client.currentAgent || null,
197
+ currentAgentName: client.currentAgent ? (agents.get(client.currentAgent)?.name || null) : null
198
+ });
199
+ }
200
+ }
201
+ res.json(Array.from(userMap.values()));
202
+ } catch (e) {
203
+ console.error('[Admin] Online users error:', e.message);
204
+ res.status(500).json({ error: 'Internal server error' });
205
+ }
206
+ });
207
+ }