Working state after implementing dark theme.

This commit is contained in:
Kalzu Rekku
2025-06-11 16:29:40 +03:00
parent d4c71e7d2c
commit d93b0ee4ee
6 changed files with 453 additions and 165 deletions

View File

@ -193,9 +193,9 @@ async def read_root(request: Request):
{"request": request, "service_uuid": SERVICE_UUID}
)
@app.get("/{service_uuid}/logs")
async def get_logs(service_uuid: str, limit: int = 100):
"""Get recent logs for the service."""
@app.get("/{service_uuid}/logs", response_class=HTMLResponse)
async def get_logs(request: Request, service_uuid: str, limit: int = 100):
"""Serve the logs web page with recent logs for the service."""
if service_uuid != SERVICE_UUID:
return JSONResponse(
status_code=404,
@ -203,12 +203,15 @@ async def get_logs(service_uuid: str, limit: int = 100):
)
logs = log_buffer.get_logs(limit)
return {
return templates.TemplateResponse(
"logs.html",
{
"request": request,
"service_uuid": service_uuid,
"log_count": len(logs),
"logs": logs
"logs": logs,
"log_count": len(logs)
}
)
@app.put("/{service_uuid}/{node_uuid}/", status_code=status.HTTP_200_OK)
async def update_node_status(
@ -284,13 +287,29 @@ async def update_node_status(
@app.get("/nodes/status")
async def get_all_nodes_status():
"""Returns the current status of all known nodes for the UI."""
"""Returns the current status of all known nodes for the UI, including ping latencies."""
logger.info("Fetching all nodes status for UI.")
response_nodes = []
for node_uuid, data in known_nodes_db.items():
# Dynamically calculate health for each node based on its current data
# Dynamically calculate health for each node
current_health = get_node_health(data)
# Build connections dictionary with raw ping latencies
connections = {}
for target_uuid in known_nodes_db:
if target_uuid != node_uuid: # Exclude self
# Fetch recent ping data (last 5 minutes to account for RRD step=60s)
ping_data = database.get_ping_data(node_uuid, target_uuid, start_time="-300s")
latency_ms = None
if ping_data and ping_data['data']['latency']:
# Get the most recent non-null latency
for latency in reversed(ping_data['data']['latency']):
if latency is not None:
latency_ms = float(latency)
break
connections[target_uuid] = latency_ms
response_nodes.append({
"uuid": node_uuid,
"last_seen": data["last_seen"],
@ -298,7 +317,8 @@ async def get_all_nodes_status():
"health_status": current_health,
"uptime_seconds": data.get("uptime_seconds"),
"load_avg": data.get("load_avg"),
"memory_usage_percent": data.get("memory_usage_percent")
"memory_usage_percent": data.get("memory_usage_percent"),
"connections": connections
})
return {"nodes": response_nodes}

126
app/web/static/logs.js Normal file
View File

@ -0,0 +1,126 @@
document.addEventListener('DOMContentLoaded', () => {
const logTableContainer = document.getElementById('log-table-container');
const logCountSpan = document.getElementById('log-count');
const POLLING_INTERVAL_MS = 5000; // Poll every 5 seconds
const serviceUuid = logTableContainer.dataset.serviceUuid || '{{ service_uuid }}'; // Fallback for non-dynamic rendering
async function fetchLogs() {
try {
const response = await fetch(`/${serviceUuid}/logs?limit=100`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
renderLogTable(data.logs);
logCountSpan.textContent = data.log_count;
} catch (error) {
console.error("Error fetching logs:", error);
logTableContainer.innerHTML = '<p class="loading-message">Error loading logs. Please check server connection.</p>';
}
}
function renderLogTable(logs) {
logTableContainer.innerHTML = ''; // Clear existing content
if (logs.length === 0) {
logTableContainer.innerHTML = '<p class="loading-message">No logs available.</p>';
return;
}
// Create table
const table = document.createElement('table');
table.classList.add('log-table');
// Create header
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
const headers = [
{ key: 'timestamp', label: 'Timestamp' },
{ key: 'level', label: 'Level' },
{ key: 'message', label: 'Message' },
{ key: 'extra', label: 'Extra' }
];
headers.forEach(header => {
const th = document.createElement('th');
th.textContent = header.label;
th.dataset.key = header.key;
th.classList.add('sortable');
th.addEventListener('click', () => sortTable(header.key));
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Create body
const tbody = document.createElement('tbody');
logs.forEach(log => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${new Date(log.timestamp).toLocaleString()}</td>
<td class="log-level log-level-${log.level.toLowerCase()}">${log.level}</td>
<td>${escapeHtml(log.message)}</td>
<td>
${log.extra ? `
<div class="json-toggle">Show JSON</div>
<pre class="json-content" style="display: none;">${JSON.stringify(log.extra, null, 2)}</pre>
` : '-'}
</td>
`;
tbody.appendChild(row);
});
table.appendChild(tbody);
logTableContainer.appendChild(table);
// Add click handlers for JSON toggles
document.querySelectorAll('.json-toggle').forEach(toggle => {
toggle.addEventListener('click', () => {
const jsonContent = toggle.nextElementSibling;
const isHidden = jsonContent.style.display === 'none';
jsonContent.style.display = isHidden ? 'block' : 'none';
toggle.textContent = isHidden ? 'Hide JSON' : 'Show JSON';
});
});
}
function sortTable(key) {
const table = document.querySelector('.log-table');
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const isAsc = table.dataset.sortKey === key && table.dataset.sortDir !== 'desc';
table.dataset.sortKey = key;
table.dataset.sortDir = isAsc ? 'desc' : 'asc';
rows.sort((a, b) => {
let aValue, bValue;
if (key === 'timestamp') {
aValue = new Date(a.cells[0].textContent);
bValue = new Date(b.cells[0].textContent);
} else if (key === 'level') {
aValue = a.cells[1].textContent.toLowerCase();
bValue = b.cells[1].textContent.toLowerCase();
} else if (key === 'message') {
aValue = a.cells[2].textContent.toLowerCase();
bValue = b.cells[2].textContent.toLowerCase();
} else {
aValue = a.cells[3].querySelector('.json-content')?.textContent || '';
bValue = b.cells[3].querySelector('.json-content')?.textContent || '';
}
return isAsc ? (aValue < bValue ? -1 : 1) : (aValue > bValue ? -1 : 1);
});
tbody.innerHTML = '';
rows.forEach(row => tbody.appendChild(row));
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// Initial fetch and polling
fetchLogs();
setInterval(fetchLogs, POLLING_INTERVAL_MS);
});

View File

@ -1,6 +1,7 @@
document.addEventListener('DOMContentLoaded', () => {
const nodeGridContainer = document.getElementById('node-grid-container');
const nodeCountSpan = document.getElementById('node-count');
const serviceUuidPara = document.getElementById('service-uuid');
const POLLING_INTERVAL_MS = 3000; // Poll every 3 seconds
async function fetchNodeData() {
@ -14,40 +15,106 @@ document.addEventListener('DOMContentLoaded', () => {
} catch (error) {
console.error("Error fetching node data:", error);
nodeGridContainer.innerHTML = '<p class="loading-message">Error loading node data. Please check server connection.</p>';
serviceUuidPara.style.display = 'block'; // Show UUID on error
}
}
function renderNodeGrid(nodes) {
nodeGridContainer.innerHTML = ''; // Clear existing nodes
nodeGridContainer.innerHTML = ''; // Clear existing content
nodeCountSpan.textContent = nodes.length; // Update total node count
serviceUuidPara.style.display = nodes.length === 0 ? 'block' : 'none'; // Toggle Service UUID
if (nodes.length === 0) {
nodeGridContainer.innerHTML = '<p class="loading-message">No nodes reporting yet. Start a client!</p>';
return;
}
// Ensure we have exactly 4 nodes
if (nodes.length !== 4) {
nodeGridContainer.innerHTML = '<p class="loading-message">Expected exactly 4 nodes, got ' + nodes.length + '.</p>';
return;
}
// Create a 4x4 grid container
const grid = document.createElement('div');
grid.classList.add('connection-grid');
// Add header row for column UUIDs
const headerRow = document.createElement('div');
headerRow.classList.add('grid-row', 'header-row');
headerRow.appendChild(createEmptyCell()); // Top-left corner
nodes.forEach(node => {
const nodeCell = document.createElement('div');
nodeCell.classList.add('node-cell');
nodeCell.classList.add(`node-${node.health_status}`); // Apply health color class
// Truncate UUID for display
const headerCell = document.createElement('div');
headerCell.classList.add('grid-cell', 'header-cell');
const displayUuid = node.uuid.substring(0, 8) + '...';
headerCell.innerHTML = `<div class="node-uuid" title="${node.uuid}">${displayUuid}</div>`;
headerRow.appendChild(headerCell);
});
grid.appendChild(headerRow);
nodeCell.innerHTML = `
<div class="node-uuid" title="${node.uuid}">${displayUuid}</div>
<div class="node-status-text">Status: ${node.health_status.toUpperCase()}</div>
// Create rows for the 4x4 grid
nodes.forEach((rowNode, rowIndex) => {
const row = document.createElement('div');
row.classList.add('grid-row');
// Add row header (UUID)
const rowHeader = document.createElement('div');
rowHeader.classList.add('grid-cell', 'header-cell');
const displayUuid = rowNode.uuid.substring(0, 8) + '...';
rowHeader.innerHTML = `<div class="node-uuid" title="${rowNode.uuid}">${displayUuid}</div>`;
row.appendChild(rowHeader);
// Add cells for connections
nodes.forEach((colNode, colIndex) => {
const cell = document.createElement('div');
cell.classList.add('grid-cell');
if (rowIndex === colIndex) {
// Diagonal: show node health status
cell.classList.add(`node-${rowNode.health_status}`);
cell.innerHTML = `
<div class="node-status-text">Status: ${rowNode.health_status.toUpperCase()}</div>
<div class="node-tooltip">
<p><strong>UUID:</strong> ${node.uuid}</p>
<p><strong>IP:</strong> ${node.ip}</p>
<p><strong>Last Seen:</strong> ${new Date(node.last_seen).toLocaleTimeString()}</p>
<p><strong>Uptime:</strong> ${node.uptime_seconds ? formatUptime(node.uptime_seconds) : 'N/A'}</p>
<p><strong>Load Avg (1m, 5m, 15m):</strong> ${node.load_avg ? node.load_avg.join(', ') : 'N/A'}</p>
<p><strong>Memory Usage:</strong> ${node.memory_usage_percent ? node.memory_usage_percent.toFixed(2) + '%' : 'N/A'}</p>
<p><strong>UUID:</strong> ${rowNode.uuid}</p>
<p><strong>IP:</strong> ${rowNode.ip}</p>
<p><strong>Last Seen:</strong> ${new Date(rowNode.last_seen).toLocaleTimeString()}</p>
<p><strong>Uptime:</strong> ${rowNode.uptime_seconds ? formatUptime(rowNode.uptime_seconds) : 'N/A'}</p>
<p><strong>Load Avg (1m, 5m, 15m):</strong> ${rowNode.load_avg ? rowNode.load_avg.join(', ') : 'N/A'}</p>
<p><strong>Memory Usage:</strong> ${rowNode.memory_usage_percent ? rowNode.memory_usage_percent.toFixed(2) + '%' : 'N/A'}</p>
</div>
`;
nodeGridContainer.appendChild(nodeCell);
} else {
// Off-diagonal: show ping latency
const latency = rowNode.connections && colNode.uuid in rowNode.connections && rowNode.connections[colNode.uuid] !== null ? rowNode.connections[colNode.uuid] : null;
const displayLatency = latency !== null && !isNaN(latency) ? `${latency.toFixed(1)} ms` : 'N/A';
const latencyClass = latency !== null && !isNaN(latency) ? getLatencyClass(latency) : 'latency-unavailable';
cell.classList.add(latencyClass);
cell.innerHTML = `
<div class="conn-status-text">Ping: ${displayLatency}</div>
<div class="node-tooltip">
<p><strong>From:</strong> ${rowNode.uuid}</p>
<p><strong>to:</strong> ${colNode.uuid}</p>
<p><strong>Latency:</strong> ${displayLatency}</p>
</div>
`;
}
row.appendChild(cell);
});
grid.appendChild(row);
});
nodeGridContainer.appendChild(grid);
}
function getLatencyClass(latency) {
if (latency <= 200) return 'latency-low';
if (latency <= 1000) return 'latency-medium';
return 'latency-high';
}
function createEmptyCell() {
const cell = document.createElement('div');
cell.classList.add('grid-cell', 'empty-cell');
return cell;
}
function formatUptime(seconds) {
@ -56,14 +123,13 @@ document.addEventListener('DOMContentLoaded', () => {
const hours = Math.floor(seconds / 3600);
seconds %= 3600;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
const remainingSeconds = seconds % 60;
let parts = [];
if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (remainingSeconds > 0 || parts.length === 0) parts.push(`${remainingSeconds}s`); // Ensure at least seconds are shown
if (remainingSeconds > 0 || parts.length === 0) parts.push(`${remainingSeconds}s`);
return parts.join(' ');
}

View File

@ -1,156 +1,275 @@
/* Nord Polar Night Palette */
:root {
--nord0: #2E3440; /* Darkest background */
--nord1: #3B4252; /* Dark background */
--nord2: #434C5E; /* Medium background */
--nord3: #4C566A; /* Light background */
--nord4: #D8DEE9; /* Lightest text */
--nord5: #E5E9F0; /* Light text */
--nord6: #ECEFF4; /* Brightest text */
--nord7: #8FBCBB; /* Teal accent */
--nord8: #88C0D0; /* Cyan accent */
--nord9: #81A1C1; /* Blue accent (unavailable) */
--nord10: #5E81AC; /* Darker blue */
--nord11: #BF616A; /* Red (critical/high latency) */
--nord12: #D08770; /* Orange (warning/medium latency) */
--nord13: #A3BE8C; /* Green (healthy/low latency) */
--nord14: #B48EAD; /* Purple */
--nord15: #D08770; /* Coral */
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
display: flex;
flex-direction: column; /* Changed to column for header + grid */
flex-direction: column;
align-items: center;
min-height: 100vh; /* Use min-height to allow content to push body height */
min-height: 100vh;
margin: 0;
background-color: #f4f7f6;
color: #333;
padding: 20px; /* Add some padding */
box-sizing: border-box; /* Include padding in element's total width and height */
background-color: var(--nord0);
color: var(--nord4);
padding: 20px;
box-sizing: border-box;
}
.header-container {
text-align: center;
padding: 20px;
background-color: white;
background-color: var(--nord1);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
margin-bottom: 20px;
width: 90%; /* Adjust width */
max-width: 800px; /* Max width for header */
width: 80vw;
max-width: 1200px;
}
h1 {
color: #0b2d48;
color: var(--nord6);
margin-bottom: 10px;
}
p {
font-size: 1rem;
color: #555;
color: var(--nord5);
margin: 5px 0;
}
code {
background-color: #e8e8e8;
background-color: var(--nord2);
padding: 3px 8px;
border-radius: 4px;
font-family: "Courier New", Courier, monospace;
font-size: 0.9rem;
color: var(--nord6);
}
#node-grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); /* Responsive grid */
gap: 15px; /* Space between grid items */
width: 90%; /* Adjust width */
max-width: 1200px; /* Max width for grid */
#node-grid-container, #log-table-container {
width: 80vw;
max-width: 1200px;
min-width: 400px;
padding: 20px;
background-color: white;
background-color: var(--nord3);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.loading-message {
grid-column: 1 / -1; /* Span across all columns */
text-align: center;
font-style: italic;
color: #888;
.connection-grid {
display: grid;
grid-template-columns: minmax(15%, 1fr) repeat(4, minmax(15%, 1fr));
gap: 8px;
}
.node-cell {
border: 1px solid #ddd;
.grid-row {
display: contents;
}
.grid-cell {
border: 1px solid var(--nord2);
border-radius: 6px;
padding: 15px;
padding: 8px;
text-align: center;
font-size: 0.9rem;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
font-size: clamp(0.8rem, 1.5vw, 0.9rem);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease, border-color 0.3s ease, transform 0.1s ease;
cursor: pointer;
position: relative; /* For tooltip positioning */
overflow: hidden; /* Hide overflow for truncated UUID */
position: relative;
overflow: hidden;
min-height: clamp(80px, 15vw, 120px);
}
.node-cell:hover {
.grid-cell:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.header-cell {
background-color: var(--nord2);
font-weight: bold;
color: var(--nord6);
}
.empty-cell {
background-color: transparent;
border: none;
box-shadow: none;
}
.node-uuid {
font-weight: bold;
margin-bottom: 5px;
color: #333;
color: var(--nord6);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis; /* Truncate long UUIDs */
text-overflow: ellipsis;
}
.node-status-text {
font-size: 0.8rem;
color: #666;
.node-status-text, .conn-status-text {
font-size: clamp(0.7rem, 1.2vw, 0.8rem);
color: var(--nord5);
}
/* Health Status Colors */
.node-healthy {
background-color: #e6ffe6; /* Light green */
border-color: #4CAF50; /* Green */
background-color: rgba(163, 190, 140, 0.2); /* --nord13 */
border-color: var(--nord13);
}
.node-warning {
background-color: #fffacd; /* Light yellow */
border-color: #FFC107; /* Orange */
background-color: rgba(208, 135, 112, 0.2); /* --nord12 */
border-color: var(--nord12);
}
.node-critical {
background-color: #ffe6e6; /* Light red */
border-color: #F44336; /* Red */
background-color: rgba(191, 97, 106, 0.2); /* --nord11 */
border-color: var(--nord11);
}
.node-unknown {
background-color: #f0f0f0; /* Light gray */
border-color: #9E9E9E; /* Gray */
background-color: rgba(129, 161, 193, 0.2); /* --nord9 */
border-color: var(--nord9);
}
/* Latency Colors */
.latency-low {
background-color: rgba(163, 190, 140, 0.2); /* --nord13, green for <=200ms */
border-color: var(--nord13);
}
.latency-medium {
background-color: rgba(208, 135, 112, 0.2); /* --nord12, orange for <=1000ms */
border-color: var(--nord12);
}
.latency-high {
background-color: rgba(191, 97, 106, 0.2); /* --nord11, red for >1000ms */
border-color: var(--nord11);
}
.latency-unavailable {
background-color: rgba(129, 161, 193, 0.2); /* --nord9, blue for N/A */
border-color: var(--nord9);
}
/* Tooltip styles */
.node-tooltip {
visibility: hidden;
opacity: 0;
width: 200px;
background-color: #333;
color: #fff;
width: clamp(180px, 20vw, 220px);
background-color: var(--nord1);
color: var(--nord4);
text-align: left;
border-radius: 6px;
padding: 10px;
position: absolute;
z-index: 1;
bottom: 100%; /* Position above the node cell */
bottom: 100%;
left: 50%;
margin-left: -100px; /* Center the tooltip */
margin-left: clamp(-90px, -10vw, -110px);
transition: opacity 0.3s;
font-size: 0.8rem;
white-space: normal; /* Allow text to wrap */
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
font-size: clamp(0.7rem, 1.2vw, 0.8rem);
white-space: normal;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.node-tooltip::after {
content: " ";
position: absolute;
top: 100%; /* At the bottom of the tooltip */
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #333 transparent transparent transparent;
border-color: var(--nord1) transparent transparent transparent;
}
.node-cell:hover .node-tooltip {
.grid-cell:hover .node-tooltip {
visibility: visible;
opacity: 1;
}
.node-tooltip p {
margin: 2px 0;
color: #eee;
color: var(--nord4);
}
/* Log Table Styles */
.log-table {
width: 100%;
border-collapse: collapse;
background-color: var(--nord2);
border-radius: 6px;
overflow: hidden;
}
.log-table th, .log-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid var(--nord3);
font-size: clamp(0.8rem, 1.2vw, 0.9rem);
}
.log-table th {
background-color: var(--nord1);
color: var(--nord6);
cursor: pointer;
}
.log-table th.sortable:hover {
background-color: var(--nord10);
}
.log-table tr:hover {
background-color: var(--nord3);
}
.log-table .log-level {
font-weight: bold;
}
.log-level-info {
color: var(--nord13); /* Green */
}
.log-level-warning {
color: var(--nord12); /* Orange */
}
.log-level-error {
color: var(--nord11); /* Red */
}
.json-toggle {
color: var(--nord8);
cursor: pointer;
text-decoration: underline;
}
.json-content {
background-color: var(--nord1);
padding: 8px;
border-radius: 4px;
font-family: "Courier New", Courier, monospace;
font-size: 0.8rem;
color: var(--nord5);
margin-top: 5px;
white-space: pre-wrap;
}

View File

@ -4,20 +4,19 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Node Monitor</title>
<link rel="stylesheet" href="/static/style.css"> <!-- NEW: Link to CSS -->
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="header-container">
<h1>Distributed Node Monitoring System</h1>
<p>Service ID: <code>{{ service_uuid }}</code></p>
<h1>Node Monitoring System</h1>
<p>Total Nodes: <span id="node-count">0</span></p>
<p id="service-uuid" style="display: none;">Service UUID: <code>{{ service_uuid }}</code></p>
</div>
<div id="node-grid-container" class="node-grid">
<!-- Node cells will be dynamically inserted here by JavaScript -->
<div id="node-grid-container">
<p class="loading-message">Loading node data...</p>
</div>
<script src="/static/script.js"></script> <!-- NEW: Link to JavaScript -->
<script src="/static/script.js"></script>
</body>
</html>

View File

@ -1,63 +1,21 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>Node Monitor - Logs</title>
<style>
body { font-family: monospace; margin: 20px; background: #1a1a1a; color: #00ff00; }
.log-entry { margin: 5px 0; padding: 5px; border-left: 3px solid #333; }
.log-info { border-left-color: #0066cc; }
.log-warning { border-left-color: #ff9900; }
.log-error { border-left-color: #cc0000; }
.timestamp { color: #666; }
.level { font-weight: bold; margin-right: 10px; }
.message { color: #fff; }
.extra { color: #888; font-size: 0.9em; margin-top: 3px; }
h1 { color: #00ff00; }
.refresh-info { color: #666; margin: 10px 0; }
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="refresh" content="5"> <!-- Fallback auto-refresh -->
<title>Node Monitor Logs - {{ service_uuid }}</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<h1>Node Monitor Logs - {{ service_uuid }}</h1>
<div class="refresh-info">Auto-refreshing every 5 seconds...</div>
<div id="logs-container">
{% for log in logs %}
<div class="log-entry log-{{ log.level.lower() }}">
<span class="timestamp">{{ log.timestamp }}</span>
<span class="level">{{ log.level }}</span>
<span class="message">{{ log.message }}</span>
{% if log.extra %}
<div class="extra">{{ log.extra | tojson }}</div>
{% endif %}
<div class="header-container">
<h1>Node Monitor Logs</h1>
<p>Service UUID: <code>{{ service_uuid }}</code></p>
<p>Total Logs: <span id="log-count">0</span> (Auto-refreshing every 5 seconds)</p>
</div>
{% endfor %}
<div id="log-table-container">
<p class="loading-message">Loading logs...</p>
</div>
<script>
// Auto-refresh logs every 5 seconds
setInterval(async function() {
try {
const response = await fetch('{{ service_uuid }}/logs/json');
const data = await response.json();
const container = document.getElementById('logs-container');
container.innerHTML = '';
data.logs.forEach(log => {
const div = document.createElement('div');
div.className = `log-entry log-${log.level.toLowerCase()}`;
div.innerHTML = `
<span class="timestamp">${log.timestamp}</span>
<span class="level">${log.level}</span>
<span class="message">${log.message}</span>
${log.extra ? `<div class="extra">${JSON.stringify(log.extra)}</div>` : ''}
`;
container.appendChild(div);
});
} catch (error) {
console.error('Failed to refresh logs:', error);
}
}, 5000);
</script>
<script src="/static/logs.js"></script>
</body>
</html>