Update: version 1.2.0, new version UI features, CSP hash auto-update, bug fixes
This commit is contained in:
+32
-1
@@ -1,9 +1,11 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const esbuild = require('esbuild');
|
||||
|
||||
const JS = (...parts) => path.join(__dirname, 'js', ...parts);
|
||||
const DIST = path.join(__dirname, 'dist');
|
||||
const INDEX_HTML = path.join(__dirname, 'index.html');
|
||||
|
||||
// Bundle definitions — files are concatenated in order, then minified
|
||||
const bundles = {
|
||||
@@ -64,6 +66,32 @@ const bundles = {
|
||||
],
|
||||
};
|
||||
|
||||
function updateInlineScriptCspHash() {
|
||||
const html = fs.readFileSync(INDEX_HTML, 'utf8');
|
||||
const scripts = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)];
|
||||
const target = scripts.find(match => {
|
||||
const block = match[1] || '';
|
||||
return block.includes("license-topbar-version") || block.includes("openVersionInfo") || block.includes("widget-");
|
||||
});
|
||||
|
||||
if (!target) {
|
||||
throw new Error('Could not find inline dashboard bootstrap script in index.html');
|
||||
}
|
||||
|
||||
const scriptContent = target[1];
|
||||
const hash = crypto.createHash('sha256').update(scriptContent).digest('base64');
|
||||
const updatedHtml = html.replace(
|
||||
/script-src 'self' 'sha256-[^']+';/,
|
||||
`script-src 'self' 'sha256-${hash}';`
|
||||
);
|
||||
|
||||
if (updatedHtml !== html) {
|
||||
fs.writeFileSync(INDEX_HTML, updatedHtml);
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
async function build() {
|
||||
// Ensure dist/ exists
|
||||
if (!fs.existsSync(DIST)) fs.mkdirSync(DIST);
|
||||
@@ -96,6 +124,8 @@ async function build() {
|
||||
results[outName] = { rawSize, minSize, fileCount: files.length };
|
||||
}
|
||||
|
||||
const cspHash = updateInlineScriptCspHash();
|
||||
|
||||
// Summary
|
||||
console.log('\n DashCaddy Frontend Build\n');
|
||||
console.log(' Bundle Files Raw Min');
|
||||
@@ -108,7 +138,8 @@ async function build() {
|
||||
}
|
||||
console.log(' ─────────────────────────────────────────');
|
||||
console.log(` ${'Total'.padEnd(18)} ${totalRaw.toFixed(1).padStart(6)} KB ${totalMin.toFixed(1).padStart(6)} KB`);
|
||||
console.log(`\n Output: ${DIST}\n`);
|
||||
console.log(`\n Output: ${DIST}`);
|
||||
console.log(` CSP Hash: sha256-${cspHash}\n`);
|
||||
}
|
||||
|
||||
// Watch mode
|
||||
|
||||
@@ -170,13 +170,20 @@ button:focus-visible {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.reload-caddy-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.license-version-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.dashboard-version {
|
||||
@@ -196,6 +203,36 @@ button:focus-visible {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.dashboard-version-inline {
|
||||
white-space: nowrap;
|
||||
align-self: center;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dashboard-version-inline.has-update {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.dashboard-version-inline.has-update .dashboard-version-bullet,
|
||||
.dashboard-version-inline.has-update #license-topbar-version-text {
|
||||
color: var(--accent) !important;
|
||||
}
|
||||
|
||||
.dashboard-version-bullet {
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#license-topbar-version-text {
|
||||
color: var(--fg) !important;
|
||||
font-weight: 700;
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.version-info-modal-content {
|
||||
min-width: 420px;
|
||||
max-width: 620px;
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+172
-33
@@ -8,7 +8,7 @@
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'sha256-6JZtsKK/PZthh+stCmmCvC2QxCiyk6SwZCBjXE+kYr0='; style-src 'self' 'unsafe-inline'; img-src 'self' https://cdn.jsdelivr.net data:; connect-src 'self' https://api.open-meteo.com https://geocoding-api.open-meteo.com; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'sha256-Nv8xzCSztfdYOL663VgPKWQn6v0lnM0ACWxkxpFfcfY='; style-src 'self' 'unsafe-inline'; img-src 'self' https://cdn.jsdelivr.net data:; connect-src 'self' https://api.open-meteo.com https://geocoding-api.open-meteo.com; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'">
|
||||
|
||||
<link rel="icon" href="/assets/dashcaddy-favicon.ico" sizes="any">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/assets/icon-192.png">
|
||||
@@ -90,16 +90,18 @@
|
||||
</button>
|
||||
<button id="theme-customize-btn" class="theme-customize-link" title="Customize theme colors">Customize Theme</button>
|
||||
</div>
|
||||
<div id="license-status-topbar" class="license-status-topbar free" title="Click to manage license">
|
||||
<span id="license-topbar-icon">☆</span>
|
||||
<span id="license-topbar-text">FREE TIER</span>
|
||||
<span id="license-topbar-time"></span>
|
||||
</div>
|
||||
<button id="reload-caddy-top" aria-label="Reload Caddy configuration" style="padding: 10px 20px; font-size: 0.95rem; font-weight: 600; background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); border: none; border-radius: 6px; color: white; cursor: pointer; box-shadow: 0 2px 6px rgba(52, 152, 219, 0.3); transition: all 0.2s ease;">
|
||||
🔄 Reload Caddy
|
||||
</button>
|
||||
<div class="license-version-row">
|
||||
<div id="license-status-topbar" class="license-status-topbar free" title="Click to manage license">
|
||||
<span id="license-topbar-icon">☆</span>
|
||||
<span id="license-topbar-text">FREE TIER</span>
|
||||
<span id="license-topbar-time"></span>
|
||||
</div>
|
||||
<button id="license-topbar-version" class="dashboard-version dashboard-version-inline" type="button" title="View DashCaddy verification info" aria-label="View DashCaddy verification info"><span class="dashboard-version-bullet" id="license-topbar-version-bullet">·</span> <span id="license-topbar-version-text">Loading…</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="dashboard-version" class="dashboard-version" type="button" title="View DashCaddy verification info" aria-label="View DashCaddy verification info">Version —</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -110,7 +112,9 @@
|
||||
<div id="version-info-status" class="version-info-status">Loading…</div>
|
||||
<div id="version-info-grid" class="version-info-grid" style="display:none;"></div>
|
||||
<div id="version-info-history" class="version-info-history" style="display:none;"></div>
|
||||
<div class="weather-modal-buttons" style="margin-top: 16px;">
|
||||
<div id="version-info-actions" class="weather-modal-buttons" style="margin-top: 16px; display:none; justify-content: space-between; gap: 10px;">
|
||||
<button id="version-info-update">Update Now</button>
|
||||
<button id="version-info-open-updates">Open Updates</button>
|
||||
<button id="version-info-close">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,9 +163,9 @@
|
||||
<div class="tools-section-items">
|
||||
<button id="manage-tokens" aria-label="Manage API tokens">🔑 Tokens</button>
|
||||
<button id="backup-restore-btn" aria-label="Backup and restore">💾 Backup</button>
|
||||
<button id="license-btn" aria-label="License management" onclick="window.openLicenseModal && window.openLicenseModal()">🔑 License</button>
|
||||
<button id="api-docs-btn" aria-label="API documentation" onclick="window.open('/api/docs', '_blank')">📖 API</button>
|
||||
<button id="help-errors-btn" aria-label="Troubleshooting guide" onclick="window.open('/help-errors.html', '_blank')">❓ Help</button>
|
||||
<button id="license-btn" aria-label="License management">🔑 License</button>
|
||||
<button id="api-docs-btn" aria-label="API documentation">📖 API</button>
|
||||
<button id="help-errors-btn" aria-label="Troubleshooting guide">❓ Help</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -555,12 +559,18 @@
|
||||
var yr = document.getElementById('footer-year');
|
||||
if (yr) yr.textContent = new Date().getFullYear();
|
||||
|
||||
var versionEl = document.getElementById('dashboard-version');
|
||||
var versionEl = document.getElementById('license-topbar-version');
|
||||
var versionTextEl = document.getElementById('license-topbar-version-text');
|
||||
var versionBulletEl = document.getElementById('license-topbar-version-bullet');
|
||||
var versionInfoModal = document.getElementById('version-info-modal');
|
||||
var versionInfoStatus = document.getElementById('version-info-status');
|
||||
var versionInfoGrid = document.getElementById('version-info-grid');
|
||||
var versionInfoHistory = document.getElementById('version-info-history');
|
||||
var versionInfoActions = document.getElementById('version-info-actions');
|
||||
var versionInfoUpdate = document.getElementById('version-info-update');
|
||||
var versionInfoOpenUpdates = document.getElementById('version-info-open-updates');
|
||||
var versionInfoClose = document.getElementById('version-info-close');
|
||||
var latestUpdateCheck = null;
|
||||
|
||||
function formatValue(value) {
|
||||
if (value == null || value === '') return '—';
|
||||
@@ -588,71 +598,197 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function setVersionLabel(text) {
|
||||
if (versionTextEl) versionTextEl.textContent = text;
|
||||
}
|
||||
|
||||
function setVersionUpdateState(hasUpdate) {
|
||||
if (!versionEl || !versionBulletEl) return;
|
||||
versionEl.classList.toggle('has-update', !!hasUpdate);
|
||||
versionEl.title = hasUpdate
|
||||
? 'Update available — click for details or to update'
|
||||
: 'View DashCaddy verification info';
|
||||
versionEl.setAttribute('aria-label', hasUpdate
|
||||
? 'DashCaddy update available — click for details or to update'
|
||||
: 'View DashCaddy verification info');
|
||||
versionBulletEl.textContent = hasUpdate ? '⬆' : '·';
|
||||
}
|
||||
|
||||
function checkVersionUpdateAvailability() {
|
||||
return fetch('/api/v1/system/update-check', { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(response) {
|
||||
if (response.status === 401) throw new Error('AUTH_PENDING');
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
latestUpdateCheck = data;
|
||||
setVersionUpdateState(!!(data && data.success && data.available && data.remote));
|
||||
return data;
|
||||
})
|
||||
.catch(function(error) {
|
||||
if (!(error && error.message === 'AUTH_PENDING')) {
|
||||
setVersionUpdateState(false);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function loadVersionLabel(retryCount) {
|
||||
if (!versionEl) return;
|
||||
fetch('/api/v1/system/version', { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(response) {
|
||||
if (response.status === 401) {
|
||||
throw new Error('AUTH_PENDING');
|
||||
}
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data && data.success && data.version) {
|
||||
setVersionLabel('v' + data.version);
|
||||
checkVersionUpdateAvailability().catch(function() {});
|
||||
return;
|
||||
}
|
||||
setVersionLabel('version unknown');
|
||||
setVersionUpdateState(false);
|
||||
})
|
||||
.catch(function(error) {
|
||||
if (error && error.message === 'AUTH_PENDING') {
|
||||
setVersionLabel('signing in…');
|
||||
if ((retryCount || 0) < 12) {
|
||||
setTimeout(function() {
|
||||
loadVersionLabel((retryCount || 0) + 1);
|
||||
}, 1500);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setVersionLabel('version unavailable');
|
||||
setVersionUpdateState(false);
|
||||
});
|
||||
}
|
||||
|
||||
function openUpdatesPanel() {
|
||||
var updatesBtn = document.getElementById('updates-btn');
|
||||
var updatesDashcaddyTab = document.getElementById('updates-dashcaddy-tab');
|
||||
if (updatesBtn) updatesBtn.click();
|
||||
setTimeout(function() {
|
||||
if (updatesDashcaddyTab) updatesDashcaddyTab.click();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function applyVersionUpdate() {
|
||||
if (window.dcApplyUpdate) {
|
||||
return window.dcApplyUpdate();
|
||||
}
|
||||
openUpdatesPanel();
|
||||
}
|
||||
|
||||
function openVersionInfo() {
|
||||
if (!versionInfoModal) return;
|
||||
versionInfoModal.classList.add('show');
|
||||
versionInfoStatus.textContent = 'Loading…';
|
||||
versionInfoGrid.style.display = 'none';
|
||||
versionInfoHistory.style.display = 'none';
|
||||
if (versionInfoActions) versionInfoActions.style.display = 'none';
|
||||
if (versionInfoUpdate) {
|
||||
versionInfoUpdate.disabled = true;
|
||||
versionInfoUpdate.textContent = 'Update Now';
|
||||
}
|
||||
versionInfoGrid.innerHTML = '';
|
||||
versionInfoHistory.innerHTML = '';
|
||||
|
||||
Promise.all([
|
||||
fetch('/api/v1/system/version', { cache: 'no-store' }).then(function(response) {
|
||||
fetch('/api/v1/system/version', { cache: 'no-store', credentials: 'same-origin' }).then(function(response) {
|
||||
if (!response.ok) throw new Error('Version check failed');
|
||||
return response.json();
|
||||
}),
|
||||
fetch('/api/v1/system/update-status', { cache: 'no-store' }).then(function(response) {
|
||||
fetch('/api/v1/system/update-status', { cache: 'no-store', credentials: 'same-origin' }).then(function(response) {
|
||||
if (!response.ok) throw new Error('Update status failed');
|
||||
return response.json();
|
||||
}),
|
||||
fetch('/api/v1/system/update-history', { cache: 'no-store' }).then(function(response) {
|
||||
fetch('/api/v1/system/update-history', { cache: 'no-store', credentials: 'same-origin' }).then(function(response) {
|
||||
if (!response.ok) throw new Error('Update history failed');
|
||||
return response.json();
|
||||
})
|
||||
}),
|
||||
checkVersionUpdateAvailability().catch(function() { return latestUpdateCheck || {}; })
|
||||
]).then(function(results) {
|
||||
var versionData = results[0] || {};
|
||||
var statusData = results[1] || {};
|
||||
var historyData = results[2] || {};
|
||||
var lastResult = statusData.lastResult || {};
|
||||
var updateCheckData = results[3] || latestUpdateCheck || {};
|
||||
var lastResult = statusData.lastResult || updateCheckData || {};
|
||||
var lastPolicy = lastResult.policy || {};
|
||||
var hasUpdate = !!(updateCheckData && updateCheckData.success && updateCheckData.available && updateCheckData.remote);
|
||||
|
||||
versionInfoStatus.textContent = 'Verification info loaded.';
|
||||
versionInfoStatus.textContent = hasUpdate
|
||||
? 'Update available — you can install it from here.'
|
||||
: 'Verification info loaded.';
|
||||
versionInfoGrid.style.display = 'grid';
|
||||
versionInfoGrid.innerHTML = [
|
||||
renderInfoRow('Version', versionData.version),
|
||||
renderInfoRow('Commit', versionData.commit),
|
||||
renderInfoRow('Updater Status', statusData.status),
|
||||
renderInfoRow('Last Check', statusData.lastCheck ? new Date(statusData.lastCheck).toLocaleString() : 'Never'),
|
||||
renderInfoRow('Update Available', lastResult.available),
|
||||
renderInfoRow('Update Available', hasUpdate),
|
||||
renderInfoRow('Available Version', updateCheckData.remote && updateCheckData.remote.version),
|
||||
renderInfoRow('Eligible', lastPolicy.eligible),
|
||||
renderInfoRow('Policy Reason', lastPolicy.reason),
|
||||
renderInfoRow('Channel', lastPolicy.releaseChannel || (lastResult.instance && lastResult.instance.channel)),
|
||||
renderInfoRow('Instance ID', lastResult.instance && lastResult.instance.instanceId)
|
||||
].join('');
|
||||
|
||||
if (versionInfoActions) versionInfoActions.style.display = 'flex';
|
||||
if (versionInfoUpdate) versionInfoUpdate.disabled = !hasUpdate;
|
||||
renderHistory(historyData.history);
|
||||
}).catch(function(error) {
|
||||
versionInfoStatus.textContent = 'Could not load verification info: ' + error.message;
|
||||
if (versionInfoActions) versionInfoActions.style.display = 'flex';
|
||||
});
|
||||
}
|
||||
|
||||
if (versionEl) {
|
||||
fetch('/api/v1/system/version', { cache: 'no-store' })
|
||||
.then(function(response) {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data && data.success && data.version) {
|
||||
versionEl.textContent = 'Version ' + data.version;
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
versionEl.textContent = 'Version unavailable';
|
||||
});
|
||||
loadVersionLabel(0);
|
||||
|
||||
versionEl.addEventListener('click', openVersionInfo);
|
||||
versionEl.addEventListener('click', function(event) {
|
||||
event.stopPropagation();
|
||||
openVersionInfo();
|
||||
});
|
||||
}
|
||||
|
||||
var licenseBtn = document.getElementById('license-btn');
|
||||
var apiDocsBtn = document.getElementById('api-docs-btn');
|
||||
var helpErrorsBtn = document.getElementById('help-errors-btn');
|
||||
|
||||
if (licenseBtn) {
|
||||
licenseBtn.addEventListener('click', function() {
|
||||
if (window.openLicenseModal) window.openLicenseModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (apiDocsBtn) {
|
||||
apiDocsBtn.addEventListener('click', function() {
|
||||
window.open('/api/docs', '_blank');
|
||||
});
|
||||
}
|
||||
|
||||
if (helpErrorsBtn) {
|
||||
helpErrorsBtn.addEventListener('click', function() {
|
||||
window.open('/help-errors.html', '_blank');
|
||||
});
|
||||
}
|
||||
|
||||
if (versionInfoUpdate) {
|
||||
versionInfoUpdate.addEventListener('click', function() {
|
||||
applyVersionUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
if (versionInfoOpenUpdates) {
|
||||
versionInfoOpenUpdates.addEventListener('click', function() {
|
||||
versionInfoModal.classList.remove('show');
|
||||
openUpdatesPanel();
|
||||
});
|
||||
}
|
||||
|
||||
if (versionInfoClose && versionInfoModal) {
|
||||
@@ -665,6 +801,9 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.openVersionInfo = openVersionInfo;
|
||||
window.applyVersionUpdate = applyVersionUpdate;
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -396,7 +396,7 @@
|
||||
}
|
||||
|
||||
async function dcApplyUpdate() {
|
||||
if (!confirm('Apply DashCaddy update? The API container will restart.')) return;
|
||||
if (!confirm('Apply DashCaddy update? The API container will restart.')) return false;
|
||||
dcApplyBtn.textContent = 'Updating...';
|
||||
dcApplyBtn.disabled = true;
|
||||
dcShowStatus('Downloading and applying update...', 'info');
|
||||
@@ -408,6 +408,7 @@
|
||||
dcApplyBtn.textContent = 'Applied!';
|
||||
// Remove notification dots
|
||||
document.querySelectorAll('.update-dot').forEach(d => d.remove());
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(data.error || 'Update failed');
|
||||
}
|
||||
@@ -415,6 +416,7 @@
|
||||
dcShowStatus('Update failed: ' + e.message, 'error');
|
||||
dcApplyBtn.textContent = 'Update Now';
|
||||
dcApplyBtn.disabled = false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +488,7 @@
|
||||
}
|
||||
|
||||
dcCheckBtn?.addEventListener('click', () => dcCheckForUpdate(false));
|
||||
dcApplyBtn?.addEventListener('click', dcApplyUpdate);
|
||||
dcApplyBtn?.addEventListener('click', () => dcApplyUpdate().catch(() => {}));
|
||||
dcRollbackBtn?.addEventListener('click', dcShowRollback);
|
||||
|
||||
checkBtn?.addEventListener('click', checkForUpdates);
|
||||
@@ -505,6 +507,9 @@
|
||||
if (!dcLastCheck) dcCheckForUpdate(true);
|
||||
});
|
||||
|
||||
window.dcApplyUpdate = dcApplyUpdate;
|
||||
window.dcCheckForUpdate = dcCheckForUpdate;
|
||||
|
||||
// Non-blocking check on page load — just adds notification dot if update available
|
||||
setTimeout(() => dcCheckForUpdate(true), 5000);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user