258 lines
7.9 KiB
HTML
258 lines
7.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Logs</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
font-family: "Courier New", Courier, monospace;
|
|
}
|
|
.log-container {
|
|
display: flex;
|
|
flex: 1;
|
|
gap: 0.5em;
|
|
margin: 0.5em;
|
|
min-height: 0;
|
|
}
|
|
.log-column {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
min-width: 0;
|
|
transition: flex 0.3s ease;
|
|
}
|
|
.log-column.minimized {
|
|
flex: 0.1;
|
|
max-width: 50px;
|
|
border: 1px solid #777;
|
|
color: green;
|
|
}
|
|
.log-controls {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto;
|
|
gap: 0.5em;
|
|
margin-bottom: 0.5em;
|
|
}
|
|
.log-controls input {
|
|
width: 100%;
|
|
padding: 4px;
|
|
}
|
|
.log-controls input:focus {
|
|
outline: none;
|
|
}
|
|
.log-stream {
|
|
flex: 1;
|
|
padding: 1em;
|
|
background: #f4f4f4;
|
|
overflow-y: auto;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
min-height: 0;
|
|
}
|
|
|
|
.regex-error {
|
|
background-color: #ff0000 !important;
|
|
}
|
|
|
|
/* Make headers clickable and show pointer cursor */
|
|
h2 {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
margin: 0 0 0.5em 0;
|
|
padding: 0.5em;
|
|
}
|
|
|
|
h2:hover {
|
|
background-color: rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
/* Dark mode styles */
|
|
@media (prefers-color-scheme: dark) {
|
|
body {
|
|
background-color: #333;
|
|
color: #fff;
|
|
}
|
|
|
|
.log-stream {
|
|
background: #444;
|
|
color: #fff;
|
|
}
|
|
|
|
.log-controls input {
|
|
background: #555;
|
|
color: #fff;
|
|
border: 1px solid #777;
|
|
}
|
|
|
|
.log-controls button {
|
|
background: #555;
|
|
color: #fff;
|
|
border: 1px solid #777;
|
|
}
|
|
|
|
h2:hover {
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
}
|
|
|
|
/* Hide content when minimized */
|
|
.log-column.minimized .log-controls,
|
|
.log-column.minimized .log-stream {
|
|
display: none;
|
|
}
|
|
|
|
.log-column.minimized h2 {
|
|
writing-mode: vertical-rl;
|
|
text-orientation: mixed;
|
|
transform: rotate(180deg);
|
|
white-space: nowrap;
|
|
margin: auto;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="log-container">
|
|
<div class="log-column">
|
|
<h2>Proxy Logs</h2>
|
|
<div class="log-controls">
|
|
<input type="text" id="proxy-filter-input" placeholder="proxy regex filter">
|
|
<button id="proxy-clear-button">clear</button>
|
|
</div>
|
|
<pre class="log-stream" id="proxy-log-stream">Waiting for proxy logs...</pre>
|
|
</div>
|
|
<div class="log-column minimized">
|
|
<h2>Upstream Logs</h2>
|
|
<div class="log-controls">
|
|
<input type="text" id="upstream-filter-input" placeholder="upstream regex filter">
|
|
<button id="upstream-clear-button">clear</button>
|
|
</div>
|
|
<pre class="log-stream" id="upstream-log-stream">Waiting for upstream logs...</pre>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
class LogStream {
|
|
constructor(streamElement, filterInput, clearButton, endpoint) {
|
|
this.streamElement = streamElement;
|
|
this.filterInput = filterInput;
|
|
this.clearButton = clearButton;
|
|
this.endpoint = endpoint;
|
|
this.logData = "";
|
|
this.regexFilter = null;
|
|
this.eventSource = null;
|
|
|
|
this.initialize();
|
|
}
|
|
|
|
initialize() {
|
|
this.filterInput.addEventListener('input', () => this.updateFilter());
|
|
this.clearButton.addEventListener('click', () => {
|
|
this.filterInput.value = "";
|
|
this.regexFilter = null;
|
|
this.render();
|
|
});
|
|
this.setupEventSource();
|
|
}
|
|
|
|
setupEventSource() {
|
|
if (typeof(EventSource) === "undefined") {
|
|
this.logData = "SSE Not supported by this browser.";
|
|
this.render();
|
|
return;
|
|
}
|
|
|
|
const connect = () => {
|
|
this.eventSource = new EventSource(this.endpoint);
|
|
|
|
this.eventSource.onmessage = (event) => {
|
|
this.logData += event.data;
|
|
this.render();
|
|
};
|
|
|
|
this.eventSource.onerror = (err) => {
|
|
// Close the current connection
|
|
this.eventSource.close();
|
|
|
|
this.logData += "\nConnection lost. Retrying in 5 seconds...\n";
|
|
this.render();
|
|
|
|
// Attempt to reconnect after 5 seconds
|
|
setTimeout(() => {
|
|
this.logData += "Attempting to reconnect...\n";
|
|
this.render();
|
|
connect();
|
|
}, 5000);
|
|
};
|
|
};
|
|
|
|
// Initial connection
|
|
connect();
|
|
}
|
|
|
|
render() {
|
|
let content = this.logData;
|
|
|
|
if (this.regexFilter) {
|
|
const lines = content.split('\n');
|
|
const filteredLines = lines.filter(line => this.regexFilter.test(line));
|
|
content = filteredLines.length > 0 ? filteredLines.join('\n') + '\n' : "";
|
|
}
|
|
|
|
this.streamElement.textContent = content;
|
|
this.streamElement.scrollTop = this.streamElement.scrollHeight;
|
|
}
|
|
|
|
updateFilter() {
|
|
const pattern = this.filterInput.value.trim();
|
|
this.filterInput.classList.remove('regex-error');
|
|
|
|
if (!pattern) {
|
|
this.regexFilter = null;
|
|
this.render();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.regexFilter = new RegExp(pattern);
|
|
} catch (e) {
|
|
console.error("Invalid regex pattern:", e);
|
|
this.regexFilter = null;
|
|
this.filterInput.classList.add('regex-error');
|
|
return;
|
|
}
|
|
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
// Initialize both log streams
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
new LogStream(
|
|
document.getElementById('proxy-log-stream'),
|
|
document.getElementById('proxy-filter-input'),
|
|
document.getElementById('proxy-clear-button'),
|
|
"/logs/streamSSE/proxy"
|
|
);
|
|
|
|
new LogStream(
|
|
document.getElementById('upstream-log-stream'),
|
|
document.getElementById('upstream-filter-input'),
|
|
document.getElementById('upstream-clear-button'),
|
|
"/logs/streamSSE/upstream"
|
|
);
|
|
|
|
// Initialize clickable headers
|
|
document.querySelectorAll('h2').forEach(header => {
|
|
header.addEventListener('click', () => {
|
|
const column = header.closest('.log-column');
|
|
column.classList.toggle('minimized');
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |