mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-29 07:08:45 +00:00
Enhance logging and UI elements in web viewer
- 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.
This commit is contained in:
@@ -8,6 +8,7 @@ import configparser
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -68,6 +69,15 @@ def _apply_werkzeug_websocket_fix() -> None:
|
||||
|
||||
_apply_werkzeug_websocket_fix()
|
||||
|
||||
# colorlog and other handlers write ANSI SGR sequences; strip for web /logs display
|
||||
_ANSI_ESCAPE_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||
|
||||
|
||||
def _strip_ansi_codes(text: str) -> str:
|
||||
"""Remove ANSI color and reset codes from log lines for SocketIO web clients."""
|
||||
return _ANSI_ESCAPE_RE.sub("", text)
|
||||
|
||||
|
||||
# Add the project root to the path so we can import bot components
|
||||
project_root = os.path.join(os.path.dirname(__file__), '..', '..')
|
||||
sys.path.insert(0, project_root)
|
||||
@@ -3638,7 +3648,7 @@ class BotDataViewer:
|
||||
with open(log_file, encoding='utf-8', errors='replace') as _fh:
|
||||
recent_lines = _fh.readlines()[-200:]
|
||||
for line in recent_lines:
|
||||
emit('log_line', {'line': line.rstrip()})
|
||||
emit('log_line', {'line': _strip_ansi_codes(line.rstrip())})
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error reading log history: {e}")
|
||||
except Exception as e:
|
||||
@@ -3753,7 +3763,9 @@ class BotDataViewer:
|
||||
if info.get('subscribed_logs', False)
|
||||
]
|
||||
if subscribed:
|
||||
self.socketio.emit('log_line', {'line': line.rstrip()}, room=None)
|
||||
self.socketio.emit(
|
||||
'log_line', {'line': _strip_ansi_codes(line.rstrip())}, room=None
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error broadcasting log line: {e}")
|
||||
|
||||
|
||||
@@ -310,6 +310,40 @@
|
||||
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);
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
{% block title %}Contacts - MeshCore Bot Data Viewer{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Square hit target for the overflow (⋯) menu trigger */
|
||||
.contacts-overflow-btn {
|
||||
width: 2rem;
|
||||
height: 2.375rem;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@@ -135,21 +150,26 @@
|
||||
<button class="btn btn-sm btn-primary" id="refresh-contacts">
|
||||
<i class="fas fa-sync"></i> Refresh
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" id="purge-contacts-btn"
|
||||
data-bs-toggle="modal" data-bs-target="#purgeContactsModal"
|
||||
title="Delete contacts not heard recently">
|
||||
<i class="fas fa-broom"></i> Purge Inactive
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" title="Export data">
|
||||
<i class="fas fa-download me-1"></i>Export
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary contacts-overflow-btn"
|
||||
data-bs-toggle="dropdown" aria-expanded="false"
|
||||
aria-label="More actions" title="More actions">
|
||||
<i class="fas fa-ellipsis-vertical" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><h6 class="dropdown-header">Contacts</h6></li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item text-danger"
|
||||
data-bs-toggle="modal" data-bs-target="#purgeContactsModal"
|
||||
title="Delete contacts not heard recently">
|
||||
<i class="fas fa-broom me-2"></i>Purge Inactive
|
||||
</button>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">Export Contacts</h6></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportData('contacts','csv');return false;"><i class="fas fa-file-csv me-2 text-success"></i>CSV</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportData('contacts','json');return false;"><i class="fas fa-file-code me-2 text-warning"></i>JSON</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">Paths</h6></li>
|
||||
<li><h6 class="dropdown-header">Export Paths</h6></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportData('paths','csv');return false;"><i class="fas fa-file-csv me-2 text-success"></i>CSV</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportData('paths','json');return false;"><i class="fas fa-file-code me-2 text-warning"></i>JSON</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
@@ -4,28 +4,203 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login — MeshCore Bot Data Viewer</title>
|
||||
|
||||
<!-- Apply theme immediately to prevent flash (same key as main app: localStorage 'theme') -->
|
||||
<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>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=Outfit:wght@600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background: #f8f9fa; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
|
||||
.login-card { width: 100%; max-width: 360px; }
|
||||
: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;
|
||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||
--brand: #007bff;
|
||||
--brand-soft: rgba(0, 123, 255, 0.14);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg-color: #141414;
|
||||
--bg-secondary: #2d2d2d;
|
||||
--bg-tertiary: #3d3d3d;
|
||||
--text-color: #e9ecef;
|
||||
--text-muted: #adb5bd;
|
||||
--border-color: #495057;
|
||||
--card-bg: #252525;
|
||||
--card-header-bg: #3d3d3d;
|
||||
--shadow-color: rgba(0, 0, 0, 0.45);
|
||||
--brand-soft: rgba(0, 123, 255, 0.18);
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 1.25rem;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Soft mesh-style atmosphere */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background:
|
||||
radial-gradient(ellipse 90% 55% at 50% -15%, var(--brand-soft), transparent 55%),
|
||||
radial-gradient(ellipse 70% 45% at 100% 100%, rgba(0, 123, 255, 0.06), transparent 50%),
|
||||
radial-gradient(ellipse 50% 35% at 0% 85%, rgba(0, 123, 255, 0.05), transparent 45%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-card .card {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background-color: var(--card-bg);
|
||||
color: var(--text-color);
|
||||
box-shadow:
|
||||
0 1px 2px var(--shadow-color),
|
||||
0 12px 40px -12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .login-card .card {
|
||||
box-shadow:
|
||||
0 1px 0 rgba(255, 255, 255, 0.04) inset,
|
||||
0 16px 48px -16px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.login-card .card-body {
|
||||
padding: 2rem 1.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 400px) {
|
||||
.login-card .card-body {
|
||||
padding: 2.25rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.login-brand-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(145deg, var(--brand-soft), transparent);
|
||||
color: var(--brand);
|
||||
font-size: 1.35rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-family: 'Outfit', 'DM Sans', sans-serif;
|
||||
font-size: clamp(1.65rem, 5vw, 2.05rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.15;
|
||||
color: var(--brand);
|
||||
margin: 0 0 0.35rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-family: 'Outfit', 'DM Sans', sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
margin: 0 0 0.5rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1.35rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .form-control {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .form-control:focus {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: #007bff;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .form-label {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .alert-danger {
|
||||
background-color: #842029;
|
||||
border-color: #b02a37;
|
||||
color: #f8d7da;
|
||||
}
|
||||
|
||||
.login-submit {
|
||||
font-weight: 600;
|
||||
padding-top: 0.55rem;
|
||||
padding-bottom: 0.55rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header text-center fw-bold">MeshCore Bot Data Viewer</div>
|
||||
<div class="card-body p-4">
|
||||
<h5 class="card-title text-center mb-3">Sign in</h5>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="text-center">
|
||||
<div class="login-brand-icon" aria-hidden="true">
|
||||
<i class="fas fa-broadcast-tower"></i>
|
||||
</div>
|
||||
<h1 class="login-title">MeshCore Bot</h1>
|
||||
<p class="login-subtitle">Data Viewer</p>
|
||||
<p class="login-hint mb-0">Enter your password to continue.</p>
|
||||
</div>
|
||||
{% if error %}
|
||||
<div class="alert alert-danger py-2">{{ error }}</div>
|
||||
<div class="alert alert-danger py-2 mt-3 mb-3">{{ error }}</div>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
<form method="post" class="{% if not error %}mt-3{% endif %}">
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<label for="password" class="form-label fw-medium">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
autofocus autocomplete="current-password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Sign in</button>
|
||||
<button type="submit" class="btn btn-primary w-100 login-submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,13 +37,13 @@
|
||||
}
|
||||
|
||||
/* Level-based coloring */
|
||||
.log-line.level-debug { color: #6e7681; }
|
||||
.log-line.level-debug { color: #2dd4bf; }
|
||||
.log-line.level-info { color: #58a6ff; }
|
||||
.log-line.level-warning { color: #d29922; }
|
||||
.log-line.level-error { color: #f85149; }
|
||||
.log-line.level-critical{ color: #ff79c6; font-weight: bold; }
|
||||
|
||||
[data-theme="light"] .log-line.level-debug { color: #6c757d; }
|
||||
[data-theme="light"] .log-line.level-debug { color: #0d9488; }
|
||||
[data-theme="light"] .log-line.level-info { color: #0d6efd; }
|
||||
[data-theme="light"] .log-line.level-warning { color: #fd7e14; }
|
||||
[data-theme="light"] .log-line.level-error { color: #dc3545; }
|
||||
@@ -61,9 +61,51 @@
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
max-width: 140px;
|
||||
.log-level-toggles {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.log-level-toggle {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 0.2rem 0.5rem;
|
||||
line-height: 1.2;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease, transform 0.1s ease;
|
||||
}
|
||||
|
||||
.log-level-toggle:focus {
|
||||
outline: 2px solid #58a6ff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-theme="light"] .log-level-toggle:focus {
|
||||
outline-color: #0d6efd;
|
||||
}
|
||||
|
||||
.log-level-toggle.is-off {
|
||||
background: transparent !important;
|
||||
color: var(--text-muted) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
border-style: dashed;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* Dark theme — on (matches log line colors) */
|
||||
.log-level-toggle[data-level="DEBUG"].is-on { background: #2dd4bf; color: #0d1117; border-color: #2dd4bf; }
|
||||
.log-level-toggle[data-level="INFO"].is-on { background: #58a6ff; color: #0d1117; border-color: #58a6ff; }
|
||||
.log-level-toggle[data-level="WARNING"].is-on { background: #d29922; color: #0d1117; border-color: #d29922; }
|
||||
.log-level-toggle[data-level="ERROR"].is-on { background: #f85149; color: #fff; border-color: #f85149; }
|
||||
.log-level-toggle[data-level="CRITICAL"].is-on { background: #ff79c6; color: #0d1117; border-color: #ff79c6; }
|
||||
|
||||
/* Light theme — on */
|
||||
[data-theme="light"] .log-level-toggle[data-level="DEBUG"].is-on { background: #0d9488; color: #fff; border-color: #0d9488; }
|
||||
[data-theme="light"] .log-level-toggle[data-level="INFO"].is-on { background: #0d6efd; color: #fff; border-color: #0d6efd; }
|
||||
[data-theme="light"] .log-level-toggle[data-level="WARNING"].is-on { background: #fd7e14; color: #212529; border-color: #fd7e14; }
|
||||
[data-theme="light"] .log-level-toggle[data-level="ERROR"].is-on { background: #dc3545; color: #fff; border-color: #dc3545; }
|
||||
[data-theme="light"] .log-level-toggle[data-level="CRITICAL"].is-on { background: #6610f2; color: #fff; border-color: #6610f2; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -87,14 +129,14 @@
|
||||
</div>
|
||||
<div class="log-controls">
|
||||
<span id="line-count">0 lines</span>
|
||||
<select class="form-select form-select-sm filter-select" id="level-filter" onchange="applyFilter()">
|
||||
<option value="">All levels</option>
|
||||
<option value="DEBUG">DEBUG+</option>
|
||||
<option value="INFO">INFO+</option>
|
||||
<option value="WARNING">WARNING+</option>
|
||||
<option value="ERROR">ERROR+</option>
|
||||
<option value="CRITICAL">CRITICAL</option>
|
||||
</select>
|
||||
<div class="log-level-toggles d-flex flex-wrap align-items-center" id="log-level-toggles" role="group" aria-label="Log level filters">
|
||||
<span class="text-muted small d-none d-md-inline me-1">Levels</span>
|
||||
<button type="button" class="log-level-toggle is-on" data-level="DEBUG" aria-pressed="true" title="Toggle DEBUG lines">DEBUG</button>
|
||||
<button type="button" class="log-level-toggle is-on" data-level="INFO" aria-pressed="true" title="Toggle INFO lines">INFO</button>
|
||||
<button type="button" class="log-level-toggle is-on" data-level="WARNING" aria-pressed="true" title="Toggle WARNING lines">WARNING</button>
|
||||
<button type="button" class="log-level-toggle is-on" data-level="ERROR" aria-pressed="true" title="Toggle ERROR lines">ERROR</button>
|
||||
<button type="button" class="log-level-toggle is-on" data-level="CRITICAL" aria-pressed="true" title="Toggle CRITICAL lines">CRITICAL</button>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="pause-btn" onclick="togglePause()">
|
||||
<i class="fas fa-pause" id="pause-icon"></i> Pause
|
||||
</button>
|
||||
@@ -120,14 +162,69 @@
|
||||
<script>
|
||||
(function () {
|
||||
const MAX_LINES = 2000;
|
||||
const LEVEL_ORDER = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3, CRITICAL: 4 };
|
||||
const LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
|
||||
const STORAGE_KEY = 'meshcore_logs_level_toggles';
|
||||
|
||||
let paused = false;
|
||||
let autoScroll = true;
|
||||
let lineCount = 0;
|
||||
let filterLevel = '';
|
||||
const container = document.getElementById('log-container');
|
||||
const placeholder = document.getElementById('log-placeholder');
|
||||
const levelToggleGroup = document.getElementById('log-level-toggles');
|
||||
|
||||
function defaultEnabledLevels() {
|
||||
return { DEBUG: true, INFO: true, WARNING: true, ERROR: true, CRITICAL: true };
|
||||
}
|
||||
|
||||
function loadLevelToggles() {
|
||||
const d = defaultEnabledLevels();
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return d;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (typeof parsed !== 'object' || parsed === null) return d;
|
||||
for (const k of LEVELS) {
|
||||
if (typeof parsed[k] === 'boolean') d[k] = parsed[k];
|
||||
}
|
||||
return d;
|
||||
} catch (e) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
let enabledLevels = loadLevelToggles();
|
||||
|
||||
function saveLevelToggles() {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(enabledLevels));
|
||||
} catch (e) {
|
||||
/* ignore quota / private mode */
|
||||
}
|
||||
}
|
||||
|
||||
function syncToggleButtons() {
|
||||
document.querySelectorAll('.log-level-toggle').forEach(function (btn) {
|
||||
const lvl = btn.getAttribute('data-level');
|
||||
const on = enabledLevels[lvl];
|
||||
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||||
btn.classList.toggle('is-on', on);
|
||||
btn.classList.toggle('is-off', !on);
|
||||
});
|
||||
}
|
||||
|
||||
syncToggleButtons();
|
||||
|
||||
if (levelToggleGroup) {
|
||||
levelToggleGroup.addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('.log-level-toggle');
|
||||
if (!btn) return;
|
||||
const lvl = btn.getAttribute('data-level');
|
||||
if (!lvl || !Object.prototype.hasOwnProperty.call(enabledLevels, lvl)) return;
|
||||
enabledLevels[lvl] = !enabledLevels[lvl];
|
||||
syncToggleButtons();
|
||||
saveLevelToggles();
|
||||
});
|
||||
}
|
||||
|
||||
const socket = io({ transports: ['websocket', 'polling'] });
|
||||
|
||||
@@ -146,18 +243,15 @@
|
||||
});
|
||||
|
||||
function detectLevel(line) {
|
||||
const upper = line.toUpperCase();
|
||||
for (const lvl of ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']) {
|
||||
if (upper.includes(lvl)) return lvl;
|
||||
}
|
||||
return '';
|
||||
/* Word boundaries: avoid false INFO from keys like routing_info (substring INFO). */
|
||||
const m = line.match(/\b(CRITICAL|ERROR|WARNING|INFO|DEBUG)\b/i);
|
||||
return m ? m[1].toUpperCase() : '';
|
||||
}
|
||||
|
||||
function levelPasses(line) {
|
||||
if (!filterLevel) return true;
|
||||
const lineLevel = detectLevel(line);
|
||||
if (!lineLevel) return true;
|
||||
return (LEVEL_ORDER[lineLevel] ?? 0) >= (LEVEL_ORDER[filterLevel] ?? 0);
|
||||
return enabledLevels[lineLevel] === true;
|
||||
}
|
||||
|
||||
function addLogLine(text) {
|
||||
@@ -211,10 +305,6 @@
|
||||
autoScroll = true;
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
filterLevel = document.getElementById('level-filter').value;
|
||||
}
|
||||
|
||||
function updateStatus(id, text, cls) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
@@ -232,7 +322,6 @@
|
||||
window.togglePause = togglePause;
|
||||
window.clearLog = clearLog;
|
||||
window.scrollToBottom = scrollToBottom;
|
||||
window.applyFilter = applyFilter;
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -24,6 +24,49 @@
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* Dark mode: scrollbars for Command / Channel Messages / Packet streams (Firefox + WebKit) */
|
||||
[data-theme="dark"] .stream-container {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.28) var(--bg-tertiary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .stream-container::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .stream-container::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .stream-container::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
border-radius: 0.25rem;
|
||||
border: 2px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .stream-container::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
/* Packet stream: no horizontal scroll; long paths wrap within the card */
|
||||
#packet-stream {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#packet-stream .stream-entry > .d-flex {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#packet-stream .stream-entry .flex-grow-1 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#packet-stream .packet-stream-path {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.stream-entry {
|
||||
background-color: var(--card-bg);
|
||||
@@ -1535,7 +1578,7 @@
|
||||
${clickHint}
|
||||
</div>
|
||||
|
||||
<div class="small text-muted mb-1">
|
||||
<div class="small text-muted mb-1 packet-stream-path">
|
||||
<strong>Path:</strong> ${pathDisplay}
|
||||
${data.path_len > 0 ? ` (${data.path_len} bytes)` : ''}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user