colana 1.0.0-beta.78 → 1.0.0-beta.79
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/public/app.js +34 -0
- package/public/index.html +6 -0
- package/server/config.js +3 -0
- package/server/index.js +28 -16
- package/server/server-control-routes.js +41 -3
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -2130,6 +2130,14 @@ function handleWebSocketMessage(message) {
|
|
|
2130
2130
|
}
|
|
2131
2131
|
renderPersonalStrip();
|
|
2132
2132
|
|
|
2133
|
+
// Show update banner if server already detected an available update
|
|
2134
|
+
if (message.updateInfo && message.updateInfo.updateAvailable) {
|
|
2135
|
+
if (!state.updateInfo || state.updateInfo.latestVersion !== message.updateInfo.latestVersion) {
|
|
2136
|
+
state.updateInfo = { ...message.updateInfo };
|
|
2137
|
+
}
|
|
2138
|
+
renderUpdateBanner();
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2133
2141
|
// Auto-start personal agent if setting enabled.
|
|
2134
2142
|
// Delay 5s (not 2s) so server-side auto-resume (3s) completes first.
|
|
2135
2143
|
// Debounce: clear any pending timer so only one auto-start attempt runs,
|
|
@@ -9286,6 +9294,8 @@ document.getElementById('btn-close-integration-modal')?.addEventListener('click'
|
|
|
9286
9294
|
async function loadServerStatus() {
|
|
9287
9295
|
const el = $('#server-status-info');
|
|
9288
9296
|
const restartBtn = $('#btn-server-restart');
|
|
9297
|
+
const serviceToggleBtn = $('#btn-service-toggle');
|
|
9298
|
+
const serviceToggleText = $('#btn-service-toggle-text');
|
|
9289
9299
|
if (!el) return;
|
|
9290
9300
|
try {
|
|
9291
9301
|
const info = await api('/server/status');
|
|
@@ -9304,6 +9314,28 @@ async function loadServerStatus() {
|
|
|
9304
9314
|
: (info?.restart?.message || 'Restart unavailable in manual mode');
|
|
9305
9315
|
}
|
|
9306
9316
|
|
|
9317
|
+
// Service toggle button
|
|
9318
|
+
if (serviceToggleBtn && serviceToggleText) {
|
|
9319
|
+
serviceToggleText.textContent = serviceInstalled ? 'Disable Auto-Start' : 'Enable Auto-Start';
|
|
9320
|
+
serviceToggleBtn.title = serviceInstalled
|
|
9321
|
+
? 'Remove background service (stop auto-starting on login)'
|
|
9322
|
+
: 'Install background service (auto-start on login)';
|
|
9323
|
+
serviceToggleBtn.onclick = async () => {
|
|
9324
|
+
const action = serviceInstalled ? 'uninstall' : 'install';
|
|
9325
|
+
serviceToggleBtn.disabled = true;
|
|
9326
|
+
serviceToggleText.textContent = serviceInstalled ? 'Removing...' : 'Installing...';
|
|
9327
|
+
try {
|
|
9328
|
+
const result = await api(`/server/service/${action}`, { method: 'POST' });
|
|
9329
|
+
showToast(result.message || `Service ${action} completed`, result.success ? 'success' : 'error');
|
|
9330
|
+
await loadServerStatus();
|
|
9331
|
+
} catch (err) {
|
|
9332
|
+
showToast(err.message || `Failed to ${action} service`, 'error');
|
|
9333
|
+
serviceToggleBtn.disabled = false;
|
|
9334
|
+
serviceToggleText.textContent = serviceInstalled ? 'Disable Auto-Start' : 'Enable Auto-Start';
|
|
9335
|
+
}
|
|
9336
|
+
};
|
|
9337
|
+
}
|
|
9338
|
+
|
|
9307
9339
|
const portBits = (info?.port && info?.preferredPort)
|
|
9308
9340
|
? ` · Port: <strong>${info.port}</strong>${info.fallbackPort ? ` (preferred <strong>${info.preferredPort}</strong> busy)` : ''}`
|
|
9309
9341
|
: '';
|
|
@@ -9311,9 +9343,11 @@ async function loadServerStatus() {
|
|
|
9311
9343
|
const serviceBits = ` · Service: <strong>${serviceInstalled ? (serviceRunning ? 'running' : 'installed, not running') : 'not installed'}</strong>`;
|
|
9312
9344
|
const restartBits = ` · Restart: <strong>${restartAvailable ? 'available' : 'blocked (manual mode)'}</strong>`;
|
|
9313
9345
|
|
|
9346
|
+
// All values come from server-controlled data (uptime, memory, client counts, port numbers) — safe for innerHTML
|
|
9314
9347
|
el.innerHTML = `Uptime: <strong>${uptimeStr}</strong> · Memory: <strong>${info.memory} MB</strong> · Clients: <strong>${info.connectedClients}</strong> · Active agents: <strong>${info.activeAgents}</strong>${serviceBits}${restartBits}${portBits}`;
|
|
9315
9348
|
} catch {
|
|
9316
9349
|
if (restartBtn) restartBtn.disabled = true;
|
|
9350
|
+
if (serviceToggleBtn) serviceToggleBtn.disabled = true;
|
|
9317
9351
|
el.textContent = 'Could not fetch server status';
|
|
9318
9352
|
}
|
|
9319
9353
|
}
|
package/public/index.html
CHANGED
|
@@ -976,6 +976,12 @@
|
|
|
976
976
|
</svg>
|
|
977
977
|
Restart Server
|
|
978
978
|
</button>
|
|
979
|
+
<button type="button" class="btn btn-secondary" id="btn-service-toggle" title="Install or remove background service (auto-start on login)">
|
|
980
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: -2px;">
|
|
981
|
+
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
|
|
982
|
+
</svg>
|
|
983
|
+
<span id="btn-service-toggle-text">Enable Auto-Start</span>
|
|
984
|
+
</button>
|
|
979
985
|
</div>
|
|
980
986
|
<p class="form-hint">Restart clears cached state and reconnects all clients. Available when co:lana runs as an installed background service.</p>
|
|
981
987
|
</div>
|
package/server/config.js
CHANGED
|
@@ -233,6 +233,9 @@ const config = {
|
|
|
233
233
|
toolResultPreviewLen: envInt(process.env.TOOL_RESULT_PREVIEW_LEN, 500),
|
|
234
234
|
maxResumableSessions: envInt(process.env.MAX_RESUMABLE_SESSIONS, 5),
|
|
235
235
|
|
|
236
|
+
// --- Update Checker ---
|
|
237
|
+
updateCheckIntervalMs: envInt(process.env.UPDATE_CHECK_INTERVAL_MS, 6 * 60 * 60 * 1000), // 6 hours
|
|
238
|
+
|
|
236
239
|
// --- Log Retention ---
|
|
237
240
|
logRetentionDays: envInt(process.env.LOG_RETENTION_DAYS, 0),
|
|
238
241
|
logRetentionIntervalMs: envInt(process.env.LOG_RETENTION_INTERVAL_MS, 24 * 60 * 60 * 1000),
|
package/server/index.js
CHANGED
|
@@ -58,7 +58,7 @@ import { createWizardRoutes } from './wizard-routes.js';
|
|
|
58
58
|
import { checkForUpdate } from './updater.js';
|
|
59
59
|
import { getSystemInfo } from './system-info.js';
|
|
60
60
|
import { autoResumeInterruptedSessions } from './auto-resume.js';
|
|
61
|
-
import { getServiceStatus } from './service-manager.js';
|
|
61
|
+
import { getServiceStatus, installService, uninstallService } from './service-manager.js';
|
|
62
62
|
import { registerServerControlRoutes } from './server-control-routes.js';
|
|
63
63
|
import { registerPersonalAgentRoutes } from './personal-agent-routes.js';
|
|
64
64
|
import { registerIntegrationRoutes } from './integration-routes.js';
|
|
@@ -117,6 +117,7 @@ let runtimePort = null;
|
|
|
117
117
|
let preferredPort = config.startPort;
|
|
118
118
|
let isFallbackPort = false;
|
|
119
119
|
let runtimeServiceManaged = false;
|
|
120
|
+
let _lastUpdateCheck = null;
|
|
120
121
|
|
|
121
122
|
// --- M8: Per-IP WebSocket connection limit ---
|
|
122
123
|
const WS_MAX_PER_IP = parseInt(process.env.WS_MAX_CONNECTIONS_PER_IP, 10) || 10;
|
|
@@ -217,6 +218,23 @@ if (ptyManager) {
|
|
|
217
218
|
ptyManager.setBroadcastCallback(broadcastFn);
|
|
218
219
|
}
|
|
219
220
|
|
|
221
|
+
// --- Update checker (cached + periodic) ---
|
|
222
|
+
let _updateCheckerInterval = null;
|
|
223
|
+
function startUpdateChecker() {
|
|
224
|
+
if (_updateCheckerInterval) return; // already running — guard against duplicate calls from port-retry path
|
|
225
|
+
const runCheck = () => {
|
|
226
|
+
checkForUpdate().then(update => {
|
|
227
|
+
_lastUpdateCheck = update;
|
|
228
|
+
if (update.updateAvailable) {
|
|
229
|
+
console.log(` ${c.info}Update available: v${update.latestVersion}${c.reset}${update.allowed ? '' : ' (subscription expired)'}`);
|
|
230
|
+
broadcastFn({ eventType: 'updateAvailable', ...update });
|
|
231
|
+
}
|
|
232
|
+
}).catch(() => {});
|
|
233
|
+
};
|
|
234
|
+
runCheck();
|
|
235
|
+
_updateCheckerInterval = setInterval(runCheck, config.updateCheckIntervalMs);
|
|
236
|
+
}
|
|
237
|
+
|
|
220
238
|
// Dashboard WebSocket connection handling
|
|
221
239
|
wss.on('connection', async (ws) => {
|
|
222
240
|
console.log('WebSocket client connected');
|
|
@@ -272,6 +290,7 @@ wss.on('connection', async (ws) => {
|
|
|
272
290
|
},
|
|
273
291
|
systemInfo,
|
|
274
292
|
personalAgentStatus,
|
|
293
|
+
updateInfo: _lastUpdateCheck,
|
|
275
294
|
}));
|
|
276
295
|
} catch (e) { /* client disconnected before init could be sent */ }
|
|
277
296
|
|
|
@@ -623,6 +642,9 @@ registerServerControlRoutes(app, {
|
|
|
623
642
|
fallbackPort: isFallbackPort,
|
|
624
643
|
serviceManagedRuntime: runtimeServiceManaged,
|
|
625
644
|
}),
|
|
645
|
+
sensitiveLimiter,
|
|
646
|
+
installServiceFn: installService,
|
|
647
|
+
uninstallServiceFn: uninstallService,
|
|
626
648
|
});
|
|
627
649
|
|
|
628
650
|
// ============================================
|
|
@@ -640,11 +662,12 @@ app.use((err, _req, res, _next) => {
|
|
|
640
662
|
function shutdown(signal) {
|
|
641
663
|
console.log(`\n${signal} received. Shutting down gracefully...`);
|
|
642
664
|
|
|
643
|
-
// Stop heartbeat intervals
|
|
665
|
+
// Stop heartbeat and background intervals
|
|
644
666
|
clearInterval(wsHeartbeatInterval);
|
|
645
667
|
clearInterval(terminalHeartbeatInterval);
|
|
646
668
|
if (logRetentionInterval) clearInterval(logRetentionInterval);
|
|
647
669
|
if (logRetentionTimeout) clearTimeout(logRetentionTimeout);
|
|
670
|
+
if (_updateCheckerInterval) clearInterval(_updateCheckerInterval);
|
|
648
671
|
|
|
649
672
|
// Track app_stopped and stop analytics scheduler
|
|
650
673
|
try { trackEvent('app_stopped', { uptimeSeconds: Math.round(process.uptime()) }); } catch { /* best-effort */ }
|
|
@@ -1050,12 +1073,7 @@ async function startServer() {
|
|
|
1050
1073
|
// Background tasks
|
|
1051
1074
|
scheduleLogRetention();
|
|
1052
1075
|
backgroundRevalidate().catch(() => {});
|
|
1053
|
-
|
|
1054
|
-
if (update.updateAvailable) {
|
|
1055
|
-
console.log(` ${c.info}Update available: v${update.latestVersion}${c.reset}${update.allowed ? '' : ' (subscription expired)'}`);
|
|
1056
|
-
broadcastFn({ eventType: 'updateAvailable', ...update });
|
|
1057
|
-
}
|
|
1058
|
-
}).catch(() => {});
|
|
1076
|
+
startUpdateChecker();
|
|
1059
1077
|
|
|
1060
1078
|
// Auto-start OpenClaw gateway (same as primary listen block)
|
|
1061
1079
|
if (ptyManager) {
|
|
@@ -1132,14 +1150,8 @@ async function startServer() {
|
|
|
1132
1150
|
trackEvent('license_tier', { tier: startupLicense.tier || 'free' });
|
|
1133
1151
|
} catch { /* best-effort */ }
|
|
1134
1152
|
|
|
1135
|
-
// Check for updates (non-blocking)
|
|
1136
|
-
|
|
1137
|
-
if (update.updateAvailable) {
|
|
1138
|
-
console.log(` ${c.info}Update available: v${update.latestVersion}${c.reset}${update.allowed ? '' : ' (subscription expired)'}`);
|
|
1139
|
-
// Broadcast to connected clients
|
|
1140
|
-
broadcastFn({ eventType: 'updateAvailable', ...update });
|
|
1141
|
-
}
|
|
1142
|
-
}).catch(() => {});
|
|
1153
|
+
// Check for updates (non-blocking, cached + periodic)
|
|
1154
|
+
startUpdateChecker();
|
|
1143
1155
|
|
|
1144
1156
|
// Auto-start OpenClaw gateway so the Personal Agent is immediately available
|
|
1145
1157
|
if (ptyManager) {
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { getServiceStatus } from './service-manager.js';
|
|
2
2
|
import { evaluateRestartMode } from './runtime-policy.js';
|
|
3
|
+
import logger from './logger.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Register server control endpoints (restart + status).
|
|
6
|
+
* Register server control endpoints (restart + status + service management).
|
|
6
7
|
*
|
|
7
8
|
* @param {import('express').Express} app
|
|
8
9
|
* @param {{
|
|
9
10
|
* onRestart: (signal: string) => void,
|
|
10
11
|
* getStatusSnapshot: () => Record<string, unknown>,
|
|
11
12
|
* getServiceStatusFn?: () => any,
|
|
12
|
-
* allowManualRestart?: boolean
|
|
13
|
+
* allowManualRestart?: boolean,
|
|
14
|
+
* sensitiveLimiter?: import('express').RequestHandler,
|
|
15
|
+
* installServiceFn?: () => { success: boolean, message: string },
|
|
16
|
+
* uninstallServiceFn?: () => { success: boolean, message: string },
|
|
13
17
|
* }} deps
|
|
14
18
|
*/
|
|
15
19
|
export function registerServerControlRoutes(app, deps) {
|
|
@@ -18,6 +22,9 @@ export function registerServerControlRoutes(app, deps) {
|
|
|
18
22
|
getStatusSnapshot,
|
|
19
23
|
getServiceStatusFn = getServiceStatus,
|
|
20
24
|
allowManualRestart = process.env.COLANA_ALLOW_MANUAL_RESTART === 'true',
|
|
25
|
+
sensitiveLimiter,
|
|
26
|
+
installServiceFn,
|
|
27
|
+
uninstallServiceFn,
|
|
21
28
|
} = deps;
|
|
22
29
|
|
|
23
30
|
if (typeof onRestart !== 'function') {
|
|
@@ -27,6 +34,8 @@ export function registerServerControlRoutes(app, deps) {
|
|
|
27
34
|
throw new Error('registerServerControlRoutes: getStatusSnapshot must be a function');
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
const limiterMiddleware = sensitiveLimiter || ((req, res, next) => next());
|
|
38
|
+
|
|
30
39
|
app.post('/api/server/restart', (req, res) => {
|
|
31
40
|
let service = null;
|
|
32
41
|
try {
|
|
@@ -84,9 +93,38 @@ export function registerServerControlRoutes(app, deps) {
|
|
|
84
93
|
},
|
|
85
94
|
});
|
|
86
95
|
});
|
|
96
|
+
|
|
97
|
+
// --- Service install/uninstall (OS-level auto-start) ---
|
|
98
|
+
|
|
99
|
+
app.post('/api/server/service/install', limiterMiddleware, (req, res) => {
|
|
100
|
+
if (typeof installServiceFn !== 'function') {
|
|
101
|
+
return res.status(501).json({ success: false, error: 'Service management not available' });
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const result = installServiceFn();
|
|
105
|
+
logger.info('server', 'Service install requested', { success: result.success });
|
|
106
|
+
res.json(result);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.error('server', 'Service install failed', { error: err.message });
|
|
109
|
+
res.status(500).json({ success: false, error: err.message || 'Service install failed' });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
app.post('/api/server/service/uninstall', limiterMiddleware, (req, res) => {
|
|
114
|
+
if (typeof uninstallServiceFn !== 'function') {
|
|
115
|
+
return res.status(501).json({ success: false, error: 'Service management not available' });
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const result = uninstallServiceFn();
|
|
119
|
+
logger.info('server', 'Service uninstall requested', { success: result.success });
|
|
120
|
+
res.json(result);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
logger.error('server', 'Service uninstall failed', { error: err.message });
|
|
123
|
+
res.status(500).json({ success: false, error: err.message || 'Service uninstall failed' });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
87
126
|
}
|
|
88
127
|
|
|
89
128
|
export default {
|
|
90
129
|
registerServerControlRoutes,
|
|
91
130
|
};
|
|
92
|
-
|