Files
twothatITandClaude Sonnet 5 189c49d505 feat(monitoring): add automatic restart of unreachable customer containers
Some customers only regain access to their NetBird web UI after a manual
container restart, and until now that required someone to notice and do it
by hand. The scheduler now optionally probes each customer's actual
dashboard URL over HTTP(S) every poll interval (not just Docker container
state, since a container can be "running" while the web UI behind it still
doesn't respond) and restarts that customer's containers if it doesn't
answer, gated by a per-deployment cooldown to avoid restart loops on a
persistently broken customer.

New system setting "Automatic Restart of Unreachable Containers" (default
off) with a configurable cooldown. Every automatic restart is logged in the
affected customer's own log, same as a manual restart.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-23 13:20:40 +02:00

1474 lines
109 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NetBird MSP Appliance</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<script>
// Apply dark mode before page renders to prevent flash
(function () {
const saved = localStorage.getItem('darkMode');
if (saved === 'dark') document.documentElement.setAttribute('data-bs-theme', 'dark');
})();
</script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="/static/css/styles.css" rel="stylesheet">
</head>
<body class="i18n-loading">
<!-- Login Page -->
<div id="login-page" class="d-none">
<div class="login-container">
<div class="card login-card shadow">
<div class="card-body p-5">
<div class="text-center mb-4">
<div id="login-logo"><i class="bi bi-hdd-network fs-1 text-primary"></i></div>
<h3 class="mt-2" id="login-title">NetBird MSP Appliance</h3>
<p class="text-muted" id="login-subtitle" data-i18n="login.subtitle">Multi-Tenant Management
Platform</p>
<p class="text-muted small mb-0" style="opacity:0.6;"><i class="bi bi-tag me-1"></i>
</p>
</div>
<div id="login-error" class="alert alert-danger d-none"></div>
<form id="login-form">
<div class="mb-3">
<label class="form-label" data-i18n="login.username">Username</label>
<input type="text" class="form-control" id="login-username" required autofocus>
</div>
<div class="mb-3">
<label class="form-label" data-i18n="login.password">Password</label>
<input type="password" class="form-control" id="login-password" required>
</div>
<button type="submit" class="btn btn-primary w-100" id="login-btn">
<span class="spinner-border spinner-border-sm d-none me-1" id="login-spinner"></span>
<span data-i18n="login.signIn">Sign In</span>
</button>
</form>
<div id="azure-login-divider" class="d-none">
<hr class="my-3">
<button type="button" class="btn btn-outline-dark w-100" id="azure-login-btn"
onclick="loginWithAzure()">
<i class="bi bi-microsoft me-2"></i><span data-i18n="login.signInWithMicrosoft">Sign in with
Microsoft</span>
</button>
</div>
<!-- MFA: TOTP Verify (existing setup) -->
<div id="mfa-verify-section" class="d-none">
<div id="mfa-verify-error" class="alert alert-danger d-none"></div>
<p class="text-muted text-center mb-3" data-i18n="mfa.enterCode">Enter your 6-digit
authenticator code</p>
<form id="mfa-verify-form">
<input type="text" class="form-control form-control-lg text-center" id="mfa-code"
maxlength="6" pattern="[0-9]{6}" inputmode="numeric" autocomplete="one-time-code"
required autofocus>
<button type="submit" class="btn btn-primary w-100 mt-3">
<span class="spinner-border spinner-border-sm d-none me-1"
id="mfa-verify-spinner"></span>
<span data-i18n="mfa.verify">Verify</span>
</button>
</form>
<a href="#" id="mfa-back-to-login" class="d-block text-center mt-2 small"
data-i18n="mfa.backToLogin">Back to login</a>
</div>
<!-- MFA: TOTP Setup (first time) -->
<div id="mfa-setup-section" class="d-none">
<div id="mfa-setup-error" class="alert alert-danger d-none"></div>
<p class="text-muted text-center mb-2" data-i18n="mfa.scanQrCode">Scan this QR code with your
authenticator app</p>
<div class="text-center mb-3">
<img id="mfa-qr-code" class="img-fluid rounded" style="max-width:200px" alt="TOTP QR Code">
</div>
<p class="text-muted small text-center mb-3">
<span data-i18n="mfa.orEnterManually">Or enter this key manually:</span><br>
<code id="mfa-secret-manual" class="user-select-all"></code>
</p>
<form id="mfa-setup-form">
<input type="text" class="form-control form-control-lg text-center" id="mfa-setup-code"
maxlength="6" pattern="[0-9]{6}" inputmode="numeric" autocomplete="one-time-code"
required>
<button type="submit" class="btn btn-success w-100 mt-3">
<span class="spinner-border spinner-border-sm d-none me-1"
id="mfa-setup-spinner"></span>
<span data-i18n="mfa.verifyAndActivate">Verify & Activate</span>
</button>
</form>
<a href="#" id="mfa-setup-back-to-login" class="d-block text-center mt-2 small"
data-i18n="mfa.backToLogin">Back to login</a>
</div>
</div>
</div>
</div>
</div>
<!-- Main Application -->
<div id="app-page" class="d-none">
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
<div class="container-fluid">
<a class="navbar-brand d-flex align-items-center" href="#"
onclick="showPage('dashboard'); return false;">
<span id="nav-logo"><i class="bi bi-hdd-network me-2"></i></span>
<span id="nav-brand-name">NetBird MSP</span>
</a>
<div class="d-flex align-items-center">
<!-- Dark Mode Toggle -->
<button class="btn btn-outline-light btn-sm me-2" id="darkmode-toggle" onclick="toggleDarkMode()" title="Toggle dark mode">
<i id="darkmode-icon" class="bi bi-moon-fill"></i>
</button>
<!-- Language Switcher -->
<div class="dropdown me-2">
<button class="btn btn-outline-light btn-sm dropdown-toggle" id="language-switcher-btn"
data-bs-toggle="dropdown" aria-expanded="false">EN</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="#" data-lang="en"
onclick="switchLanguage('en'); return false;">English</a></li>
<li><a class="dropdown-item" href="#" data-lang="de"
onclick="switchLanguage('de'); return false;">Deutsch</a></li>
</ul>
</div>
<button class="btn btn-success btn-sm me-3" onclick="showNewCustomerModal()">
<i class="bi bi-plus-lg me-1"></i><span data-i18n="nav.newCustomer">New Customer</span>
</button>
<div class="dropdown">
<button class="btn btn-outline-light btn-sm dropdown-toggle" data-bs-toggle="dropdown">
<i class="bi bi-person-circle me-1"></i><span id="nav-username">Admin</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="#" onclick="showPage('settings')"><i
class="bi bi-gear me-2"></i><span data-i18n="nav.settings">Settings</span></a>
</li>
<li><a class="dropdown-item" href="#" onclick="showPage('monitoring')"><i
class="bi bi-activity me-2"></i><span
data-i18n="nav.monitoring">Monitoring</span></a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item text-danger" href="#" onclick="logout()"><i
class="bi bi-box-arrow-right me-2"></i><span
data-i18n="nav.logout">Logout</span></a></li>
</ul>
</div>
</div>
</div>
</nav>
<!-- Page: Dashboard -->
<div id="page-dashboard" class="page-content">
<div class="container-fluid p-4">
<!-- Stats Cards -->
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card stat-card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<div class="text-muted small" data-i18n="dashboard.totalCustomers">Total
Customers</div>
<div class="fs-3 fw-bold" id="stat-total">0</div>
</div>
<div class="stat-icon bg-primary bg-opacity-10 text-primary"><i
class="bi bi-people"></i></div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<div class="text-muted small" data-i18n="dashboard.active">Active</div>
<div class="fs-3 fw-bold text-success" id="stat-active">0</div>
</div>
<div class="stat-icon bg-success bg-opacity-10 text-success"><i
class="bi bi-check-circle"></i></div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<div class="text-muted small" data-i18n="dashboard.inactive">Inactive</div>
<div class="fs-3 fw-bold text-warning" id="stat-inactive">0</div>
</div>
<div class="stat-icon bg-warning bg-opacity-10 text-warning"><i
class="bi bi-pause-circle"></i></div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<div class="text-muted small" data-i18n="dashboard.errors">Errors</div>
<div class="fs-3 fw-bold text-danger" id="stat-error">0</div>
</div>
<div class="stat-icon bg-danger bg-opacity-10 text-danger"><i
class="bi bi-exclamation-triangle"></i></div>
</div>
</div>
</div>
</div>
</div>
<!-- Search & Filter -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="row g-2">
<div class="col-md-6">
<input type="text" class="form-control" id="search-input"
data-i18n-placeholder="dashboard.searchPlaceholder"
placeholder="Search by name, subdomain, email...">
</div>
<div class="col-md-3">
<select class="form-select" id="status-filter">
<option value="" data-i18n="dashboard.allStatuses">All Statuses</option>
<option value="active" data-i18n="dashboard.statusActive">Active</option>
<option value="inactive" data-i18n="dashboard.statusInactive">Inactive</option>
<option value="deploying" data-i18n="dashboard.statusDeploying">Deploying</option>
<option value="error" data-i18n="dashboard.statusError">Error</option>
</select>
</div>
<div class="col-md-3 text-end">
<button class="btn btn-outline-secondary" onclick="loadCustomers()"><i
class="bi bi-arrow-clockwise me-1"></i><span
data-i18n="dashboard.refresh">Refresh</span></button>
</div>
</div>
</div>
</div>
<!-- Customers Table -->
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="sortable-th" data-sort-col="id" onclick="setCustomerSort('id')"><span data-i18n="dashboard.thId">ID</span><i class="bi bi-arrow-down-up sort-icon ms-1"></i></th>
<th class="sortable-th" data-sort-col="name" onclick="setCustomerSort('name')"><span data-i18n="dashboard.thName">Name</span><i class="bi bi-arrow-down-up sort-icon ms-1"></i></th>
<th class="sortable-th" data-sort-col="subdomain" onclick="setCustomerSort('subdomain')"><span data-i18n="dashboard.thSubdomain">Subdomain</span><i class="bi bi-arrow-down-up sort-icon ms-1"></i></th>
<th class="sortable-th" data-sort-col="status" onclick="setCustomerSort('status')"><span data-i18n="dashboard.thStatus">Status</span><i class="bi bi-arrow-down-up sort-icon ms-1"></i></th>
<th data-i18n="dashboard.thDashboard">Dashboard</th>
<th class="sortable-th" data-sort-col="max_devices" onclick="setCustomerSort('max_devices')"><span data-i18n="dashboard.thDevices">Devices</span><i class="bi bi-arrow-down-up sort-icon ms-1"></i></th>
<th class="sortable-th" data-sort-col="created_at" onclick="setCustomerSort('created_at')"><span data-i18n="dashboard.thCreated">Created</span><i class="bi bi-arrow-down-up sort-icon ms-1"></i></th>
<th data-i18n="dashboard.thActions">Actions</th>
</tr>
</thead>
<tbody id="customers-table-body">
<tr>
<td colspan="8" class="text-center text-muted py-4" data-i18n="common.loading">
Loading...</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="card-footer d-flex justify-content-between align-items-center">
<div class="text-muted small" id="pagination-info" data-i18n="dashboard.showingEmpty">Showing 0
of 0</div>
<nav>
<ul class="pagination pagination-sm mb-0" id="pagination-controls"></ul>
</nav>
</div>
</div>
</div>
</div>
<!-- Page: Customer Detail -->
<div id="page-customer-detail" class="page-content d-none">
<div class="container-fluid p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<button class="btn btn-outline-secondary btn-sm me-2" onclick="showPage('dashboard')"><i
class="bi bi-arrow-left me-1"></i><span data-i18n="common.back">Back</span></button>
<span class="fs-4 fw-bold" id="detail-customer-name">Customer</span>
<span class="badge ms-2" id="detail-customer-status">active</span>
</div>
<div>
<button class="btn btn-outline-primary btn-sm me-1" onclick="editCurrentCustomer()"><i
class="bi bi-pencil me-1"></i><span data-i18n="customer.edit">Edit</span></button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteCurrentCustomer()"><i
class="bi bi-trash me-1"></i><span data-i18n="customer.delete">Delete</span></button>
</div>
</div>
<ul class="nav nav-tabs mb-3" id="detail-tabs">
<li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#tab-info"
data-i18n="customer.tabInfo">Info</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-deployment"
data-i18n="customer.tabDeployment">Deployment</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-logs"
data-i18n="customer.tabLogs">Logs</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-health"
data-i18n="customer.tabHealth">Health</a></li>
</ul>
<div class="tab-content">
<!-- Tab: Info -->
<div class="tab-pane fade show active" id="tab-info">
<div class="card shadow-sm">
<div class="card-body" id="detail-info-content" data-i18n="common.loading">Loading...</div>
</div>
</div>
<!-- Tab: Deployment -->
<div class="tab-pane fade" id="tab-deployment">
<div class="card shadow-sm">
<div class="card-body" id="detail-deployment-content" data-i18n="common.loading">Loading...
</div>
</div>
</div>
<!-- Tab: Logs -->
<div class="tab-pane fade" id="tab-logs">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between">
<span data-i18n="customer.containerLogs">Container Logs</span>
<button class="btn btn-sm btn-outline-secondary" onclick="loadCustomerLogs()"><i
class="bi bi-arrow-clockwise"></i> <span
data-i18n="dashboard.refresh">Refresh</span></button>
</div>
<div class="card-body">
<div id="detail-logs-content" class="log-viewer" data-i18n="customer.noLogsLoaded">No
logs loaded.</div>
</div>
</div>
</div>
<!-- Tab: Health -->
<div class="tab-pane fade" id="tab-health">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between">
<span data-i18n="customer.healthCheck">Health Check</span>
<button class="btn btn-sm btn-outline-secondary" onclick="loadCustomerHealth()"><i
class="bi bi-arrow-clockwise"></i> <span
data-i18n="customer.check">Check</span></button>
</div>
<div class="card-body" id="detail-health-content" data-i18n="customer.clickCheck">Click
"Check" to run a health check.</div>
</div>
</div>
</div>
</div>
</div>
<!-- Page: Settings -->
<div id="page-settings" class="page-content d-none">
<div class="container-fluid p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">
<button class="btn btn-outline-secondary btn-sm me-2" onclick="showPage('dashboard')"><i
class="bi bi-arrow-left me-1"></i><span data-i18n="common.back">Back</span></button>
<i class="bi bi-gear me-2"></i><span data-i18n="settings.title">System Settings</span>
</h4>
</div>
<div id="settings-alert" class="d-none"></div>
<div class="row">
<!-- Sidebar -->
<div class="col-md-3 mb-4 mb-md-0 border-end pe-3">
<ul class="nav nav-pills flex-column" id="settings-tabs">
<div class="text-uppercase text-muted fw-bold mb-2 ps-3"
style="font-size: 0.75rem; letter-spacing: 0.05em;"><i
class="bi bi-shield-check me-1"></i><span
data-i18n="settings.groupUsers">Benutzerverwaltung</span></div>
<li class="nav-item"><a class="nav-link" data-bs-toggle="pill" href="#settings-azure"><i
class="bi bi-microsoft me-2"></i><span data-i18n="settings.tabAzure">Azure
AD</span></a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="pill" href="#settings-users"
onclick="loadUsers()"><i class="bi bi-people me-2"></i><span
data-i18n="settings.tabUsers">Benutzer</span></a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="pill" href="#settings-ldap"><i
class="bi bi-diagram-3 me-2"></i><span data-i18n="settings.tabLdap">LDAP /
AD</span></a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="pill" href="#settings-security"><i
class="bi bi-shield-lock me-2"></i><span
data-i18n="settings.tabSecurity">Sicherheit</span></a></li>
<hr class="my-3 border-secondary opacity-25">
<div class="text-uppercase text-muted fw-bold mb-2 ps-3"
style="font-size: 0.75rem; letter-spacing: 0.05em;"><i
class="bi bi-gear-wide-connected me-1"></i><span
data-i18n="settings.groupSystem">Systemkonfiguration</span></div>
<li class="nav-item"><a class="nav-link" data-bs-toggle="pill" href="#settings-branding"><i
class="bi bi-palette me-2"></i><span
data-i18n="settings.tabBranding">Branding</span></a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="pill" href="#settings-images"><i
class="bi bi-box me-2"></i><span data-i18n="settings.tabImages">NetBird Docker
Images</span></a></li>
<li class="nav-item"><a class="nav-link active" data-bs-toggle="pill"
href="#settings-system"><i class="bi bi-pc-display me-2"></i><span
data-i18n="settings.tabSystem">NetBird MSP System</span></a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="pill" href="#settings-update"
onclick="loadVersionInfo()"><i class="bi bi-cloud-arrow-down me-2"></i><span
data-i18n="settings.tabUpdate">NetBird MSP Updates</span></a></li>
<hr class="my-3 border-secondary opacity-25">
<div class="text-uppercase text-muted fw-bold mb-2 ps-3"
style="font-size: 0.75rem; letter-spacing: 0.05em;"><i
class="bi bi-link-45deg me-1"></i><span
data-i18n="settings.groupExternal">Umsysteme</span></div>
<li class="nav-item"><a class="nav-link" data-bs-toggle="pill" href="#settings-npm"><i
class="bi bi-router me-2"></i><span data-i18n="settings.tabNpm">NPM
Proxy</span></a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="pill" href="#settings-dns"><i
class="bi bi-hdd-network me-2"></i><span data-i18n="settings.tabDns">Windows
DNS</span></a></li>
</ul>
</div>
<!-- Content -->
<div class="col-md-9">
<div class="tab-content">
<!-- System Config -->
<div class="tab-pane fade show active" id="settings-system">
<div class="card shadow-sm">
<div class="card-body">
<form id="settings-system-form">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" data-i18n="settings.baseDomain">Base
Domain</label>
<input type="text" class="form-control" id="cfg-base-domain"
data-i18n-placeholder="settings.baseDomainPlaceholder"
placeholder="yourdomain.com">
<div class="form-text" data-i18n="settings.baseDomainHint">Customers
get subdomains: customer.yourdomain.com</div>
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.adminEmail">Admin
Email</label>
<input type="email" class="form-control" id="cfg-admin-email"
data-i18n-placeholder="settings.adminEmailPlaceholder"
placeholder="[email protected]">
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.dataDir">Data
Directory</label>
<input type="text" class="form-control" id="cfg-data-dir"
data-i18n-placeholder="settings.dataDirPlaceholder"
placeholder="/opt/netbird-instances">
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.dockerNetwork">Docker
Network</label>
<input type="text" class="form-control" id="cfg-docker-network"
data-i18n-placeholder="settings.dockerNetworkPlaceholder"
placeholder="npm-network">
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.relayBasePort">Relay
Base Port</label>
<input type="number" class="form-control" id="cfg-relay-base-port"
min="1024" max="65535">
<div class="form-text" data-i18n="settings.relayBasePortHint">First
UDP port for relay. Range: base to base+99</div>
</div>
<div class="col-md-6">
<label class="form-label"
data-i18n="settings.dashboardBasePort">Dashboard Base
Port</label>
<input type="number" class="form-control"
id="cfg-dashboard-base-port" min="1024" max="65535">
<div class="form-text" data-i18n="settings.dashboardBasePortHint">
Base port for customer dashboards. Customer N gets base+N</div>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary"><i
class="bi bi-save me-1"></i><span
data-i18n="settings.saveSystemSettings">Save System
Settings</span></button>
</div>
</form>
</div>
</div>
</div>
<!-- NPM Integration -->
<div class="tab-pane fade" id="settings-npm">
<div class="card shadow-sm">
<div class="card-body">
<form id="settings-npm-form">
<p class="text-muted mb-3" data-i18n="settings.npmDescription">NPM uses JWT
authentication. Enter your NPM login credentials (email + password). The
system will automatically log in and obtain tokens for API calls.</p>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label" data-i18n="settings.npmApiUrl">NPM API
URL</label>
<input type="text" class="form-control" id="cfg-npm-api-url"
data-i18n-placeholder="settings.npmApiUrlPlaceholder"
placeholder="http://nginx-proxy-manager:81/api">
<div class="form-text" data-i18n="settings.npmApiUrlHint">http:// or
https:// - must include /api at the end</div>
</div>
<div class="col-md-8">
<label class="form-label" data-i18n="settings.npmLoginEmail">NPM
Login Email</label>
<input type="text" class="form-control" id="cfg-npm-api-email"
data-i18n-placeholder="settings.npmLoginEmailPlaceholder"
placeholder="Leave empty to keep current">
<div class="form-text" id="npm-credentials-status"></div>
</div>
<div class="col-md-8">
<label class="form-label" data-i18n="settings.npmLoginPassword">NPM
Login Password</label>
<div class="input-group">
<input type="password" class="form-control"
id="cfg-npm-api-password"
data-i18n-placeholder="settings.npmLoginPasswordPlaceholder"
placeholder="Leave empty to keep current">
<button class="btn btn-outline-secondary" type="button"
onclick="togglePasswordVisibility('cfg-npm-api-password')"><i
class="bi bi-eye"></i></button>
</div>
</div>
<!-- SSL Certificate Mode -->
<div class="col-12 mt-3">
<hr class="my-2">
<h6 class="mb-2" data-i18n="settings.sslModeTitle">SSL Certificate
Mode</h6>
</div>
<div class="col-md-8">
<label class="form-label" data-i18n="settings.sslMode">SSL
Mode</label>
<select class="form-select" id="cfg-ssl-mode"
onchange="onSslModeChange()">
<option value="letsencrypt"
data-i18n="settings.sslModeLetsencrypt">Per-Customer Let's
Encrypt Certificate</option>
<option value="wildcard" data-i18n="settings.sslModeWildcard">
Wildcard Certificate (pre-configured in NPM)</option>
</select>
<div class="form-text" data-i18n="settings.sslModeHint">Choose how
SSL certificates are assigned to customer proxy hosts.</div>
</div>
<div class="col-md-8" id="wildcard-cert-section" style="display:none;">
<label class="form-label"
data-i18n="settings.wildcardCertificate">Wildcard
Certificate</label>
<div class="input-group">
<select class="form-select" id="cfg-wildcard-cert-id">
<option value="" data-i18n="settings.selectCertificate">--
Select a certificate --</option>
</select>
<button type="button" class="btn btn-outline-secondary"
onclick="loadNpmCertificates()" title="Refresh">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="form-text" data-i18n="settings.wildcardCertHint">Select
the wildcard certificate (e.g. *.example.com) already uploaded
in NPM.</div>
<div id="wildcard-cert-status" class="mt-1"></div>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary me-2"><i
class="bi bi-save me-1"></i><span
data-i18n="settings.saveNpmSettings">Save NPM
Settings</span></button>
<button type="button" class="btn btn-outline-info" id="test-npm-btn"
onclick="testNpmConnection()">
<span class="spinner-border spinner-border-sm d-none me-1"
id="npm-test-spinner"></span>
<i class="bi bi-plug me-1"></i><span
data-i18n="settings.testConnection">Test Connection</span>
</button>
</div>
</form>
<div id="npm-test-result" class="mt-3 d-none"></div>
</div>
</div>
</div>
<!-- Docker Images -->
<div class="tab-pane fade" id="settings-images">
<div class="card shadow-sm">
<div class="card-body">
<form id="settings-images-form">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label"
data-i18n="settings.managementImage">Management Image</label>
<input type="text" class="form-control" id="cfg-mgmt-image"
data-i18n-placeholder="settings.managementImagePlaceholder"
placeholder="netbirdio/management:latest">
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.signalImage">Signal
Image</label>
<input type="text" class="form-control" id="cfg-signal-image"
data-i18n-placeholder="settings.signalImagePlaceholder"
placeholder="netbirdio/signal:latest">
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.relayImage">Relay
Image</label>
<input type="text" class="form-control" id="cfg-relay-image"
data-i18n-placeholder="settings.relayImagePlaceholder"
placeholder="netbirdio/relay:latest">
</div>
<div class="col-md-6">
<label class="form-label"
data-i18n="settings.dashboardImage">Dashboard Image</label>
<input type="text" class="form-control" id="cfg-dashboard-image"
data-i18n-placeholder="settings.dashboardImagePlaceholder"
placeholder="netbirdio/dashboard:latest">
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary"><i
class="bi bi-save me-1"></i><span
data-i18n="settings.saveImageSettings">Save Image
Settings</span></button>
</div>
</form>
<hr>
<h6 data-i18n="settings.pullImagesTitle">Pull Latest Images from Docker Hub</h6>
<p class="text-muted small" data-i18n="settings.pullImagesHint">Downloads the latest versions of all configured NetBird images. After pulling, use Monitoring to update customer containers.</p>
<button class="btn btn-outline-primary" onclick="pullAllImagesSettings()" id="btn-pull-images-settings">
<i class="bi bi-cloud-download me-1"></i><span data-i18n="settings.pullImages">Pull from Docker Hub</span>
</button>
<span id="pull-images-settings-status" class="ms-2 text-muted small"></span>
<hr>
<h6 data-i18n="monitoring.autoUpdateTitle">Automatic Updates</h6>
<p class="text-muted small" data-i18n="monitoring.autoUpdateHint">Automatically checks for new NetBird images daily at the chosen time.</p>
<form id="settings-auto-update-form">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="cfg-auto-update-check-enabled">
<label class="form-check-label" for="cfg-auto-update-check-enabled" data-i18n="monitoring.autoUpdateCheckEnabled">Enable automatic update check</label>
</div>
<div class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label" data-i18n="monitoring.autoUpdateCheckTime">Daily check time</label>
<input type="time" class="form-control" id="cfg-auto-update-check-time" value="03:00">
</div>
</div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="cfg-auto-update-apply-enabled">
<label class="form-check-label" for="cfg-auto-update-apply-enabled" data-i18n="monitoring.autoUpdateApplyEnabled">Automatically update customer containers after check</label>
<div class="form-text" data-i18n="monitoring.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.</div>
</div>
<div class="mt-3 small text-muted">
<span data-i18n="monitoring.autoUpdateLastRun">Last automatic check</span>:
<span id="auto-update-last-run">-</span>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-save me-1"></i><span data-i18n="monitoring.saveAutoUpdateSettings">Save Automation</span></button>
</div>
</form>
<hr>
<h6 data-i18n="monitoring.autoRestartTitle">Automatic Restart of Unreachable Containers</h6>
<p class="text-muted small" data-i18n="monitoring.autoRestartHint">Some customers only regain access to their NetBird web UI after a container restart. When enabled, the scheduler periodically checks whether each customer's dashboard actually responds and restarts their containers if it doesn't, logging the action in the customer's log.</p>
<form id="settings-auto-restart-form">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="cfg-auto-restart-enabled">
<label class="form-check-label" for="cfg-auto-restart-enabled" data-i18n="monitoring.autoRestartEnabled">Automatically restart unreachable customer containers</label>
</div>
<div class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label" data-i18n="monitoring.autoRestartCooldown">Cooldown between restarts (minutes)</label>
<input type="number" class="form-control" id="cfg-auto-restart-cooldown" min="1" max="1440" value="30">
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-save me-1"></i><span data-i18n="monitoring.saveAutoRestartSettings">Save Automation</span></button>
</div>
</form>
<hr>
<h6 data-i18n="customer.nbuMasterTitle">NetBird Client Auto-Updates (all customers)</h6>
<p class="text-muted small" data-i18n="customer.nbuMasterHint">Controls the "Automatic Updates" setting inside every customer's own NetBird dashboard (Settings &gt; Clients). Set the default here, then push it to all customers at once. Individual customers can still be overridden from their detail page.</p>
<form id="settings-nbu-master-form">
<div class="row g-2 align-items-end">
<div class="col-auto">
<label class="form-label small mb-1" data-i18n="customer.nbuVersion">Client version</label>
<select class="form-select form-select-sm" id="cfg-nbu-version-select" onchange="document.getElementById('cfg-nbu-custom-version').classList.toggle('d-none', this.value !== 'custom')">
<option value="disabled" data-i18n="customer.nbuDisabled">Disabled</option>
<option value="latest" data-i18n="customer.nbuLatest">Latest</option>
<option value="custom" data-i18n="customer.nbuCustom">Specific version</option>
</select>
</div>
<div class="col-auto">
<input type="text" class="form-control form-control-sm d-none" id="cfg-nbu-custom-version" placeholder="0.61.0">
</div>
<div class="col-auto form-check pb-1">
<input class="form-check-input" type="checkbox" id="cfg-nbu-always">
<label class="form-check-label small" for="cfg-nbu-always" data-i18n="customer.nbuForce">Force automatic updates</label>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary btn-sm me-2"><i class="bi bi-save me-1"></i><span data-i18n="customer.nbuSaveDefault">Save Default</span></button>
<button type="button" class="btn btn-outline-warning btn-sm" id="btn-nbu-apply-all" onclick="applyNetbirdUpdatesToAll()"><i class="bi bi-broadcast me-1"></i><span data-i18n="customer.nbuApplyAll">Apply to All Customers</span></button>
</div>
</form>
<div id="nbu-apply-all-result" class="mt-3"></div>
</div>
</div>
</div>
<!-- Branding -->
<div class="tab-pane fade" id="settings-branding">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="mb-3" data-i18n="settings.brandingTitle">Branding Settings</h5>
<form id="settings-branding-form">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" data-i18n="settings.companyName">Company /
Application Name</label>
<input type="text" class="form-control" id="cfg-branding-name"
data-i18n-placeholder="settings.companyNamePlaceholder"
placeholder="NetBird MSP Appliance" maxlength="255">
<div class="form-text" data-i18n="settings.companyNameHint">
Displayed on login page and navbar</div>
</div>
<div class="col-md-6">
<label class="form-label"
data-i18n="settings.brandingSubtitle">Subtitle</label>
<input type="text" class="form-control" id="cfg-branding-subtitle"
data-i18n-placeholder="settings.brandingSubtitlePlaceholder"
placeholder="Multi-Tenant Management Platform" maxlength="255">
<div class="form-text" data-i18n="settings.brandingSubtitleHint">
Shown below the title on the login page</div>
</div>
<div class="col-md-6">
<label class="form-label"
data-i18n="settings.defaultLanguage">Default Language</label>
<select class="form-select" id="cfg-default-language">
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
<div class="form-text" data-i18n="settings.defaultLanguageHint">
Default language for users without a preference</div>
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.logoPreview">Logo
Preview</label>
<div class="border rounded p-3 text-center"
id="branding-logo-preview" style="min-height:80px;">
<i class="bi bi-hdd-network fs-1 text-primary"></i>
<div class="text-muted small mt-1"
data-i18n="settings.defaultIcon">Default icon (no logo
uploaded)</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.uploadLogo">Upload
Logo (PNG, JPG, SVG, max 500KB)</label>
<div class="input-group">
<input type="file" class="form-control" id="branding-logo-file"
accept=".png,.jpg,.jpeg,.svg">
<button type="button" class="btn btn-outline-primary"
onclick="uploadLogo()"><i
class="bi bi-upload me-1"></i><span
data-i18n="settings.uploadBtn">Upload</span></button>
</div>
</div>
<div class="col-md-6 d-flex align-items-end">
<button type="button" class="btn btn-outline-danger"
onclick="deleteLogo()"><i class="bi bi-trash me-1"></i><span
data-i18n="settings.removeLogo">Remove Logo</span></button>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary"><i
class="bi bi-save me-1"></i><span
data-i18n="settings.saveBranding">Save Branding</span></button>
</div>
</form>
</div>
</div>
</div>
<!-- Users -->
<div class="tab-pane fade" id="settings-users">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<span data-i18n="settings.userManagement">User Management</span>
<button class="btn btn-sm btn-success" onclick="showNewUserModal()"><i
class="bi bi-plus-lg me-1"></i><span data-i18n="settings.newUser">New
User</span></button>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th data-i18n="settings.thId">ID</th>
<th data-i18n="settings.thUsername">Username</th>
<th data-i18n="settings.thEmail">Email</th>
<th data-i18n="settings.thRole">Role</th>
<th data-i18n="settings.thAuth">Auth</th>
<th data-i18n="settings.thLanguage">Language</th>
<th>MFA</th>
<th data-i18n="settings.thStatus">Status</th>
<th data-i18n="settings.thActions">Actions</th>
</tr>
</thead>
<tbody id="users-table-body">
<tr>
<td colspan="9" class="text-center text-muted py-4"
data-i18n="common.loading">Loading...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Azure AD -->
<div class="tab-pane fade" id="settings-azure">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="mb-3" data-i18n="settings.azureTitle">Azure AD / Entra ID Integration
</h5>
<form id="settings-azure-form">
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox"
id="cfg-azure-enabled">
<label class="form-check-label" for="cfg-azure-enabled"
data-i18n="settings.enableAzureSso">Enable Azure AD
SSO</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.tenantId">Tenant
ID</label>
<input type="text" class="form-control" id="cfg-azure-tenant"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.clientId">Client ID
(Application ID)</label>
<input type="text" class="form-control" id="cfg-azure-client-id"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.clientSecret">Client
Secret</label>
<div class="input-group">
<input type="password" class="form-control"
id="cfg-azure-client-secret"
data-i18n-placeholder="settings.clientSecretPlaceholder"
placeholder="Leave empty to keep current">
<button class="btn btn-outline-secondary" type="button"
data-toggle-pw
onclick="togglePasswordVisibility('cfg-azure-client-secret')"><i
class="bi bi-eye"></i></button>
</div>
<div class="form-text" id="azure-secret-status"></div>
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.azureGroupId">Allowed
Group Object ID (optional)</label>
<input type="text" class="form-control" id="cfg-azure-group-id"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
<div class="form-text" data-i18n="settings.azureGroupIdHint">If set,
only Azure AD members of this group can log in.</div>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary"><i
class="bi bi-save me-1"></i><span
data-i18n="settings.saveAzureSettings">Save Azure AD
Settings</span></button>
</div>
</form>
</div>
</div>
</div>
<!-- Windows DNS -->
<div class="tab-pane fade" id="settings-dns">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="mb-3" data-i18n="settings.dnsTitle">Windows DNS Integration</h5>
<form id="settings-dns-form">
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox"
id="cfg-dns-enabled">
<label class="form-check-label" for="cfg-dns-enabled"
data-i18n="settings.enableDns">Enable Windows DNS
Integration</label>
</div>
<div class="form-text" data-i18n="settings.dnsDescription">
Automatically create/delete DNS A-records when deploying
customers.</div>
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.dnsServer">DNS Server
Address</label>
<input type="text" class="form-control" id="cfg-dns-server"
placeholder="192.168.1.10">
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.dnsZone">DNS
Zone</label>
<input type="text" class="form-control" id="cfg-dns-zone"
placeholder="example.com">
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.dnsUsername">Username
(NTLM)</label>
<input type="text" class="form-control" id="cfg-dns-username"
placeholder="DOMAIN\svcuser">
</div>
<div class="col-md-6">
<label class="form-label"
data-i18n="settings.dnsPassword">Password</label>
<div class="input-group">
<input type="password" class="form-control"
id="cfg-dns-password"
data-i18n-placeholder="settings.leaveEmptyToKeep"
placeholder="Leave empty to keep current">
<button class="btn btn-outline-secondary" type="button"
onclick="togglePasswordVisibility('cfg-dns-password')"><i
class="bi bi-eye"></i></button>
</div>
<div class="form-text" id="dns-password-status"></div>
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.dnsRecordIp">A-Record
Target IP</label>
<input type="text" class="form-control" id="cfg-dns-record-ip"
placeholder="1.2.3.4">
<div class="form-text" data-i18n="settings.dnsRecordIpHint">IP
address that customer A-records will point to (usually your NPM
server IP).</div>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary me-2"><i
class="bi bi-save me-1"></i><span
data-i18n="settings.saveDnsSettings">Save DNS
Settings</span></button>
<button type="button" class="btn btn-outline-info"
onclick="testDnsConnection()">
<span class="spinner-border spinner-border-sm d-none me-1"
id="dns-test-spinner"></span>
<i class="bi bi-plug me-1"></i><span
data-i18n="settings.testConnection">Test Connection</span>
</button>
</div>
</form>
<div id="dns-test-result" class="mt-3 d-none"></div>
</div>
</div>
</div>
<!-- LDAP / Active Directory -->
<div class="tab-pane fade" id="settings-ldap">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="mb-3" data-i18n="settings.ldapTitle">LDAP / Active Directory
Authentication</h5>
<form id="settings-ldap-form">
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox"
id="cfg-ldap-enabled">
<label class="form-check-label" for="cfg-ldap-enabled"
data-i18n="settings.enableLdap">Enable LDAP / AD
Authentication</label>
</div>
<div class="form-text" data-i18n="settings.ldapDescription">Allow
Active Directory users to log in. Local admin accounts always
work as fallback.</div>
</div>
<div class="col-md-5">
<label class="form-label" data-i18n="settings.ldapServer">LDAP
Server</label>
<input type="text" class="form-control" id="cfg-ldap-server"
placeholder="192.168.1.10 or dc.example.com">
</div>
<div class="col-md-3">
<label class="form-label" data-i18n="settings.ldapPort">Port</label>
<input type="number" class="form-control" id="cfg-ldap-port"
value="389" min="1" max="65535">
</div>
<div class="col-md-4 d-flex align-items-end pb-1">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox"
id="cfg-ldap-use-ssl">
<label class="form-check-label" for="cfg-ldap-use-ssl"
data-i18n="settings.ldapUseSsl">Use SSL/TLS (LDAPS)</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.ldapBindDn">Bind DN
(Service Account)</label>
<input type="text" class="form-control" id="cfg-ldap-bind-dn"
placeholder="CN=svcUser,OU=Service,DC=example,DC=com">
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.ldapBindPassword">Bind
Password</label>
<div class="input-group">
<input type="password" class="form-control"
id="cfg-ldap-bind-password"
data-i18n-placeholder="settings.leaveEmptyToKeep"
placeholder="Leave empty to keep current">
<button class="btn btn-outline-secondary" type="button"
onclick="togglePasswordVisibility('cfg-ldap-bind-password')"><i
class="bi bi-eye"></i></button>
</div>
<div class="form-text" id="ldap-password-status"></div>
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.ldapBaseDn">Base
DN</label>
<input type="text" class="form-control" id="cfg-ldap-base-dn"
placeholder="DC=example,DC=com">
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.ldapUserFilter">User
Filter</label>
<input type="text" class="form-control" id="cfg-ldap-user-filter"
placeholder="(sAMAccountName={username})">
<div class="form-text" data-i18n="settings.ldapUserFilterHint">Use
{username} as placeholder for the login name.</div>
</div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.ldapGroupDn">Group
Restriction DN (optional)</label>
<input type="text" class="form-control" id="cfg-ldap-group-dn"
placeholder="CN=NetBirdAdmins,OU=Groups,DC=example,DC=com">
<div class="form-text" data-i18n="settings.ldapGroupDnHint">If set,
only members of this group can log in via LDAP.</div>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary me-2"><i
class="bi bi-save me-1"></i><span
data-i18n="settings.saveLdapSettings">Save LDAP
Settings</span></button>
<button type="button" class="btn btn-outline-info"
onclick="testLdapConnection()">
<span class="spinner-border spinner-border-sm d-none me-1"
id="ldap-test-spinner"></span>
<i class="bi bi-plug me-1"></i><span
data-i18n="settings.testConnection">Test Connection</span>
</button>
</div>
</form>
<div id="ldap-test-result" class="mt-3 d-none"></div>
</div>
</div>
</div>
<!-- Updates -->
<div class="tab-pane fade" id="settings-update">
<div class="card shadow-sm mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span data-i18n="settings.versionTitle">Version &amp; Updates</span>
<button class="btn btn-sm btn-outline-secondary" onclick="loadVersionInfo()">
<i class="bi bi-arrow-clockwise me-1"></i><span
data-i18n="dashboard.refresh">Refresh</span>
</button>
</div>
<div class="card-body" id="version-info-content">
<div class="text-muted" data-i18n="common.loading">Loading...</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<h5 class="mb-3" data-i18n="settings.gitTitle">Git Repository Settings</h5>
<form id="settings-git-form">
<div class="row g-3">
<div class="col-md-8">
<label class="form-label" data-i18n="settings.gitRepoUrl">Repository
URL</label>
<input type="text" class="form-control" id="cfg-git-repo-url"
placeholder="https://git.example.com/owner/repo">
<div class="form-text" data-i18n="settings.gitRepoUrlHint">Used for
version checks and one-click updates via Gitea API.</div>
</div>
<div class="col-md-4">
<label class="form-label"
data-i18n="settings.gitBranch">Branch</label>
<div class="input-group">
<select class="form-select" id="cfg-git-branch">
<option value="main">main</option>
</select>
<button class="btn btn-outline-secondary" type="button"
onclick="loadGitBranches()" title="Aktualisieren">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
</div>
<div class="col-md-8">
<label class="form-label" data-i18n="settings.gitToken">Access Token
(optional)</label>
<div class="input-group">
<input type="password" class="form-control" id="cfg-git-token"
data-i18n-placeholder="settings.leaveEmptyToKeep"
placeholder="Leave empty to keep current">
<button class="btn btn-outline-secondary" type="button"
onclick="togglePasswordVisibility('cfg-git-token')"><i
class="bi bi-eye"></i></button>
</div>
<div class="form-text" id="git-token-status"></div>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary"><i
class="bi bi-save me-1"></i><span
data-i18n="settings.saveGitSettings">Save Git
Settings</span></button>
</div>
</form>
</div>
</div>
</div>
<!-- Security -->
<div class="tab-pane fade" id="settings-security">
<!-- MFA Settings -->
<div class="card shadow-sm mb-4" id="mfa-settings-card">
<div class="card-body">
<h5 class="mb-3" data-i18n="mfa.title">Multi-Factor Authentication (MFA)</h5>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="cfg-mfa-enabled">
<label class="form-check-label" for="cfg-mfa-enabled"
data-i18n="mfa.enableMfa">Enable MFA for all local users</label>
</div>
<p class="text-muted small" data-i18n="mfa.mfaDescription">When enabled, local
users must verify with a TOTP authenticator app after entering their
password. Azure AD users are not affected.</p>
<button class="btn btn-primary btn-sm" id="save-mfa-settings"
onclick="saveMfaSettings()">
<i class="bi bi-save me-1"></i><span data-i18n="mfa.saveMfaSettings">Save
MFA Settings</span>
</button>
<hr class="my-3" id="mfa-own-status-divider">
<h6 data-i18n="mfa.yourTotpStatus">Your TOTP Status</h6>
<div id="mfa-own-status" class="mb-2"></div>
<button class="btn btn-outline-danger btn-sm d-none" id="mfa-disable-own"
onclick="disableOwnTotp()">
<i class="bi bi-shield-x me-1"></i><span
data-i18n="mfa.disableMyTotp">Disable my TOTP</span>
</button>
</div>
</div>
<!-- Change Password -->
<div class="card shadow-sm">
<div class="card-body">
<h5 class="mb-3" data-i18n="settings.securityTitle">Change Admin Password</h5>
<form id="change-password-form">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label"
data-i18n="settings.currentPassword">Current Password</label>
<input type="password" class="form-control" id="pw-current"
required>
</div>
<div class="col-md-6"></div>
<div class="col-md-6">
<label class="form-label" data-i18n="settings.newPassword">New
Password (min 12 chars)</label>
<input type="password" class="form-control" id="pw-new" required
minlength="12">
</div>
<div class="col-md-6">
<label class="form-label"
data-i18n="settings.confirmPassword">Confirm New
Password</label>
<input type="password" class="form-control" id="pw-confirm" required
minlength="12">
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-warning"><i
class="bi bi-shield-lock me-1"></i><span
data-i18n="settings.changePassword">Change
Password</span></button>
</div>
</form>
<div id="password-result" class="mt-3 d-none"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Page: Monitoring -->
<div id="page-monitoring" class="page-content d-none">
<div class="container-fluid p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">
<button class="btn btn-outline-secondary btn-sm me-2" onclick="showPage('dashboard')"><i
class="bi bi-arrow-left me-1"></i><span data-i18n="common.back">Back</span></button>
<i class="bi bi-activity me-2"></i><span data-i18n="monitoring.title">System Monitoring</span>
</h4>
<button class="btn btn-outline-secondary btn-sm" onclick="loadMonitoring()"><i
class="bi bi-arrow-clockwise me-1"></i><span
data-i18n="monitoring.refresh">Refresh</span></button>
</div>
<!-- Host Resources -->
<div class="card shadow-sm mb-4">
<div class="card-header" data-i18n="monitoring.hostResources">Host Resources</div>
<div class="card-body" id="monitoring-resources" data-i18n="common.loading">Loading...</div>
</div>
<!-- Customer Statuses -->
<div class="card shadow-sm mb-4">
<div class="card-header" data-i18n="monitoring.allCustomerDeployments">All Customer Deployments
</div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th data-i18n="monitoring.thId">ID</th>
<th data-i18n="monitoring.thName">Name</th>
<th data-i18n="monitoring.thSubdomain">Subdomain</th>
<th data-i18n="monitoring.thStatus">Status</th>
<th data-i18n="monitoring.thDeployment">Deployment</th>
<th data-i18n="monitoring.thDashboard">Dashboard</th>
<th data-i18n="monitoring.thRelayPort">Relay Port</th>
<th data-i18n="monitoring.thContainers">Containers</th>
</tr>
</thead>
<tbody id="monitoring-customers-body">
<tr>
<td colspan="8" class="text-center text-muted py-4" data-i18n="common.loading">
Loading...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- NetBird Container Updates -->
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-arrow-repeat me-2"></i><span data-i18n="monitoring.imageUpdates">NetBird Container Updates</span></span>
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary btn-sm" onclick="checkImageUpdates()" id="btn-check-updates">
<i class="bi bi-search me-1"></i><span data-i18n="monitoring.checkUpdates">Check for Updates</span>
</button>
<button class="btn btn-outline-primary btn-sm" onclick="pullAllImages()" id="btn-pull-images">
<i class="bi bi-cloud-download me-1"></i><span data-i18n="monitoring.pullImages">Pull Latest Images</span>
</button>
<button class="btn btn-warning btn-sm d-none" onclick="updateAllCustomers()" id="btn-update-all">
<i class="bi bi-lightning-charge-fill me-1"></i><span data-i18n="monitoring.updateAll">Update All</span>
</button>
</div>
</div>
<div class="card-body" id="image-updates-body">
<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="monitoring.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>
<!-- Modal: New/Edit Customer -->
<div class="modal fade" id="customer-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="customer-modal-title" data-i18n="customerModal.newCustomer">New Customer
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="customer-modal-error" class="alert alert-danger d-none"></div>
<form id="customer-form">
<input type="hidden" id="customer-edit-id">
<div class="mb-3">
<label class="form-label" data-i18n="customerModal.nameLabel">Name *</label>
<input type="text" class="form-control" id="cust-name" required>
</div>
<div class="mb-3">
<label class="form-label" data-i18n="customerModal.companyLabel">Company</label>
<input type="text" class="form-control" id="cust-company">
</div>
<div class="mb-3">
<label class="form-label" data-i18n="customerModal.subdomainLabel">Subdomain *</label>
<div class="input-group">
<input type="text" class="form-control" id="cust-subdomain" required
pattern="[a-z0-9][-a-z0-9]*[a-z0-9]">
<span class="input-group-text" id="cust-subdomain-suffix">.domain.com</span>
</div>
<div class="form-text" data-i18n="customerModal.subdomainHint">Lowercase, alphanumeric +
hyphens</div>
</div>
<div class="mb-3">
<label class="form-label" data-i18n="customerModal.emailLabel">Email *</label>
<input type="email" class="form-control" id="cust-email" required>
</div>
<div class="mb-3">
<label class="form-label" data-i18n="customerModal.maxDevicesLabel">Max Devices</label>
<input type="number" class="form-control" id="cust-max-devices" value="20" min="1">
</div>
<div class="mb-3">
<label class="form-label" data-i18n="customerModal.notesLabel">Notes</label>
<textarea class="form-control" id="cust-notes" rows="2"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
data-i18n="common.cancel">Cancel</button>
<button type="button" class="btn btn-primary" id="customer-save-btn" onclick="saveCustomer()">
<span class="spinner-border spinner-border-sm d-none me-1" id="customer-save-spinner"></span>
<span data-i18n="customerModal.saveAndDeploy">Save & Deploy</span>
</button>
</div>
</div>
</div>
</div>
<!-- Modal: Redeploy Confirmation -->
<div class="modal fade" id="redeploy-modal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" data-i18n="redeployModal.title">Redeploy Customer</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p><span data-i18n="redeployModal.intro">How should</span> <strong id="redeploy-customer-name"></strong> <span data-i18n="redeployModal.intro2">be redeployed?</span></p>
<input type="hidden" id="redeploy-customer-id">
<div class="row g-3 mt-1">
<div class="col-md-6">
<div class="card h-100 border-success" style="cursor:pointer" onclick="confirmRedeploy(true)" id="redeploy-card-keep">
<div class="card-body">
<h6 class="card-title text-success"><i class="bi bi-shield-check me-2"></i><span data-i18n="redeployModal.keepTitle">Keep Data</span></h6>
<p class="card-text small" data-i18n="redeployModal.keepDesc">Containers are stopped and restarted. The NetBird database, peer configurations, and encryption keys are preserved. Use this after a config change or image update.</p>
</div>
<div class="card-footer bg-success bg-opacity-10 text-success small" data-i18n="redeployModal.keepNote">
Peers stay connected after restart.
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-danger" style="cursor:pointer" onclick="confirmRedeploy(false)" id="redeploy-card-fresh">
<div class="card-body">
<h6 class="card-title text-danger"><i class="bi bi-trash me-2"></i><span data-i18n="redeployModal.freshTitle">Fresh Deploy</span></h6>
<p class="card-text small" data-i18n="redeployModal.freshDesc">All existing data is deleted — containers, volumes, config files, and the NetBird database. A completely new instance is created. All peers must re-enroll.</p>
</div>
<div class="card-footer bg-danger bg-opacity-10 text-danger small" data-i18n="redeployModal.freshNote">
All peer data is lost. Cannot be undone.
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="common.cancel">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Modal: Delete Confirmation -->
<div class="modal fade" id="delete-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" data-i18n="deleteModal.title">Confirm Deletion</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p><span data-i18n="deleteModal.confirmText">Are you sure you want to delete customer</span> <strong
id="delete-customer-name"></strong>?</p>
<p class="text-danger" data-i18n="deleteModal.warning">This will remove all containers, NPM entries,
and data. This action cannot be undone.</p>
<input type="hidden" id="delete-customer-id">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
data-i18n="common.cancel">Cancel</button>
<button type="button" class="btn btn-danger" onclick="confirmDeleteCustomer()">
<span class="spinner-border spinner-border-sm d-none me-1" id="delete-spinner"></span>
<span data-i18n="common.delete">Delete</span>
</button>
</div>
</div>
</div>
</div>
<!-- Modal: New User -->
<div class="modal fade" id="user-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" data-i18n="userModal.title">New User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="user-modal-error" class="alert alert-danger d-none"></div>
<form id="user-form">
<div class="mb-3">
<label class="form-label" data-i18n="userModal.usernameLabel">Username *</label>
<input type="text" class="form-control" id="new-user-username" required minlength="3">
</div>
<div class="mb-3">
<label class="form-label" data-i18n="userModal.passwordLabel">Password * (min 8
chars)</label>
<input type="password" class="form-control" id="new-user-password" required minlength="8">
</div>
<div class="mb-3">
<label class="form-label" data-i18n="userModal.emailLabel">Email</label>
<input type="email" class="form-control" id="new-user-email">
</div>
<div class="mb-3">
<label class="form-label" data-i18n="userModal.languageLabel">Default Language</label>
<select class="form-select" id="new-user-language">
<option value="" data-i18n="settings.systemDefault">System Default</option>
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
data-i18n="common.cancel">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveNewUser()"
data-i18n="userModal.createUser">Create User</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/i18n.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>