neoagent 1.0.1 → 1.1.0
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/package.json +1 -1
- package/server/public/app.html +25 -1
- package/server/public/css/styles.css +104 -1
- package/server/public/js/app.js +117 -18
- package/server/routes/settings.js +35 -4
package/package.json
CHANGED
package/server/public/app.html
CHANGED
|
@@ -537,6 +537,30 @@
|
|
|
537
537
|
<!-- Rendered dynamically by app.js -->
|
|
538
538
|
</div>
|
|
539
539
|
</div>
|
|
540
|
+
<div class="settings-update-panel" id="settingsUpdatePanel">
|
|
541
|
+
<div class="settings-update-head">
|
|
542
|
+
<div class="settings-update-title">App Update</div>
|
|
543
|
+
<span class="badge badge-neutral" id="updateStateBadge">Idle</span>
|
|
544
|
+
</div>
|
|
545
|
+
<div class="settings-update-progress-wrap">
|
|
546
|
+
<div class="settings-update-progress-bar" id="updateProgressBar"></div>
|
|
547
|
+
</div>
|
|
548
|
+
<div class="settings-update-row">
|
|
549
|
+
<span id="updatePhaseLabel">No update running</span>
|
|
550
|
+
<span id="updatePercentLabel">0%</span>
|
|
551
|
+
</div>
|
|
552
|
+
<div class="settings-update-meta" id="updateVersionMeta">Version: —</div>
|
|
553
|
+
<div class="settings-update-section">
|
|
554
|
+
<div class="settings-update-label">Changelog</div>
|
|
555
|
+
<ul class="settings-update-changelog" id="updateChangelog">
|
|
556
|
+
<li class="settings-update-empty">No changes yet</li>
|
|
557
|
+
</ul>
|
|
558
|
+
</div>
|
|
559
|
+
<div class="settings-update-section">
|
|
560
|
+
<div class="settings-update-label">Live Output</div>
|
|
561
|
+
<pre class="settings-update-logs" id="updateLogs">Waiting for update job output…</pre>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
540
564
|
</div>
|
|
541
565
|
<div class="modal-footer" style="justify-content: space-between;">
|
|
542
566
|
<button class="btn btn-primary" id="updateAppBtn" style="background-color: var(--color-warning);">Update
|
|
@@ -556,4 +580,4 @@
|
|
|
556
580
|
<script src="/js/app.js"></script>
|
|
557
581
|
</body>
|
|
558
582
|
|
|
559
|
-
</html>
|
|
583
|
+
</html>
|
|
@@ -325,6 +325,110 @@ button, input, textarea, select {
|
|
|
325
325
|
border-top: 1px solid var(--border);
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
.settings-update-panel {
|
|
329
|
+
margin-top: 18px;
|
|
330
|
+
border: 1px solid var(--border);
|
|
331
|
+
border-radius: 12px;
|
|
332
|
+
padding: 14px;
|
|
333
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.settings-update-head {
|
|
337
|
+
display: flex;
|
|
338
|
+
align-items: center;
|
|
339
|
+
justify-content: space-between;
|
|
340
|
+
gap: 12px;
|
|
341
|
+
margin-bottom: 10px;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.settings-update-title {
|
|
345
|
+
font-size: 12px;
|
|
346
|
+
font-weight: 700;
|
|
347
|
+
letter-spacing: .6px;
|
|
348
|
+
text-transform: uppercase;
|
|
349
|
+
color: var(--text-secondary);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.settings-update-progress-wrap {
|
|
353
|
+
width: 100%;
|
|
354
|
+
height: 8px;
|
|
355
|
+
border-radius: 999px;
|
|
356
|
+
background: rgba(255,255,255,.08);
|
|
357
|
+
overflow: hidden;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.settings-update-progress-bar {
|
|
361
|
+
height: 100%;
|
|
362
|
+
width: 0%;
|
|
363
|
+
background: linear-gradient(90deg, #22c55e, #06b6d4);
|
|
364
|
+
border-radius: inherit;
|
|
365
|
+
transition: width 240ms ease;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.settings-update-row {
|
|
369
|
+
display: flex;
|
|
370
|
+
justify-content: space-between;
|
|
371
|
+
align-items: center;
|
|
372
|
+
gap: 10px;
|
|
373
|
+
margin-top: 8px;
|
|
374
|
+
font-size: 12px;
|
|
375
|
+
color: var(--text-secondary);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.settings-update-meta {
|
|
379
|
+
margin-top: 6px;
|
|
380
|
+
font-family: 'JetBrains Mono', monospace;
|
|
381
|
+
font-size: 11px;
|
|
382
|
+
color: var(--text-muted);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.settings-update-section {
|
|
386
|
+
margin-top: 12px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.settings-update-label {
|
|
390
|
+
font-size: 11px;
|
|
391
|
+
font-weight: 600;
|
|
392
|
+
letter-spacing: .4px;
|
|
393
|
+
text-transform: uppercase;
|
|
394
|
+
color: var(--text-secondary);
|
|
395
|
+
margin-bottom: 6px;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.settings-update-changelog {
|
|
399
|
+
margin: 0;
|
|
400
|
+
padding-left: 18px;
|
|
401
|
+
max-height: 110px;
|
|
402
|
+
overflow-y: auto;
|
|
403
|
+
font-size: 12px;
|
|
404
|
+
color: var(--text-primary);
|
|
405
|
+
display: flex;
|
|
406
|
+
flex-direction: column;
|
|
407
|
+
gap: 4px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.settings-update-empty {
|
|
411
|
+
list-style: none;
|
|
412
|
+
margin-left: -18px;
|
|
413
|
+
color: var(--text-muted);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.settings-update-logs {
|
|
417
|
+
margin: 0;
|
|
418
|
+
padding: 10px;
|
|
419
|
+
background: rgba(5,10,18,.8);
|
|
420
|
+
border: 1px solid var(--border);
|
|
421
|
+
border-radius: 8px;
|
|
422
|
+
font-family: 'JetBrains Mono', monospace;
|
|
423
|
+
font-size: 11px;
|
|
424
|
+
line-height: 1.45;
|
|
425
|
+
color: #cfd7ff;
|
|
426
|
+
max-height: 140px;
|
|
427
|
+
overflow-y: auto;
|
|
428
|
+
white-space: pre-wrap;
|
|
429
|
+
word-break: break-word;
|
|
430
|
+
}
|
|
431
|
+
|
|
328
432
|
@keyframes modalIn {
|
|
329
433
|
from { transform: translateY(14px) scale(.96); opacity: 0; }
|
|
330
434
|
to { transform: translateY(0) scale(1); opacity: 1; }
|
|
@@ -469,4 +573,3 @@ button, input, textarea, select {
|
|
|
469
573
|
}
|
|
470
574
|
|
|
471
575
|
.animate-in { animation: fadeSlideUp 200ms cubic-bezier(.16,1,.3,1) both; }
|
|
472
|
-
|
package/server/public/js/app.js
CHANGED
|
@@ -1176,6 +1176,108 @@ if (copyLogsBtn) {
|
|
|
1176
1176
|
|
|
1177
1177
|
// ── Settings ──
|
|
1178
1178
|
|
|
1179
|
+
let updateStatusPollTimer = null;
|
|
1180
|
+
let updateFinishNotifiedAt = null;
|
|
1181
|
+
|
|
1182
|
+
function clearUpdatePoll() {
|
|
1183
|
+
if (updateStatusPollTimer) {
|
|
1184
|
+
clearInterval(updateStatusPollTimer);
|
|
1185
|
+
updateStatusPollTimer = null;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function setUpdateBadgeState(state) {
|
|
1190
|
+
const badge = $("#updateStateBadge");
|
|
1191
|
+
if (!badge) return;
|
|
1192
|
+
|
|
1193
|
+
badge.classList.remove("badge-neutral", "badge-info", "badge-success", "badge-error", "badge-warning");
|
|
1194
|
+
if (state === "running") {
|
|
1195
|
+
badge.classList.add("badge-info");
|
|
1196
|
+
badge.textContent = "Running";
|
|
1197
|
+
} else if (state === "completed") {
|
|
1198
|
+
badge.classList.add("badge-success");
|
|
1199
|
+
badge.textContent = "Completed";
|
|
1200
|
+
} else if (state === "failed") {
|
|
1201
|
+
badge.classList.add("badge-error");
|
|
1202
|
+
badge.textContent = "Failed";
|
|
1203
|
+
} else {
|
|
1204
|
+
badge.classList.add("badge-neutral");
|
|
1205
|
+
badge.textContent = "Idle";
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function renderUpdateStatus(status) {
|
|
1210
|
+
const state = status?.state || "idle";
|
|
1211
|
+
const progress = Math.max(0, Math.min(100, Number(status?.progress || 0)));
|
|
1212
|
+
|
|
1213
|
+
setUpdateBadgeState(state);
|
|
1214
|
+
$("#updateProgressBar").style.width = `${progress}%`;
|
|
1215
|
+
$("#updatePercentLabel").textContent = `${progress}%`;
|
|
1216
|
+
$("#updatePhaseLabel").textContent = status?.message || "No update running";
|
|
1217
|
+
|
|
1218
|
+
const before = status?.versionBefore || "—";
|
|
1219
|
+
const after = status?.versionAfter || "—";
|
|
1220
|
+
$("#updateVersionMeta").textContent = `Version: ${before}${after !== "—" ? ` -> ${after}` : ""}`;
|
|
1221
|
+
|
|
1222
|
+
const changelog = $("#updateChangelog");
|
|
1223
|
+
changelog.innerHTML = "";
|
|
1224
|
+
const entries = Array.isArray(status?.changelog) ? status.changelog : [];
|
|
1225
|
+
if (!entries.length) {
|
|
1226
|
+
const li = document.createElement("li");
|
|
1227
|
+
li.className = "settings-update-empty";
|
|
1228
|
+
li.textContent = "No commit changes captured";
|
|
1229
|
+
changelog.appendChild(li);
|
|
1230
|
+
} else {
|
|
1231
|
+
for (const line of entries) {
|
|
1232
|
+
const li = document.createElement("li");
|
|
1233
|
+
li.textContent = line;
|
|
1234
|
+
changelog.appendChild(li);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const logs = Array.isArray(status?.logs) ? status.logs : [];
|
|
1239
|
+
const logsText = logs.length ? logs.slice(-120).join("\n") : "Waiting for update job output…";
|
|
1240
|
+
const logsEl = $("#updateLogs");
|
|
1241
|
+
logsEl.textContent = logsText;
|
|
1242
|
+
logsEl.scrollTop = logsEl.scrollHeight;
|
|
1243
|
+
|
|
1244
|
+
const btn = $("#updateAppBtn");
|
|
1245
|
+
if (state === "running") {
|
|
1246
|
+
btn.disabled = true;
|
|
1247
|
+
btn.textContent = "Updating…";
|
|
1248
|
+
} else {
|
|
1249
|
+
btn.disabled = false;
|
|
1250
|
+
btn.textContent = "Update App";
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if ((state === "completed" || state === "failed") && status?.completedAt && updateFinishNotifiedAt !== status.completedAt) {
|
|
1254
|
+
updateFinishNotifiedAt = status.completedAt;
|
|
1255
|
+
toast(state === "completed" ? "Update completed." : "Update failed. See logs in Settings.", state === "completed" ? "success" : "error");
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
async function refreshUpdateStatus() {
|
|
1260
|
+
try {
|
|
1261
|
+
const status = await api("/settings/update/status");
|
|
1262
|
+
renderUpdateStatus(status);
|
|
1263
|
+
|
|
1264
|
+
if (status?.state !== "running") {
|
|
1265
|
+
clearUpdatePoll();
|
|
1266
|
+
}
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
// During restart window, polling can fail briefly; keep trying.
|
|
1269
|
+
$("#updatePhaseLabel").textContent = "Reconnecting to server…";
|
|
1270
|
+
setUpdateBadgeState("running");
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function ensureUpdatePolling(force = false) {
|
|
1275
|
+
if (force) clearUpdatePoll();
|
|
1276
|
+
if (!updateStatusPollTimer) {
|
|
1277
|
+
updateStatusPollTimer = setInterval(refreshUpdateStatus, 1800);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1179
1281
|
$("#settingsBtn").addEventListener("click", async () => {
|
|
1180
1282
|
try {
|
|
1181
1283
|
const meta = await api("/settings/meta/models");
|
|
@@ -1251,15 +1353,19 @@ $("#settingsBtn").addEventListener("click", async () => {
|
|
|
1251
1353
|
console.error("Failed to load settings:", err);
|
|
1252
1354
|
$("#settingHeadlessBrowser").checked = true; // default headless
|
|
1253
1355
|
}
|
|
1356
|
+
await refreshUpdateStatus();
|
|
1357
|
+
ensureUpdatePolling(true);
|
|
1254
1358
|
$("#settingsModal").classList.remove("hidden");
|
|
1255
1359
|
});
|
|
1256
1360
|
|
|
1257
|
-
$("#closeSettings").addEventListener("click", () =>
|
|
1258
|
-
|
|
1259
|
-
);
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
);
|
|
1361
|
+
$("#closeSettings").addEventListener("click", () => {
|
|
1362
|
+
clearUpdatePoll();
|
|
1363
|
+
$("#settingsModal").classList.add("hidden");
|
|
1364
|
+
});
|
|
1365
|
+
$("#cancelSettings").addEventListener("click", () => {
|
|
1366
|
+
clearUpdatePoll();
|
|
1367
|
+
$("#settingsModal").classList.add("hidden");
|
|
1368
|
+
});
|
|
1263
1369
|
|
|
1264
1370
|
$("#saveSettings").addEventListener("click", async () => {
|
|
1265
1371
|
try {
|
|
@@ -1301,21 +1407,14 @@ $("#saveSettings").addEventListener("click", async () => {
|
|
|
1301
1407
|
$("#updateAppBtn").addEventListener("click", async () => {
|
|
1302
1408
|
if (!confirm("Are you sure you want to run the update script? This will trigger neoagent update and restart the server.")) return;
|
|
1303
1409
|
try {
|
|
1304
|
-
const btn = $("#updateAppBtn");
|
|
1305
|
-
btn.disabled = true;
|
|
1306
|
-
btn.textContent = "Updating...";
|
|
1307
1410
|
await api("/settings/update", { method: "POST" });
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
btn.disabled = false;
|
|
1313
|
-
btn.textContent = "Update App";
|
|
1314
|
-
}, 10000);
|
|
1411
|
+
updateFinishNotifiedAt = null;
|
|
1412
|
+
toast("Update started. Live progress is shown below.", "success");
|
|
1413
|
+
await refreshUpdateStatus();
|
|
1414
|
+
ensureUpdatePolling(true);
|
|
1315
1415
|
} catch (err) {
|
|
1316
1416
|
toast("Failed to trigger update: " + err.message, "error");
|
|
1317
|
-
|
|
1318
|
-
$("#updateAppBtn").textContent = "Update App";
|
|
1417
|
+
await refreshUpdateStatus();
|
|
1319
1418
|
}
|
|
1320
1419
|
});
|
|
1321
1420
|
|
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
2
4
|
const router = express.Router();
|
|
3
5
|
const db = require('../db/database');
|
|
4
6
|
const { requireAuth } = require('../middleware/auth');
|
|
5
7
|
|
|
6
8
|
router.use(requireAuth);
|
|
7
9
|
|
|
10
|
+
const UPDATE_STATUS_FILE = path.join(process.cwd(), 'data', 'update-status.json');
|
|
11
|
+
|
|
12
|
+
function readUpdateStatus() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(UPDATE_STATUS_FILE, 'utf8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return {
|
|
17
|
+
state: 'idle',
|
|
18
|
+
progress: 0,
|
|
19
|
+
phase: 'idle',
|
|
20
|
+
message: 'No update running',
|
|
21
|
+
startedAt: null,
|
|
22
|
+
completedAt: null,
|
|
23
|
+
versionBefore: null,
|
|
24
|
+
versionAfter: null,
|
|
25
|
+
changelog: [],
|
|
26
|
+
logs: []
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
8
31
|
// Get supported models metadata
|
|
9
32
|
router.get('/meta/models', (req, res) => {
|
|
10
33
|
const { SUPPORTED_MODELS } = require('../services/ai/models');
|
|
@@ -82,17 +105,25 @@ router.delete('/:key', (req, res) => {
|
|
|
82
105
|
// Trigger auto-update script
|
|
83
106
|
router.post('/update', (req, res) => {
|
|
84
107
|
const { spawn } = require('child_process');
|
|
85
|
-
|
|
108
|
+
const status = readUpdateStatus();
|
|
109
|
+
if (status.state === 'running') {
|
|
110
|
+
return res.status(409).json({ success: false, error: 'An update is already running' });
|
|
111
|
+
}
|
|
112
|
+
console.log('[Settings] Triggering update-runner...');
|
|
86
113
|
|
|
87
|
-
// Spawn
|
|
88
|
-
const child = spawn(process.execPath, ['
|
|
114
|
+
// Spawn detached runner so status survives server restarts.
|
|
115
|
+
const child = spawn(process.execPath, ['scripts/update-runner.js'], {
|
|
89
116
|
detached: true,
|
|
90
117
|
stdio: 'ignore',
|
|
91
118
|
cwd: process.cwd()
|
|
92
119
|
});
|
|
93
120
|
|
|
94
121
|
child.unref();
|
|
95
|
-
res.json({ success: true, message: 'Update triggered' });
|
|
122
|
+
res.json({ success: true, message: 'Update triggered', pid: child.pid });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
router.get('/update/status', (req, res) => {
|
|
126
|
+
res.json(readUpdateStatus());
|
|
96
127
|
});
|
|
97
128
|
|
|
98
129
|
module.exports = router;
|