Skip to content
4 changes: 2 additions & 2 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ <h3 style="margin-top:0">About</h3>
</div>
<div class="info-row">
<span class="info-label">Environment</span>
<span class="info-val">Node.js (Systemd)</span>
<span class="info-val" id="runtime-env">Node.js (Systemd)</span>
</div>
<div class="info-row">
<span class="info-label">Server Timezone</span>
Expand Down Expand Up @@ -451,4 +451,4 @@ <h3 style="margin-top:0">About</h3>
<script src="js/dashboard.js" defer></script>
</body>

</html>
</html>
59 changes: 49 additions & 10 deletions public/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@
return res;
}

function isUnsupportedMetric(data, key) {
return Array.isArray(data.unsupportedMonitoring) && data.unsupportedMonitoring.includes(key);
}

async function readErrorMessage(res, fallbackMessage) {
try {
const data = await res.json();
if (data && typeof data.error === 'string' && data.error.trim()) return data.error;
if (data && typeof data.message === 'string' && data.message.trim()) return data.message;
} catch (e) { }
return fallbackMessage;
}

function setMetricValue(id, value, fallback = 'N/A') {
document.getElementById(id).innerText = value == null ? fallback : value;
}

// --- TAB MANAGEMENT ---
let currentTab = 'home';
let cronInterval = null;
Expand Down Expand Up @@ -169,25 +186,32 @@
const res = await fetchAuth(API + '/status');
const data = await res.json();

document.getElementById('cpu-val').innerText = data.cpu + '%';
document.getElementById('mem-val').innerText = data.mem + '%';
if (data.disk) document.getElementById('disk-val').innerText = data.disk;
setMetricValue('cpu-val', isUnsupportedMetric(data, 'cpu') ? 'N/A' : (data.cpu == null ? '--%' : data.cpu + '%'));
setMetricValue('mem-val', isUnsupportedMetric(data, 'mem') ? 'N/A' : (data.mem == null ? '--%' : data.mem + '%'));
setMetricValue('disk-val', isUnsupportedMetric(data, 'disk') ? 'N/A' : (data.disk == null ? '--%' : data.disk));
if (data.timezone) document.getElementById('server-tz').innerText = data.timezone;
if (data.environment && typeof data.environment.isDocker === 'boolean') {
document.getElementById('runtime-env').innerText = data.environment.isDocker ? 'Docker Mode' : 'Node.js (Systemd)';
}
if (data.versions) {
document.getElementById('ver-core').innerText = data.versions.core;
document.getElementById('ver-num').innerText = 'v' + data.versions.dashboard;
}

