Files
pmomusic/pmolog/index.html

402 lines
12 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>🚀 Real-Time Logs</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--border: #30363d;
--text-primary: #e6edf3;
--text-secondary: #7d8590;
--error: #f85149;
--warning: #d29922;
--info: #58a6ff;
--debug: #8957e5;
--success: #3fb950;
}
body {
background: var(--bg-primary);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
margin: 0;
line-height: 1.5;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 100;
}
h1 {
margin: 0;
font-size: 20px;
color: var(--info);
}
#controls {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.control-group {
display: flex;
gap: 8px;
align-items: center;
}
#logs {
height: calc(100vh - 60px);
overflow-y: auto;
background: var(--bg-secondary);
padding: 15px;
}
.log {
margin: 8px 0;
padding: 8px 12px;
border-left: 4px solid;
border-radius: 6px;
background: rgba(255, 255, 255, 0.03);
}
.log.error { border-color: var(--error); }
.log.warning { border-color: var(--warning); }
.log.info { border-color: var(--info); }
.log.debug { border-color: var(--debug); }
.log-time {
color: var(--text-secondary);
font-size: 12px;
margin-right: 10px;
}
.log-level {
font-weight: bold;
margin-right: 8px;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
.level-error { background: var(--error); color: white; }
.level-warning { background: var(--warning); color: black; }
.level-info { background: var(--info); color: white; }
.level-debug { background: var(--debug); color: white; }
.log-content {
margin-top: 6px;
white-space: pre-wrap;
font-family: ui-monospace, SFMono-Regular, SF Mono, Consolas, Liberation Mono, Menlo, monospace;
}
.log-fields {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border);
font-size: 12px;
color: var(--text-secondary);
}
input[type="checkbox"] {
margin-right: 4px;
accent-color: var(--info);
}
#search {
padding: 6px 10px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--bg-primary);
color: var(--text-primary);
width: 180px;
}
button {
background: #238636;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
}
button:hover { background: #2ea043; }
#status {
font-size: 12px;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 6px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.status-connected { background: var(--success); }
.status-disconnected { background: var(--error); }
.status-connecting { background: var(--warning); }
#log-count {
background: var(--bg-primary);
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
}
.highlight {
background: rgba(255, 255, 0, 0.2);
padding: 0 2px;
border-radius: 2px;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
</head>
<body>
<header>
<h1>📝 Real-Time Logs</h1>
<div id="controls">
<div class="control-group">
<label><input type="checkbox" value="error" checked>❌ Error</label>
<label><input type="checkbox" value="warning" checked>⚠️ Warning</label>
<label><input type="checkbox" value="info" checked> Info</label>
<label><input type="checkbox" value="debug" checked>🐛 Debug</label>
</div>
<input id="search" type="text" placeholder="Search...">
<div class="control-group">
<label><input type="checkbox" id="autoscroll" checked> Auto-scroll</label>
<button id="clear">Clear</button>
<button id="export">Export</button>
</div>
</div>
<div id="status">
<span class="status-indicator status-connecting" id="status-indicator"></span>
<span id="status-text">Connecting...</span>
<span id="log-count">0 logs</span>
</div>
</header>
<div id="logs"></div>
<script>
const logsContainer = document.getElementById('logs');
const statusIndicator = document.getElementById('status-indicator');
const statusText = document.getElementById('status-text');
const logCountElement = document.getElementById('log-count');
const maxLogs = 1000;
let eventSource = null;
let filters = {
error: true,
warning: true,
info: true,
debug: true
};
let searchTerm = "";
let autoScroll = true;
let logCount = 0;
let reconnectAttempts = 0;
let isConnected = false;
// Initialize Marked and Highlight.js
marked.setOptions({
breaks: true,
highlight: (code, lang) => {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
return hljs.highlightAuto(code).value;
}
});
// Format log level with colors
function formatLevel(level) {
const levelClasses = {
error: 'level-error',
warning: 'level-warning',
info: 'level-info',
debug: 'level-debug'
};
return '<span class="log-level ' + (levelClasses[level] || '') + '">' + level.toUpperCase() + '</span>';
}
// Format timestamp
function formatTimestamp(timestamp) {
const date = new Date(timestamp);
return '<span class="log-time">' + date.toLocaleTimeString() + '.' + date.getMilliseconds().toString().padStart(3, '0') + '</span>';
}
// Highlight search terms in text
function highlightText(text, term) {
if (!term) return text;
const regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
return text.replace(regex, '<span class="highlight">$1</span>');
}
// Format log fields
function formatFields(fields) {
if (!fields || Object.keys(fields).length === 0) return '';
let html = '<div class="log-fields">';
for (const [key, value] of Object.entries(fields)) {
html += '<div><strong>' + key + ':</strong> ' + JSON.stringify(value) + '</div>';
}
html += '</div>';
return html;
}
// Add a log entry to the UI
function addLogEntry(data) {
// Apply filtering
if (!filters[data.level]) return;
const contentLower = data.content.toLowerCase();
const levelLower = data.level.toLowerCase();
if (searchTerm &&
!contentLower.includes(searchTerm) &&
!levelLower.includes(searchTerm)) {
return;
}
const line = document.createElement('div');
line.className = 'log ' + data.level;
// Add timestamp and level
line.innerHTML = '<div>' +
formatTimestamp(data.time) +
formatLevel(data.level) +
'</div>' +
'<div class="log-content">' + highlightText(marked.parse(data.content), searchTerm) + '</div>' +
formatFields(data.fields);
logsContainer.appendChild(line);
logCount++;
logCountElement.textContent = logCount + ' logs';
// Limit the number of logs displayed
if (logsContainer.children.length > maxLogs) {
logsContainer.removeChild(logsContainer.firstChild);
}
// Auto-scroll if enabled
if (autoScroll) {
logsContainer.scrollTop = logsContainer.scrollHeight;
}
}
// Connect to the SSE server
function connect() {
if (eventSource) {
eventSource.close();
}
// Build URL with query parameters for filters
const params = new URLSearchParams();
Object.entries(filters).forEach(([level, enabled]) => {
if (!enabled) params.set(level, 'false');
});
if (searchTerm) params.set('search', searchTerm);
const url = '/log-sse' + (params.toString() ? '?' + params.toString() : '');
eventSource = new EventSource(url);
eventSource.addEventListener('message', (e) => {
if (e.data) {
try {
const data = JSON.parse(e.data);
addLogEntry(data);
} catch (err) {
console.error('Error parsing log message:', err);
}
}
});
eventSource.addEventListener('heartbeat', (e) => {
// Update last activity timestamp
updateStatus(true);
});
eventSource.onopen = () => {
updateStatus(true);
reconnectAttempts = 0;
};
eventSource.onerror = (e) => {
console.error('SSE error', e);
updateStatus(false);
// Attempt to reconnect with exponential backoff
eventSource.close();
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
reconnectAttempts++;
setTimeout(connect, delay);
};
}
// Update connection status UI
function updateStatus(connected) {
isConnected = connected;
statusIndicator.className = 'status-indicator ' +
(connected ? 'status-connected' : 'status-disconnected');
statusText.textContent = connected ? 'Connected' : 'Disconnected';
}
// Initialize controls
function initControls() {
// Filter checkboxes
document.querySelectorAll('input[type=checkbox][value]').forEach(cb => {
cb.checked = filters[cb.value];
cb.addEventListener('change', () => {
filters[cb.value] = cb.checked;
// Reconnect with new filters
connect();
});
});
// Search input
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', e => {
searchTerm = e.target.value.toLowerCase();
// Reconnect with new search term
connect();
});
// Auto-scroll
document.getElementById('autoscroll').addEventListener('change', e => {
autoScroll = e.target.checked;
if (autoScroll) {
logsContainer.scrollTop = logsContainer.scrollHeight;
}
});
// Clear button
document.getElementById('clear').addEventListener('click', () => {
logsContainer.innerHTML = '';
logCount = 0;
logCountElement.textContent = '0 logs';
});
// Export button
document.getElementById('export').addEventListener('click', () => {
const logs = Array.from(logsContainer.children).map(log => log.textContent).join('\n');
const blob = new Blob([logs], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'logs-' + new Date().toISOString().slice(0, 10) + '.txt';
a.click();
URL.revokeObjectURL(url);
});
}
// Initialize the application
function init() {
initControls();
connect();
// Handle visibility change - reconnect when tab becomes visible again
document.addEventListener('visibilitychange', () => {
if (!document.hidden && !isConnected) {
connect();
}
});
}
// Start the application
init();
</script>
</body>
</html>