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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -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
-
@@ -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
- $("#settingsModal").classList.add("hidden"),
1259
- );
1260
- $("#cancelSettings").addEventListener("click", () =>
1261
- $("#settingsModal").classList.add("hidden"),
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
- toast("Update started! Please wait for the application to restart.", "success");
1309
- $("#settingsModal").classList.add("hidden");
1310
- // Give it a few seconds before resetting the UI locally just in case
1311
- setTimeout(() => {
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
- $("#updateAppBtn").disabled = false;
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
- console.log('[Settings] Triggering neoagent update...');
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 the update command in detached mode so it survives the node process exiting
88
- const child = spawn(process.execPath, ['bin/neoagent.js', 'update'], {
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;