neoagent 2.4.1-beta.43 → 2.4.1-beta.45
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.
- package/docs/configuration.md +6 -0
- package/flutter_app/lib/main_app_shell.dart +0 -2
- package/flutter_app/lib/main_chat.dart +1 -0
- package/flutter_app/lib/main_controller.dart +14 -2
- package/flutter_app/lib/main_models.dart +36 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_settings.dart +4 -441
- package/flutter_app/lib/main_shared.dart +138 -3
- package/flutter_app/lib/main_unified.dart +5 -100
- package/lib/manager.js +32 -0
- package/package.json +1 -1
- package/runtime/paths.js +3 -1
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/index.html +532 -0
- package/server/admin/login.html +164 -0
- package/server/admin/logo.svg +43 -0
- package/server/http/static.js +4 -1
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +11 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +73953 -74690
- package/server/routes/admin.js +321 -0
- package/server/routes/settings.js +10 -2
- package/server/services/widgets/service.js +17 -1
- package/server/utils/logger.js +44 -6
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const express = require('express');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const { requireAdminAuth } = require('../middleware/adminAuth');
|
|
7
|
+
const { getVersionInfo } = require('../utils/version');
|
|
8
|
+
const {
|
|
9
|
+
readUpdateStatus,
|
|
10
|
+
writeUpdateStatusFile: writeUpdateStatus,
|
|
11
|
+
} = require('../utils/update_status');
|
|
12
|
+
const {
|
|
13
|
+
parseReleaseChannel,
|
|
14
|
+
writeReleaseChannelToEnvFile,
|
|
15
|
+
getReleaseChannelBranchPolicy,
|
|
16
|
+
getReleaseChannelNpmPolicy,
|
|
17
|
+
} = require('../../runtime/release_channel');
|
|
18
|
+
const { APP_DIR, ENV_FILE, upsertEnvValue } = require('../../runtime/paths');
|
|
19
|
+
const { isManagedDeployment } = require('../utils/deployment');
|
|
20
|
+
const rateLimit = require('express-rate-limit');
|
|
21
|
+
|
|
22
|
+
const router = express.Router();
|
|
23
|
+
const ADMIN_DIR = path.join(__dirname, '..', 'admin');
|
|
24
|
+
|
|
25
|
+
const loginLimiter = rateLimit({
|
|
26
|
+
windowMs: 15 * 60 * 1000,
|
|
27
|
+
max: 20,
|
|
28
|
+
standardHeaders: true,
|
|
29
|
+
legacyHeaders: false,
|
|
30
|
+
skipSuccessfulRequests: true,
|
|
31
|
+
message: { error: 'Too many login attempts, try again later' },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const updateTriggerLimiter = rateLimit({
|
|
35
|
+
windowMs: 15 * 60 * 1000,
|
|
36
|
+
max: 5,
|
|
37
|
+
standardHeaders: true,
|
|
38
|
+
legacyHeaders: false,
|
|
39
|
+
message: { error: 'Too many update requests, try again later' },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// --- Auth ---
|
|
43
|
+
|
|
44
|
+
router.get('/login', (req, res) => {
|
|
45
|
+
if (req.session?.isAdmin) return res.redirect('/admin');
|
|
46
|
+
res.sendFile(path.join(ADMIN_DIR, 'login.html'));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
router.post('/api/login', loginLimiter, express.json(), (req, res) => {
|
|
50
|
+
const { username, password } = req.body || {};
|
|
51
|
+
const expectedUsername = process.env.ADMIN_USERNAME || 'admin';
|
|
52
|
+
const expectedPassword = process.env.ADMIN_PASSWORD || '';
|
|
53
|
+
if (!expectedPassword) {
|
|
54
|
+
return res.status(503).json({ error: 'Admin credentials not configured. Run `neoagent setup`.' });
|
|
55
|
+
}
|
|
56
|
+
if (username === expectedUsername && password === expectedPassword) {
|
|
57
|
+
req.session.isAdmin = true;
|
|
58
|
+
req.session.save((err) => {
|
|
59
|
+
if (err) return res.status(500).json({ error: 'Session error' });
|
|
60
|
+
res.json({ ok: true });
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
res.status(401).json({ error: 'Invalid credentials' });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
router.post('/api/logout', (req, res) => {
|
|
68
|
+
req.session.destroy(() => res.json({ ok: true }));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// --- Protected API ---
|
|
72
|
+
|
|
73
|
+
router.get('/api/logs', requireAdminAuth, (req, res) => {
|
|
74
|
+
const logHistory = req.app.locals.logHistory || [];
|
|
75
|
+
res.json({ logs: logHistory });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
router.get('/api/version', requireAdminAuth, (req, res) => {
|
|
79
|
+
const version = getVersionInfo();
|
|
80
|
+
const status = readUpdateStatus();
|
|
81
|
+
res.json({
|
|
82
|
+
version: version.version,
|
|
83
|
+
installedVersion: version.installedVersion,
|
|
84
|
+
packageVersion: version.packageVersion,
|
|
85
|
+
gitVersion: version.gitVersion,
|
|
86
|
+
gitSha: version.gitSha,
|
|
87
|
+
gitBranch: version.gitBranch,
|
|
88
|
+
releaseChannel: status.releaseChannel || version.releaseChannel,
|
|
89
|
+
deploymentMode: version.deploymentMode,
|
|
90
|
+
deploymentProfile: version.deploymentProfile,
|
|
91
|
+
allowSelfUpdate: version.allowSelfUpdate,
|
|
92
|
+
updateStatus: {
|
|
93
|
+
state: status.state,
|
|
94
|
+
progress: status.progress,
|
|
95
|
+
phase: status.phase,
|
|
96
|
+
message: status.message,
|
|
97
|
+
},
|
|
98
|
+
uptime: process.uptime(),
|
|
99
|
+
nodeVersion: process.version,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
router.get('/api/health', requireAdminAuth, async (req, res) => {
|
|
104
|
+
const runtimeManager = req.app?.locals?.runtimeManager;
|
|
105
|
+
const desktopRegistry = req.app?.locals?.desktopCompanionRegistry;
|
|
106
|
+
const extensionRegistry = req.app?.locals?.browserExtensionRegistry;
|
|
107
|
+
const results = [];
|
|
108
|
+
|
|
109
|
+
results.push({ id: 'backend', label: 'Backend server', passed: true, detail: 'Running' });
|
|
110
|
+
|
|
111
|
+
const version = getVersionInfo();
|
|
112
|
+
results.push({
|
|
113
|
+
id: 'version',
|
|
114
|
+
label: 'Server version',
|
|
115
|
+
passed: true,
|
|
116
|
+
detail: version.version || version.packageVersion || 'Unknown',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const db = require('../db/database');
|
|
121
|
+
db.prepare('SELECT 1').get();
|
|
122
|
+
results.push({ id: 'database', label: 'Database', passed: true, detail: 'SQLite connected' });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
results.push({ id: 'database', label: 'Database', passed: false, detail: String(err?.message || err).slice(0, 120) });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const updateStatus = readUpdateStatus();
|
|
128
|
+
results.push({
|
|
129
|
+
id: 'update',
|
|
130
|
+
label: 'Update status',
|
|
131
|
+
passed: updateStatus.state !== 'failed',
|
|
132
|
+
detail: updateStatus.state === 'idle'
|
|
133
|
+
? 'No update running'
|
|
134
|
+
: `${updateStatus.state} — ${updateStatus.message || ''}`.trim(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const { getRuntimeValidation } = require('../services/runtime/validation');
|
|
138
|
+
const runtimeValidation = getRuntimeValidation(runtimeManager);
|
|
139
|
+
const runtimeReady = Boolean(runtimeValidation?.ready);
|
|
140
|
+
results.push({
|
|
141
|
+
id: 'vm_runtime',
|
|
142
|
+
label: 'Cloud VM runtime',
|
|
143
|
+
passed: runtimeReady,
|
|
144
|
+
detail: runtimeReady ? 'Available' : String(runtimeValidation?.issues?.[0] || 'Not configured'),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (desktopRegistry) {
|
|
148
|
+
try {
|
|
149
|
+
const connectedUsers = [];
|
|
150
|
+
for (const socket of (req.app?.locals?.io?.sockets?.sockets?.values?.() || [])) {
|
|
151
|
+
const uid = socket.request?.session?.userId;
|
|
152
|
+
if (uid && !connectedUsers.includes(uid)) connectedUsers.push(uid);
|
|
153
|
+
}
|
|
154
|
+
results.push({
|
|
155
|
+
id: 'desktop',
|
|
156
|
+
label: 'Desktop companion',
|
|
157
|
+
passed: true,
|
|
158
|
+
detail: 'Registry available',
|
|
159
|
+
});
|
|
160
|
+
} catch {
|
|
161
|
+
results.push({ id: 'desktop', label: 'Desktop companion', passed: false, detail: 'Registry error' });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (extensionRegistry) {
|
|
166
|
+
results.push({ id: 'extension', label: 'Chrome extension registry', passed: true, detail: 'Available' });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const configuredProviders = [];
|
|
170
|
+
if (process.env.ANTHROPIC_API_KEY) configuredProviders.push('Anthropic');
|
|
171
|
+
if (process.env.OPENAI_API_KEY) configuredProviders.push('OpenAI');
|
|
172
|
+
if (process.env.XAI_API_KEY) configuredProviders.push('xAI');
|
|
173
|
+
if (process.env.GOOGLE_AI_KEY) configuredProviders.push('Google');
|
|
174
|
+
if (process.env.OPENROUTER_API_KEY) configuredProviders.push('OpenRouter');
|
|
175
|
+
results.push({
|
|
176
|
+
id: 'ai_providers',
|
|
177
|
+
label: 'AI providers',
|
|
178
|
+
passed: configuredProviders.length > 0,
|
|
179
|
+
detail: configuredProviders.length > 0
|
|
180
|
+
? configuredProviders.join(', ')
|
|
181
|
+
: 'No providers configured',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (process.env.DEEPGRAM_API_KEY) {
|
|
185
|
+
results.push({ id: 'deepgram', label: 'Deepgram (voice)', passed: true, detail: 'API key configured' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const allPassed = results.every((r) => r.passed);
|
|
189
|
+
res.json({ passed: allPassed, results });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
router.get('/api/config', requireAdminAuth, (req, res) => {
|
|
193
|
+
const safe = [
|
|
194
|
+
'PORT', 'NODE_ENV', 'PUBLIC_URL', 'NEOAGENT_DEPLOYMENT_MODE',
|
|
195
|
+
'NEOAGENT_PROFILE', 'NEOAGENT_RELEASE_CHANNEL', 'ALLOWED_ORIGINS',
|
|
196
|
+
'SECURE_COOKIES', 'TRUST_PROXY', 'ADMIN_USERNAME',
|
|
197
|
+
];
|
|
198
|
+
const config = {};
|
|
199
|
+
for (const key of safe) {
|
|
200
|
+
config[key] = process.env[key] || '';
|
|
201
|
+
}
|
|
202
|
+
res.json({ config });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
router.post('/api/update', requireAdminAuth, updateTriggerLimiter, (req, res) => {
|
|
206
|
+
if (isManagedDeployment()) {
|
|
207
|
+
return res.status(403).json({ error: 'Updates are managed by this deployment.' });
|
|
208
|
+
}
|
|
209
|
+
const status = readUpdateStatus();
|
|
210
|
+
if (status.state === 'running') {
|
|
211
|
+
return res.status(409).json({ error: 'An update is already running' });
|
|
212
|
+
}
|
|
213
|
+
console.log('[Admin] Triggering update-runner...');
|
|
214
|
+
const child = spawn(process.execPath, ['scripts/update-runner.js'], {
|
|
215
|
+
detached: true,
|
|
216
|
+
stdio: 'ignore',
|
|
217
|
+
cwd: APP_DIR,
|
|
218
|
+
});
|
|
219
|
+
writeUpdateStatus({
|
|
220
|
+
state: 'running',
|
|
221
|
+
progress: 1,
|
|
222
|
+
phase: 'starting',
|
|
223
|
+
message: 'Launching update job',
|
|
224
|
+
startedAt: new Date().toISOString(),
|
|
225
|
+
completedAt: null,
|
|
226
|
+
versionBefore: null,
|
|
227
|
+
versionAfter: null,
|
|
228
|
+
runnerPid: child.pid,
|
|
229
|
+
changelog: [],
|
|
230
|
+
logs: [],
|
|
231
|
+
});
|
|
232
|
+
child.once('error', (error) => {
|
|
233
|
+
writeUpdateStatus({
|
|
234
|
+
state: 'failed',
|
|
235
|
+
progress: 100,
|
|
236
|
+
phase: 'failed',
|
|
237
|
+
message: `Failed to launch update job: ${error.message}`,
|
|
238
|
+
completedAt: new Date().toISOString(),
|
|
239
|
+
runnerPid: null,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
child.unref();
|
|
243
|
+
res.json({ ok: true, message: 'Update triggered', pid: child.pid });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
router.put('/api/update/channel', requireAdminAuth, (req, res) => {
|
|
247
|
+
if (isManagedDeployment()) {
|
|
248
|
+
return res.status(403).json({ error: 'Release channel changes are managed by this deployment.' });
|
|
249
|
+
}
|
|
250
|
+
const requested = req.body?.channel;
|
|
251
|
+
const releaseChannel = parseReleaseChannel(requested);
|
|
252
|
+
if (!releaseChannel) {
|
|
253
|
+
return res.status(400).json({ error: 'Release channel must be "stable" or "beta".' });
|
|
254
|
+
}
|
|
255
|
+
writeReleaseChannelToEnvFile(releaseChannel);
|
|
256
|
+
process.env.NEOAGENT_RELEASE_CHANNEL = releaseChannel;
|
|
257
|
+
res.json({
|
|
258
|
+
ok: true,
|
|
259
|
+
releaseChannel,
|
|
260
|
+
targetBranch: getReleaseChannelBranchPolicy(releaseChannel),
|
|
261
|
+
npmDistTag: getReleaseChannelNpmPolicy(releaseChannel),
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// --- Providers ---
|
|
266
|
+
|
|
267
|
+
const PROVIDERS = [
|
|
268
|
+
{ key: 'ANTHROPIC_API_KEY', label: 'Anthropic (Claude)', type: 'key' },
|
|
269
|
+
{ key: 'OPENAI_API_KEY', label: 'OpenAI', type: 'key' },
|
|
270
|
+
{ key: 'XAI_API_KEY', label: 'xAI (Grok)', type: 'key' },
|
|
271
|
+
{ key: 'GOOGLE_AI_KEY', label: 'Google (Gemini)', type: 'key' },
|
|
272
|
+
{ key: 'MINIMAX_API_KEY', label: 'MiniMax', type: 'key' },
|
|
273
|
+
{ key: 'NVIDIA_API_KEY', label: 'NVIDIA NIM', type: 'key' },
|
|
274
|
+
{ key: 'OPENROUTER_API_KEY', label: 'OpenRouter', type: 'key' },
|
|
275
|
+
{ key: 'BRAVE_SEARCH_API_KEY', label: 'Brave Search', type: 'key' },
|
|
276
|
+
{ key: 'DEEPGRAM_API_KEY', label: 'Deepgram (Voice)', type: 'key' },
|
|
277
|
+
{ key: 'OLLAMA_URL', label: 'Ollama (Local)', type: 'url' },
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
const ALLOWED_PROVIDER_KEYS = new Set(PROVIDERS.map((p) => p.key));
|
|
281
|
+
|
|
282
|
+
router.get('/api/providers', requireAdminAuth, (req, res) => {
|
|
283
|
+
const result = PROVIDERS.map(({ key, label, type }) => {
|
|
284
|
+
const value = process.env[key] || '';
|
|
285
|
+
let hint = '';
|
|
286
|
+
if (value) {
|
|
287
|
+
hint = type === 'url'
|
|
288
|
+
? value
|
|
289
|
+
: value.length > 8
|
|
290
|
+
? `${value.slice(0, 4)}${'•'.repeat(4)}${value.slice(-4)}`
|
|
291
|
+
: '•'.repeat(value.length);
|
|
292
|
+
}
|
|
293
|
+
return { key, label, type, configured: Boolean(value), hint };
|
|
294
|
+
});
|
|
295
|
+
res.json({ providers: result });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
router.put('/api/providers', requireAdminAuth, express.json(), (req, res) => {
|
|
299
|
+
const { key, value } = req.body || {};
|
|
300
|
+
if (!ALLOWED_PROVIDER_KEYS.has(key)) {
|
|
301
|
+
return res.status(400).json({ error: 'Unknown provider key' });
|
|
302
|
+
}
|
|
303
|
+
const trimmed = String(value || '').trim();
|
|
304
|
+
upsertEnvValue(ENV_FILE, key, trimmed);
|
|
305
|
+
if (trimmed) {
|
|
306
|
+
process.env[key] = trimmed;
|
|
307
|
+
} else {
|
|
308
|
+
delete process.env[key];
|
|
309
|
+
}
|
|
310
|
+
res.json({ ok: true });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// --- Static files ---
|
|
314
|
+
|
|
315
|
+
router.use(express.static(ADMIN_DIR));
|
|
316
|
+
|
|
317
|
+
router.get('*', requireAdminAuth, (req, res) => {
|
|
318
|
+
res.sendFile(path.join(ADMIN_DIR, 'index.html'));
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
module.exports = router;
|
|
@@ -562,8 +562,16 @@ router.delete('/:key', (req, res) => {
|
|
|
562
562
|
res.json({ success: true });
|
|
563
563
|
});
|
|
564
564
|
|
|
565
|
+
function requireAdminSession(req, res, next) {
|
|
566
|
+
if (req.session?.isAdmin === true) return next();
|
|
567
|
+
return res.status(403).json({
|
|
568
|
+
success: false,
|
|
569
|
+
error: 'Server updates are only available from the admin dashboard.',
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
565
573
|
// Trigger auto-update script
|
|
566
|
-
router.post('/update', requireAuth, updateTriggerLimiter, (req, res) => {
|
|
574
|
+
router.post('/update', requireAuth, requireAdminSession, updateTriggerLimiter, (req, res) => {
|
|
567
575
|
if (isManagedDeployment()) {
|
|
568
576
|
return res.status(403).json({
|
|
569
577
|
success: false,
|
|
@@ -611,7 +619,7 @@ router.post('/update', requireAuth, updateTriggerLimiter, (req, res) => {
|
|
|
611
619
|
res.json({ success: true, message: 'Update triggered', pid: child.pid });
|
|
612
620
|
});
|
|
613
621
|
|
|
614
|
-
router.put('/update/channel', (req, res) => {
|
|
622
|
+
router.put('/update/channel', requireAuth, requireAdminSession, (req, res) => {
|
|
615
623
|
if (isManagedDeployment()) {
|
|
616
624
|
return res.status(403).json({
|
|
617
625
|
success: false,
|
|
@@ -366,7 +366,23 @@ class WidgetService {
|
|
|
366
366
|
throw new Error('Widget not found.');
|
|
367
367
|
}
|
|
368
368
|
if (existingRow.is_system) {
|
|
369
|
-
|
|
369
|
+
if (!('enabled' in input)) {
|
|
370
|
+
throw new Error('System widgets cannot be edited directly.');
|
|
371
|
+
}
|
|
372
|
+
// Only the enabled state may be toggled on system widgets.
|
|
373
|
+
const newEnabled = input.enabled !== false;
|
|
374
|
+
db.prepare(
|
|
375
|
+
`UPDATE ai_widgets SET enabled = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`
|
|
376
|
+
).run(newEnabled ? 1 : 0, widgetId, userId);
|
|
377
|
+
if (existingRow.scheduled_task_id && this.taskRuntime) {
|
|
378
|
+
await this.taskRuntime.updateTask(
|
|
379
|
+
existingRow.scheduled_task_id,
|
|
380
|
+
userId,
|
|
381
|
+
{ enabled: newEnabled },
|
|
382
|
+
{ allowManaged: true },
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return this.getWidget(userId, widgetId);
|
|
370
386
|
}
|
|
371
387
|
|
|
372
388
|
const current = this._serializeWidget(existingRow, null, []);
|
package/server/utils/logger.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
3
6
|
const SENSITIVE_KEY_RE = /(?:^|_|-)(?:token|secret|password|api[_-]?key|authorization|cookie|session(?:id)?|sid)(?:$|_|-)/i;
|
|
4
7
|
|
|
5
8
|
function redactSecrets(input) {
|
|
@@ -96,22 +99,57 @@ function logRequestSummary(level, req, message, extra = null) {
|
|
|
96
99
|
console[level](redactSecrets(`${prefix} ${message}`), summary);
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
function getLogFile() {
|
|
103
|
+
try {
|
|
104
|
+
const { DATA_DIR } = require('../../runtime/paths');
|
|
105
|
+
return path.join(DATA_DIR, 'server-logs.jsonl');
|
|
106
|
+
} catch {
|
|
107
|
+
return path.join(require('os').homedir(), '.neoagent', 'data', 'server-logs.jsonl');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function loadPersistedLogs(logFile) {
|
|
112
|
+
try {
|
|
113
|
+
const raw = fs.readFileSync(logFile, 'utf8');
|
|
114
|
+
const entries = [];
|
|
115
|
+
for (const line of raw.split('\n')) {
|
|
116
|
+
if (!line.trim()) continue;
|
|
117
|
+
try { entries.push(JSON.parse(line)); } catch {}
|
|
118
|
+
}
|
|
119
|
+
return entries.slice(-1000);
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
99
125
|
/**
|
|
100
|
-
* Intercepts console methods and broadcasts logs via Socket.IO
|
|
101
|
-
*
|
|
126
|
+
* Intercepts console methods and broadcasts logs via Socket.IO.
|
|
127
|
+
* Persists up to 1000 entries to disk across server restarts.
|
|
128
|
+
* @param {import('socket.io').Server} io
|
|
102
129
|
*/
|
|
103
130
|
function setupConsoleInterceptor(io) {
|
|
104
|
-
const
|
|
105
|
-
const
|
|
131
|
+
const MAX_LOG_HISTORY = 1000;
|
|
132
|
+
const TRIM_AT = 1100;
|
|
133
|
+
const logFile = getLogFile();
|
|
106
134
|
const allowLogHistoryRequests = String(process.env.NEOAGENT_ENABLE_LOG_HISTORY_REQUESTS || '').trim().toLowerCase() === 'true';
|
|
107
135
|
|
|
136
|
+
try { fs.mkdirSync(path.dirname(logFile), { recursive: true }); } catch {}
|
|
137
|
+
const logHistory = loadPersistedLogs(logFile);
|
|
138
|
+
|
|
108
139
|
function broadcastLog(type, args) {
|
|
109
140
|
const msg = formatLogArgs(args);
|
|
110
141
|
const logEntry = { type, message: msg, timestamp: new Date().toISOString() };
|
|
111
142
|
logHistory.push(logEntry);
|
|
112
|
-
if (logHistory.length > MAX_LOG_HISTORY) logHistory.shift();
|
|
113
143
|
|
|
114
|
-
|
|
144
|
+
try { fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n'); } catch {}
|
|
145
|
+
|
|
146
|
+
if (logHistory.length > TRIM_AT) {
|
|
147
|
+
logHistory.splice(0, logHistory.length - MAX_LOG_HISTORY);
|
|
148
|
+
try {
|
|
149
|
+
fs.writeFileSync(logFile, logHistory.map(e => JSON.stringify(e)).join('\n') + '\n');
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
|
|
115
153
|
for (const [, socket] of io.sockets.sockets) {
|
|
116
154
|
const uid = socket.request?.session?.userId;
|
|
117
155
|
if (uid) socket.emit('server:log', logEntry);
|