mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-07-27 12:02:38 +02:00
f54dc1d481
- Login retry prompt now prefills the saved password (a wrong stored password and an unreachable repeater are indistinguishable, so a connection-caused failure no longer forces retyping). Adds GET /api/repeaters/<pk>/password for the trusted local UI. - Repeater list: "last login" moves to its own line so a long path keeps the full row width on narrow phones. - Settings -> Location: "Pick from map" button opens a Leaflet picker; clicking the map fills lat/lon and marks the section dirty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
423 lines
16 KiB
HTML
423 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-theme="light" data-bs-theme="light">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<title>Repeater Management - mc-webui</title>
|
|
|
|
<!-- Theme: apply saved preference before CSS loads to prevent flash -->
|
|
<script>
|
|
(function() {
|
|
var t = localStorage.getItem('mc-webui-theme') || 'light';
|
|
document.documentElement.setAttribute('data-theme', t);
|
|
document.documentElement.setAttribute('data-bs-theme', t);
|
|
})();
|
|
</script>
|
|
|
|
<!-- Favicon -->
|
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='images/apple-touch-icon.png') }}">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/favicon-32x32.png') }}">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='images/favicon-16x16.png') }}">
|
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
|
|
|
<!-- Bootstrap 5 CSS (local) -->
|
|
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
|
|
<!-- Bootstrap Icons (local) -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/bootstrap-icons/bootstrap-icons.css') }}">
|
|
<!-- Leaflet CSS (neighbors map view) -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin="" />
|
|
<!-- Custom CSS -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
<!-- Theme CSS (light/dark mode) -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/theme.css') }}">
|
|
|
|
<style>
|
|
/* Standalone page: allow normal scrolling (style.css sets overflow hidden) */
|
|
html, body {
|
|
overflow: auto !important;
|
|
height: 100%;
|
|
}
|
|
|
|
body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background-color: var(--bg-body);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.manage-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.6rem 1rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.manage-content {
|
|
flex: 1 1 0;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
padding: 1rem;
|
|
}
|
|
|
|
/* Header card */
|
|
.rpt-header-card {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 0.5rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.rpt-header-icon {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
background: rgba(25, 135, 84, 0.12);
|
|
border: 2px solid #198754;
|
|
color: #198754;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.6rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.rpt-header-meta {
|
|
font-size: 0.85rem;
|
|
color: var(--text-secondary, #6c757d);
|
|
}
|
|
|
|
.rpt-pubkey {
|
|
font-family: var(--bs-font-monospace, monospace);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
/* Tools grid */
|
|
.tool-tile {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 0.5rem;
|
|
padding: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.85rem;
|
|
cursor: pointer;
|
|
transition: box-shadow 0.15s;
|
|
height: 100%;
|
|
}
|
|
|
|
.tool-tile:hover {
|
|
box-shadow: var(--card-shadow-hover, 0 2px 8px rgba(0, 0, 0, 0.15));
|
|
}
|
|
|
|
.tool-tile.disabled {
|
|
opacity: 0.55;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.tool-tile.disabled:hover {
|
|
box-shadow: none;
|
|
}
|
|
|
|
.tool-icon {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 0.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.3rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.tool-icon.status { background: rgba(13, 110, 253, 0.12); color: #0d6efd; }
|
|
.tool-icon.telemetry { background: rgba(111, 66, 193, 0.12); color: #6f42c1; }
|
|
.tool-icon.neighbors { background: rgba(25, 135, 84, 0.12); color: #198754; }
|
|
.tool-icon.cli { background: rgba(255, 143, 0, 0.12); color: #e65100; }
|
|
.tool-icon.settings { background: rgba(220, 53, 69, 0.12); color: #dc3545; }
|
|
.tool-icon.actions { background: rgba(13, 202, 240, 0.12); color: #0aa2c0; }
|
|
|
|
.tool-tile h6 {
|
|
margin: 0;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.tool-tile .tool-desc {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary, #6c757d);
|
|
margin: 0;
|
|
}
|
|
|
|
/* Tool pane */
|
|
.tool-pane-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.tool-pane-body {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 0.5rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
/* CLI terminal (dark regardless of theme, like the Console module) */
|
|
.cli-terminal {
|
|
background-color: #1a1a2e;
|
|
color: #e0e0e0;
|
|
font-family: 'Courier New', Consolas, monospace;
|
|
font-size: 0.85rem;
|
|
border-radius: 0.375rem;
|
|
padding: 0.75rem;
|
|
/* Fill the viewport below header card + pane chrome, never tiny */
|
|
height: max(260px, calc(100vh - 430px));
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.cli-line {
|
|
margin-bottom: 0.4rem;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.cli-line.cmd { color: #00ff88; }
|
|
.cli-line.cmd::before { content: '> '; color: #888; }
|
|
|
|
.cli-line.reply {
|
|
background-color: #16213e;
|
|
padding: 0.4rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
border-left: 3px solid #0f3460;
|
|
}
|
|
|
|
.cli-line.error { color: #ff6b6b; }
|
|
|
|
.cli-line.meta {
|
|
color: #4ecdc4;
|
|
font-style: italic;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.cli-pending::after {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 11px;
|
|
height: 11px;
|
|
border: 2px solid #4ecdc4;
|
|
border-top-color: transparent;
|
|
border-radius: 50%;
|
|
animation: cli-spin 1s linear infinite;
|
|
margin-left: 0.4rem;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
@keyframes cli-spin { to { transform: rotate(360deg); } }
|
|
|
|
/* SNR labels on neighbor map connection lines */
|
|
.nb-snr-tooltip {
|
|
background: rgba(13, 110, 253, 0.92);
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 0.5rem;
|
|
padding: 1px 6px;
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.nb-snr-tooltip::before {
|
|
display: none;
|
|
}
|
|
|
|
/* Settings tool */
|
|
#settingsAccordion .accordion-button {
|
|
padding: 0.6rem 0.9rem;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.sf-row.sf-dirty .sf-input,
|
|
.sf-row.sf-dirty .sf-sub {
|
|
border-color: var(--bs-warning);
|
|
}
|
|
|
|
.sf-badge .spinner-border {
|
|
width: 0.8rem;
|
|
height: 0.8rem;
|
|
}
|
|
|
|
/* Centered state screens */
|
|
.state-screen {
|
|
text-align: center;
|
|
padding: 3rem 1rem;
|
|
color: var(--text-secondary, #6c757d);
|
|
}
|
|
|
|
.state-screen i {
|
|
font-size: 3rem;
|
|
display: block;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Toolbar -->
|
|
<div class="manage-toolbar">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="backBtn" title="Back to My Repeaters">
|
|
<i class="bi bi-arrow-left"></i>
|
|
</button>
|
|
<span class="fw-semibold flex-grow-1">Repeater Management</span>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary d-none" id="logoutBtn" title="Log out of this repeater">
|
|
<i class="bi bi-box-arrow-right"></i> Logout
|
|
</button>
|
|
</div>
|
|
|
|
<div class="manage-content">
|
|
<!-- Loading / logging-in state -->
|
|
<div class="state-screen" id="loadingState">
|
|
<div class="spinner-border text-success" role="status" style="width: 3rem; height: 3rem;"></div>
|
|
<p class="mt-3 mb-0" id="loadingText">Loading…</p>
|
|
</div>
|
|
|
|
<!-- Error state -->
|
|
<div class="state-screen" id="errorState" style="display: none;">
|
|
<i class="bi bi-exclamation-triangle text-warning"></i>
|
|
<p class="mb-3" id="errorText">Something went wrong.</p>
|
|
<div class="d-flex gap-2 justify-content-center">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="errorBackBtn">
|
|
<i class="bi bi-arrow-left"></i> Back to list
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-primary" id="errorRetryBtn">
|
|
<i class="bi bi-arrow-clockwise"></i> Try again
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Panel content (after login) -->
|
|
<div id="panelContent" style="display: none;">
|
|
<!-- Header card -->
|
|
<div class="rpt-header-card">
|
|
<div class="rpt-header-icon"><i class="bi bi-diagram-3"></i></div>
|
|
<div class="flex-grow-1" style="min-width: 0;">
|
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
<h5 class="mb-0 text-truncate" id="rptName"></h5>
|
|
<span class="badge" id="roleBadge"></span>
|
|
</div>
|
|
<div class="rpt-pubkey text-muted mt-1">
|
|
<span id="rptPubkey"></span>
|
|
<button type="button" class="btn btn-link btn-sm p-0 ms-1 align-baseline" id="copyPubkeyBtn" title="Copy full public key">
|
|
<i class="bi bi-copy"></i>
|
|
</button>
|
|
</div>
|
|
<div class="rpt-header-meta mt-1">
|
|
<i class="bi bi-signpost-split"></i> <span class="font-monospace" id="rptPath">—</span>
|
|
<span class="ms-2"><i class="bi bi-geo-alt"></i> <span id="rptLocation">—</span></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tools grid -->
|
|
<div id="toolsGrid">
|
|
<div class="text-muted small text-uppercase fw-bold mb-2">Management Tools</div>
|
|
<div class="row g-3" id="toolsRow"></div>
|
|
</div>
|
|
|
|
<!-- Tool pane (shown instead of the grid when a tool is open) -->
|
|
<div id="toolPane" style="display: none;">
|
|
<div class="tool-pane-header">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="paneBackBtn" title="Back to tools">
|
|
<i class="bi bi-arrow-left"></i>
|
|
</button>
|
|
<span class="tool-icon" id="paneIcon" style="width: 32px; height: 32px; font-size: 1rem;"></span>
|
|
<h6 class="mb-0" id="paneTitle"></h6>
|
|
</div>
|
|
<div class="tool-pane-body" id="paneBody"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Password Modal (login prompt) -->
|
|
<div class="modal fade" id="passwordModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-key"></i> <span id="passwordModalTitle">Log in</span></h6>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-2 small text-muted" id="passwordModalInfo"></div>
|
|
<div class="input-group input-group-sm mb-2">
|
|
<input type="password" class="form-control" id="passwordInput"
|
|
placeholder="Repeater password" autocomplete="off">
|
|
<button type="button" class="btn btn-outline-secondary" id="togglePasswordBtn" title="Show/hide password">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="savePasswordCheck" checked>
|
|
<label class="form-check-label small" for="savePasswordCheck">
|
|
Remember password (stored in the app database)
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button type="button" class="btn btn-sm btn-secondary" id="passwordCancelBtn">Back to list</button>
|
|
<button type="button" class="btn btn-sm btn-primary" id="passwordSubmitBtn">Log in</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Location Map Picker Modal (Settings → Location) -->
|
|
<div class="modal fade" id="locationMapModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-geo-alt"></i> Pick Location</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<div class="px-3 py-2 border-bottom small text-muted">
|
|
Click the map to set the repeater location.
|
|
</div>
|
|
<div id="locLeafletMap" style="height: 400px; width: 100%;"></div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<span class="me-auto small font-monospace" id="locMapSelected">—</span>
|
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-sm btn-primary" id="locMapUseBtn" disabled>Use this location</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast container for notifications (position applied by JS from ui_settings) -->
|
|
<div class="toast-container position-fixed top-0 start-0 p-3" data-toast-container>
|
|
<div id="notificationToast" class="toast" role="alert">
|
|
<div class="toast-header">
|
|
<strong class="me-auto">Repeater Management</strong>
|
|
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
<div class="toast-body"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bootstrap JS Bundle (local) -->
|
|
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
|
<!-- Leaflet JS (neighbors map view) -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""></script>
|
|
<!-- Repeater Management JS -->
|
|
<script src="{{ url_for('static', filename='js/repeater-manage.js') }}"></script>
|
|
</body>
|
|
</html>
|