Update: version 1.2.0, new version UI features, CSP hash auto-update, bug fixes

This commit is contained in:
Krystie
2026-05-14 21:04:35 -07:00
parent a216dd882d
commit 947007438f
12 changed files with 385 additions and 44 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "dashcaddy-api", "name": "dashcaddy-api",
"version": "1.1.5", "version": "1.2.0",
"description": "DashCaddy API server - Dashboard backend for Docker, Caddy & DNS management", "description": "DashCaddy API server - Dashboard backend for Docker, Caddy & DNS management",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+15 -3
View File
@@ -311,13 +311,25 @@ class SelfUpdater extends EventEmitter {
// Delete the result file so we don't process it again // Delete the result file so we don't process it again
await fsp.unlink(resultPath).catch(() => {}); await fsp.unlink(resultPath).catch(() => {});
// Update history // Update the matching history entry, preferring the newest pending item
// for the same target version. Fall back to the newest pending item if
// older result files lack enough metadata to match more precisely.
const history = this.getUpdateHistory(); const history = this.getUpdateHistory();
const pending = history.find(h => h.status === 'pending'); const pendingIndex = history.findIndex(
if (pending) { h => h.status === 'pending' && (!result.version || h.version === result.version)
);
const fallbackIndex = pendingIndex === -1
? history.findIndex(h => h.status === 'pending')
: -1;
const historyIndex = pendingIndex !== -1 ? pendingIndex : fallbackIndex;
if (historyIndex !== -1) {
const pending = history[historyIndex];
pending.status = result.success ? 'success' : 'rolled-back'; pending.status = result.success ? 'success' : 'rolled-back';
pending.duration = result.duration; pending.duration = result.duration;
if (result.error) pending.error = result.error; if (result.error) pending.error = result.error;
if (result.version) pending.version = result.version;
if (result.timestamp) pending.completedAt = result.timestamp;
this._saveHistory(history); this._saveHistory(history);
} }
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
a8a670311c60172ef38098819436a6ff081e5379da9e3ab18a291c74a1ea4f5c latest.tar.gz
+9
View File
@@ -0,0 +1,9 @@
{
"version": "1.2.0",
"commit": "a216dd8",
"channel": "stable",
"url": "https://get.dashcaddy.net/release/latest.tar.gz",
"sha256": "a8a670311c60172ef38098819436a6ff081e5379da9e3ab18a291c74a1ea4f5c",
"publishedAt": "2026-05-06T23:06:32Z",
"changelog": "Release published via scripts/publish-release.sh"
}
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
STATUS_DIR="${ROOT_DIR}/status"
API_DIR="${ROOT_DIR}/dashcaddy-api"
log() { echo "[prepare-release] $*"; }
fail() { echo "[prepare-release] ERROR: $*" >&2; exit 1; }
[[ -f "${STATUS_DIR}/package.json" ]] || fail "Missing status/package.json"
[[ -f "${API_DIR}/package.json" ]] || fail "Missing dashcaddy-api/package.json"
log "Building dashboard frontend..."
cd "${STATUS_DIR}"
npm run build
log "Verifying CSP hash present in status/index.html..."
grep -q "Content-Security-Policy" "${STATUS_DIR}/index.html" || fail "Missing CSP meta tag"
grep -q "sha256-" "${STATUS_DIR}/index.html" || fail "Missing CSP script hash"
grep -q "license-topbar-version" "${STATUS_DIR}/index.html" || fail "Expected version-chip markup missing"
log "Verifying API version metadata..."
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('${API_DIR}/package.json','utf8')); if(!pkg.version) process.exit(1); console.log('[prepare-release] API version', pkg.version);"
log "Release prep complete. Package from repo root after this step."
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
API_DIR="${ROOT_DIR}/dashcaddy-api"
STATUS_DIR="${ROOT_DIR}/status"
PREP_SCRIPT="${ROOT_DIR}/scripts/prepare-release.sh"
OUT_DIR="${1:-${ROOT_DIR}/release}"
CHANNEL="${CHANNEL:-stable}"
BASE_URL="${BASE_URL:-https://get.dashcaddy.net/release}"
CHANGELOG_FILE="${CHANGELOG_FILE:-}"
log() { echo "[publish-release] $*"; }
fail() { echo "[publish-release] ERROR: $*" >&2; exit 1; }
[[ -x "$PREP_SCRIPT" ]] || fail "Missing prepare script: $PREP_SCRIPT"
[[ -f "${API_DIR}/package.json" ]] || fail "Missing API package.json"
VERSION="$(node -pe "require('${API_DIR}/package.json').version")"
COMMIT="$(git -C "${ROOT_DIR}" rev-parse --short HEAD)"
TARBALL_NAME="latest.tar.gz"
VERSION_JSON="version.json"
SHA256_FILE="latest.tar.gz.sha256"
mkdir -p "$OUT_DIR"
rm -f "${OUT_DIR:?}/${TARBALL_NAME}" "${OUT_DIR}/${VERSION_JSON}" "${OUT_DIR}/${SHA256_FILE}"
log "Preparing release inputs..."
"$PREP_SCRIPT"
TMP_DIR="$(mktemp -d)"
cleanup() { rm -rf "$TMP_DIR"; }
trap cleanup EXIT
STAGE_DIR="${TMP_DIR}/dashcaddy"
mkdir -p "$STAGE_DIR"
log "Staging API and dashboard files..."
cp -a "${API_DIR}" "$STAGE_DIR/dashcaddy-api"
cp -a "${STATUS_DIR}" "$STAGE_DIR/status"
cp -a "${ROOT_DIR}/ca" "$STAGE_DIR/ca" 2>/dev/null || true
cp -a "${ROOT_DIR}/dashcaddy-installer" "$STAGE_DIR/dashcaddy-installer" 2>/dev/null || true
cp -a "${ROOT_DIR}/README.md" "$STAGE_DIR/README.md" 2>/dev/null || true
find "$STAGE_DIR" \( -name node_modules -o -name .git -o -name coverage -o -name .nyc_output \) -prune -exec rm -rf {} +
find "$STAGE_DIR" -type f \( -name '*.test.js' -o -name '*.spec.js' \) -delete
log "Creating tarball..."
tar -czf "${OUT_DIR}/${TARBALL_NAME}" -C "$TMP_DIR" dashcaddy
SHA256="$(sha256sum "${OUT_DIR}/${TARBALL_NAME}" | awk '{print $1}')"
printf '%s %s\n' "$SHA256" "$TARBALL_NAME" > "${OUT_DIR}/${SHA256_FILE}"
CHANGELOG=""
if [[ -n "$CHANGELOG_FILE" && -f "$CHANGELOG_FILE" ]]; then
CHANGELOG="$(python3 - <<'PY' "$CHANGELOG_FILE"
from pathlib import Path
import json, sys
print(json.dumps(Path(sys.argv[1]).read_text().strip()))
PY
)"
else
CHANGELOG='"Release published via scripts/publish-release.sh"'
fi
log "Writing version manifest..."
cat > "${OUT_DIR}/${VERSION_JSON}" <<EOF
{
"version": "${VERSION}",
"commit": "${COMMIT}",
"channel": "${CHANNEL}",
"url": "${BASE_URL}/${TARBALL_NAME}",
"sha256": "${SHA256}",
"publishedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"changelog": ${CHANGELOG}
}
EOF
log "Release artifacts created in ${OUT_DIR}"
log " - ${TARBALL_NAME}"
log " - ${SHA256_FILE}"
log " - ${VERSION_JSON}"
+32 -1
View File
@@ -1,9 +1,11 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const crypto = require('crypto');
const esbuild = require('esbuild'); const esbuild = require('esbuild');
const JS = (...parts) => path.join(__dirname, 'js', ...parts); const JS = (...parts) => path.join(__dirname, 'js', ...parts);
const DIST = path.join(__dirname, 'dist'); const DIST = path.join(__dirname, 'dist');
const INDEX_HTML = path.join(__dirname, 'index.html');
// Bundle definitions — files are concatenated in order, then minified // Bundle definitions — files are concatenated in order, then minified
const bundles = { 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() { async function build() {
// Ensure dist/ exists // Ensure dist/ exists
if (!fs.existsSync(DIST)) fs.mkdirSync(DIST); if (!fs.existsSync(DIST)) fs.mkdirSync(DIST);
@@ -96,6 +124,8 @@ async function build() {
results[outName] = { rawSize, minSize, fileCount: files.length }; results[outName] = { rawSize, minSize, fileCount: files.length };
} }
const cspHash = updateInlineScriptCspHash();
// Summary // Summary
console.log('\n DashCaddy Frontend Build\n'); console.log('\n DashCaddy Frontend Build\n');
console.log(' Bundle Files Raw Min'); console.log(' Bundle Files Raw Min');
@@ -108,7 +138,8 @@ async function build() {
} }
console.log(' ─────────────────────────────────────────'); console.log(' ─────────────────────────────────────────');
console.log(` ${'Total'.padEnd(18)} ${totalRaw.toFixed(1).padStart(6)} KB ${totalMin.toFixed(1).padStart(6)} KB`); 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 // Watch mode
+39 -2
View File
@@ -170,13 +170,20 @@ button:focus-visible {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
gap: 6px;
} }
.reload-caddy-main { .reload-caddy-main {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
.license-version-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 18px; gap: 8px;
margin-top: 12px;
} }
.dashboard-version { .dashboard-version {
@@ -196,6 +203,36 @@ button:focus-visible {
opacity: 0.9; 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 { .version-info-modal-content {
min-width: 420px; min-width: 420px;
max-width: 620px; max-width: 620px;
+2 -2
View File
File diff suppressed because one or more lines are too long
+170 -31
View File
@@ -8,7 +8,7 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" /> <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" href="/assets/dashcaddy-favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/icon-192.png"> <link rel="icon" type="image/png" sizes="192x192" href="/assets/icon-192.png">
@@ -90,16 +90,18 @@
</button> </button>
<button id="theme-customize-btn" class="theme-customize-link" title="Customize theme colors">Customize Theme</button> <button id="theme-customize-btn" class="theme-customize-link" title="Customize theme colors">Customize Theme</button>
</div> </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"> <div id="license-status-topbar" class="license-status-topbar free" title="Click to manage license">
<span id="license-topbar-icon">&#9734;</span> <span id="license-topbar-icon">&#9734;</span>
<span id="license-topbar-text">FREE TIER</span> <span id="license-topbar-text">FREE TIER</span>
<span id="license-topbar-time"></span> <span id="license-topbar-time"></span>
</div> </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;"> <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>
🔄 Reload Caddy </div>
</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>
</div> </div>
@@ -110,7 +112,9 @@
<div id="version-info-status" class="version-info-status">Loading…</div> <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-grid" class="version-info-grid" style="display:none;"></div>
<div id="version-info-history" class="version-info-history" 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> <button id="version-info-close">Close</button>
</div> </div>
</div> </div>
@@ -159,9 +163,9 @@
<div class="tools-section-items"> <div class="tools-section-items">
<button id="manage-tokens" aria-label="Manage API tokens">🔑 Tokens</button> <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="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="license-btn" aria-label="License management">🔑 License</button>
<button id="api-docs-btn" aria-label="API documentation" onclick="window.open('/api/docs', '_blank')">📖 API</button> <button id="api-docs-btn" aria-label="API documentation">📖 API</button>
<button id="help-errors-btn" aria-label="Troubleshooting guide" onclick="window.open('/help-errors.html', '_blank')">❓ Help</button> <button id="help-errors-btn" aria-label="Troubleshooting guide">❓ Help</button>
</div> </div>
</div> </div>
</div> </div>
@@ -555,12 +559,18 @@
var yr = document.getElementById('footer-year'); var yr = document.getElementById('footer-year');
if (yr) yr.textContent = new Date().getFullYear(); 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 versionInfoModal = document.getElementById('version-info-modal');
var versionInfoStatus = document.getElementById('version-info-status'); var versionInfoStatus = document.getElementById('version-info-status');
var versionInfoGrid = document.getElementById('version-info-grid'); var versionInfoGrid = document.getElementById('version-info-grid');
var versionInfoHistory = document.getElementById('version-info-history'); 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 versionInfoClose = document.getElementById('version-info-close');
var latestUpdateCheck = null;
function formatValue(value) { function formatValue(value) {
if (value == null || value === '') return '—'; if (value == null || value === '') return '—';
@@ -588,71 +598,197 @@
}).join(''); }).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() { function openVersionInfo() {
if (!versionInfoModal) return; if (!versionInfoModal) return;
versionInfoModal.classList.add('show'); versionInfoModal.classList.add('show');
versionInfoStatus.textContent = 'Loading…'; versionInfoStatus.textContent = 'Loading…';
versionInfoGrid.style.display = 'none'; versionInfoGrid.style.display = 'none';
versionInfoHistory.style.display = 'none'; versionInfoHistory.style.display = 'none';
if (versionInfoActions) versionInfoActions.style.display = 'none';
if (versionInfoUpdate) {
versionInfoUpdate.disabled = true;
versionInfoUpdate.textContent = 'Update Now';
}
versionInfoGrid.innerHTML = ''; versionInfoGrid.innerHTML = '';
versionInfoHistory.innerHTML = ''; versionInfoHistory.innerHTML = '';
Promise.all([ 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'); if (!response.ok) throw new Error('Version check failed');
return response.json(); 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'); if (!response.ok) throw new Error('Update status failed');
return response.json(); 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'); if (!response.ok) throw new Error('Update history failed');
return response.json(); return response.json();
}) }),
checkVersionUpdateAvailability().catch(function() { return latestUpdateCheck || {}; })
]).then(function(results) { ]).then(function(results) {
var versionData = results[0] || {}; var versionData = results[0] || {};
var statusData = results[1] || {}; var statusData = results[1] || {};
var historyData = results[2] || {}; var historyData = results[2] || {};
var lastResult = statusData.lastResult || {}; var updateCheckData = results[3] || latestUpdateCheck || {};
var lastResult = statusData.lastResult || updateCheckData || {};
var lastPolicy = lastResult.policy || {}; 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.style.display = 'grid';
versionInfoGrid.innerHTML = [ versionInfoGrid.innerHTML = [
renderInfoRow('Version', versionData.version), renderInfoRow('Version', versionData.version),
renderInfoRow('Commit', versionData.commit), renderInfoRow('Commit', versionData.commit),
renderInfoRow('Updater Status', statusData.status), renderInfoRow('Updater Status', statusData.status),
renderInfoRow('Last Check', statusData.lastCheck ? new Date(statusData.lastCheck).toLocaleString() : 'Never'), 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('Eligible', lastPolicy.eligible),
renderInfoRow('Policy Reason', lastPolicy.reason), renderInfoRow('Policy Reason', lastPolicy.reason),
renderInfoRow('Channel', lastPolicy.releaseChannel || (lastResult.instance && lastResult.instance.channel)), renderInfoRow('Channel', lastPolicy.releaseChannel || (lastResult.instance && lastResult.instance.channel)),
renderInfoRow('Instance ID', lastResult.instance && lastResult.instance.instanceId) renderInfoRow('Instance ID', lastResult.instance && lastResult.instance.instanceId)
].join(''); ].join('');
if (versionInfoActions) versionInfoActions.style.display = 'flex';
if (versionInfoUpdate) versionInfoUpdate.disabled = !hasUpdate;
renderHistory(historyData.history); renderHistory(historyData.history);
}).catch(function(error) { }).catch(function(error) {
versionInfoStatus.textContent = 'Could not load verification info: ' + error.message; versionInfoStatus.textContent = 'Could not load verification info: ' + error.message;
if (versionInfoActions) versionInfoActions.style.display = 'flex';
}); });
} }
if (versionEl) { if (versionEl) {
fetch('/api/v1/system/version', { cache: 'no-store' }) loadVersionLabel(0);
.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';
});
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) { if (versionInfoClose && versionInfoModal) {
@@ -665,6 +801,9 @@
} }
}); });
} }
window.openVersionInfo = openVersionInfo;
window.applyVersionUpdate = applyVersionUpdate;
})(); })();
</script> </script>
+7 -2
View File
@@ -396,7 +396,7 @@
} }
async function dcApplyUpdate() { 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.textContent = 'Updating...';
dcApplyBtn.disabled = true; dcApplyBtn.disabled = true;
dcShowStatus('Downloading and applying update...', 'info'); dcShowStatus('Downloading and applying update...', 'info');
@@ -408,6 +408,7 @@
dcApplyBtn.textContent = 'Applied!'; dcApplyBtn.textContent = 'Applied!';
// Remove notification dots // Remove notification dots
document.querySelectorAll('.update-dot').forEach(d => d.remove()); document.querySelectorAll('.update-dot').forEach(d => d.remove());
return true;
} else { } else {
throw new Error(data.error || 'Update failed'); throw new Error(data.error || 'Update failed');
} }
@@ -415,6 +416,7 @@
dcShowStatus('Update failed: ' + e.message, 'error'); dcShowStatus('Update failed: ' + e.message, 'error');
dcApplyBtn.textContent = 'Update Now'; dcApplyBtn.textContent = 'Update Now';
dcApplyBtn.disabled = false; dcApplyBtn.disabled = false;
throw e;
} }
} }
@@ -486,7 +488,7 @@
} }
dcCheckBtn?.addEventListener('click', () => dcCheckForUpdate(false)); dcCheckBtn?.addEventListener('click', () => dcCheckForUpdate(false));
dcApplyBtn?.addEventListener('click', dcApplyUpdate); dcApplyBtn?.addEventListener('click', () => dcApplyUpdate().catch(() => {}));
dcRollbackBtn?.addEventListener('click', dcShowRollback); dcRollbackBtn?.addEventListener('click', dcShowRollback);
checkBtn?.addEventListener('click', checkForUpdates); checkBtn?.addEventListener('click', checkForUpdates);
@@ -505,6 +507,9 @@
if (!dcLastCheck) dcCheckForUpdate(true); if (!dcLastCheck) dcCheckForUpdate(true);
}); });
window.dcApplyUpdate = dcApplyUpdate;
window.dcCheckForUpdate = dcCheckForUpdate;
// Non-blocking check on page load — just adds notification dot if update available // Non-blocking check on page load — just adds notification dot if update available
setTimeout(() => dcCheckForUpdate(true), 5000); setTimeout(() => dcCheckForUpdate(true), 5000);
})(); })();