const express = require('express'); const router = express.Router(); const fs = require('fs'); const path = require('path'); const platformPaths = require('../platform-paths'); // DC-048 — the canonical disk-settings.json path. Shared by GET + POST. function getSettingsFile() { return path.join(platformPaths.dataDir, 'disk-settings.json'); } // GET current disk settings + actual disk usage router.get('/', (req, res) => { try { const settings = { healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL || '30000'), healthMaxEntries: parseInt(process.env.HEALTH_MAX_ENTRIES || '500'), // DC-048 — align route default to engine default (health-checker.js:34 // reads 30 from env when unset; the route previously showed 14 as the // "no override" value, which silently disagreed with the engine). healthRetentionDays: parseInt(process.env.HEALTH_HISTORY_RETENTION || '30'), statsMaxEntries: parseInt(process.env.CONTAINER_STATS_MAX_ENTRIES || '500'), auditMaxEntries: parseInt(process.env.AUDIT_MAX_ENTRIES || '1000'), backupMaxStorageBytes: parseInt(process.env.BACKUP_MAX_STORAGE_BYTES || '0'), }; // Get actual disk usage let diskUsage = { total: 0, used: 0, free: 0, dataDirSize: 0 }; try { const { execSync } = require('child_process'); const dfOut = execSync("df -B1 /opt/dashcaddy/dashcaddy-api/data 2>/dev/null || df -B1 / 2>/dev/null").toString().trim().split('\n'); if (dfOut.length > 1) { const parts = dfOut[1].split(/\s+/); diskUsage.total = parseInt(parts[1]) || 0; diskUsage.used = parseInt(parts[2]) || 0; diskUsage.free = parseInt(parts[3]) || 0; } const duOut = execSync("du -sb /opt/dashcaddy/dashcaddy-api/data 2>/dev/null || echo 0").toString().trim().split(/\s+/); diskUsage.dataDirSize = parseInt(duOut[0]) || 0; } catch {} // Load persisted settings const settingsFile = getSettingsFile(); let persisted = {}; try { persisted = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); } catch {} res.json({ success: true, current: { ...settings, ...persisted }, diskUsage }); } catch (e) { res.status(500).json({ success: false, error: e.message }); } }); // POST update settings router.post('/', (req, res) => { try { const { healthInterval, healthMaxEntries, healthRetentionDays, statsMaxEntries, auditMaxEntries } = req.body; // DC-048 — coerce + validate EVERY numeric input before persisting. // Without this gate, parseInt('abc') === NaN → String(NaN) === 'NaN' → // process.env.HEALTH_CHECK_INTERVAL becomes 'NaN' at runtime AND the // persisted file gets JSON.stringify({x: NaN}) === {"x": null} which // the loader silently drops on next boot. Validation now rejects the // request with 400 BEFORE any env mutation or file write. const intField = (name, value) => { const n = Number(value); if (!Number.isFinite(n) || !Number.isInteger(n)) { throw new Error(`${name} must be an integer (received ${JSON.stringify(value)})`); } return n; }; const updates = {}; if (healthInterval !== undefined) { const n = intField('healthInterval', healthInterval); updates.healthCheckInterval = n; process.env.HEALTH_CHECK_INTERVAL = String(n); } if (healthMaxEntries !== undefined) { const n = intField('healthMaxEntries', healthMaxEntries); updates.healthMaxEntries = n; process.env.HEALTH_MAX_ENTRIES = String(n); } if (healthRetentionDays !== undefined) { const n = intField('healthRetentionDays', healthRetentionDays); updates.healthRetentionDays = n; process.env.HEALTH_HISTORY_RETENTION = String(n); } if (statsMaxEntries !== undefined) { const n = intField('statsMaxEntries', statsMaxEntries); updates.statsMaxEntries = n; process.env.CONTAINER_STATS_MAX_ENTRIES = String(n); } if (auditMaxEntries !== undefined) { const n = intField('auditMaxEntries', auditMaxEntries); updates.auditMaxEntries = n; process.env.AUDIT_MAX_ENTRIES = String(n); } // Persist to file const settingsFile = getSettingsFile(); let existing = {}; try { existing = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); } catch {} fs.writeFileSync(settingsFile, JSON.stringify({ ...existing, ...updates }, null, 2)); res.json({ success: true, updated: updates, message: 'Settings saved. Some changes apply on next container restart.' }); } catch (e) { res.status(e.statusCode || 400).json({ success: false, error: e.message }); } }); // POST trigger immediate cleanup router.post('/cleanup', async (req, res) => { try { const results = { cleaned: {} }; // Clean health history try { const healthChecker = require('../monitoring/health-checker'); if (healthChecker.instance && healthChecker.instance.cleanupHistory) { healthChecker.instance.cleanupHistory(); results.cleaned.healthHistory = 'Cleaned old entries'; } } catch (e) { results.cleaned.healthHistory = 'Skipped: ' + e.message; } // Clean container stats try { const resourceMonitor = require('../managers/resource-monitor'); if (resourceMonitor.instance && resourceMonitor.instance.cleanupOldStats) { resourceMonitor.instance.cleanupOldStats(); results.cleaned.containerStats = 'Cleaned old entries'; } } catch (e) { results.cleaned.containerStats = 'Skipped: ' + e.message; } res.json({ success: true, results }); } catch (e) { res.status(500).json({ success: false, error: e.message }); } }); module.exports = router;