feat(monitoring): add update-run log, fix scheduler skipping stale customer containers
The automatic scheduler only recreated customer containers when it had just found a NEW image on Docker Hub. If an image was already pulled locally (e.g. from an earlier manual pull, or auto-apply was off at the time) but customer containers hadn't been recreated yet, the daily check reported "already up to date" and silently never touched those customers again — the recreate step needs to run independently of whether Hub had anything new, based on each customer's actual container-vs-local-image state. Also adds a persistent UpdateRunLog: every scheduled and manual pull/update run now records per-image pull results and per-customer recreate results, visible in Monitoring > Update Log, instead of only existing in container logs.
This commit is contained in:
@@ -1260,6 +1260,19 @@
|
||||
<p class="text-muted mb-0" data-i18n="monitoring.clickCheckUpdates">Click "Check for Updates" to compare local images with Docker Hub.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Run Log -->
|
||||
<div class="card shadow-sm mt-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-clock-history me-2"></i><span data-i18n="monitoring.updateLogTitle">Update Log</span></span>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="loadUpdateLog()" id="btn-refresh-update-log">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i><span data-i18n="common.refresh">Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body" id="update-log-body">
|
||||
<p class="text-muted mb-0" data-i18n="common.loading">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+84
-1
@@ -1943,7 +1943,90 @@ function togglePasswordVisibility(inputId) {
|
||||
// Monitoring
|
||||
// ---------------------------------------------------------------------------
|
||||
async function loadMonitoring() {
|
||||
await Promise.all([loadResources(), loadAllCustomerStatuses()]);
|
||||
await Promise.all([loadResources(), loadAllCustomerStatuses(), loadUpdateLog()]);
|
||||
}
|
||||
|
||||
const _updateLogStatusBadge = {
|
||||
success: 'bg-success',
|
||||
no_update: 'bg-secondary',
|
||||
partial: 'bg-warning text-dark',
|
||||
failed: 'bg-danger',
|
||||
running: 'bg-info',
|
||||
};
|
||||
|
||||
async function loadUpdateLog() {
|
||||
const body = document.getElementById('update-log-body');
|
||||
body.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${t('common.loading')}`;
|
||||
try {
|
||||
const logs = await api('GET', '/monitoring/update-logs?limit=30');
|
||||
if (logs.length === 0) {
|
||||
body.innerHTML = `<p class="text-muted mb-0">${t('monitoring.noUpdateLogs')}</p>`;
|
||||
return;
|
||||
}
|
||||
const rows = logs.map((l, i) => {
|
||||
const badgeClass = _updateLogStatusBadge[l.status] || 'bg-secondary';
|
||||
const typeLabel = l.run_type === 'scheduled' ? t('monitoring.logTypeScheduled') : t('monitoring.logTypeManual');
|
||||
const pullSummary = l.pull_attempted
|
||||
? (l.pull_success ? t('monitoring.logPullOk') : t('monitoring.logPullFailed'))
|
||||
: t('monitoring.logPullSkipped');
|
||||
const applySummary = l.apply_attempted
|
||||
? t('monitoring.logApplySummary', { updated: l.customers_updated ?? 0, total: l.customers_total ?? 0 })
|
||||
: t('monitoring.logApplySkipped');
|
||||
const detailId = `update-log-detail-${i}`;
|
||||
const hasDetails = (l.pull_details && Object.keys(l.pull_details).length > 0) ||
|
||||
(l.customer_details && l.customer_details.length > 0) || l.error;
|
||||
const detailsHtml = hasDetails ? `
|
||||
<div class="collapse mt-2" id="${detailId}">
|
||||
${l.error ? `<div class="alert alert-danger py-2 small mb-2">${esc(l.error)}</div>` : ''}
|
||||
${l.pull_details ? `
|
||||
<table class="table table-sm mb-2">
|
||||
<thead><tr><th>${t('monitoring.thImage')}</th><th>${t('monitoring.thStatus')}</th><th></th></tr></thead>
|
||||
<tbody>${Object.entries(l.pull_details).map(([img, r]) => `<tr>
|
||||
<td><code class="small">${esc(img)}</code></td>
|
||||
<td>${r.success ? `<span class="badge bg-success">OK</span>` : `<span class="badge bg-danger">Error</span>`}</td>
|
||||
<td class="small text-muted">${esc(r.error || '')}</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>` : ''}
|
||||
${l.customer_details && l.customer_details.length > 0 ? `
|
||||
<table class="table table-sm mb-0">
|
||||
<thead><tr><th>${t('monitoring.thName')}</th><th>${t('monitoring.thStatus')}</th><th></th></tr></thead>
|
||||
<tbody>${l.customer_details.map(r => `<tr>
|
||||
<td>${esc(r.customer_name)}</td>
|
||||
<td>${r.success ? `<span class="badge bg-success">OK</span>` : `<span class="badge bg-danger">Error</span>`}</td>
|
||||
<td class="small text-muted">${esc(r.error || '')}</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>` : ''}
|
||||
</div>` : '';
|
||||
return `<tr>
|
||||
<td class="small text-nowrap">${formatDate(l.started_at)}</td>
|
||||
<td><span class="badge bg-light text-dark border">${typeLabel}</span></td>
|
||||
<td><span class="badge ${badgeClass}">${t('monitoring.logStatus_' + l.status)}</span></td>
|
||||
<td class="small">${pullSummary}</td>
|
||||
<td class="small">${applySummary}</td>
|
||||
<td class="small text-muted">${esc(l.trigger || '')}</td>
|
||||
<td>${hasDetails ? `<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="collapse" data-bs-target="#${detailId}"><i class="bi bi-chevron-down"></i></button>` : ''}</td>
|
||||
</tr>${detailsHtml ? `<tr class="d-none"></tr><tr><td colspan="7" class="p-0 border-0">${detailsHtml}</td></tr>` : ''}`;
|
||||
}).join('');
|
||||
body.innerHTML = `
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>${t('monitoring.logTime')}</th>
|
||||
<th>${t('monitoring.logType')}</th>
|
||||
<th>${t('monitoring.thStatus')}</th>
|
||||
<th>${t('monitoring.logPull')}</th>
|
||||
<th>${t('monitoring.logApply')}</th>
|
||||
<th>${t('monitoring.logTrigger')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
} catch (err) {
|
||||
body.innerHTML = `<div class="alert alert-danger">${esc(err.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadResources() {
|
||||
|
||||
+20
-1
@@ -454,6 +454,25 @@
|
||||
"autoUpdateApplyHint": "Wenn aktiviert, werden nach einer gefundenen Aktualisierung automatisch alle Kunden-Container neu erstellt (kurzer Neustart der Dienste). Wenn deaktiviert, werden nur die Images geladen — die Aktualisierung der Kunden erfolgt weiterhin manuell.",
|
||||
"autoUpdateLastRun": "Letzte automatische Prüfung",
|
||||
"autoUpdateNever": "Noch nie ausgeführt",
|
||||
"saveAutoUpdateSettings": "Automatisierung speichern"
|
||||
"saveAutoUpdateSettings": "Automatisierung speichern",
|
||||
"updateLogTitle": "Update-Verlauf",
|
||||
"noUpdateLogs": "Noch keine Update-Läufe protokolliert.",
|
||||
"logTypeScheduled": "Automatisch",
|
||||
"logTypeManual": "Manuell",
|
||||
"logTime": "Zeitpunkt",
|
||||
"logType": "Typ",
|
||||
"logPull": "Image-Download",
|
||||
"logApply": "Kunden-Update",
|
||||
"logTrigger": "Ausgelöst von",
|
||||
"logPullOk": "Erfolgreich",
|
||||
"logPullFailed": "Fehlgeschlagen",
|
||||
"logPullSkipped": "Nicht nötig",
|
||||
"logApplySummary": "{updated}/{total} Kunden",
|
||||
"logApplySkipped": "Nicht ausgeführt",
|
||||
"logStatus_success": "Erfolgreich",
|
||||
"logStatus_no_update": "Kein Update nötig",
|
||||
"logStatus_partial": "Teilweise",
|
||||
"logStatus_failed": "Fehlgeschlagen",
|
||||
"logStatus_running": "Läuft…"
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -361,7 +361,26 @@
|
||||
"autoUpdateApplyHint": "When enabled, all customer containers are automatically recreated after a new image is found (services briefly restart). When disabled, only the images are pulled — updating customers stays a manual step.",
|
||||
"autoUpdateLastRun": "Last automatic check",
|
||||
"autoUpdateNever": "Never run",
|
||||
"saveAutoUpdateSettings": "Save Automation"
|
||||
"saveAutoUpdateSettings": "Save Automation",
|
||||
"updateLogTitle": "Update Log",
|
||||
"noUpdateLogs": "No update runs logged yet.",
|
||||
"logTypeScheduled": "Automatic",
|
||||
"logTypeManual": "Manual",
|
||||
"logTime": "Time",
|
||||
"logType": "Type",
|
||||
"logPull": "Image Pull",
|
||||
"logApply": "Customer Update",
|
||||
"logTrigger": "Triggered by",
|
||||
"logPullOk": "Succeeded",
|
||||
"logPullFailed": "Failed",
|
||||
"logPullSkipped": "Not needed",
|
||||
"logApplySummary": "{updated}/{total} customers",
|
||||
"logApplySkipped": "Not run",
|
||||
"logStatus_success": "Success",
|
||||
"logStatus_no_update": "No update needed",
|
||||
"logStatus_partial": "Partial",
|
||||
"logStatus_failed": "Failed",
|
||||
"logStatus_running": "Running…"
|
||||
},
|
||||
"userModal": {
|
||||
"title": "New User",
|
||||
|
||||
Reference in New Issue
Block a user