516 lines
30 KiB
HTML
516 lines
30 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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css" rel="stylesheet">
|
|
<link href="/static/css/styles.css" rel="stylesheet">
|
|
</head>
|
|
<body>
|
|
<!-- 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">
|
|
<i class="bi bi-hdd-network fs-1 text-primary"></i>
|
|
<h3 class="mt-2">NetBird MSP Appliance</h3>
|
|
<p class="text-muted">Multi-Tenant Management Platform</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">Username</label>
|
|
<input type="text" class="form-control" id="login-username" required autofocus>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">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>
|
|
Sign In
|
|
</button>
|
|
</form>
|
|
</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" href="#"><i class="bi bi-hdd-network me-2"></i>NetBird MSP</a>
|
|
<div class="d-flex align-items-center">
|
|
<button class="btn btn-success btn-sm me-3" onclick="showNewCustomerModal()">
|
|
<i class="bi bi-plus-lg me-1"></i>New Customer
|
|
</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>Settings</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="showPage('monitoring')"><i class="bi bi-activity me-2"></i>Monitoring</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>Logout</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">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">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">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">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" placeholder="Search by name, subdomain, email...">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select class="form-select" id="status-filter">
|
|
<option value="">All Statuses</option>
|
|
<option value="active">Active</option>
|
|
<option value="inactive">Inactive</option>
|
|
<option value="deploying">Deploying</option>
|
|
<option value="error">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>Refresh</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>ID</th>
|
|
<th>Name</th>
|
|
<th>Company</th>
|
|
<th>Subdomain</th>
|
|
<th>Status</th>
|
|
<th>Devices</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="customers-table-body">
|
|
<tr><td colspan="8" class="text-center text-muted py-4">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">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>Back</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>Edit</button>
|
|
<button class="btn btn-outline-danger btn-sm" onclick="deleteCurrentCustomer()"><i class="bi bi-trash me-1"></i>Delete</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">Info</a></li>
|
|
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-deployment">Deployment</a></li>
|
|
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-logs">Logs</a></li>
|
|
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-health">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">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">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>Container Logs</span>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="loadCustomerLogs()"><i class="bi bi-arrow-clockwise"></i> Refresh</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="detail-logs-content" class="log-viewer">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>Health Check</span>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="loadCustomerHealth()"><i class="bi bi-arrow-clockwise"></i> Check</button>
|
|
</div>
|
|
<div class="card-body" id="detail-health-content">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">
|
|
<h4 class="mb-4"><i class="bi bi-gear me-2"></i>System Settings</h4>
|
|
<div id="settings-alert" class="d-none"></div>
|
|
|
|
<ul class="nav nav-tabs mb-3">
|
|
<li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#settings-system">System Configuration</a></li>
|
|
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#settings-npm">NPM Integration</a></li>
|
|
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#settings-images">Docker Images</a></li>
|
|
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#settings-security">Security</a></li>
|
|
</ul>
|
|
|
|
<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">Base Domain</label>
|
|
<input type="text" class="form-control" id="cfg-base-domain" placeholder="yourdomain.com">
|
|
<div class="form-text">Customers get subdomains: kunde.yourdomain.com</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Admin Email</label>
|
|
<input type="email" class="form-control" id="cfg-admin-email" placeholder="admin@yourdomain.com">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Data Directory</label>
|
|
<input type="text" class="form-control" id="cfg-data-dir" placeholder="/opt/netbird-instances">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Docker Network</label>
|
|
<input type="text" class="form-control" id="cfg-docker-network" placeholder="npm-network">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Relay Base Port</label>
|
|
<input type="number" class="form-control" id="cfg-relay-base-port" min="1024" max="65535">
|
|
<div class="form-text">First UDP port for relay. Range: base to base+99</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4">
|
|
<button type="submit" class="btn btn-primary"><i class="bi bi-save me-1"></i>Save System Settings</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">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">NPM API URL</label>
|
|
<input type="text" class="form-control" id="cfg-npm-api-url" placeholder="http://nginx-proxy-manager:81/api">
|
|
<div class="form-text">http:// or https:// - must include /api at the end</div>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<label class="form-label">NPM Login Email</label>
|
|
<input type="text" class="form-control" id="cfg-npm-api-email" 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">NPM Login Password</label>
|
|
<div class="input-group">
|
|
<input type="password" class="form-control" id="cfg-npm-api-password" 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>
|
|
</div>
|
|
<div class="mt-4">
|
|
<button type="submit" class="btn btn-primary me-2"><i class="bi bi-save me-1"></i>Save NPM Settings</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>Test Connection
|
|
</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">Management Image</label>
|
|
<input type="text" class="form-control" id="cfg-mgmt-image" placeholder="netbirdio/management:latest">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Signal Image</label>
|
|
<input type="text" class="form-control" id="cfg-signal-image" placeholder="netbirdio/signal:latest">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Relay Image</label>
|
|
<input type="text" class="form-control" id="cfg-relay-image" placeholder="netbirdio/relay:latest">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Dashboard Image</label>
|
|
<input type="text" class="form-control" id="cfg-dashboard-image" 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>Save Image Settings</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Security -->
|
|
<div class="tab-pane fade" id="settings-security">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<h5 class="mb-3">Change Admin Password</h5>
|
|
<form id="change-password-form">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">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">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">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>Change Password</button>
|
|
</div>
|
|
</form>
|
|
<div id="password-result" class="mt-3 d-none"></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"><i class="bi bi-activity me-2"></i>System Monitoring</h4>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="loadMonitoring()"><i class="bi bi-arrow-clockwise me-1"></i>Refresh</button>
|
|
</div>
|
|
|
|
<!-- Host Resources -->
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-header">Host Resources</div>
|
|
<div class="card-body" id="monitoring-resources">Loading...</div>
|
|
</div>
|
|
|
|
<!-- Customer Statuses -->
|
|
<div class="card shadow-sm">
|
|
<div class="card-header">All Customer Deployments</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Name</th>
|
|
<th>Subdomain</th>
|
|
<th>Status</th>
|
|
<th>Deployment</th>
|
|
<th>Relay Port</th>
|
|
<th>Containers</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="monitoring-customers-body">
|
|
<tr><td colspan="7" class="text-center text-muted py-4">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</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">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">Name *</label>
|
|
<input type="text" class="form-control" id="cust-name" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Company</label>
|
|
<input type="text" class="form-control" id="cust-company">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">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">Lowercase, alphanumeric + hyphens</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Email *</label>
|
|
<input type="email" class="form-control" id="cust-email" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">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">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">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>
|
|
Save & Deploy
|
|
</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">Confirm Deletion</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Are you sure you want to delete customer <strong id="delete-customer-name"></strong>?</p>
|
|
<p class="text-danger">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">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>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="/static/js/app.js"></script>
|
|
</body>
|
|
</html>
|