cmboulanger's picture
disaster-recovery deploy of pdf-tei-editor
6a49f21 verified
Raw
History Blame Contribute Delete
10 kB
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Log Viewer</title>
<script src="/api/v1/plugins/sandbox-client.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace;
font-size: 13px;
background: #1e1e1e;
color: #d4d4d4;
height: 100vh;
display: flex;
flex-direction: column;
}
#controls {
position: sticky;
top: 0;
background: #2d2d2d;
padding: 8px 12px;
border-bottom: 1px solid #444;
display: flex;
gap: 12px;
align-items: center;
flex-shrink: 0;
z-index: 1;
}
#log-container {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.log-entry {
padding: 2px 12px;
white-space: pre-wrap;
word-break: break-all;
border-bottom: 1px solid #2a2a2a;
line-height: 1.4;
}
.log-entry:hover { background: #2a2a2a; }
.level-DEBUG { color: #888; }
.level-INFO { color: #d4d4d4; }
.level-WARNING { color: #e5a84b; }
.level-ERROR { color: #f44747; }
.level-CRITICAL { color: #f44747; font-weight: bold; }
.timestamp { color: #6a9955; }
.logger-name { color: #569cd6; }
.level-badge {
display: inline-block;
min-width: 70px;
}
button, select {
background: #3c3c3c;
color: #d4d4d4;
border: 1px solid #555;
padding: 4px 12px;
cursor: pointer;
border-radius: 3px;
font-size: 12px;
}
button:hover { background: #4c4c4c; }
label {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
}
#status {
margin-left: auto;
font-size: 11px;
color: #888;
}
#count {
font-size: 11px;
color: #888;
}
</style>
</head>
<body>
<div id="controls">
<button id="clearBtn">Clear</button>
<label>Level:
<select id="levelFilter">
<option value="DEBUG">DEBUG</option>
<option value="INFO" selected>INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
</select>
</label>
<label><input type="checkbox" id="autoScroll" checked> Auto-scroll</label>
<label><input type="checkbox" id="pauseBtn"> Pause</label>
<span id="status">Connecting...</span>
<span id="count">0 entries</span>
</div>
<div id="log-container"></div>
<script>
const LEVELS = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3, CRITICAL: 4 };
const container = document.getElementById('log-container');
const statusEl = document.getElementById('status');
const countEl = document.getElementById('count');
const levelFilter = document.getElementById('levelFilter');
const autoScrollCb = document.getElementById('autoScroll');
const pauseCb = document.getElementById('pauseBtn');
let entryCount = 0;
let paused = false;
let pendingEntries = [];
let currentSessionId = null;
const MAX_ENTRIES = 5000;
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text || '';
return d.innerHTML;
}
function appendEntry(data) {
const minLevel = LEVELS[levelFilter.value] || 0;
const entryLevel = LEVELS[data.level] || 0;
if (entryLevel < minLevel) return;
const div = document.createElement('div');
div.className = 'log-entry level-' + data.level;
div.innerHTML =
'<span class="timestamp">' + escapeHtml(data.timestamp) + '</span> '
+ '<span class="level-badge level-' + data.level + '">[' + (data.level || '').padEnd(8) + ']</span> '
+ '<span class="logger-name">' + escapeHtml(data.logger) + '</span> - '
+ escapeHtml(data.message);
container.appendChild(div);
entryCount++;
countEl.textContent = entryCount + ' entries';
while (container.children.length > MAX_ENTRIES) {
container.removeChild(container.firstChild);
}
if (autoScrollCb.checked) {
div.scrollIntoView({ block: 'end' });
}
}
function onLogEntry(data) {
if (paused) {
pendingEntries.push(data);
return;
}
appendEntry(data);
}
async function loadRecentLogs(sessionId) {
try {
const response = await fetch('/api/plugins/log-viewer/recent?session_id=' + encodeURIComponent(sessionId));
if (!response.ok) {
throw new Error('Failed to load recent logs: ' + response.status);
}
const data = await response.json();
if (data.entries && data.entries.length > 0) {
data.entries.forEach(appendEntry);
}
} catch (e) {
console.error('Error loading recent logs:', e);
}
}
async function setLogLevel(level) {
if (!currentSessionId) return;
try {
const response = await fetch(
'/api/plugins/log-viewer/level?level=' + encodeURIComponent(level) +
'&session_id=' + encodeURIComponent(currentSessionId),
{ method: 'POST' }
);
if (!response.ok) {
console.error('Failed to set log level:', response.status);
}
} catch (e) {
console.error('Error setting log level:', e);
}
}
function updateStatus(state, detail) {
if (state === 'connected') {
statusEl.textContent = 'Connected';
statusEl.style.color = '#6a9955';
} else if (state === 'reconnecting') {
statusEl.textContent = 'Reconnecting...';
statusEl.style.color = '#e5a84b';
} else if (state === 'error') {
statusEl.textContent = detail || 'Connection failed';
statusEl.style.color = '#f44747';
}
}
async function subscribeBackend() {
try {
const response = await fetch(
'/api/plugins/log-viewer/subscribe?session_id=' + encodeURIComponent(currentSessionId),
{ method: 'POST' }
);
if (!response.ok) {
console.error('Backend subscribe failed:', response.status);
return false;
}
return true;
} catch (e) {
console.error('Backend subscribe error:', e);
return false;
}
}
function onSSEConnected(_data) {
// SSE stream was (re-)established — re-subscribe to the backend
console.log('Log viewer: SSE reconnected, re-subscribing to backend');
subscribeBackend();
updateStatus('connected');
}
async function init() {
try {
currentSessionId = new URLSearchParams(location.search).get('session_id');
// Load recent log entries first
await loadRecentLogs(currentSessionId);
// Verify sandbox is available before subscribing
if (typeof window.sandbox === 'undefined' || typeof window.sandbox.subscribeSSE !== 'function') {
throw new Error(
'Sandbox connection not available. '
+ 'Please reopen the log viewer from the plugin menu. '
+ '(If the problem persists, try reloading the application.)'
);
}
// Subscribe to SSE event forwarding FIRST so the listener is
// ready before the backend starts sending events
await sandbox.subscribeSSE('logEntry', onLogEntry);
// Listen for SSE reconnection events to re-subscribe on the backend
await sandbox.subscribeSSE('connected', onSSEConnected);
// NOW subscribe on the backend (events start flowing, listener already active)
await subscribeBackend();
updateStatus('connected');
// Periodic re-subscription as a safety net (idempotent)
setInterval(() => {
if (currentSessionId) {
subscribeBackend();
}
}, 60000);
} catch (e) {
console.error('Log viewer initialization failed:', e);
updateStatus('error', 'Connection failed. Reopen log viewer from the plugin menu.');
}
}
document.getElementById('clearBtn').addEventListener('click', () => {
container.innerHTML = '';
entryCount = 0;
countEl.textContent = '0 entries';
});
pauseCb.addEventListener('change', (e) => {
paused = e.target.checked;
if (!paused) {
pendingEntries.forEach(appendEntry);
pendingEntries = [];
}
});
levelFilter.addEventListener('change', (e) => {
setLogLevel(e.target.value);
});
init();
</script>
</body>
</html>