mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
Replace 3-way branching (configured_paths/has_path/else) with 4-scenario matrix based on (has_path × has_configured_paths): - S1: No path, no configured paths → FLOOD only - S2: Has path, no configured paths → DIRECT + optional FLOOD - S3: No path, has configured paths → FLOOD first, then ŚD rotation - S4: Has path, has configured paths → DIRECT on ŚK, ŚD rotation, optional FLOOD Key changes: - S3: FLOOD before configured paths (discover new routes) - S4: exhaust retries on current ŚK before rotating ŚD - S4: dedup ŚG/ŚK to skip redundant retries on same path - Add _paths_match() helper for path deduplication - Update tooltip text for settings clarity Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
720 lines
41 KiB
HTML
720 lines
41 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>{% block title %}mc-webui{% endblock %}</title>
|
|
|
|
<!-- 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="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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 -->
|
|
<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') }}">
|
|
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
<!-- Navbar -->
|
|
<nav class="navbar navbar-dark bg-primary">
|
|
<div class="container-fluid">
|
|
<span class="navbar-brand mb-0 h1">
|
|
<i class="bi bi-broadcast"></i> mc-webui
|
|
{% if device_name %}
|
|
<small class="text-white-50 d-none d-sm-inline">- {{ device_name }}</small>
|
|
{% endif %}
|
|
</span>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div id="notificationBell" class="btn btn-outline-light position-relative navbar-touch-btn" style="cursor: pointer;" onclick="markAllChannelsRead()" title="Mark all as read">
|
|
<i class="bi bi-bell"></i>
|
|
</div>
|
|
<select id="channelSelector" class="form-select navbar-touch-select" style="width: auto; min-width: 100px;" title="Select channel">
|
|
<option value="0">Public</option>
|
|
<!-- Channels loaded dynamically via JavaScript -->
|
|
</select>
|
|
<button class="btn btn-outline-light navbar-touch-btn" data-bs-toggle="offcanvas" data-bs-target="#mainMenu" title="Menu">
|
|
<i class="bi bi-list"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Offcanvas Menu -->
|
|
<div class="offcanvas offcanvas-end" tabindex="-1" id="mainMenu">
|
|
<div class="offcanvas-header">
|
|
<h5 class="offcanvas-title"><i class="bi bi-menu-button-wide"></i> Menu</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
|
|
</div>
|
|
<div class="px-3 pb-2 text-muted small border-bottom">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<span id="versionDisplay">
|
|
<i class="bi bi-tag"></i> <span id="versionText">{{ version }}</span>
|
|
<span class="badge bg-secondary ms-1" id="branchBadge">{{ git_branch }}</span>
|
|
</span>
|
|
<button id="checkUpdateBtn" class="btn btn-sm btn-outline-secondary py-0 px-1" title="Check for updates">
|
|
<i class="bi bi-arrow-repeat" id="checkUpdateIcon"></i>
|
|
</button>
|
|
</div>
|
|
<div id="updateLinkContainer" class="d-none mt-1"></div>
|
|
</div>
|
|
<div class="offcanvas-body">
|
|
<div class="list-group list-group-flush">
|
|
<!-- Messages -->
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" id="refreshBtn">
|
|
<i class="bi bi-arrow-clockwise" style="font-size: 1.5rem;"></i>
|
|
<span>Refresh Messages</span>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#channelsModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-broadcast-pin" style="font-size: 1.5rem;"></i>
|
|
<span>Manage Channels</span>
|
|
</button>
|
|
<button id="notificationsToggle" class="list-group-item list-group-item-action d-flex align-items-center gap-3" type="button">
|
|
<i class="bi bi-bell" style="font-size: 1.5rem;"></i>
|
|
<div class="flex-grow-1">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span>Notifications</span>
|
|
<span id="notificationStatus" class="badge bg-secondary">Disabled</span>
|
|
</div>
|
|
<small class="text-muted d-block mt-1" style="font-size: 0.75rem;">
|
|
Works when app is hidden
|
|
</small>
|
|
</div>
|
|
</button>
|
|
<div class="list-group-item">
|
|
<div class="d-flex align-items-center gap-3 mb-2">
|
|
<i class="bi bi-calendar3" style="font-size: 1.5rem;"></i>
|
|
<label for="dateSelector" class="form-label mb-0">Message History</label>
|
|
</div>
|
|
<select id="dateSelector" class="form-select" title="Select date">
|
|
<option value="">Today (Live)</option>
|
|
<!-- Archive dates loaded dynamically via JavaScript -->
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Network -->
|
|
<div class="list-group-item py-2 mt-2">
|
|
<small class="text-muted fw-bold text-uppercase">Network</small>
|
|
</div>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" id="advertBtn" title="Send single advertisement (recommended for normal operation)">
|
|
<i class="bi bi-megaphone" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>Send Advert</span>
|
|
<small class="d-block text-muted">Announce presence (normal)</small>
|
|
</div>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3 text-warning" id="floodadvBtn" title="Flood advertisement - use sparingly! High airtime usage.">
|
|
<i class="bi bi-broadcast" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>Flood Advert</span>
|
|
<small class="d-block text-muted">Network recovery only!</small>
|
|
</div>
|
|
</button>
|
|
|
|
<!-- Tools -->
|
|
<div class="list-group-item py-2 mt-2">
|
|
<small class="text-muted fw-bold text-uppercase">Tools</small>
|
|
</div>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3"
|
|
id="mapBtn" title="Show all contacts with GPS on map">
|
|
<i class="bi bi-map" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>Map</span>
|
|
<small class="d-block text-muted">All contacts with GPS</small>
|
|
</div>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#consoleModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-terminal" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>Console</span>
|
|
<small class="d-block text-muted">Direct meshcli commands</small>
|
|
</div>
|
|
</button>
|
|
|
|
<!-- System -->
|
|
<div class="list-group-item py-2 mt-2">
|
|
<small class="text-muted fw-bold text-uppercase">System</small>
|
|
</div>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#deviceInfoModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-cpu" style="font-size: 1.5rem;"></i>
|
|
<span>Device Info</span>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#logsModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-journal-text" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>System Log</span>
|
|
<small class="d-block text-muted">Real-time application logs</small>
|
|
</div>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#backupModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-database-down" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>Backup</span>
|
|
<small class="d-block text-muted">Database backup & restore</small>
|
|
</div>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#settingsModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-gear" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>Settings</span>
|
|
<small class="d-block text-muted">Application settings</small>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<main>
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<!-- Channels Management Modal -->
|
|
<div class="modal fade" id="channelsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-broadcast-pin"></i> Manage Channels</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<!-- Channel List -->
|
|
<h6>Your Channels</h6>
|
|
<div id="channelsList" class="list-group mb-3">
|
|
<div class="text-center text-muted py-3">
|
|
<div class="spinner-border spinner-border-sm"></div> Loading...
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="btn-group w-100 mb-3" role="group">
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#addChannelForm">
|
|
<i class="bi bi-plus-circle"></i> Add New Channel
|
|
</button>
|
|
<button type="button" class="btn btn-success" data-bs-toggle="collapse" data-bs-target="#joinChannelForm">
|
|
<i class="bi bi-box-arrow-in-right"></i> Join Existing
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Add Channel Form (collapsed) -->
|
|
<div class="collapse" id="addChannelForm">
|
|
<div class="card card-body mb-3">
|
|
<h6>Create New Channel</h6>
|
|
<form id="createChannelForm">
|
|
<div class="mb-2">
|
|
<label for="newChannelName" class="form-label">Channel Name</label>
|
|
<input type="text" class="form-control" id="newChannelName"
|
|
placeholder="e.g., Malopolska"
|
|
pattern="[a-zA-Z0-9_\-]+"
|
|
required>
|
|
<small class="text-muted">Only letters, numbers, _ and -</small>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-plus-circle"></i> Create & Auto-generate Key
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Join Channel Form (collapsed) -->
|
|
<div class="collapse" id="joinChannelForm">
|
|
<div class="card card-body mb-3">
|
|
<h6>Join Existing Channel</h6>
|
|
<form id="joinChannelFormSubmit">
|
|
<div class="mb-2">
|
|
<label for="joinChannelName" class="form-label">Channel Name</label>
|
|
<input type="text" class="form-control" id="joinChannelName" required>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label for="joinChannelKey" class="form-label">Channel Key (32 hex chars) <small class="text-muted">- optional for channels starting with #</small></label>
|
|
<input type="text" class="form-control" id="joinChannelKey"
|
|
placeholder="485af7e164459d280d8818d9c99fb30d (leave empty for # channels)"
|
|
pattern="[a-fA-F0-9]{32}"
|
|
maxlength="32">
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-success">
|
|
<i class="bi bi-box-arrow-in-right"></i> Join Channel
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" id="scanQRBtn">
|
|
<i class="bi bi-qr-code-scan"></i> Scan QR Code
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Share Channel Modal -->
|
|
<div class="modal fade" id="shareChannelModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-share"></i> Share Channel</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<h6 id="shareChannelName"></h6>
|
|
|
|
<!-- QR Code -->
|
|
<div class="text-center mb-3">
|
|
<img id="shareChannelQR" src="" alt="QR Code" class="img-fluid" style="max-width: 300px;">
|
|
</div>
|
|
|
|
<!-- Channel Details -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Channel Key:</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control font-monospace" id="shareChannelKey" readonly>
|
|
<button class="btn btn-outline-secondary" type="button" onclick="copyChannelKey()">
|
|
<i class="bi bi-clipboard"></i> Copy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-info small">
|
|
<i class="bi bi-info-circle"></i>
|
|
Share this QR code or key with others to let them join this channel.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Device Info Modal -->
|
|
<div class="modal fade" id="deviceInfoModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-cpu"></i> Device</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabDeviceInfo" type="button">Info</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabDeviceStats" type="button" id="statsTabBtn">Stats</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabDeviceShare" type="button" id="shareTabBtn">Share</button>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content">
|
|
<div class="tab-pane fade show active" id="tabDeviceInfo">
|
|
<div id="deviceInfoContent">
|
|
<div class="text-center py-3">
|
|
<div class="spinner-border spinner-border-sm"></div> Loading...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabDeviceStats">
|
|
<div id="deviceStatsContent">
|
|
<div class="text-center py-3 text-muted">
|
|
Click to load stats
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabDeviceShare">
|
|
<div id="deviceShareContent">
|
|
<div class="text-center py-3 text-muted">
|
|
Click to generate share code
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Modal -->
|
|
<div class="modal fade" id="settingsModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-gear"></i> Settings</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabSettingsMessages" type="button">Messages</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsChat" type="button">Group Chat</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsAppearance" type="button">Appearance</button>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content">
|
|
<div class="tab-pane fade show active" id="tabSettingsMessages">
|
|
<div id="settingsMessagesContent">
|
|
<form id="dmRetrySettingsForm">
|
|
<p class="text-muted small mb-3">Retries are counted after the initial send, e.g. 3 retries = 4 total attempts.</p>
|
|
<h6 class="text-muted mb-2">When path is known (DIRECT)</h6>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">Direct retries <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Attempts via known path before switching to flood"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settDirectMaxRetries" min="0" max="20" value="3"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">Flood retries <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Flood attempts after direct retries exhausted (when no configured paths)"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0"><input type="number" class="form-control form-control-sm" id="settDirectFloodRetries" min="0" max="5" value="1"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">Interval (s) <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Seconds to wait between direct retries"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0"><input type="number" class="form-control form-control-sm" id="settDirectInterval" min="5" max="300" value="30"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h6 class="text-muted mb-2">When no path (FLOOD)</h6>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">Max retries <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Flood retry attempts (also used after path rotation)"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settFloodMaxRetries" min="0" max="10" value="3"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">Interval (s) <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Seconds to wait between flood retries"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0"><input type="number" class="form-control form-control-sm" id="settFloodInterval" min="5" max="300" value="60"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h6 class="text-muted mb-2">Other</h6>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">Grace period (s) <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Wait for late ACKs after all retries exhausted"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settGracePeriod" min="10" max="300" value="60"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="settingsResetBtn">Reset to defaults</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabSettingsChat">
|
|
<form id="chatSettingsForm">
|
|
<h6 class="text-muted mb-2">Quote</h6>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">Quote length (bytes) <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Default max UTF-8 bytes for truncated quotes"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settQuoteMaxBytes" min="5" max="120" value="20"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="chatSettingsResetBtn">Reset to defaults</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabSettingsAppearance">
|
|
<h6 class="text-muted mb-3">Theme</h6>
|
|
<div class="d-flex flex-column gap-2">
|
|
<div class="theme-option active" data-theme-value="light" onclick="setTheme('light')">
|
|
<div class="theme-option-preview light">
|
|
<i class="bi bi-sun"></i>
|
|
</div>
|
|
<div>
|
|
<div class="theme-option-label">Light</div>
|
|
<div class="theme-option-desc">Classic bright interface</div>
|
|
</div>
|
|
</div>
|
|
<div class="theme-option" data-theme-value="dark" onclick="setTheme('dark')">
|
|
<div class="theme-option-preview dark">
|
|
<i class="bi bi-moon-stars" style="color: #60a5fa;"></i>
|
|
</div>
|
|
<div>
|
|
<div class="theme-option-label">Dark</div>
|
|
<div class="theme-option-desc">Easy on the eyes, deep navy palette</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quote Dialog Modal -->
|
|
<div class="modal fade" id="quoteModal" tabindex="-1">
|
|
<div class="modal-dialog modal-sm modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-quote"></i> Quote message</h6>
|
|
<button type="button" class="btn-close btn-close-sm" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body py-2">
|
|
<p class="text-muted small mb-2" id="quotePreview"></p>
|
|
<div class="d-flex gap-2 align-items-center mb-2">
|
|
<label class="form-label mb-0 small text-nowrap" for="quoteBytesInput">Max bytes:</label>
|
|
<input type="number" class="form-control form-control-sm" id="quoteBytesInput" min="5" max="120" style="width:5rem">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer py-1">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="quoteTruncatedBtn">Truncated</button>
|
|
<button type="button" class="btn btn-primary btn-sm" id="quoteFullBtn">Full quote</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Map Modal (Leaflet) -->
|
|
<div class="modal fade" id="mapModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-geo-alt"></i> <span id="mapModalTitle">Map</span></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<!-- Type filter (hidden for single contact view) -->
|
|
<div id="mapTypeFilter" class="d-none px-3 py-2 border-bottom bg-light">
|
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
|
<span class="map-filter-badge active" data-type="1" id="mapFilterCOM">COM</span>
|
|
<span class="map-filter-badge active" data-type="2" id="mapFilterREP">REP</span>
|
|
<span class="map-filter-badge active" data-type="3" id="mapFilterROOM">ROOM</span>
|
|
<span class="map-filter-badge active" data-type="4" id="mapFilterSENS">SENS</span>
|
|
<div class="form-check form-switch ms-auto mb-0">
|
|
<input class="form-check-input" type="checkbox" id="mapCachedSwitch">
|
|
<label class="form-check-label small" for="mapCachedSwitch">Cached</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="leafletMap" style="height: 400px; width: 100%;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Update Modal -->
|
|
<div class="modal fade" id="updateModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-cloud-download"></i> Update mc-webui</h5>
|
|
</div>
|
|
<div class="modal-body text-center py-4">
|
|
<div id="updateStatus">
|
|
<div class="spinner-border text-primary mb-3" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p id="updateMessage" class="mb-0">Checking for updates...</p>
|
|
<p id="updateWhatsNew" class="d-none mt-2 mb-0"><a href="#" target="_blank" class="text-muted small"><i class="bi bi-github"></i> What's new?</a></p>
|
|
</div>
|
|
<div id="updateProgress" class="d-none">
|
|
<div class="progress mb-3" style="height: 8px;">
|
|
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
|
role="progressbar" style="width: 100%"></div>
|
|
</div>
|
|
<p id="updateProgressMessage" class="text-muted mb-0">Updating...</p>
|
|
</div>
|
|
<div id="updateResult" class="d-none">
|
|
<i id="updateResultIcon" class="bi fs-1 mb-3 d-block"></i>
|
|
<p id="updateResultMessage" class="mb-0"></p>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer justify-content-center" id="updateFooter">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="updateCancelBtn">Cancel</button>
|
|
<button type="button" class="btn btn-primary d-none" id="updateConfirmBtn">Update Now</button>
|
|
<button type="button" class="btn btn-primary d-none" id="updateReloadBtn" onclick="location.reload()">Reload Page</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup Modal -->
|
|
<div class="modal fade" id="backupModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-database-down"></i> Database Backup</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<button class="btn btn-primary" id="createBackupBtn" onclick="createBackup()">
|
|
<i class="bi bi-plus-circle"></i> Create Backup
|
|
</button>
|
|
<span id="backupAutoStatus" class="text-muted small"></span>
|
|
</div>
|
|
<div id="backupList">
|
|
<div class="text-center text-muted py-3">
|
|
<div class="spinner-border spinner-border-sm"></div> Loading...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Modal -->
|
|
<div class="modal fade" id="searchModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-search"></i> Search Messages</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="input-group mb-3">
|
|
<input type="text" class="form-control" id="searchInput" placeholder="Search all messages..." autofocus>
|
|
<button class="btn btn-primary" type="button" id="searchBtn">
|
|
<i class="bi bi-search"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary" type="button" id="searchHelpBtn" title="Search syntax help">
|
|
<i class="bi bi-question-circle"></i>
|
|
</button>
|
|
</div>
|
|
<div id="searchHelp" class="alert alert-light small mb-3" style="display:none;">
|
|
<strong>Search tips:</strong>
|
|
<ul class="mb-1 ps-3">
|
|
<li><code>hello world</code> — messages containing both words</li>
|
|
<li><code>"hello world"</code> — exact phrase</li>
|
|
<li><code>hello OR world</code> — either word</li>
|
|
<li><code>hello NOT world</code> — hello but not world</li>
|
|
<li><code>hell*</code> — prefix match (hello,hellas...)</li>
|
|
</ul>
|
|
<div class="text-muted">Special characters (<code>. , - :</code>) should be wrapped in quotes.<br>
|
|
<a href="https://www.sqlite.org/fts5.html#full_text_query_syntax" target="_blank" rel="noopener">Full FTS5 syntax reference <i class="bi bi-box-arrow-up-right"></i></a></div>
|
|
</div>
|
|
<div id="searchResults">
|
|
<div class="text-center text-muted py-4">
|
|
<i class="bi bi-search" style="font-size: 2rem;"></i>
|
|
<p class="mt-2 mb-0">Search across all channel and direct messages</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast container for notifications -->
|
|
<div class="toast-container position-fixed top-0 start-0 p-3">
|
|
<div id="notificationToast" class="toast" role="alert">
|
|
<div class="toast-header">
|
|
<strong class="me-auto">mc-webui</strong>
|
|
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
<div class="toast-body"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bootstrap 5 JS Bundle (local) -->
|
|
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
|
|
|
<!-- Leaflet JS -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""></script>
|
|
|
|
<!-- Message Content Processing Utilities (must load before app.js and dm.js) -->
|
|
<script src="{{ url_for('static', filename='js/message-utils.js') }}"></script>
|
|
|
|
<!-- Filter Utilities (must load before app.js) -->
|
|
<script src="{{ url_for('static', filename='js/filter-utils.js') }}"></script>
|
|
|
|
<!-- SocketIO for real-time updates -->
|
|
<script src="{{ url_for('static', filename='vendor/socket.io/socket.io.min.js') }}"></script>
|
|
|
|
<!-- Custom JS -->
|
|
<!-- QR Code generator (for Device Share) -->
|
|
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
|
|
|
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
|
|
|
<!-- PWA Viewport Fix for Android -->
|
|
<script>
|
|
// Fix viewport corruption on Android PWA after page reload
|
|
window.addEventListener('load', function() {
|
|
// Force viewport recalculation without modifying body height
|
|
setTimeout(() => {
|
|
window.scrollTo(0, 0);
|
|
// Force reflow using a less invasive method
|
|
void(document.documentElement.offsetHeight);
|
|
// Trigger resize event to recalculate layout
|
|
window.dispatchEvent(new Event('resize'));
|
|
}, 150);
|
|
});
|
|
</script>
|
|
|
|
<!-- Service Worker Registration for PWA -->
|
|
<script>
|
|
// Register Service Worker for PWA functionality
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register('/static/js/sw.js')
|
|
.then((registration) => {
|
|
console.log('Service Worker registered:', registration.scope);
|
|
})
|
|
.catch((error) => {
|
|
console.error('Service Worker registration failed:', error);
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<!-- Theme Switching -->
|
|
<script>
|
|
function setTheme(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
|
localStorage.setItem('mc-webui-theme', theme);
|
|
// Update theme selector UI
|
|
document.querySelectorAll('.theme-option').forEach(function(el) {
|
|
el.classList.toggle('active', el.getAttribute('data-theme-value') === theme);
|
|
});
|
|
}
|
|
|
|
// Initialize theme selector UI on settings modal open
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var current = localStorage.getItem('mc-webui-theme') || 'light';
|
|
document.querySelectorAll('.theme-option').forEach(function(el) {
|
|
el.classList.toggle('active', el.getAttribute('data-theme-value') === current);
|
|
});
|
|
});
|
|
</script>
|
|
|
|
{% block extra_scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|