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]>
1474 lines
109 KiB
HTML
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 > 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 & 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> |