1
0
forked from iarv/mc-webui
Files
mc-webui/app/templates/base.html
MarekWo 7ca3f4d2dd feat: Add dynamic Git-based versioning system
- Add app/version.py module generating version from Git metadata
- Format: YYYY.MM.DD+<commit_hash> (e.g., 2025.01.18+576c8ca9)
- Add +dirty suffix for uncommitted changes (ignores .env, technotes/)
- Add /api/version endpoint for monitoring
- Display version in hamburger menu
- Add freeze mechanism for Docker builds

Deploy command updated:
git push && ssh ... "cd ~/mc-webui && git pull && python -m app.version freeze && docker compose up -d --build"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 11:47:22 +01:00

385 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<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') }}">
<!-- 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') }}">
{% 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 btn-sm position-relative" style="cursor: default;" title="Unread messages">
<i class="bi bi-bell"></i>
</div>
<select id="channelSelector" class="form-select form-select-sm" 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 btn-sm" 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">
<i class="bi bi-tag"></i> {{ version }}
</div>
<div class="offcanvas-body">
<div class="list-group list-group-flush">
<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>
<!-- Notifications Toggle -->
<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 Commands Section -->
<div class="list-group-item py-2 mt-2">
<small class="text-muted fw-bold text-uppercase">Network Commands</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>
<div class="list-group-item py-2 mt-2">
<small class="text-muted fw-bold text-uppercase">Configuration</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="#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>
<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="#settingsModal" data-bs-dismiss="offcanvas">
<i class="bi bi-gear" style="font-size: 1.5rem;"></i>
<span>Settings</span>
</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>
<!-- 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">
<h6>Device Information</h6>
<div id="deviceInfo" class="small text-muted">
Loading...
</div>
</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-3 align-items-center">
<span class="small text-muted me-1">Show:</span>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterCLI" value="1" checked>
<label class="form-check-label small" for="mapFilterCLI">
<span class="badge" style="background-color: #2196F3;">CLI</span>
</label>
</div>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterREP" value="2" checked>
<label class="form-check-label small" for="mapFilterREP">
<span class="badge" style="background-color: #4CAF50;">REP</span>
</label>
</div>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterROOM" value="3" checked>
<label class="form-check-label small" for="mapFilterROOM">
<span class="badge" style="background-color: #9C27B0;">ROOM</span>
</label>
</div>
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" id="mapFilterSENS" value="4" checked>
<label class="form-check-label small" for="mapFilterSENS">
<span class="badge" style="background-color: #FF9800;">SENS</span>
</label>
</div>
</div>
</div>
<div id="leafletMap" style="height: 400px; width: 100%;"></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>
<!-- Custom JS -->
<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>
{% block extra_scripts %}{% endblock %}
</body>
</html>