mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-05-11 01:57:03 +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.
328 lines
12 KiB
HTML
328 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Live Log Viewer - MeshCore Bot Data Viewer{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.log-container {
|
|
height: calc(100vh - 260px);
|
|
min-height: 400px;
|
|
overflow-y: auto;
|
|
background-color: #0d1117;
|
|
border: 1px solid #30363d;
|
|
border-radius: 0.375rem;
|
|
padding: 0.75rem 1rem;
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
font-size: 0.82rem;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
[data-theme="light"] .log-container {
|
|
background-color: #f8f9fa;
|
|
border-color: var(--border-color);
|
|
color: #212529;
|
|
}
|
|
|
|
.log-line {
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
padding: 1px 0;
|
|
color: #c9d1d9;
|
|
border-bottom: 1px solid rgba(48, 54, 61, 0.3);
|
|
}
|
|
|
|
[data-theme="light"] .log-line {
|
|
color: #212529;
|
|
border-bottom-color: rgba(0,0,0,0.05);
|
|
}
|
|
|
|
/* Level-based coloring */
|
|
.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: #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; }
|
|
[data-theme="light"] .log-line.level-critical{ color: #6610f2; font-weight: bold; }
|
|
|
|
.log-controls {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
#line-count {
|
|
font-size: 0.8rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.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 %}
|
|
|
|
{% block content %}
|
|
<div class="row">
|
|
<div class="col-12 d-flex justify-content-between align-items-center mb-4">
|
|
<h1 class="mb-0">
|
|
<i class="fas fa-file-alt"></i> Live Log Viewer
|
|
</h1>
|
|
<a href="/realtime" class="btn btn-outline-secondary">
|
|
<i class="fas fa-broadcast-tower"></i> Real-time Monitor
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<h6 class="mb-0"><i class="fas fa-terminal"></i> Bot Log Stream</h6>
|
|
<span class="badge bg-success" id="log-status">Connecting…</span>
|
|
</div>
|
|
<div class="log-controls">
|
|
<span id="line-count">0 lines</span>
|
|
<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>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="clearLog()">
|
|
<i class="fas fa-trash"></i> Clear
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="scrollToBottom()">
|
|
<i class="fas fa-arrow-down"></i> Bottom
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div id="log-container" class="log-container">
|
|
<div class="text-muted py-3 text-center" id="log-placeholder">
|
|
<i class="fas fa-hourglass-half"></i> Waiting for log data…
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
(function () {
|
|
const MAX_LINES = 2000;
|
|
const LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
|
|
const STORAGE_KEY = 'meshcore_logs_level_toggles';
|
|
|
|
let paused = false;
|
|
let autoScroll = true;
|
|
let lineCount = 0;
|
|
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'] });
|
|
|
|
socket.on('connect', function () {
|
|
updateStatus('log-status', 'Connected', 'success');
|
|
socket.emit('subscribe_logs');
|
|
});
|
|
|
|
socket.on('disconnect', function () {
|
|
updateStatus('log-status', 'Disconnected', 'danger');
|
|
});
|
|
|
|
socket.on('log_line', function (data) {
|
|
if (paused) return;
|
|
addLogLine(data.line || '');
|
|
});
|
|
|
|
function detectLevel(line) {
|
|
/* 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) {
|
|
const lineLevel = detectLevel(line);
|
|
if (!lineLevel) return true;
|
|
return enabledLevels[lineLevel] === true;
|
|
}
|
|
|
|
function addLogLine(text) {
|
|
if (!levelPasses(text)) return;
|
|
if (placeholder) placeholder.remove();
|
|
|
|
const div = document.createElement('div');
|
|
div.className = 'log-line';
|
|
const lvl = detectLevel(text);
|
|
if (lvl) div.classList.add('level-' + lvl.toLowerCase());
|
|
div.textContent = text;
|
|
container.appendChild(div);
|
|
|
|
lineCount++;
|
|
document.getElementById('line-count').textContent = lineCount + ' lines';
|
|
|
|
// Trim old lines
|
|
while (container.children.length > MAX_LINES) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
|
|
if (autoScroll) {
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
}
|
|
|
|
function togglePause() {
|
|
paused = !paused;
|
|
const icon = document.getElementById('pause-icon');
|
|
const btn = document.getElementById('pause-btn');
|
|
if (paused) {
|
|
icon.className = 'fas fa-play';
|
|
btn.innerHTML = '<i class="fas fa-play" id="pause-icon"></i> Resume';
|
|
autoScroll = false;
|
|
} else {
|
|
icon.className = 'fas fa-pause';
|
|
btn.innerHTML = '<i class="fas fa-pause" id="pause-icon"></i> Pause';
|
|
autoScroll = true;
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
}
|
|
|
|
function clearLog() {
|
|
container.innerHTML = '';
|
|
lineCount = 0;
|
|
document.getElementById('line-count').textContent = '0 lines';
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
container.scrollTop = container.scrollHeight;
|
|
autoScroll = true;
|
|
}
|
|
|
|
function updateStatus(id, text, cls) {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
el.className = 'badge bg-' + cls;
|
|
el.textContent = text;
|
|
}
|
|
|
|
// Pause auto-scroll when user scrolls up
|
|
container.addEventListener('scroll', function () {
|
|
const atBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 40;
|
|
autoScroll = atBottom;
|
|
});
|
|
|
|
// Expose for inline handlers
|
|
window.togglePause = togglePause;
|
|
window.clearLog = clearLog;
|
|
window.scrollToBottom = scrollToBottom;
|
|
})();
|
|
</script>
|
|
{% endblock %}
|