feat(update): add release policy checks and dashboard version verification
This commit is contained in:
@@ -14,8 +14,8 @@ const fs = require('fs');
|
||||
const fsp = require('fs').promises;
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const os = require('os');
|
||||
const { execSync } = require('child_process');
|
||||
const zlib = require('zlib');
|
||||
const platformPaths = require('./platform-paths');
|
||||
const isWindows = platformPaths.isWindows;
|
||||
|
||||
@@ -31,6 +31,10 @@ const DEFAULTS = {
|
||||
MAX_BACKUPS: 3,
|
||||
HEALTH_TIMEOUT: 60000,
|
||||
DOWNLOAD_TIMEOUT: 120000,
|
||||
CHANNEL: 'stable',
|
||||
INSTANCE_ID_FILE: platformPaths.isWindows
|
||||
? path.join(platformPaths.caddyBase, 'instance-id')
|
||||
: '/etc/dashcaddy/instance-id',
|
||||
};
|
||||
|
||||
class SelfUpdater extends EventEmitter {
|
||||
@@ -48,12 +52,15 @@ class SelfUpdater extends EventEmitter {
|
||||
apiSourceDir: options.apiSourceDir || DEFAULTS.API_SOURCE_DIR,
|
||||
frontendDir: options.frontendDir || DEFAULTS.FRONTEND_DIR,
|
||||
maxBackups: parseInt(options.maxBackups || DEFAULTS.MAX_BACKUPS, 10),
|
||||
channel: options.channel || process.env.DASHCADDY_UPDATE_CHANNEL || DEFAULTS.CHANNEL,
|
||||
instanceIdFile: options.instanceIdFile || process.env.DASHCADDY_INSTANCE_ID_FILE || DEFAULTS.INSTANCE_ID_FILE,
|
||||
};
|
||||
|
||||
this.status = 'idle'; // idle | checking | downloading | applying | waiting
|
||||
this.checkTimer = null;
|
||||
this.lastCheckTime = null;
|
||||
this.lastCheckResult = null;
|
||||
this.instanceId = this._loadOrCreateInstanceId();
|
||||
|
||||
// Ensure directories exist
|
||||
this._ensureDirs();
|
||||
@@ -80,7 +87,7 @@ class SelfUpdater extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Version Info ──
|
||||
// ── Version / Identity Info ──
|
||||
|
||||
getLocalVersion() {
|
||||
try {
|
||||
@@ -95,6 +102,18 @@ class SelfUpdater extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
getInstanceInfo() {
|
||||
return {
|
||||
instanceId: this.instanceId,
|
||||
channel: this.config.channel,
|
||||
hostname: os.hostname(),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
isWindows,
|
||||
version: this.getLocalVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
@@ -122,10 +141,18 @@ class SelfUpdater extends EventEmitter {
|
||||
}
|
||||
|
||||
const local = this.getLocalVersion();
|
||||
const available = this._isNewer(local, remote);
|
||||
const policy = this._evaluateReleasePolicy(local, remote);
|
||||
const available = policy.eligible && policy.newer;
|
||||
|
||||
this.lastCheckTime = Date.now();
|
||||
this.lastCheckResult = { available, local, remote, sourceUrl };
|
||||
this.lastCheckResult = {
|
||||
available,
|
||||
local,
|
||||
remote,
|
||||
sourceUrl,
|
||||
policy,
|
||||
instance: this.getInstanceInfo(),
|
||||
};
|
||||
this.status = 'idle';
|
||||
|
||||
if (available) {
|
||||
@@ -149,12 +176,17 @@ class SelfUpdater extends EventEmitter {
|
||||
}
|
||||
|
||||
const local = this.getLocalVersion();
|
||||
const policy = this._evaluateReleasePolicy(local, remoteInfo);
|
||||
if (!policy.eligible) {
|
||||
throw new Error(`Release not eligible for this instance: ${policy.reason}`);
|
||||
}
|
||||
|
||||
const stagingDir = path.join(this.config.updatesDir, 'staging');
|
||||
|
||||
try {
|
||||
// 1. Download (try primary, fallback to mirror)
|
||||
this.status = 'downloading';
|
||||
this.emit('update-progress', { step: 'downloading', version: remoteInfo.version });
|
||||
this.emit('update-progress', { step: 'downloading', version: remoteInfo.version, policy });
|
||||
|
||||
const tarballPath = path.join(this.config.updatesDir, remoteInfo.tarball);
|
||||
const primaryUrl = `${this.config.updateUrl}/${remoteInfo.tarball}`;
|
||||
@@ -207,6 +239,8 @@ class SelfUpdater extends EventEmitter {
|
||||
stagingDir: hostApiSrc,
|
||||
apiSourceDir: this.config.apiSourceDir,
|
||||
timestamp: new Date().toISOString(),
|
||||
channel: this.config.channel,
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
await fsp.writeFile(
|
||||
path.join(this.config.updatesDir, 'trigger.json'),
|
||||
@@ -222,6 +256,8 @@ class SelfUpdater extends EventEmitter {
|
||||
status: 'pending',
|
||||
frontendUpdated: !!frontendSrc,
|
||||
apiUpdated: true,
|
||||
channel: this.config.channel,
|
||||
instanceId: this.instanceId,
|
||||
});
|
||||
} else if (isWindows) {
|
||||
// Windows: frontend updated, API needs manual restart
|
||||
@@ -233,6 +269,8 @@ class SelfUpdater extends EventEmitter {
|
||||
frontendUpdated: !!frontendSrc,
|
||||
apiUpdated: false,
|
||||
note: 'API update requires manual container restart on Windows',
|
||||
channel: this.config.channel,
|
||||
instanceId: this.instanceId,
|
||||
});
|
||||
this.status = 'idle';
|
||||
}
|
||||
@@ -246,6 +284,7 @@ class SelfUpdater extends EventEmitter {
|
||||
toVersion: remoteInfo.version,
|
||||
frontendUpdated: !!frontendSrc,
|
||||
apiUpdated: !isWindows && !!apiSrc,
|
||||
policy,
|
||||
};
|
||||
} catch (e) {
|
||||
this.status = 'idle';
|
||||
@@ -255,6 +294,8 @@ class SelfUpdater extends EventEmitter {
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'failed',
|
||||
error: e.message,
|
||||
channel: this.config.channel,
|
||||
instanceId: this.instanceId,
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
@@ -308,6 +349,8 @@ class SelfUpdater extends EventEmitter {
|
||||
stagingDir: hostBackupDir,
|
||||
apiSourceDir: this.config.apiSourceDir,
|
||||
timestamp: new Date().toISOString(),
|
||||
channel: this.config.channel,
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
|
||||
this.status = 'waiting';
|
||||
@@ -322,6 +365,8 @@ class SelfUpdater extends EventEmitter {
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'pending',
|
||||
rollback: true,
|
||||
channel: this.config.channel,
|
||||
instanceId: this.instanceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -362,20 +407,143 @@ class SelfUpdater extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_evaluateReleasePolicy(local, remote) {
|
||||
const releaseChannel = remote?.channel || remote?.releaseChannel || 'stable';
|
||||
const allowedChannels = Array.isArray(remote?.channels)
|
||||
? remote.channels
|
||||
: Array.isArray(remote?.eligibleChannels)
|
||||
? remote.eligibleChannels
|
||||
: [releaseChannel];
|
||||
|
||||
if (!allowedChannels.includes(this.config.channel)) {
|
||||
return {
|
||||
eligible: false,
|
||||
newer: this._isNewer(local, remote),
|
||||
reason: `channel mismatch (${this.config.channel} not in ${allowedChannels.join(', ')})`,
|
||||
releaseChannel,
|
||||
allowedChannels,
|
||||
};
|
||||
}
|
||||
|
||||
if (remote?.revoked === true) {
|
||||
return {
|
||||
eligible: false,
|
||||
newer: this._isNewer(local, remote),
|
||||
reason: 'release revoked',
|
||||
releaseChannel,
|
||||
allowedChannels,
|
||||
};
|
||||
}
|
||||
|
||||
const minUpdaterVersion = remote?.minUpdaterVersion;
|
||||
if (minUpdaterVersion && this._compareVersions(local.version, minUpdaterVersion) < 0) {
|
||||
return {
|
||||
eligible: false,
|
||||
newer: this._isNewer(local, remote),
|
||||
reason: `requires updater >= ${minUpdaterVersion}`,
|
||||
releaseChannel,
|
||||
allowedChannels,
|
||||
};
|
||||
}
|
||||
|
||||
const rollout = this._normalizeRollout(remote?.rollout);
|
||||
if (rollout < 100) {
|
||||
const bucket = this._getRolloutBucket(this.instanceId);
|
||||
if (bucket >= rollout) {
|
||||
return {
|
||||
eligible: false,
|
||||
newer: this._isNewer(local, remote),
|
||||
reason: `outside rollout (${bucket} >= ${rollout})`,
|
||||
releaseChannel,
|
||||
allowedChannels,
|
||||
rollout,
|
||||
rolloutBucket: bucket,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const targets = remote?.targets;
|
||||
if (targets && typeof targets === 'object') {
|
||||
const platformKey = `${process.platform}-${process.arch}`;
|
||||
const matchedTarget = targets[platformKey] || targets[process.platform] || targets.default;
|
||||
if (!matchedTarget) {
|
||||
return {
|
||||
eligible: false,
|
||||
newer: this._isNewer(local, remote),
|
||||
reason: `no target for ${platformKey}`,
|
||||
releaseChannel,
|
||||
allowedChannels,
|
||||
rollout,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
eligible: true,
|
||||
newer: this._isNewer(local, remote),
|
||||
reason: 'eligible',
|
||||
releaseChannel,
|
||||
allowedChannels,
|
||||
rollout,
|
||||
rolloutBucket: this._getRolloutBucket(this.instanceId),
|
||||
};
|
||||
}
|
||||
|
||||
_isNewer(local, remote) {
|
||||
if (!remote || !remote.version) return false;
|
||||
// Compare semver: split into [major, minor, patch]
|
||||
const lv = (local.version || '0.0.0').split('.').map(Number);
|
||||
const rv = remote.version.split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((rv[i] || 0) > (lv[i] || 0)) return true;
|
||||
if ((rv[i] || 0) < (lv[i] || 0)) return false;
|
||||
}
|
||||
const versionCompare = this._compareVersions(local.version || '0.0.0', remote.version);
|
||||
if (versionCompare < 0) return true;
|
||||
if (versionCompare > 0) return false;
|
||||
// Same version — check commit hash
|
||||
if (remote.commit && local.commit && remote.commit !== local.commit) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
_compareVersions(a, b) {
|
||||
const av = String(a || '0.0.0').split('.').map(part => parseInt(part, 10) || 0);
|
||||
const bv = String(b || '0.0.0').split('.').map(part => parseInt(part, 10) || 0);
|
||||
const len = Math.max(av.length, bv.length, 3);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const left = av[i] || 0;
|
||||
const right = bv[i] || 0;
|
||||
if (left > right) return 1;
|
||||
if (left < right) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
_normalizeRollout(value) {
|
||||
if (value == null) return 100;
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return 100;
|
||||
return Math.max(0, Math.min(100, Math.floor(parsed)));
|
||||
}
|
||||
|
||||
_getRolloutBucket(instanceId) {
|
||||
const digest = crypto.createHash('sha256').update(String(instanceId || 'unknown')).digest();
|
||||
return digest[0] % 100;
|
||||
}
|
||||
|
||||
_loadOrCreateInstanceId() {
|
||||
try {
|
||||
if (fs.existsSync(this.config.instanceIdFile)) {
|
||||
const existing = fs.readFileSync(this.config.instanceIdFile, 'utf8').trim();
|
||||
if (existing) return existing;
|
||||
}
|
||||
} catch (_) {
|
||||
// Fall through and regenerate
|
||||
}
|
||||
|
||||
const instanceId = crypto.randomUUID();
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(this.config.instanceIdFile), { recursive: true });
|
||||
fs.writeFileSync(this.config.instanceIdFile, `${instanceId}\n`, 'utf8');
|
||||
} catch (error) {
|
||||
console.warn('[SelfUpdater] Failed to persist instance ID:', error.message);
|
||||
}
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
_addToHistory(entry) {
|
||||
const history = this.getUpdateHistory();
|
||||
history.unshift(entry);
|
||||
@@ -527,6 +695,9 @@ const selfUpdater = new SelfUpdater({
|
||||
hostUpdatesDir: process.env.DASHCADDY_HOST_UPDATES_DIR,
|
||||
apiSourceDir: process.env.DASHCADDY_API_SOURCE_DIR,
|
||||
frontendDir: process.env.DASHCADDY_FRONTEND_DIR,
|
||||
channel: process.env.DASHCADDY_UPDATE_CHANNEL,
|
||||
instanceIdFile: process.env.DASHCADDY_INSTANCE_ID_FILE,
|
||||
});
|
||||
|
||||
module.exports = selfUpdater;
|
||||
module.exports.SelfUpdater = SelfUpdater;
|
||||
@@ -167,11 +167,103 @@ button:focus-visible {
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.reload-caddy-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.dashboard-version {
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
line-height: 1;
|
||||
padding: 0 2px;
|
||||
user-select: text;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.dashboard-version:hover {
|
||||
color: var(--fg);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.version-info-modal-content {
|
||||
min-width: 420px;
|
||||
max-width: 620px;
|
||||
}
|
||||
|
||||
.version-info-subtitle {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.version-info-status {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.version-info-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.version-info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.version-info-label {
|
||||
font-weight: 600;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.version-info-value {
|
||||
color: var(--muted);
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.version-info-history h4 {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.version-history-entry {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.version-history-status {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.version-history-meta {
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.license-status-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -83,20 +83,36 @@
|
||||
|
||||
<!-- License status + Reload Caddy Button - top right corner -->
|
||||
<div class="reload-caddy-container">
|
||||
<div class="theme-toggle-group">
|
||||
<button id="theme" class="theme-toggle-btn" aria-label="Cycle theme" title="Click to cycle themes">
|
||||
<span id="theme-icon">🎨</span> <span id="theme-label">Dark</span>
|
||||
<div class="reload-caddy-main">
|
||||
<div class="theme-toggle-group">
|
||||
<button id="theme" class="theme-toggle-btn" aria-label="Cycle theme" title="Click to cycle themes">
|
||||
<span id="theme-icon">🎨</span> <span id="theme-label">Dark</span>
|
||||
</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>
|
||||
<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>
|
||||
<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 id="version-info-modal" class="weather-modal">
|
||||
<div class="weather-modal-content version-info-modal-content">
|
||||
<h3>DashCaddy Verification Info</h3>
|
||||
<p class="version-info-subtitle">Current version details and updater verification status.</p>
|
||||
<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;">
|
||||
<button id="version-info-close">Close</button>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -538,6 +554,117 @@
|
||||
}
|
||||
var yr = document.getElementById('footer-year');
|
||||
if (yr) yr.textContent = new Date().getFullYear();
|
||||
|
||||
var versionEl = document.getElementById('dashboard-version');
|
||||
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 versionInfoClose = document.getElementById('version-info-close');
|
||||
|
||||
function formatValue(value) {
|
||||
if (value == null || value === '') return '—';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function renderInfoRow(label, value) {
|
||||
return '<div class="version-info-row"><span class="version-info-label">' + label + '</span><span class="version-info-value">' + formatValue(value) + '</span></div>';
|
||||
}
|
||||
|
||||
function renderHistory(history) {
|
||||
if (!Array.isArray(history) || !history.length) {
|
||||
versionInfoHistory.style.display = 'none';
|
||||
versionInfoHistory.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
versionInfoHistory.style.display = '';
|
||||
versionInfoHistory.innerHTML = '<h4>Recent Update History</h4>' + history.slice(0, 5).map(function(entry) {
|
||||
return '<div class="version-history-entry">'
|
||||
+ '<div><strong>' + formatValue(entry.version) + '</strong> <span class="version-history-status">' + formatValue(entry.status) + '</span></div>'
|
||||
+ '<div class="version-history-meta">From ' + formatValue(entry.fromVersion) + ' · ' + formatValue(entry.timestamp) + '</div>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function openVersionInfo() {
|
||||
if (!versionInfoModal) return;
|
||||
versionInfoModal.classList.add('show');
|
||||
versionInfoStatus.textContent = 'Loading…';
|
||||
versionInfoGrid.style.display = 'none';
|
||||
versionInfoHistory.style.display = 'none';
|
||||
versionInfoGrid.innerHTML = '';
|
||||
versionInfoHistory.innerHTML = '';
|
||||
|
||||
Promise.all([
|
||||
fetch('/api/v1/system/version', { cache: 'no-store' }).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) {
|
||||
if (!response.ok) throw new Error('Update status failed');
|
||||
return response.json();
|
||||
}),
|
||||
fetch('/api/v1/system/update-history', { cache: 'no-store' }).then(function(response) {
|
||||
if (!response.ok) throw new Error('Update history failed');
|
||||
return response.json();
|
||||
})
|
||||
]).then(function(results) {
|
||||
var versionData = results[0] || {};
|
||||
var statusData = results[1] || {};
|
||||
var historyData = results[2] || {};
|
||||
var lastResult = statusData.lastResult || {};
|
||||
var lastPolicy = lastResult.policy || {};
|
||||
|
||||
versionInfoStatus.textContent = '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('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('');
|
||||
|
||||
renderHistory(historyData.history);
|
||||
}).catch(function(error) {
|
||||
versionInfoStatus.textContent = 'Could not load verification info: ' + error.message;
|
||||
});
|
||||
}
|
||||
|
||||
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';
|
||||
});
|
||||
|
||||
versionEl.addEventListener('click', openVersionInfo);
|
||||
}
|
||||
|
||||
if (versionInfoClose && versionInfoModal) {
|
||||
versionInfoClose.addEventListener('click', function() {
|
||||
versionInfoModal.classList.remove('show');
|
||||
});
|
||||
versionInfoModal.addEventListener('click', function(event) {
|
||||
if (event.target === versionInfoModal) {
|
||||
versionInfoModal.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user