[grade=A urn:ump:josd2g7kqv476vux6euigmygdkijya2s4tsbyivtobw5ftzuf6ba] fix(DC-170): revive 19 CSP-blocked inline handlers across the dashboard — audit P0-3

The meta CSP (no unsafe-inline/unsafe-hashes) blocks ALL inline event
handlers; 19 controls were silently dead in every modern browser.

Revived:
- Disk Safety: toolbar button, injected modal Close/Save/Clean Up, and
  the 5 settings sliders (oninput -> input listeners; value labels
  never updated)
- AI chat FAB click; hover scale moved to a #ai-chat-fab:hover rule
- TOTP/login logo broken-image fallbacks: capture-phase error
  delegation + WeakSet + complete/naturalWidth race guard in
  totp-auth.js; updateTotpLogo never resurrects a broken logo
  (6 onerror attrs stripped from index.html + auth-gate.js templates)
- favicon-preview data-URI fallback (same race guard)
- app-selector folder-chip remove buttons (data-remove-folder)
- premium upsells in backup-restore + workflows (data-upgrade-msg) —
  their old onclick also called scrollToSection('license'), a function
  that has never existed anywhere in the codebase; now opens the real
  license UI via window.openLicenseModal (license.js)
- health-check Edit/Delete dispatch the same CustomEvents via
  data-health-action/data-service-id
- smart-arr Test button via data-smart-test

Verified: zero inline handler attributes remain across index.html +
hand-written js (grep); node --check clean x12; status suite 68/68;
dist rebuilt (SW dashcaddy-shell-36d9aa3364, CSP hash unchanged).
r1 grade C (missed sliders + logo onerror) -> r2 grade A:
urn:ump:mptri3qhywc2e47f2ba4qjafhwm3olzvrvt5vphxoyhea3vgt32a /
urn:ump:josd2g7kqv476vux6euigmygdkijya2s4tsbyivtobw5ftzuf6ba
This commit is contained in:
Hermes committed 2026-10-28 00:48:32 -07:00
1 parent 8a6aed546d
commit 94ee76e9de
15 files changed
+439 -348

No files matched your search

