mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-06-07 16:11:39 +00:00
ae52be4d2b
- Added a function to strip ANSI color codes from log lines for better display in SocketIO web clients, improving log readability. - Implemented dark mode styles for dropdown menus and other UI components to enhance user experience in dark theme. - Updated the contacts template to include a new overflow menu for additional actions, improving accessibility and usability. - Enhanced the login page with a more visually appealing layout and improved theme handling to prevent flash of unstyled content. - Refined log level toggles in the logs template for better user interaction and visibility of log levels. These changes improve the overall functionality and aesthetics of the web viewer.
702 lines
24 KiB
HTML
702 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}MeshCore Bot Data Viewer{% endblock %}</title>
|
|
|
|
<!-- Manifest served from Flask static folder -->
|
|
<link rel="manifest" href="{{ url_for('static', filename='ico/site.webmanifest') }}">
|
|
|
|
<!-- Apply theme immediately to prevent flash -->
|
|
<script>
|
|
(function() {
|
|
const storedTheme = localStorage.getItem('theme');
|
|
const systemPreference = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
const theme = storedTheme || systemPreference;
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
})();
|
|
</script>
|
|
|
|
<!-- Bootstrap 5.1.3 -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
|
|
<!-- Font Awesome for icons -->
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
|
|
|
<!-- Custom styles -->
|
|
<style>
|
|
/* CSS Variables for Light Mode (Default) */
|
|
:root {
|
|
--bg-color: #ffffff;
|
|
--bg-secondary: #f8f9fa;
|
|
--bg-tertiary: #e9ecef;
|
|
--text-color: #212529;
|
|
--text-muted: #6c757d;
|
|
--border-color: #dee2e6;
|
|
--card-bg: #ffffff;
|
|
--card-header-bg: #f8f9fa;
|
|
--footer-bg: #f8f9fa;
|
|
--log-entry-bg: #f8f9fa;
|
|
--connection-info-bg: #e9ecef;
|
|
--shadow-color: rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
/* CSS Variables for Dark Mode */
|
|
[data-theme="dark"] {
|
|
--bg-color: #1a1a1a;
|
|
--bg-secondary: #2d2d2d;
|
|
--bg-tertiary: #3d3d3d;
|
|
--text-color: #e9ecef;
|
|
--text-muted: #adb5bd;
|
|
--border-color: #495057;
|
|
--card-bg: #2d2d2d;
|
|
--card-header-bg: #3d3d3d;
|
|
--footer-bg: #2d2d2d;
|
|
--log-entry-bg: #2d2d2d;
|
|
--connection-info-bg: #3d3d3d;
|
|
--shadow-color: rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
/* Apply theme variables */
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--bg-color);
|
|
color: var(--text-color);
|
|
transition: background-color 0.3s ease, color 0.3s ease;
|
|
}
|
|
|
|
.navbar-brand {
|
|
font-weight: bold;
|
|
color: #007bff !important;
|
|
}
|
|
|
|
.status-indicator {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
margin-right: 8px;
|
|
}
|
|
.status-connected { background-color: #28a745; }
|
|
.status-disconnected { background-color: #dc3545; }
|
|
.status-warning { background-color: #ffc107; }
|
|
|
|
.log-entry {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 0.9em;
|
|
margin-bottom: 5px;
|
|
padding: 8px;
|
|
border-left: 3px solid #007bff;
|
|
background-color: var(--log-entry-bg);
|
|
border-radius: 0 5px 5px 0;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.command-entry {
|
|
border-left-color: #28a745;
|
|
}
|
|
|
|
.packet-entry {
|
|
border-left-color: #ffc107;
|
|
}
|
|
|
|
.connection-info {
|
|
background-color: var(--connection-info-bg);
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.card {
|
|
box-shadow: 0 2px 4px var(--shadow-color);
|
|
border: none;
|
|
background-color: var(--card-bg);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.card-header {
|
|
background-color: var(--card-header-bg);
|
|
border-bottom: 1px solid var(--border-color);
|
|
font-weight: 600;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.card-body {
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.btn-group .btn {
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.table-responsive {
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Dark mode table styles */
|
|
[data-theme="dark"] .table {
|
|
color: var(--text-color);
|
|
}
|
|
|
|
[data-theme="dark"] .table-striped > tbody > tr:nth-of-type(odd) > td {
|
|
background-color: var(--bg-tertiary);
|
|
}
|
|
|
|
[data-theme="dark"] .table-hover > tbody > tr:hover > td {
|
|
background-color: var(--bg-tertiary);
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.8em;
|
|
}
|
|
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 20px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.error {
|
|
color: #dc3545;
|
|
background-color: #f8d7da;
|
|
border: 1px solid #f5c6cb;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
[data-theme="dark"] .error {
|
|
color: #f5c6cb;
|
|
background-color: #721c24;
|
|
border-color: #842029;
|
|
}
|
|
|
|
.success {
|
|
color: #155724;
|
|
background-color: #d4edda;
|
|
border: 1px solid #c3e6cb;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
[data-theme="dark"] .success {
|
|
color: #d1e7dd;
|
|
background-color: #0f5132;
|
|
border-color: #146c43;
|
|
}
|
|
|
|
/* Dark mode toggle button */
|
|
.dark-mode-toggle {
|
|
background: none;
|
|
border: none;
|
|
color: rgba(255, 255, 255, 0.85);
|
|
font-size: 1.2rem;
|
|
padding: 0.5rem 0.75rem;
|
|
cursor: pointer;
|
|
border-radius: 0.375rem;
|
|
transition: background-color 0.2s ease, color 0.2s ease;
|
|
}
|
|
|
|
.dark-mode-toggle:hover {
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
color: #ffffff;
|
|
}
|
|
|
|
.dark-mode-toggle:focus {
|
|
outline: none;
|
|
box-shadow: 0 0 0 0.2rem rgba(255, 255, 255, 0.25);
|
|
}
|
|
|
|
/* Dark mode form controls */
|
|
[data-theme="dark"] .form-control,
|
|
[data-theme="dark"] .form-select {
|
|
background-color: var(--bg-tertiary);
|
|
border-color: var(--border-color);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
[data-theme="dark"] .form-control:focus,
|
|
[data-theme="dark"] .form-select:focus {
|
|
background-color: var(--bg-tertiary);
|
|
border-color: #007bff;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
/* Dark mode text muted */
|
|
[data-theme="dark"] .text-muted {
|
|
color: var(--text-muted) !important;
|
|
}
|
|
|
|
/* Dark mode borders */
|
|
[data-theme="dark"] .border {
|
|
border-color: var(--border-color) !important;
|
|
}
|
|
|
|
/* Dark mode alerts */
|
|
[data-theme="dark"] .alert-info {
|
|
background-color: #084298;
|
|
border-color: #0a58ca;
|
|
color: #cfe2ff;
|
|
}
|
|
|
|
[data-theme="dark"] .alert-warning {
|
|
background-color: #664d03;
|
|
border-color: #997404;
|
|
color: #fffbeb;
|
|
}
|
|
|
|
[data-theme="dark"] .alert-danger {
|
|
background-color: #842029;
|
|
border-color: #b02a37;
|
|
color: #f8d7da;
|
|
}
|
|
|
|
[data-theme="dark"] .alert-success {
|
|
background-color: #0f5132;
|
|
border-color: #146c43;
|
|
color: #d1e7dd;
|
|
}
|
|
|
|
/* Dark mode modal styling */
|
|
[data-theme="dark"] .modal-content {
|
|
background-color: var(--card-bg);
|
|
color: var(--text-color);
|
|
border-color: var(--border-color);
|
|
}
|
|
|
|
[data-theme="dark"] .modal-header {
|
|
background-color: var(--card-header-bg);
|
|
border-bottom-color: var(--border-color);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
[data-theme="dark"] .modal-title {
|
|
color: var(--text-color);
|
|
}
|
|
|
|
[data-theme="dark"] .modal-body {
|
|
background-color: var(--card-bg);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
[data-theme="dark"] .modal-footer {
|
|
background-color: var(--card-bg);
|
|
border-top-color: var(--border-color);
|
|
}
|
|
|
|
[data-theme="dark"] .modal-backdrop {
|
|
background-color: rgba(0, 0, 0, 0.7);
|
|
}
|
|
|
|
/* Dark mode code/pre elements in modals */
|
|
[data-theme="dark"] .modal pre,
|
|
[data-theme="dark"] .modal code {
|
|
background-color: var(--bg-tertiary);
|
|
color: var(--text-color);
|
|
border-color: var(--border-color);
|
|
}
|
|
|
|
[data-theme="dark"] .modal .bg-light {
|
|
background-color: var(--bg-tertiary) !important;
|
|
color: var(--text-color) !important;
|
|
}
|
|
|
|
/* Dark mode dropdown menus */
|
|
[data-theme="dark"] .dropdown-menu {
|
|
background-color: var(--card-bg);
|
|
border-color: var(--border-color);
|
|
box-shadow: 0 0.5rem 1rem var(--shadow-color);
|
|
}
|
|
|
|
[data-theme="dark"] .dropdown-item {
|
|
color: var(--text-color);
|
|
}
|
|
|
|
[data-theme="dark"] .dropdown-item:hover,
|
|
[data-theme="dark"] .dropdown-item:focus {
|
|
background-color: var(--bg-tertiary);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
[data-theme="dark"] .dropdown-item.text-danger {
|
|
color: #f5c2c7;
|
|
}
|
|
|
|
[data-theme="dark"] .dropdown-item.text-danger:hover,
|
|
[data-theme="dark"] .dropdown-item.text-danger:focus {
|
|
color: #f8d7da;
|
|
}
|
|
|
|
[data-theme="dark"] .dropdown-header {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
[data-theme="dark"] .dropdown-divider {
|
|
border-top-color: var(--border-color);
|
|
}
|
|
|
|
/* Dark mode accordion styling */
|
|
[data-theme="dark"] .accordion-item {
|
|
background-color: var(--card-bg);
|
|
border-color: var(--border-color);
|
|
}
|
|
|
|
[data-theme="dark"] .accordion-button {
|
|
background-color: var(--card-header-bg);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
[data-theme="dark"] .accordion-button:not(.collapsed) {
|
|
background-color: var(--bg-tertiary);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
[data-theme="dark"] .accordion-button:focus {
|
|
box-shadow: 0 0 0 0.25rem rgba(0, 123, 255, 0.25);
|
|
}
|
|
|
|
[data-theme="dark"] .accordion-body {
|
|
background-color: var(--card-bg);
|
|
color: var(--text-color);
|
|
}
|
|
</style>
|
|
|
|
{% block extra_css %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
<!-- Navigation -->
|
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
<div class="container-fluid">
|
|
<a class="navbar-brand" href="/">
|
|
<i class="fas fa-satellite-dish"></i> {{ bot_name|default('MeshCore Bot') }}
|
|
</a>
|
|
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
|
|
<div class="collapse navbar-collapse" id="navbarNav">
|
|
<ul class="navbar-nav me-auto">
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/">
|
|
<i class="fas fa-tachometer-alt"></i> Dashboard
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/realtime">
|
|
<i class="fas fa-broadcast-tower"></i> Real-time
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/contacts">
|
|
<i class="fas fa-users"></i> Contacts
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/mesh">
|
|
<i class="fas fa-project-diagram"></i> Mesh Graph
|
|
</a>
|
|
</li>
|
|
{% if greeter_enabled %}
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/greeter">
|
|
<i class="fas fa-hand-sparkles"></i> Greeter
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
{% if feed_manager_enabled %}
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/feeds">
|
|
<i class="fas fa-rss"></i> Feeds
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/radio">
|
|
<i class="fas fa-broadcast-tower"></i> Radio
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/config">
|
|
<i class="fas fa-cog"></i> Config
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/logs">
|
|
<i class="fas fa-file-alt"></i> Logs
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Dark Mode Toggle and Connection Status -->
|
|
<div class="navbar-nav d-flex align-items-center">
|
|
<div class="nav-item me-3">
|
|
<button class="dark-mode-toggle" id="dark-mode-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">
|
|
<i class="fas fa-moon" id="dark-mode-icon"></i>
|
|
</button>
|
|
</div>
|
|
<div class="nav-item">
|
|
<span class="navbar-text">
|
|
<span class="status-indicator" id="connection-status"></span>
|
|
<span id="connection-text">Disconnected</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main Content -->
|
|
<div class="container-fluid mt-4">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
|
|
<!-- Footer links (bottom of page, visible when scrolled down) -->
|
|
<footer class="text-center py-2" style="font-size: 0.7rem;">
|
|
<a href="https://github.com/agessaman/meshcore-bot" target="_blank" rel="noopener noreferrer" style="color: var(--text-muted, #6c757d); text-decoration: none;">meshcore-bot</a>
|
|
{% if version_info.tag %}
|
|
<span class="mx-1" style="color: var(--text-muted, #6c757d);"> {{ version_info.tag }}</span>
|
|
{% elif version_info.branch or version_info.commit or version_info.date %}
|
|
<span class="mx-1" style="color: var(--text-muted, #6c757d);">
|
|
{% if version_info.branch %}{{ version_info.branch }}{% endif %}
|
|
{% if version_info.commit %}{% if version_info.branch %} · {% endif %}<code class="text-muted" style="font-size: inherit;">{{ version_info.commit }}</code>{% endif %}
|
|
{% if version_info.date %}{% if version_info.branch or version_info.commit %} · {% endif %}{{ version_info.date }}{% endif %}
|
|
</span>
|
|
{% endif %}
|
|
<span class="mx-1" style="color: var(--text-muted, #6c757d);"> for </span>
|
|
<a href="https://meshcore.co.uk/index.html" target="_blank" rel="noopener noreferrer" style="color: var(--text-muted, #6c757d); text-decoration: none;">MeshCore</a>
|
|
</footer>
|
|
|
|
<!-- Bootstrap JS -->
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
|
|
<!-- Socket.IO Client -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script>
|
|
|
|
<!-- Moment.js for date formatting -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
|
|
|
|
<!-- Chart.js for graphs -->
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
|
|
<!-- SHA-256 for channel key derivation (works in non-HTTPS contexts) -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-sha256/0.11.0/sha256.min.js"></script>
|
|
|
|
<!-- Base JavaScript -->
|
|
<script>
|
|
// Global connection management
|
|
class ModernConnectionManager {
|
|
constructor() {
|
|
this.socket = null;
|
|
this.connected = false;
|
|
this.lastActivity = null;
|
|
this.pingInterval = null;
|
|
|
|
this.initializeSocket();
|
|
this.startPingInterval();
|
|
}
|
|
|
|
initializeSocket() {
|
|
// Use io() without forceNew so that realtime.html's io() shares
|
|
// this same manager/socket. forceNew created a second independent
|
|
// connection that interfered with the realtime-page socket.
|
|
this.socket = io({
|
|
transports: ['websocket', 'polling'],
|
|
timeout: 5000
|
|
});
|
|
|
|
this.setupSocketEvents();
|
|
}
|
|
|
|
setupSocketEvents() {
|
|
this.socket.on('connect', () => {
|
|
console.log('Connected to server');
|
|
this.connected = true;
|
|
this.updateConnectionStatus('Connected', 'connected');
|
|
this.lastActivity = new Date();
|
|
this.updateLastActivity();
|
|
});
|
|
|
|
this.socket.on('disconnect', (reason) => {
|
|
console.log('Disconnected from server:', reason);
|
|
this.connected = false;
|
|
this.updateConnectionStatus('Disconnected', 'disconnected');
|
|
});
|
|
|
|
this.socket.on('status', (data) => {
|
|
console.log('Server status:', data.message);
|
|
this.showNotification(data.message, 'info');
|
|
});
|
|
|
|
this.socket.on('error', (data) => {
|
|
console.error('Server error:', data.message);
|
|
this.showNotification(`Error: ${data.message}`, 'danger');
|
|
});
|
|
|
|
// Modern ping/pong pattern
|
|
this.socket.on('pong', () => {
|
|
console.log('Pong received from server');
|
|
this.lastActivity = new Date();
|
|
this.updateLastActivity();
|
|
});
|
|
}
|
|
|
|
startPingInterval() {
|
|
this.pingInterval = setInterval(() => {
|
|
if (this.socket && this.socket.connected) {
|
|
this.socket.emit('ping');
|
|
}
|
|
}, 20000);
|
|
}
|
|
|
|
stopPingInterval() {
|
|
if (this.pingInterval) {
|
|
clearInterval(this.pingInterval);
|
|
this.pingInterval = null;
|
|
}
|
|
}
|
|
|
|
updateConnectionStatus(text, status) {
|
|
const statusElement = document.getElementById('connection-text');
|
|
const indicatorElement = document.getElementById('connection-status');
|
|
|
|
if (statusElement) {
|
|
statusElement.textContent = text;
|
|
}
|
|
if (indicatorElement) {
|
|
indicatorElement.className = `status-indicator status-${status}`;
|
|
}
|
|
}
|
|
|
|
updateLastActivity() {
|
|
if (this.lastActivity) {
|
|
const lastUpdateElement = document.getElementById('last-update');
|
|
if (lastUpdateElement) {
|
|
lastUpdateElement.textContent = moment(this.lastActivity).format('YYYY-MM-DD HH:mm:ss');
|
|
}
|
|
}
|
|
}
|
|
|
|
showNotification(message, type = 'info') {
|
|
// Create notification element
|
|
const notification = document.createElement('div');
|
|
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
|
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; max-width: 300px;';
|
|
|
|
notification.innerHTML = `
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
// Auto-remove after 3 seconds
|
|
setTimeout(() => {
|
|
if (notification.parentNode) {
|
|
notification.parentNode.removeChild(notification);
|
|
}
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
// Update timestamp function
|
|
function updateTimestamp() {
|
|
const now = new Date();
|
|
const timestamp = now.toISOString().replace('T', ' ').substring(0, 19);
|
|
const lastUpdateElement = document.getElementById('last-update');
|
|
if (lastUpdateElement) {
|
|
lastUpdateElement.textContent = timestamp;
|
|
}
|
|
}
|
|
|
|
// Dark Mode Management
|
|
class DarkModeManager {
|
|
constructor() {
|
|
this.theme = this.getStoredTheme() || this.getSystemPreference();
|
|
this.applyTheme();
|
|
this.setupToggle();
|
|
}
|
|
|
|
getStoredTheme() {
|
|
return localStorage.getItem('theme');
|
|
}
|
|
|
|
getSystemPreference() {
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
}
|
|
|
|
setTheme(theme) {
|
|
this.theme = theme;
|
|
localStorage.setItem('theme', theme);
|
|
this.applyTheme();
|
|
}
|
|
|
|
applyTheme() {
|
|
const root = document.documentElement;
|
|
if (this.theme === 'dark') {
|
|
root.setAttribute('data-theme', 'dark');
|
|
const icon = document.getElementById('dark-mode-icon');
|
|
if (icon) {
|
|
icon.classList.remove('fa-moon');
|
|
icon.classList.add('fa-sun');
|
|
}
|
|
} else {
|
|
root.setAttribute('data-theme', 'light');
|
|
const icon = document.getElementById('dark-mode-icon');
|
|
if (icon) {
|
|
icon.classList.remove('fa-sun');
|
|
icon.classList.add('fa-moon');
|
|
}
|
|
}
|
|
}
|
|
|
|
toggleTheme() {
|
|
const newTheme = this.theme === 'dark' ? 'light' : 'dark';
|
|
this.setTheme(newTheme);
|
|
}
|
|
|
|
setupToggle() {
|
|
const toggle = document.getElementById('dark-mode-toggle');
|
|
if (toggle) {
|
|
toggle.addEventListener('click', () => {
|
|
this.toggleTheme();
|
|
});
|
|
}
|
|
|
|
// Listen for system theme changes
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
// Only apply system preference if user hasn't set a preference
|
|
if (!this.getStoredTheme()) {
|
|
this.setTheme(e.matches ? 'dark' : 'light');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize connection manager when page loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Initialize dark mode manager first
|
|
window.darkModeManager = new DarkModeManager();
|
|
|
|
window.connectionManager = new ModernConnectionManager();
|
|
|
|
// Update timestamp immediately and every second
|
|
updateTimestamp();
|
|
setInterval(updateTimestamp, 1000);
|
|
});
|
|
</script>
|
|
|
|
<script src="{{ url_for('static', filename='js/channel_operations.js') }}"></script>
|
|
{% block extra_js %}{% endblock %}
|
|
</body>
|
|
</html>
|