fix(monitoring): repair silent-false-positive update badge + auto-update scheduling

Customer container status checks looked up containers by an exact expected
name. When a docker compose recreate got interrupted (e.g. a hung command
previously killed the whole update-all batch on timeout), Compose could leave
the old container renamed with a random hash prefix instead of removed. The
exact-name lookup then found nothing, returned None, and that silently
counted as "up to date" (green "Aktuell") instead of surfacing as unknown —
affecting 5 customers on the appliance whose containers were actually still
running under orphaned names.

- _run_cmd no longer raises on subprocess timeout, so one stuck customer
  can't abort the rest of a batch update
- repair_container_naming() self-heals orphaned hash-renamed containers by
  renaming them back before every status check and before recreate
- update-all loop now catches per-customer exceptions instead of aborting
- status responses expose "unknown" separately from "needs_update" so the UI
  shows a distinct grey badge instead of a false-positive green one
- new settings: automatic daily update check (on/off + time), with an
  independent toggle for whether it also auto-recreates customer containers
This commit is contained in:
2026-08-19 09:10:29 +02:00
parent 0e38b8083c
commit e53539231e
11 changed files with 359 additions and 18 deletions
+25 -1
View File
@@ -940,6 +940,13 @@ async function loadSettings() {
document.getElementById('cfg-relay-image').value = cfg.netbird_relay_image || '';
document.getElementById('cfg-dashboard-image').value = cfg.netbird_dashboard_image || '';
document.getElementById('cfg-auto-update-check-enabled').checked = cfg.auto_update_check_enabled || false;
document.getElementById('cfg-auto-update-check-time').value = cfg.auto_update_check_time || '03:00';
document.getElementById('cfg-auto-update-apply-enabled').checked = cfg.auto_update_apply_enabled || false;
document.getElementById('auto-update-last-run').textContent = cfg.auto_update_last_run_at
? new Date(cfg.auto_update_last_run_at).toLocaleString()
: t('monitoring.autoUpdateNever');
// Branding tab
document.getElementById('cfg-branding-name').value = cfg.branding_name || '';
document.getElementById('cfg-branding-subtitle').value = cfg.branding_subtitle || '';
@@ -1058,6 +1065,21 @@ document.getElementById('settings-images-form').addEventListener('submit', async
}
});
// Automatic update settings form
document.getElementById('settings-auto-update-form').addEventListener('submit', async (e) => {
e.preventDefault();
try {
await api('PUT', '/settings/system', {
auto_update_check_enabled: document.getElementById('cfg-auto-update-check-enabled').checked,
auto_update_check_time: document.getElementById('cfg-auto-update-check-time').value || '03:00',
auto_update_apply_enabled: document.getElementById('cfg-auto-update-apply-enabled').checked,
});
showSettingsAlert('success', t('messages.imageSettingsSaved'));
} catch (err) {
showSettingsAlert('danger', t('errors.failed', { error: err.message }));
}
});
// Test NPM connection
async function testNpmConnection() {
const spinner = document.getElementById('npm-test-spinner');
@@ -1807,7 +1829,9 @@ async function checkImageUpdates() {
: data.customer_status.map(c => {
const badge = c.needs_update
? `<span class="badge bg-warning text-dark">${t('monitoring.needsUpdate')}</span>`
: `<span class="badge bg-success">${t('monitoring.upToDate')}</span>`;
: c.unknown
? `<span class="badge bg-secondary" title="${t('monitoring.statusUnknownHint')}">${t('monitoring.statusUnknown')}</span>`
: `<span class="badge bg-success">${t('monitoring.upToDate')}</span>`;
const updateBtn = c.needs_update
? `<button class="btn btn-sm btn-outline-warning ms-2 btn-update-customer" onclick="updateCustomerImages(${c.customer_id})"
title="${t('monitoring.updateCustomer')}"><i class="bi bi-arrow-repeat"></i></button>`