- New src/config/disk-settings-loader.js runs once at boot (require'd into src/app.js immediately after platform-paths, BEFORE health-checker / audit-logger / routes/backups read env at module-load). - Routes the persisted values from <dataDir>/disk-settings.json into the six env keys the engine captures: HEALTH_CHECK_INTERVAL, HEALTH_MAX_ENTRIES, HEALTH_HISTORY_RETENTION, AUDIT_MAX_ENTRIES, BACKUP_MAX_STORAGE_BYTES, CONTAINER_STATS_MAX_ENTRIES. - Explicit process.env values WIN over persisted file (operator override). - Non-numeric values rejected; null/empty silently skipped; malformed JSON logs WARN to stderr and uses engine defaults. - Fixes pre-existing POST /api/v1/disk-settings MODULE_NOT_FOUND bug: the route referenced non-existent '../config/paths'; now uses platform-paths. - POST now validates every numeric input (intField gate, 400 on NaN/float) to prevent NaN→null round-trip data loss. - Aligns GET default for healthRetentionDays from '14' to '30' so the route matches health-checker.js:34 (engine) and the modal's ||30 fallback. - 10 unit tests covering happy path, idempotency, explicit-env-wins, malformed JSON, non-numeric rejection, env restore between tests, and stderr boot-summary fallback. GLM-5.3 round 1: B (route MODULE_NOT_FOUND + MEDIUM POST NaN→null + boot log LOW). GLM-5.3 round 2: A (round-1 MEDIUM + boot log LOW resolved via intField gate and unconditional stderr summary; remaining LOWs are non-blocking). Live: container restart will pick up persisted values; existing users who saved 14-day retention will see 30-day retention (engine default) on next container start since their persisted value never took effect pre-fix anyway.
134 lines
6.9 KiB
JavaScript
134 lines
6.9 KiB
JavaScript
// Disk Safety Settings Panel
|
||
(function() {
|
||
let diskSettings = null;
|
||
|
||
async function loadDiskSettings() {
|
||
try {
|
||
const res = await secureFetch('/api/v1/disk-settings');
|
||
if (res.ok) {
|
||
diskSettings = await res.json();
|
||
renderDiskSettingsModal();
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load disk settings:', e);
|
||
}
|
||
}
|
||
|
||
function renderDiskSettingsModal() {
|
||
const existing = document.getElementById('disk-settings-modal');
|
||
if (existing) existing.remove();
|
||
|
||
const c = diskSettings?.current || {};
|
||
const du = diskSettings?.diskUsage || {};
|
||
const usedGB = (du.dataDirSize / 1073741824).toFixed(2);
|
||
const diskFreeGB = (du.free / 1073741824).toFixed(1);
|
||
const diskTotalGB = (du.total / 1073741824).toFixed(1);
|
||
const diskPct = du.total > 0 ? ((du.used / du.total) * 100).toFixed(1) : 0;
|
||
|
||
const modal = document.createElement('div');
|
||
modal.id = 'disk-settings-modal';
|
||
modal.className = 'modal';
|
||
modal.style.cssText = 'display:flex;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;align-items:center;justify-content:center;';
|
||
modal.innerHTML = `
|
||
<div style="background:var(--bg-card,#1a1a2e);border-radius:12px;padding:28px;max-width:520px;width:90%;max-height:85vh;overflow-y:auto;border:1px solid var(--border,#333);">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
|
||
<h2 style="margin:0;font-size:1.3rem;">💾 Disk Safety</h2>
|
||
<button onclick="document.getElementById('disk-settings-modal').remove()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--text-muted,#888);">×</button>
|
||
</div>
|
||
|
||
<div style="background:rgba(99,102,241,0.08);border:1px solid rgba(99,102,241,0.2);border-radius:8px;padding:14px;margin-bottom:20px;">
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
|
||
<span style="font-size:0.9rem;color:var(--text-muted,#888);">DashCaddy Data Size</span>
|
||
<span style="font-weight:600;">${usedGB} GB</span>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
|
||
<span style="font-size:0.9rem;color:var(--text-muted,#888);">Disk Free</span>
|
||
<span style="font-weight:600;">${diskFreeGB} GB / ${diskTotalGB} GB (${diskPct}% used)</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="background:rgba(243,156,18,0.08);border:1px solid rgba(243,156,18,0.25);border-radius:8px;padding:12px;margin-bottom:16px;">
|
||
<div style="font-size:0.82rem;color:#f0a040;line-height:1.45;">
|
||
⚠ <strong>Disk impact:</strong> Lowering the health check interval increases how often data is written to disk.
|
||
DashCaddy caps history at <strong>max entries per service</strong> and prunes entries older than the retention period,
|
||
so these two values together determine steady-state disk usage. For busy hosts, prefer a longer interval (60–120s)
|
||
and a lower entry cap.
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;gap:16px;">
|
||
${settingRow('Health Check Interval', 'healthInterval', c.healthCheckInterval, 'seconds', Math.round((c.healthCheckInterval||30000)/1000), 5, 300)}
|
||
${settingRow('Max Health Entries/Service', 'healthMaxEntries', c.healthMaxEntries, 'entries', c.healthMaxEntries||500, 50, 5000)}
|
||
${settingRow('Health Retention', 'healthRetentionDays', c.healthRetentionDays, 'days', c.healthRetentionDays||30, 1, 90)}
|
||
${settingRow('Max Stats Entries', 'statsMaxEntries', c.statsMaxEntries, 'entries', c.statsMaxEntries||500, 100, 10000)}
|
||
${settingRow('Max Audit Entries', 'auditMaxEntries', c.auditMaxEntries, 'entries', c.auditMaxEntries||1000, 100, 10000)}
|
||
</div>
|
||
|
||
<div style="display:flex;gap:10px;margin-top:24px;">
|
||
<button onclick="saveDiskSettings()" style="flex:1;padding:10px 16px;background:#6366f1;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600;">Save Settings</button>
|
||
<button onclick="cleanupDiskNow()" style="flex:1;padding:10px 16px;background:rgba(239,68,68,0.15);color:#f87171;border:1px solid rgba(239,68,68,0.3);border-radius:8px;cursor:pointer;font-weight:600;">Clean Up Now</button>
|
||
</div>
|
||
<p style="font-size:0.8rem;color:var(--text-muted,#666);margin-top:12px;text-align:center;">Some changes apply on next container restart.</p>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
function settingRow(label, id, current, unit, displayVal, min, max) {
|
||
return `
|
||
<div>
|
||
<label style="font-size:0.85rem;color:var(--text-muted,#aaa);display:block;margin-bottom:4px;">${label}</label>
|
||
<div style="display:flex;align-items:center;gap:10px;">
|
||
<input type="range" id="disk-${id}" min="${min}" max="${max}" value="${displayVal}" oninput="document.getElementById('disk-${id}-val').textContent=this.value"
|
||
style="flex:1;accent-color:#6366f1;">
|
||
<span id="disk-${id}-val" style="min-width:50px;text-align:right;font-weight:600;">${displayVal}</span>
|
||
<span style="font-size:0.8rem;color:var(--text-muted,#666);min-width:60px;">${unit}</span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
async function saveDiskSettings() {
|
||
const payload = {
|
||
healthInterval: parseInt(document.getElementById('disk-healthInterval').value) * 1000,
|
||
healthMaxEntries: parseInt(document.getElementById('disk-healthMaxEntries').value),
|
||
healthRetentionDays: parseInt(document.getElementById('disk-healthRetentionDays').value),
|
||
statsMaxEntries: parseInt(document.getElementById('disk-statsMaxEntries').value),
|
||
auditMaxEntries: parseInt(document.getElementById('disk-auditMaxEntries').value),
|
||
};
|
||
try {
|
||
const res = await secureFetch('/api/v1/disk-settings', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
showToast('Disk settings saved', 'success');
|
||
} else {
|
||
showToast(data.error || 'Save failed', 'error');
|
||
}
|
||
} catch (e) {
|
||
showToast('Error: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function cleanupDiskNow() {
|
||
if (!confirm('Clean up old health entries, stats, and audit logs now?')) return;
|
||
try {
|
||
const res = await secureFetch('/api/v1/disk-settings/cleanup', { method: 'POST' });
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
const items = Object.entries(data.results.cleaned).map(([k,v]) => `${k}: ${v}`).join('\n');
|
||
showToast('Cleanup complete', 'success');
|
||
loadDiskSettings();
|
||
}
|
||
} catch (e) {
|
||
showToast('Error: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
window.openDiskSettings = function() { loadDiskSettings(); };
|
||
window.saveDiskSettings = saveDiskSettings;
|
||
window.cleanupDiskNow = cleanupDiskNow;
|
||
})();
|