Files
MarekWo f72f6d418a feat(db): VACUUM after retention and an Optimize button in Backup modal
SQLite DELETE marks pages free but doesn't shrink the file, so the
new retention job would keep DBs at their bloated size forever without
a follow-up VACUUM. Add db.vacuum() that runs PRAGMA-free VACUUM and
reports size_before/size_after/elapsed so callers can surface results.

The retention job now calls vacuum() automatically when it deleted at
least 1000 rows. Threshold avoids the multi-second VACUUM cost on quiet
days. Failure is logged, not raised — a missed VACUUM never crashes
the scheduler.

Power-user override: new "Optimize now" button in the Database Backup
modal triggers VACUUM on demand via POST /api/db/vacuum, alongside a
GET /api/db/size that drives the live "Current size" label. This way
users don't have to wait until 03:30 to reclaim space after the first
big retention pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 10:55:23 +02:00

1410 lines
93 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>
<!-- Sidebar breakpoint: apply saved preference before CSS loads to prevent layout flash -->
<script>
(function() {
try {
var stored = parseInt(localStorage.getItem('mc-webui-sidebar-breakpoint'), 10);
var bp = (isNaN(stored) || stored < 600 || stored > 2000) ? 992 : stored;
if (window.innerWidth >= bp) {
document.documentElement.classList.add('layout-wide');
}
} catch (e) {}
})();
</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 %}
{% if transport_type == 'ble' %}
<span class="badge bg-info ms-1 d-none d-sm-inline" title="Bluetooth Low Energy">BLE</span>
{% elif transport_type == 'tcp' %}
<span class="badge bg-warning text-dark ms-1 d-none d-sm-inline" title="TCP connection">TCP</span>
{% 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>
<div class="position-relative channel-mobile-selector" id="channelSelectorWrapper" style="min-width: 80px; max-width: 140px;">
<input type="text"
id="channelSelectorInput"
class="form-control form-select navbar-touch-select"
placeholder="Public"
autocomplete="off"
title="Select channel"
style="cursor: pointer;">
<div id="channelSelectorDropdown" class="channel-selector-dropdown" style="display: none;"></div>
</div>
<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 id="menu-filter" class="list-group-item list-group-item-action d-flex align-items-center gap-3 d-none" type="button">
<i class="bi bi-funnel" style="font-size: 1.5rem;"></i>
<div class="flex-grow-1">
<div>Filter messages</div>
<small class="d-block text-muted">Filter current chat</small>
</div>
</button>
<button id="menu-search" class="list-group-item list-group-item-action d-flex align-items-center gap-3 d-none" type="button"
data-bs-toggle="modal" data-bs-target="#searchModal" data-bs-dismiss="offcanvas">
<i class="bi bi-search" style="font-size: 1.5rem;"></i>
<div class="flex-grow-1">
<div>Search messages</div>
<small class="d-block text-muted">Search all messages</small>
</div>
</button>
<button id="menu-dm" class="list-group-item list-group-item-action d-flex align-items-center gap-3 d-none" type="button"
data-bs-toggle="modal" data-bs-target="#dmModal" data-bs-dismiss="offcanvas">
<i class="bi bi-envelope-fill" style="font-size: 1.5rem;"></i>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-center">
<span>Direct Messages</span>
<span class="badge rounded-pill bg-danger d-none menu-badge-dm">0</span>
</div>
<small class="d-block text-muted">Private conversations</small>
</div>
</button>
<button id="menu-contacts" class="list-group-item list-group-item-action d-flex align-items-center gap-3 d-none" type="button"
data-bs-toggle="modal" data-bs-target="#contactsModal" data-bs-dismiss="offcanvas">
<i class="bi bi-person-lines-fill" style="font-size: 1.5rem;"></i>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-center">
<span>Contact Management</span>
<span class="badge rounded-pill bg-warning d-none menu-badge-contacts">0</span>
</div>
<small class="d-block text-muted">Manage known contacts</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="#channelsModal" data-bs-dismiss="offcanvas">
<i class="bi bi-broadcast-pin" style="font-size: 1.5rem;"></i>
<span>Manage Channels</span>
</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 id="consoleBtn" 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 id="deviceInfoBtn" 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 id="logsBtn" 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 id="menu-settings" class="list-group-item list-group-item-action d-flex align-items-center gap-3 d-none" 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>
<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>
</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>
<!-- Set Channel Region Scope Modal -->
<div class="modal fade" id="regionPickerModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-pin-map"></i>
Set Region Scope for <span id="regionPickerChannelName" class="fw-bold"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info py-2 small mb-3">
<i class="bi bi-info-circle"></i> Only repeaters allowing the selected region will forward messages from this channel.
</div>
<div id="regionPickerList" class="list-group mb-0"></div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-sm" id="regionPickerSaveBtn">Save</button>
</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="#tabSettingsDevice" type="button">Device</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" 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="#tabSettingsInterface" type="button">Interface</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>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsContacts" type="button">Contacts</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsRegions" type="button">Regions</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsAnalyzer" type="button">Analyzer</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsNotifications" type="button">Notifications</button>
</li>
</ul>
<div class="tab-content">
<!-- Device Settings Tab -->
<div class="tab-pane fade show active" id="tabSettingsDevice">
<ul class="nav nav-pills nav-fill mb-3" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active py-1 px-2" data-bs-toggle="pill"
data-bs-target="#tabDevicePublicInfo" type="button">Public Info</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link py-1 px-2" data-bs-toggle="pill"
data-bs-target="#tabDeviceRadio" type="button">Radio Settings</button>
</li>
</ul>
<div class="tab-content">
<!-- Public Info sub-tab -->
<div class="tab-pane fade show active" id="tabDevicePublicInfo">
<form id="devicePublicInfoForm">
<table class="table table-sm table-borderless mb-3 align-middle">
<tbody>
<tr>
<td class="ps-0">Name</td>
<td class="pe-0"><input type="text" class="form-control form-control-sm"
id="settDeviceName" maxlength="32" placeholder="Device name"></td>
</tr>
<tr>
<td class="ps-0">Latitude</td>
<td class="pe-0">
<div class="input-group input-group-sm">
<input type="number" class="form-control form-control-sm"
id="settDeviceLat" step="0.000001" min="-90" max="90" placeholder="0.000000">
<button type="button" class="btn btn-outline-secondary" id="settDevicePickMapBtn"
title="Pick from map"><i class="bi bi-geo-alt"></i></button>
</div>
</td>
</tr>
<tr>
<td class="ps-0">Longitude</td>
<td class="pe-0"><input type="number" class="form-control form-control-sm"
id="settDeviceLon" step="0.000001" min="-180" max="180" placeholder="0.000000"></td>
</tr>
<tr>
<td class="ps-0">Share position in advert
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
title="Include GPS coordinates in device advertisement"><i class="bi bi-info-circle"></i></span>
</td>
<td class="pe-0">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="settDeviceAdvertLoc">
</div>
</td>
</tr>
<tr>
<td class="ps-0">Path hash mode
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
title="Bytes per hop in routing paths. 1B = shortest path, more collisions; 3B = longest, fewest collisions."><i class="bi bi-info-circle"></i></span>
</td>
<td class="pe-0">
<select class="form-select form-select-sm" id="settDevicePathHashMode">
<option value="0">1 byte (default)</option>
<option value="1">2 bytes</option>
<option value="2">3 bytes</option>
</select>
</td>
</tr>
</tbody>
</table>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm">Save</button>
</div>
</form>
</div>
<!-- Radio Settings sub-tab -->
<div class="tab-pane fade" id="tabDeviceRadio">
<form id="deviceRadioForm">
<table class="table table-sm table-borderless mb-3 align-middle">
<tbody>
<tr>
<td class="ps-0" colspan="2">
<select class="form-select form-select-sm" id="settRadioPreset">
<option value="">Load preset...</option>
</select>
</td>
</tr>
<tr>
<td class="ps-0">Frequency (MHz)</td>
<td class="pe-0" style="width:7rem"><input type="number" class="form-control form-control-sm"
id="settRadioFreq" step="0.001" min="100" max="1000"></td>
</tr>
<tr>
<td class="ps-0">Bandwidth (kHz)</td>
<td class="pe-0">
<select class="form-select form-select-sm" id="settRadioBw" style="width:7rem">
<option value="7.8">7.8</option>
<option value="10.4">10.4</option>
<option value="15.6">15.6</option>
<option value="20.8">20.8</option>
<option value="31.25">31.25</option>
<option value="41.7">41.7</option>
<option value="62.5">62.5</option>
<option value="125">125</option>
<option value="250">250</option>
<option value="500">500</option>
</select>
</td>
</tr>
<tr>
<td class="ps-0">Spreading Factor</td>
<td class="pe-0" style="width:7rem"><input type="number" class="form-control form-control-sm"
id="settRadioSf" min="5" max="12"></td>
</tr>
<tr>
<td class="ps-0">Coding Rate</td>
<td class="pe-0" style="width:7rem"><input type="number" class="form-control form-control-sm"
id="settRadioCr" min="5" max="8"></td>
</tr>
<tr>
<td class="ps-0">TX Power (dBm)</td>
<td class="pe-0" style="width:7rem"><input type="number" class="form-control form-control-sm"
id="settRadioTxPower" min="0" max="30"></td>
</tr>
</tbody>
</table>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm">Save</button>
</div>
</form>
</div>
</div>
</div>
<div class="tab-pane fade" 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>
<h6 class="text-muted mb-2">Route popup</h6>
<p class="text-muted small mb-2">The popup shown when tapping "SNR | Hops" under a message. Also applies to DMs.</p>
<table class="table table-sm table-borderless mb-3 align-middle">
<tbody>
<tr>
<td class="ps-0">Auto-close after (s) <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Seconds before the route popup closes automatically"><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="settPathPopupTimeout" min="1" max="60" value="8"></td>
</tr>
<tr>
<td class="ps-0">Don't close automatically <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Popup stays open until you tap outside it"><i class="bi bi-info-circle"></i></span></td>
<td class="pe-0">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="settPathPopupNoAutoclose">
</div>
</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="tabSettingsInterface">
<h6 class="text-muted mb-2">Layout</h6>
<p class="text-muted small mb-2">Window width above which the channel/contact list is shown as a sidebar. Below this width it collapses to a dropdown at the top of the screen. Saved per device (browser).</p>
<table class="table table-sm table-borderless mb-3 align-middle">
<tbody>
<tr>
<td class="ps-0">Sidebar breakpoint (px) <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Default: 992. Range: 600-2000."><i class="bi bi-info-circle"></i></span></td>
<td class="pe-0" style="width:10rem">
<div class="d-flex gap-1">
<input type="number" class="form-control form-control-sm" id="settSidebarBreakpoint" min="600" max="2000" step="1" value="992">
<button type="button" class="btn btn-outline-secondary btn-sm" id="settSidebarBreakpointReset" title="Reset to default (992)"><i class="bi bi-arrow-counterclockwise"></i></button>
</div>
</td>
</tr>
</tbody>
</table>
<hr>
<form id="uiSettingsForm">
<h6 class="text-muted mb-2">Notifications</h6>
<p class="text-muted small mb-2">Controls the small toasts shown after actions (e.g. "Advert Sent", errors).</p>
<table class="table table-sm table-borderless mb-3 align-middle">
<tbody>
<tr>
<td class="ps-0">Auto-close after (s) <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Seconds before a notification closes automatically"><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="settToastTimeout" min="1" max="60" step="0.5" value="2"></td>
</tr>
<tr>
<td class="ps-0">Don't close automatically <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Notifications stay until dismissed via their close button"><i class="bi bi-info-circle"></i></span></td>
<td class="pe-0">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="settToastNoAutoclose">
</div>
</td>
</tr>
<tr>
<td class="ps-0">Position on screen</td>
<td class="pe-0">
<select class="form-select form-select-sm" id="settToastPosition">
<option value="top-left">Top left</option>
<option value="top-right">Top right</option>
<option value="bottom-left">Bottom left</option>
<option value="bottom-right">Bottom right</option>
<option value="center">Center</option>
</select>
</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="uiSettingsResetBtn">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>
<hr>
<h6 class="text-muted mb-3">Quick Access Buttons</h6>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="settHideFab">
<label class="form-check-label" for="settHideFab">Hide Quick Access buttons</label>
<small class="d-block text-muted">When hidden, all items appear in the Main Menu.</small>
</div>
<div id="fabAppearanceControls">
<table class="table table-sm align-middle mb-3" id="settItemPlacementTable">
<thead>
<tr>
<th class="ps-0 fw-normal text-muted small">Item</th>
<th class="pe-0 text-end fw-normal text-muted small">Placement</th>
</tr>
</thead>
<tbody>
<tr data-placement-key="filter">
<td class="ps-0">Filter messages</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Filter placement">
<input type="radio" class="btn-check" name="place-filter" id="place-filter-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-filter-fab">Quick Access</label>
<input type="radio" class="btn-check" name="place-filter" id="place-filter-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-filter-menu">Main Menu</label>
</div>
</td>
</tr>
<tr data-placement-key="search">
<td class="ps-0">Search messages</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Search placement">
<input type="radio" class="btn-check" name="place-search" id="place-search-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-search-fab">Quick Access</label>
<input type="radio" class="btn-check" name="place-search" id="place-search-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-search-menu">Main Menu</label>
</div>
</td>
</tr>
<tr data-placement-key="dm">
<td class="ps-0">Direct Messages</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="DM placement">
<input type="radio" class="btn-check" name="place-dm" id="place-dm-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-dm-fab">Quick Access</label>
<input type="radio" class="btn-check" name="place-dm" id="place-dm-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-dm-menu">Main Menu</label>
</div>
</td>
</tr>
<tr data-placement-key="contacts">
<td class="ps-0">Contact Management</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Contacts placement">
<input type="radio" class="btn-check" name="place-contacts" id="place-contacts-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-contacts-fab">Quick Access</label>
<input type="radio" class="btn-check" name="place-contacts" id="place-contacts-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-contacts-menu">Main Menu</label>
</div>
</td>
</tr>
<tr data-placement-key="settings">
<td class="ps-0">Settings</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Settings placement">
<input type="radio" class="btn-check" name="place-settings" id="place-settings-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-settings-fab">Quick Access</label>
<input type="radio" class="btn-check" name="place-settings" id="place-settings-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-settings-menu">Main Menu</label>
</div>
</td>
</tr>
<tr data-placement-key="advert">
<td class="ps-0">Send Advert</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Advert placement">
<input type="radio" class="btn-check" name="place-advert" id="place-advert-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-advert-fab">Quick Access</label>
<input type="radio" class="btn-check" name="place-advert" id="place-advert-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-advert-menu">Main Menu</label>
</div>
</td>
</tr>
<tr data-placement-key="floodadvert">
<td class="ps-0">Flood Advert</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Flood Advert placement">
<input type="radio" class="btn-check" name="place-floodadvert" id="place-floodadvert-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-floodadvert-fab">Quick Access</label>
<input type="radio" class="btn-check" name="place-floodadvert" id="place-floodadvert-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-floodadvert-menu">Main Menu</label>
</div>
</td>
</tr>
<tr data-placement-key="map">
<td class="ps-0">Map</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Map placement">
<input type="radio" class="btn-check" name="place-map" id="place-map-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-map-fab">Quick Access</label>
<input type="radio" class="btn-check" name="place-map" id="place-map-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-map-menu">Main Menu</label>
</div>
</td>
</tr>
<tr data-placement-key="console">
<td class="ps-0">Console</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Console placement">
<input type="radio" class="btn-check" name="place-console" id="place-console-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-console-fab">Quick Access</label>
<input type="radio" class="btn-check" name="place-console" id="place-console-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-console-menu">Main Menu</label>
</div>
</td>
</tr>
<tr data-placement-key="deviceinfo">
<td class="ps-0">Device Info</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Device Info placement">
<input type="radio" class="btn-check" name="place-deviceinfo" id="place-deviceinfo-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-deviceinfo-fab">Quick Access</label>
<input type="radio" class="btn-check" name="place-deviceinfo" id="place-deviceinfo-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-deviceinfo-menu">Main Menu</label>
</div>
</td>
</tr>
<tr data-placement-key="syslog">
<td class="ps-0">System Log</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="System Log placement">
<input type="radio" class="btn-check" name="place-syslog" id="place-syslog-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-syslog-fab">Quick Access</label>
<input type="radio" class="btn-check" name="place-syslog" id="place-syslog-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-syslog-menu">Main Menu</label>
</div>
</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-borderless mb-3 align-middle">
<tbody>
<tr>
<td class="ps-0">Button size (px)</td>
<td class="pe-0 d-flex align-items-center gap-2" style="width:12rem">
<input type="range" class="form-range flex-grow-1" id="settFabSize" min="28" max="72" step="2" value="56">
<span class="text-muted small" id="settFabSizeVal" style="min-width:2.5rem;text-align:right">56</span>
</td>
</tr>
<tr>
<td class="ps-0">Spacing (px)</td>
<td class="pe-0 d-flex align-items-center gap-2">
<input type="range" class="form-range flex-grow-1" id="settFabGap" min="2" max="24" step="1" value="12">
<span class="text-muted small" id="settFabGapVal" style="min-width:2.5rem;text-align:right">12</span>
</td>
</tr>
<tr>
<td class="ps-0">Position</td>
<td class="pe-0">
<button type="button" class="btn btn-outline-secondary btn-sm" id="settFabResetPos">Reset to default</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="tabSettingsContacts">
<table class="table table-sm table-borderless mb-0 align-middle">
<tbody>
<tr>
<td class="ps-0">Manual approval enabled
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
title="When enabled, new contacts must be manually approved before they can communicate with your node"><i class="bi bi-info-circle"></i></span>
</td>
<td class="pe-0">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="settManualApproval">
</div>
</td>
</tr>
<tr>
<td class="ps-0" id="settSuppressAdvertNotifsLabel">Suppress new advert notifications
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
title="Hide the badge over Contact Management and the browser notification when new pending contacts arrive. The Pending Contacts list itself still shows them. Requires Manual approval ON."><i class="bi bi-info-circle"></i></span>
</td>
<td class="pe-0">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="settSuppressAdvertNotifs">
</div>
</td>
</tr>
<tr>
<td class="ps-0" id="settAutoIgnoreAdvertsLabel">Automatically add new contacts to "Ignored"
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
title="Every new advert is automatically marked as Ignored — no notifications, no badge. Contacts still appear under Existing Contacts (Cache) and can be promoted with 'To Device'. Requires Manual approval ON."><i class="bi bi-info-circle"></i></span>
</td>
<td class="pe-0">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="settAutoIgnoreAdverts">
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="tabSettingsRegions">
<div class="alert alert-info py-2 small mb-3 mt-2">
<i class="bi bi-info-circle"></i> Only repeaters allowing a region will forward messages tagged with it.
Find standardised region names at
<a href="https://regions.meshcore.nz/" target="_blank" rel="noopener">regions.meshcore.nz</a>.
</div>
<h6 class="mb-2">Region Registry</h6>
<div id="regionsList" class="list-group mb-3">
<div class="text-center text-muted py-3 small">
<div class="spinner-border spinner-border-sm"></div> Loading...
</div>
</div>
<form id="addRegionForm" class="row g-2 mb-0">
<div class="col-8">
<input type="text" class="form-control form-control-sm" id="newRegionName"
placeholder="Region name (e.g. pl, pl-ma)" required maxlength="30" autocomplete="off">
</div>
<div class="col-4">
<button type="submit" class="btn btn-primary btn-sm w-100">
<i class="bi bi-plus-circle"></i> Add
</button>
</div>
</form>
<div class="form-text small">
Tip: pick the default region via the radio button, or select <em>None</em> to fall back to the firmware default. The chosen region is also pushed to the firmware so any untagged channel uses it.
</div>
</div>
<div class="tab-pane fade" id="tabSettingsAnalyzer">
<div class="alert alert-info py-2 small mb-3 mt-2">
<i class="bi bi-info-circle"></i> Add MeshCore Analyzer services to choose from when clicking
<i class="bi bi-clipboard-data"></i> under a group chat message. The URL must contain the
<code>{packetHash}</code> placeholder &mdash; it is replaced with the message's packet hash.
</div>
<h6 class="mb-2">Analyzer Services</h6>
<div id="analyzersList" class="list-group mb-3">
<div class="text-center text-muted py-3 small">
<div class="spinner-border spinner-border-sm"></div> Loading...
</div>
</div>
<button type="button" class="btn btn-primary btn-sm" id="addAnalyzerBtn">
<i class="bi bi-plus-circle"></i> Add analyzer
</button>
<div class="form-text small mt-2">
Tip: star one analyzer to use it without being asked. Clear the star to be prompted on every click.
Disabled analyzers stay in the list but are hidden from the chooser.
</div>
</div>
<div class="tab-pane fade" id="tabSettingsNotifications">
<p class="text-muted small mb-3">Browser notifications appear when the app is hidden or in the background.</p>
<button id="notificationsToggle" class="list-group-item list-group-item-action d-flex align-items-center gap-3 border rounded" type="button" style="width: 100%;">
<i class="bi bi-bell" style="font-size: 1.5rem;"></i>
<div class="flex-grow-1 text-start">
<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>
</div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Analyzer Edit Modal (create + edit) -->
<div class="modal fade" id="analyzerEditModal" tabindex="-1" style="z-index: 1080;">
<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-clipboard-data"></i> <span id="analyzerEditModalTitle">Add analyzer</span></h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="analyzerEditForm">
<div class="modal-body">
<input type="hidden" id="analyzerEditId" value="">
<div class="mb-3">
<label class="form-label small mb-1" for="analyzerEditName">Name</label>
<input type="text" class="form-control form-control-sm" id="analyzerEditName"
placeholder="e.g. MarWoj Analyzer" required maxlength="60" autocomplete="off">
</div>
<div class="mb-3">
<label class="form-label small mb-1" for="analyzerEditUrl">URL template</label>
<input type="text" class="form-control form-control-sm" id="analyzerEditUrl"
placeholder="https://analyzer.example.com/#/packets/{packetHash}" required autocomplete="off">
<div class="form-text small">Must include <code>{packetHash}</code>.</div>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="analyzerEditDisabled">
<label class="form-check-label small" for="analyzerEditDisabled">Disabled</label>
</div>
<div id="analyzerEditError" class="alert alert-danger py-1 small mt-3 d-none"></div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary btn-sm">Save</button>
</div>
</form>
</div>
</div>
</div>
<!-- Analyzer Chooser Modal -->
<div class="modal fade" id="analyzerChooserModal" tabindex="-1" style="z-index: 1080;">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title"><i class="bi bi-clipboard-data"></i> Choose analyzer</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-2">
<div id="analyzerChooserList" class="list-group"></div>
</div>
</div>
</div>
</div>
<!-- Coordinate Picker Map Modal -->
<div class="modal fade" id="coordPickerModal" tabindex="-1" style="z-index: 1080;">
<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 Coordinates</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<div id="coordPickerMap" style="height: 400px; width: 100%;"></div>
</div>
<div class="modal-footer py-2">
<span class="me-auto small text-muted" id="coordPickerLabel">Click on the map to select coordinates</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="coordPickerConfirmBtn" disabled>Confirm</button>
</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>
<hr class="my-3">
<h6 class="mb-2"><i class="bi bi-arrows-collapse"></i> Optimize database</h6>
<p class="text-muted small mb-2">
Reclaim space freed by message retention. Runs SQLite <code>VACUUM</code>;
readers keep working but writes are paused for a few seconds.
</p>
<div class="d-flex justify-content-between align-items-center">
<button class="btn btn-outline-primary btn-sm" id="vacuumDbBtn" onclick="optimizeDatabase()">
<i class="bi bi-arrows-collapse"></i> Optimize now
</button>
<span id="vacuumDbStatus" class="text-muted small">Current size: …</span>
</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 (position classes 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">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>
<!-- FAB Utilities (drag, sizing — must load before app.js) -->
<script src="{{ url_for('static', filename='js/fab-utils.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);
});
// --- FAB appearance controls ---
var fabSizeSlider = document.getElementById('settFabSize');
var fabSizeVal = document.getElementById('settFabSizeVal');
var fabGapSlider = document.getElementById('settFabGap');
var fabGapVal = document.getElementById('settFabGapVal');
var fabResetPos = document.getElementById('settFabResetPos');
// Load saved values
var savedSize = localStorage.getItem('mc-webui-fab-size');
var savedGap = localStorage.getItem('mc-webui-fab-gap');
if (savedSize && fabSizeSlider) { fabSizeSlider.value = savedSize; fabSizeVal.textContent = savedSize; }
if (savedGap && fabGapSlider) { fabGapSlider.value = savedGap; fabGapVal.textContent = savedGap; }
// Live preview on slider change
if (fabSizeSlider) {
fabSizeSlider.addEventListener('input', function() {
var v = this.value;
fabSizeVal.textContent = v;
document.documentElement.style.setProperty('--fab-custom-size', v + 'px');
localStorage.setItem('mc-webui-fab-size', v);
});
}
if (fabGapSlider) {
fabGapSlider.addEventListener('input', function() {
var v = this.value;
fabGapVal.textContent = v;
document.documentElement.style.setProperty('--fab-custom-gap', v + 'px');
localStorage.setItem('mc-webui-fab-gap', v);
});
}
// Reset position button
if (fabResetPos) {
fabResetPos.addEventListener('click', function() {
localStorage.removeItem('mc-webui-fab-pos');
localStorage.removeItem('mc-webui-fab-pos-dm');
var container = document.getElementById('fabContainer');
if (container) {
container.style.left = '';
container.style.right = '16px';
container.style.top = '80px';
}
});
}
});
</script>
{% block extra_scripts %}{% endblock %}
</body>
</html>