Update: version 1.2.0, new version UI features, CSP hash auto-update, bug fixes
This commit is contained in:
@@ -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": {
|
||||||
|
|||||||
@@ -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.
@@ -0,0 +1 @@
|
|||||||
|
a8a670311c60172ef38098819436a6ff081e5379da9e3ab18a291c74a1ea4f5c latest.tar.gz
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
Executable
+26
@@ -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."
|
||||||
Executable
+81
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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="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>
|
||||||
<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;">
|
<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
|
🔄 Reload Caddy
|
||||||
</button>
|
</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>
|
</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>
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user