Working state after implementing dark theme.
This commit is contained in:
40
app/main.py
40
app/main.py
@ -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
126
app/web/static/logs.js
Normal 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);
|
||||
});
|
@ -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(' ');
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
@ -1,63 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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>
|
||||
</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 %}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
<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>
|
||||
</body>
|
||||
<script src="/static/logs.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Reference in New Issue
Block a user