+5
View File
@@ -345,6 +345,11 @@
if (e.target && e.target.id === 'ai-settings-btn') openSettings();
}, true);
// Audit P0-3: the FAB's inline onclick/onmouseover/onmouseout were dead
// under the meta CSP. Click wired here; hover moved to a CSS :hover rule.
const fab = document.getElementById('ai-chat-fab');
if (fab) fab.addEventListener('click', () => toggle());
window.aiChat = { send, deploy, toggle };
window.toggleAIChat = toggle;
})();
+5 -1
View File
@@ -1090,9 +1090,13 @@
folderBrowserSelectedList.innerHTML = selectedFolders.map(path => `
<span style="padding: 6px 12px; background: var(--card-bg); border: 1px solid var(--success); border-radius: 4px; display: flex; align-items: center; gap: 6px;">
${escapeHtml(path)}
<button type="button" onclick="removeSelectedFolder('${escapeHtml(path)}')" style="background: none; border: none; cursor: pointer; color: var(--error); font-size: 1rem; padding: 0;">×</button>
<button type="button" data-remove-folder="${escapeHtml(path)}" aria-label="Remove ${escapeHtml(path)}" style="background: none; border: none; cursor: pointer; color: var(--error); font-size: 1rem; padding: 0;">×</button>
</span>
`).join('');
// Audit P0-3: was inline onclick (dead under the meta CSP)
folderBrowserSelectedList.querySelectorAll('[data-remove-folder]').forEach((btn) => {
btn.addEventListener('click', () => window.removeSelectedFolder(btn.dataset.removeFolder));
});
}
window.removeSelectedFolder = function(path) {
+4 -4
View File
@@ -56,8 +56,8 @@
}).join('\n');
card.innerHTML = `
<img class="totp-logo totp-logo-dark" src="/assets/dashcaddy-logo-dark.png" alt="DashCaddy" onerror="this.style.display='none'">
<img class="totp-logo totp-logo-light" src="/assets/dashcaddy-logo-light.png" alt="DashCaddy" onerror="this.style.display='none'">
<img class="totp-logo totp-logo-dark" src="/assets/dashcaddy-logo-dark.png" alt="DashCaddy">
<img class="totp-logo totp-logo-light" src="/assets/dashcaddy-logo-light.png" alt="DashCaddy">
<p class="subtitle">Choose how to sign in</p>
<div id="auth-gate-providers" style="margin: 14px 0 10px;">${providerButtons}</div>
<div id="auth-gate-message" style="margin-top: 8px; color: var(--muted); font-size: 0.8rem;"></div>
@@ -102,8 +102,8 @@
if (provider.name === 'email') {
card.innerHTML = `
<img class="totp-logo totp-logo-dark" src="/assets/dashcaddy-logo-dark.png" alt="DashCaddy" onerror="this.style.display='none'">
<img class="totp-logo totp-logo-light" src="/assets/dashcaddy-logo-light.png" alt="DashCaddy" onerror="this.style.display='none'">
<img class="totp-logo totp-logo-dark" src="/assets/dashcaddy-logo-dark.png" alt="DashCaddy">
<img class="totp-logo totp-logo-light" src="/assets/dashcaddy-logo-light.png" alt="DashCaddy">
<p class="subtitle">Sign in with email</p>
<p style="margin: 6px 0 14px; font-size: 0.85rem; color: var(--muted);">
We'll email you a one-time sign-in link.
+18 -2
View File
@@ -204,6 +204,20 @@
var restoreBtn = document.getElementById('backup-do-restore-btn');
var resultDiv = document.getElementById('backup-result');
var scheduleContainer = document.getElementById('backup-schedules-container');
// Audit P0-3 (2026-10-27): premium upsell buttons used inline onclick,
// which the meta CSP blocks — dead buttons. (Their onclick also called
// scrollToSection('license'), which has NEVER existed — doubly dead.)
// data-upgrade-msg + wiring; license UI opens via window.openLicenseModal.
function wireUpgradeButtons(root) {
if (!root || !root.querySelectorAll) return;
root.querySelectorAll('[data-upgrade-msg]').forEach((btn) => {
btn.addEventListener('click', () => {
showNotification(btn.dataset.upgradeMsg, 'info');
if (window.openLicenseModal) window.openLicenseModal();
});
});
}
var historyContainer = document.getElementById('backup-history-container');
var diskContainer = document.getElementById('backup-disk-container');
var pointintimeContainer = document.getElementById('pointintime-container');
@@ -420,8 +434,9 @@
'<div style="font-size: 2rem; margin-bottom: 12px;">⭐</div>' +
'<div style="font-weight: 600; margin-bottom: 8px;">Premium Feature</div>' +
'<div style="font-size: 0.85rem; color: var(--muted);">Auto-backup scheduling requires a DashCaddy Premium subscription.</div>' +
'<button onclick="showNotification(\'Upgrade to Premium to enable auto-backups!\', \'info\'); scrollToSection(\'license\');" style="margin-top: 16px; padding: 8px 20px; background: linear-gradient(135deg, #f39c12, #e67e22); border: none; color: white; border-radius: 8px; cursor: pointer; font-weight: 600;">Upgrade to Premium</button>' +
'<button type="button" data-upgrade-msg="Upgrade to Premium to enable auto-backups!" style="margin-top: 16px; padding: 8px 20px; background: linear-gradient(135deg, #f39c12, #e67e22); border: none; color: white; border-radius: 8px; cursor: pointer; font-weight: 600;">Upgrade to Premium</button>' +
'</div>';
wireUpgradeButtons(scheduleContainer);
return;
}
@@ -749,8 +764,9 @@
'<div style="font-size: 2rem; margin-bottom: 12px;">⭐</div>' +
'<div style="font-weight: 600; margin-bottom: 8px;">Premium Feature</div>' +
'<div style="font-size: 0.85rem; color: var(--muted);">Point-in-time restore requires DashCaddy Premium with auto-backup enabled.</div>' +
'<button onclick="showNotification(\'Upgrade to Premium for point-in-time restore!\', \'info\'); scrollToSection(\'license\');" style="margin-top: 16px; padding: 8px 20px; background: linear-gradient(135deg, #f39c12, #e67e22); border: none; color: white; border-radius: 8px; cursor: pointer; font-weight: 600;">Upgrade to Premium</button>' +
'<button type="button" data-upgrade-msg="Upgrade to Premium for point-in-time restore!" style="margin-top: 16px; padding: 8px 20px; background: linear-gradient(135deg, #f39c12, #e67e22); border: none; color: white; border-radius: 8px; cursor: pointer; font-weight: 600;">Upgrade to Premium</button>' +
'</div>';
wireUpgradeButtons(pointintimeContainer);
return;
}
} catch (e) { /* ignore */ }
+9 -1
View File
@@ -114,11 +114,19 @@
if (!isPremium) {
html += `<div class="accent-info-box" style="margin-top: 16px; text-align: center;">
<span>🔒 Upgrade to unlock automated workflows</span>
<button onclick="showNotification('Upgrade to Premium to enable workflows!', 'info'); scrollToSection('license');" style="margin-left: 12px; padding: 6px 16px; background: linear-gradient(135deg, #f39c12, #e67e22); border: none; color: white; border-radius: 6px; cursor: pointer; font-weight: 600;">Upgrade to Premium</button>
<button type="button" data-upgrade-msg="Upgrade to Premium to enable workflows!" style="margin-left: 12px; padding: 6px 16px; background: linear-gradient(135deg, #f39c12, #e67e22); border: none; color: white; border-radius: 6px; cursor: pointer; font-weight: 600;">Upgrade to Premium</button>
</div>`;
}
container.innerHTML = `<div class="workflow-list">${html}</div>`;
// Audit P0-3: was inline onclick (dead under the meta CSP); it also
// called the nonexistent scrollToSection — openLicenseModal instead
container.querySelectorAll('[data-upgrade-msg]').forEach((btn) => {
btn.addEventListener('click', () => {
showNotification(btn.dataset.upgradeMsg, 'info');
if (window.openLicenseModal) window.openLicenseModal();
});
});
wireWorkflowEvents();
} else {
container.innerHTML = '<div class="panel-empty"><span class="empty-icon">⚠️</span>Failed to load workflows</div>';
+22 -4
View File
@@ -33,7 +33,7 @@
<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>
<button id="disk-close-btn" aria-label="Close" 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;">
@@ -65,13 +65,26 @@
</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>
<button id="disk-save-btn" style="flex:1;padding:10px 16px;background:#6366f1;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600;">Save Settings</button>
<button id="disk-cleanup-btn" 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);
// Audit P0-3 (2026-10-27): the meta CSP blocks inline event handlers, so
// the injected onclick/oninput attributes were dead — Save/Clean Up/
// Close did nothing and slider value labels never updated. Wire after
// injection instead.
document.getElementById('disk-close-btn').addEventListener('click', () => modal.remove());
document.getElementById('disk-save-btn').addEventListener('click', () => saveDiskSettings());
document.getElementById('disk-cleanup-btn').addEventListener('click', () => cleanupDiskNow());
modal.querySelectorAll('input[type="range"]').forEach((inp) => {
inp.addEventListener('input', () => {
const out = document.getElementById(`${inp.id}-val`);
if (out) out.textContent = inp.value;
});
});
}
function settingRow(label, id, current, unit, displayVal, min, max) {
@@ -79,7 +92,7 @@
<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"
<input type="range" id="disk-${id}" min="${min}" max="${max}" value="${displayVal}"
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>
@@ -130,4 +143,9 @@
window.openDiskSettings = function() { loadDiskSettings(); };
window.saveDiskSettings = saveDiskSettings;
window.cleanupDiskNow = cleanupDiskNow;
// Audit P0-3: index.html's Disk button carried an inline handler calling
// openDiskSettings() — dead under the meta CSP. Wired here instead.
const diskOpenBtn = document.getElementById('disk-settings-open');
if (diskOpenBtn) diskOpenBtn.addEventListener('click', () => loadDiskSettings());
})();
+9 -2
View File
@@ -375,12 +375,19 @@
html += `<td style="padding: 8px; color: ${isUp ? 'var(--ok-fg)' : 'var(--bad-fg)'};">${isUp ? 'Up' : 'Down'}</td>`;
html += `<td style="padding: 8px;">${s.sla?.target ? s.sla.target + '%' : '-'}</td>`;
html += `<td style="padding: 8px; text-align: right;">`;
html += `<button onclick="document.dispatchEvent(new CustomEvent('health-edit', {detail:'${escapeHtml(s.serviceId)}'}))" style="padding: 4px 10px; font-size: 0.78rem; margin-right: 4px;">Edit</button>`;
html += `<button onclick="document.dispatchEvent(new CustomEvent('health-delete', {detail:'${escapeHtml(s.serviceId)}'}))" style="padding: 4px 10px; font-size: 0.78rem; color: var(--bad-fg); border-color: var(--bad-fg);">Delete</button>`;
html += `<button data-health-action="health-edit" data-service-id="${escapeHtml(s.serviceId)}" style="padding: 4px 10px; font-size: 0.78rem; margin-right: 4px;">Edit</button>`;
html += `<button data-health-action="health-delete" data-service-id="${escapeHtml(s.serviceId)}" style="padding: 4px 10px; font-size: 0.78rem; color: var(--bad-fg); border-color: var(--bad-fg);">Delete</button>`;
html += '</td></tr>';
}
html += '</table>';
configContainer.innerHTML = html;
// Audit P0-3: the Edit/Delete buttons dispatched CustomEvents from
// inline onclick — dead under the meta CSP. Same events, wired.
configContainer.querySelectorAll('[data-health-action]').forEach((btn) => {
btn.addEventListener('click', () => {
document.dispatchEvent(new CustomEvent(btn.dataset.healthAction, { detail: btn.dataset.serviceId }));
});
});
} catch (e) {
configContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${escapeHtml(e.message)}</div>`;
}
+10 -1
View File
@@ -70,7 +70,7 @@
<div class="mb-16">
<label class="form-label-bold">Favicon (Browser Tab Icon):</label>
<div id="favicon-preview-container" style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px; padding: 12px; border-radius: 6px; background: var(--card-bg);">
<img id="favicon-preview" src="/assets/dashcaddy-favicon.ico" alt="Current favicon" style="width: 32px; height: 32px; image-rendering: pixelated;" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 32 32%22><rect fill=%22%23667%22 width=%2232%22 height=%2232%22 rx=%224%22/></svg>'" />
<img id="favicon-preview" src="/assets/dashcaddy-favicon.ico" alt="Current favicon" style="width: 32px; height: 32px; image-rendering: pixelated;" />
<span id="favicon-status" class="text-tiny-muted">Using DashCaddy favicon</span>
</div>
<input type="file" id="favicon-upload" accept="image/png,image/svg+xml" class="input-card-alt" />
@@ -344,6 +344,15 @@
const faviconPreview = document.getElementById('favicon-preview');
const faviconStatus = document.getElementById('favicon-status');
const faviconUpload = document.getElementById('favicon-upload');
// Audit P0-3: the missing-favicon fallback used inline onerror (dead under
// the meta CSP). Same data-URI fallback, wired + race-guarded.
if (faviconPreview) {
const faviconFallback = () => {
faviconPreview.src = 'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 32 32%22><rect fill=%22%23667%22 width=%2232%22 height=%2232%22 rx=%224%22/></svg>';
};
faviconPreview.addEventListener('error', faviconFallback);
if (faviconPreview.complete && faviconPreview.naturalWidth === 0) faviconFallback();
}
const faviconLink = document.querySelector('link[rel="icon"]') || document.createElement('link');
let pendingFaviconData = null;
+5 -1
View File
@@ -221,7 +221,7 @@
style="width: 100%; font-size: 0.85rem; padding: 6px 8px; background: var(--card-bg); color: var(--fg); border: 1px solid var(--border); border-radius: 4px;" />
</div>
</div>
<button onclick="smartTestConnection('${svc}')" style="margin-top: 8px; padding: 4px 12px; font-size: 0.75rem; cursor: pointer;">Test</button>
<button type="button" data-smart-test="${svc}" style="margin-top: 8px; padding: 4px 12px; font-size: 0.75rem; cursor: pointer;">Test</button>
</div>`;
}
@@ -263,6 +263,10 @@
}
credentialInputs.innerHTML = html;
// Audit P0-3: was inline onclick calling smartTestConnection (dead under CSP)
credentialInputs.querySelectorAll('[data-smart-test]').forEach((btn) => {
btn.addEventListener('click', () => window.smartTestConnection(btn.dataset.smartTest));
});
}
// Test connection (global for onclick)
+18 -2
View File
@@ -1,5 +1,21 @@
// ===== TOTP AUTHENTICATION GATE =====
(function() {
// Audit P0-3 (2026-10-27): broken-logo hiding used inline onerror, which
// the meta CSP blocks (dead handler). Track failures via capture-phase
// delegation (catches injected <img>s at any time) + a load-state sweep,
// and never resurrect a broken logo in updateTotpLogo().
const brokenLogos = new WeakSet();
document.addEventListener('error', (e) => {
const t = e.target;
if (t && t.tagName === 'IMG' && t.classList.contains('totp-logo')) {
brokenLogos.add(t);
t.style.display = 'none';
}
}, true);
document.querySelectorAll('.totp-logo').forEach((img) => {
if (img.complete && img.naturalWidth === 0) { brokenLogos.add(img); img.style.display = 'none'; }
});
function updateTotpLogo() {
const card = document.querySelector('.totp-card');
if (!card) return;
@@ -9,8 +25,8 @@
const lum = (0.299 * +m[0] + 0.587 * +m[1] + 0.114 * +m[2]) / 255;
const dark = card.querySelector('.totp-logo-dark');
const light = card.querySelector('.totp-logo-light');
if (dark) dark.style.display = lum > 0.5 ? 'none' : '';
if (light) light.style.display = lum > 0.5 ? '' : 'none';
if (dark) dark.style.display = (lum > 0.5 || brokenLogos.has(dark)) ? 'none' : '';
if (light) light.style.display = (lum <= 0.5 || brokenLogos.has(light)) ? 'none' : '';
}
function showTotpOverlay() {