// Update PID
if (data.gatewayPid) {
if (isUnsupportedMetric(data, 'gatewayPid')) {
document.getElementById('gateway-pid').innerText = 'N/A in Docker Mode';
} else if (data.gatewayPid) {
document.getElementById('gateway-pid').innerText = data.gatewayPid;
} else {
document.getElementById('gateway-pid').innerText = 'Stopped / Not Found';
}

// Update Scripts List
const scriptList = document.getElementById('running-scripts-list');
if (data.scripts && data.scripts.length > 0) {
if (isUnsupportedMetric(data, 'scripts')) {
scriptList.innerHTML = '<div style="opacity:0.7; text-align:center;">Unavailable in Docker Mode</div>';
} else if (data.scripts && data.scripts.length > 0) {
const items = data.scripts.map(s =>
`<div style="display:flex; justify-content:space-between; border-bottom:1px solid rgba(255,255,255,0.05); padding:2px 0;">
<span>${s.name}</span>
Expand Down Expand Up @@ -339,12 +363,18 @@
async function fetchJobs() {
try {
const res = await fetchAuth(API + '/cron');
const jobs = await res.json();
const data = await res.json();
const jobs = Array.isArray(data) ? data : (Array.isArray(data.jobs) ? data.jobs : []);
jobs.sort((a, b) => (b.state?.lastRunAtMs || 0) - (a.state?.lastRunAtMs || 0));

const container = document.getElementById('job-list');
container.innerHTML = '';

if (data && data.dockerMode === true) {
container.innerHTML = '<div style="text-align:center; opacity:0.7; padding:20px;">Unavailable in Docker Mode</div>';
return;
}

if (jobs.length === 0) {
container.innerHTML = '<div style="text-align:center; opacity:0.5; padding:20px;">No jobs found</div>';
return;
Expand Down Expand Up @@ -406,18 +436,28 @@

async function runJob(id) {
if (!confirm('Execute task?')) return;
await fetchAuth(API + '/run/' + id, { method: 'POST' });
const res = await fetchAuth(API + '/run/' + id, { method: 'POST' });
if (!res.ok) {
alert(await readErrorMessage(res, 'Failed to run job.'));
return;
}
setTimeout(fetchJobs, 2000);
}

async function killAll() {
if (!confirm('⚠️ STOP ALL SCRIPTS?')) return;
await fetchAuth(API + '/kill', { method: 'POST' });
const res = await fetchAuth(API + '/kill', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ confirm: true }) });
if (!res.ok) {
alert(await readErrorMessage(res, 'Failed to stop scripts.'));
}
}

async function restartGateway() {
if (!confirm('♻️ RESTART GATEWAY?')) return;
await fetchAuth(API + '/gateway/restart', { method: 'POST' });
const res = await fetchAuth(API + '/gateway/restart', { method: 'POST' });
if (!res.ok) {
alert(await readErrorMessage(res, 'Failed to restart gateway.'));
}
}

async function refreshTokenStats() {
Expand Down Expand Up @@ -674,4 +714,3 @@
}
return 0;
}

4 changes: 3 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const TOKEN_FILE = path.join(APP_DIR, 'data/token_stats/latest.json');
const ID_FILE = path.join(APP_DIR, 'data/last_id.txt');
const ANALYZE_SCRIPT = path.join(APP_DIR, 'scripts/analyze.js');

// --- Constants ---
// --- Constants & Environment Flags ---
const IS_DOCKER = fs.existsSync('/.dockerenv') || ['true', '1', 'yes'].includes((process.env.IS_DOCKER || '').toLowerCase());
const CACHE_TTL_MS = 60000; // 60s cache

module.exports = {
Expand All @@ -40,4 +41,5 @@ module.exports = {
ID_FILE,
ANALYZE_SCRIPT,
CACHE_TTL_MS,
IS_DOCKER,
};
11 changes: 8 additions & 3 deletions src/routes/cron.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const router = require('express').Router();
const { exec, execFile } = require('child_process');
const path = require('path');
const fs = require('fs');
const { HOME_DIR, STATE_DIR } = require('../config');
const { HOME_DIR, STATE_DIR, IS_DOCKER } = require('../config');
const { getOpenClawCommand } = require('../services/openclaw');

router.get('/api/cron', (req, res) => {
Expand All @@ -25,9 +25,11 @@ router.get('/api/cron', (req, res) => {
// Silent fail, fallthrough to CLI
}

// 2. SLOW PATH: CLI Fallback
// 2. SLOW PATH: CLI Fallback (n/a in Docker)
if (IS_DOCKER) return res.json({ dockerMode: true, jobs: [] });

const cmd = `${getOpenClawCommand()} cron list --json`;
exec(cmd, { maxBuffer: 1024 * 1024 * 5 }, (err, stdout, stderr) => {
exec(cmd, { maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => {
try {
const data = JSON.parse(stdout);
if (data.jobs) return res.json(data.jobs);
Expand All @@ -40,6 +42,9 @@ router.get('/api/cron', (req, res) => {

router.post('/api/run/:id', (req, res) => {
const id = req.params.id;
if (IS_DOCKER) {
return res.status(403).json({ error: 'Running cron jobs is not supported in Docker Mode. Please interact with the host CLI directly.' });
}
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
return res.status(400).json({ error: 'Invalid job ID format' });
}
Expand Down
7 changes: 7 additions & 0 deletions src/routes/process.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ const { exec } = require('child_process');
const path = require('path');
const { rateLimit } = require('../utils/rateLimit');
const { getOpenClawCommand, WORKSPACE_DIR } = require('../services/openclaw');
const { IS_DOCKER } = require('../config');

router.post('/api/kill', (req, res) => {
if (IS_DOCKER) {
return res.status(403).json({ error: 'Process termination is not supported in Docker Container Mode. Please restart the container or run host commands directly.' });
}
if (req.body?.confirm !== true) {
return res.status(400).json({ error: 'Confirmation required. Send { "confirm": true } in request body.' });
}
Expand Down Expand Up @@ -62,6 +66,9 @@ router.post('/api/kill', (req, res) => {
});

router.post('/api/gateway/restart', (req, res) => {
if (IS_DOCKER) {
return res.status(403).json({ error: 'Gateway process restarts are not supported inside Docker containers. To restart the gateway, please restart the Docker container.' });
}
if (!rateLimit('gateway_restart', 10000)) {
return res.status(429).json({ error: 'Please wait at least 10 seconds before retrying.' });
}
Expand Down
Loading
Loading