Gemini, trying to make this "done".
This commit is contained in:
@ -1,16 +1,34 @@
|
||||
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
|
||||
const levelRadios = document.querySelectorAll('input[name="log-level"]');
|
||||
const sinceFilter = document.getElementById('since-filter');
|
||||
const applyFiltersButton = document.getElementById('apply-filters');
|
||||
const POLLING_INTERVAL_MS = 5000;
|
||||
const serviceUuid = logTableContainer.dataset.serviceUuid; // Get service UUID from data attribute
|
||||
|
||||
let currentLevel = '';
|
||||
let currentSince = '';
|
||||
|
||||
async function fetchLogs() {
|
||||
console.log('Fetching logs with params:', { level: currentLevel, since: currentSince });
|
||||
try {
|
||||
const response = await fetch(`/${serviceUuid}/logs?limit=100`);
|
||||
const params = new URLSearchParams({
|
||||
format: 'json',
|
||||
limit: '100'
|
||||
});
|
||||
if (currentLevel) params.append('level', currentLevel);
|
||||
if (currentSince) params.append('since', currentSince);
|
||||
// Use window.API_BASE_PATH for dynamic base URL
|
||||
const url = `${window.API_BASE_PATH}/${serviceUuid}/logs?${params.toString()}`;
|
||||
console.log('Fetch URL:', url);
|
||||
const response = await fetch(url);
|
||||
console.log('Response status:', response.status);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log('Received logs:', data.logs.length);
|
||||
renderLogTable(data.logs);
|
||||
logCountSpan.textContent = data.log_count;
|
||||
} catch (error) {
|
||||
@ -20,18 +38,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function renderLogTable(logs) {
|
||||
logTableContainer.innerHTML = ''; // Clear existing content
|
||||
console.log('Rendering logs:', logs.length);
|
||||
logTableContainer.innerHTML = '';
|
||||
|
||||
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 = [
|
||||
@ -52,7 +69,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
thead.appendChild(headerRow);
|
||||
table.appendChild(thead);
|
||||
|
||||
// Create body
|
||||
const tbody = document.createElement('tbody');
|
||||
logs.forEach(log => {
|
||||
const row = document.createElement('tr');
|
||||
@ -73,7 +89,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
logTableContainer.appendChild(table);
|
||||
|
||||
// Add click handlers for JSON toggles
|
||||
document.querySelectorAll('.json-toggle').forEach(toggle => {
|
||||
toggle.addEventListener('click', () => {
|
||||
const jsonContent = toggle.nextElementSibling;
|
||||
@ -120,7 +135,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Initial fetch and polling
|
||||
applyFiltersButton.addEventListener('click', () => {
|
||||
const selectedRadio = document.querySelector('input[name="log-level"]:checked');
|
||||
currentLevel = selectedRadio ? selectedRadio.value : '';
|
||||
const sinceValue = sinceFilter.value;
|
||||
// Convert local datetime input to ISO string for backend, handling potential timezone issues
|
||||
currentSince = sinceValue ? new Date(sinceValue).toISOString().replace(/\.\d{3}Z$/, 'Z') : ''; // Ensure 'Z' for UTC
|
||||
console.log('Applying filters:', { level: currentLevel, since: currentSince });
|
||||
fetchLogs();
|
||||
});
|
||||
|
||||
console.log('Initializing logs page');
|
||||
fetchLogs();
|
||||
setInterval(fetchLogs, POLLING_INTERVAL_MS);
|
||||
});
|
||||
});
|
||||
|
@ -1,12 +1,12 @@
|
||||
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() {
|
||||
try {
|
||||
const response = await fetch('/nodes/status');
|
||||
// Use window.API_BASE_PATH for dynamic base URL
|
||||
const response = await fetch(`${window.API_BASE_PATH}/nodes/status`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
@ -15,29 +15,24 @@ 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 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
|
||||
// Create a dynamic grid container
|
||||
const grid = document.createElement('div');
|
||||
grid.classList.add('connection-grid');
|
||||
// Dynamically set grid columns based on number of nodes + 1 for the header column
|
||||
// minmax(100px, 1fr) for the row header, then repeat for each node column
|
||||
grid.style.gridTemplateColumns = `minmax(100px, 1fr) repeat(${nodes.length}, minmax(100px, 1fr))`;
|
||||
|
||||
// Add header row for column UUIDs
|
||||
const headerRow = document.createElement('div');
|
||||
@ -52,7 +47,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
grid.appendChild(headerRow);
|
||||
|
||||
// Create rows for the 4x4 grid
|
||||
// Create rows for the grid
|
||||
nodes.forEach((rowNode, rowIndex) => {
|
||||
const row = document.createElement('div');
|
||||
row.classList.add('grid-row');
|
||||
@ -78,7 +73,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<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>Load Avg (1m, 5m, 15m):</strong> ${rowNode.load_avg ? rowNode.load_avg.map(l => l.toFixed(2)).join(', ') : 'N/A'}</p>
|
||||
<p><strong>Memory Usage:</strong> ${rowNode.memory_usage_percent ? rowNode.memory_usage_percent.toFixed(2) + '%' : 'N/A'}</p>
|
||||
</div>
|
||||
`;
|
||||
@ -91,8 +86,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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>From:</strong> ${rowNode.uuid.substring(0, 8)}...</p>
|
||||
<p><strong>to:</strong> ${colNode.uuid.substring(0, 8)}...</p>
|
||||
<p><strong>Latency:</strong> ${displayLatency}</p>
|
||||
</div>
|
||||
`;
|
||||
|
@ -9,11 +9,11 @@
|
||||
--nord6: #ECEFF4; /* Brightest text */
|
||||
--nord7: #8FBCBB; /* Teal accent */
|
||||
--nord8: #88C0D0; /* Cyan accent */
|
||||
--nord9: #81A1C1; /* Blue accent (unavailable) */
|
||||
--nord9: #81A1C1; /* Blue accent */
|
||||
--nord10: #5E81AC; /* Darker blue */
|
||||
--nord11: #BF616A; /* Red (critical/high latency) */
|
||||
--nord12: #D08770; /* Orange (warning/medium latency) */
|
||||
--nord13: #A3BE8C; /* Green (healthy/low latency) */
|
||||
--nord11: #BF616A; /* Red (critical/error) */
|
||||
--nord12: #D08770; /* Orange (warning) */
|
||||
--nord13: #A3BE8C; /* Green (info) */
|
||||
--nord14: #B48EAD; /* Purple */
|
||||
--nord15: #D08770; /* Coral */
|
||||
}
|
||||
@ -74,7 +74,7 @@ code {
|
||||
|
||||
.connection-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(15%, 1fr) repeat(4, minmax(15%, 1fr));
|
||||
/* grid-template-columns will be set dynamically by JavaScript */
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ code {
|
||||
text-align: center;
|
||||
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;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, transform: 0.1s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@ -129,43 +129,43 @@ code {
|
||||
|
||||
/* Health Status Colors */
|
||||
.node-healthy {
|
||||
background-color: rgba(163, 190, 140, 0.2); /* --nord13 */
|
||||
background-color: rgba(163, 190, 140, 0.2);
|
||||
border-color: var(--nord13);
|
||||
}
|
||||
|
||||
.node-warning {
|
||||
background-color: rgba(208, 135, 112, 0.2); /* --nord12 */
|
||||
background-color: rgba(208, 135, 112, 0.2);
|
||||
border-color: var(--nord12);
|
||||
}
|
||||
|
||||
.node-critical {
|
||||
background-color: rgba(191, 97, 106, 0.2); /* --nord11 */
|
||||
background-color: rgba(191, 62, 106, 0.2);
|
||||
border-color: var(--nord11);
|
||||
}
|
||||
|
||||
.node-unknown {
|
||||
background-color: rgba(129, 161, 193, 0.2); /* --nord9 */
|
||||
background-color: rgba(129, 161, 193, 0.2);
|
||||
border-color: var(--nord9);
|
||||
}
|
||||
|
||||
/* Latency Colors */
|
||||
.latency-low {
|
||||
background-color: rgba(163, 190, 140, 0.2); /* --nord13, green for <=200ms */
|
||||
background-color: rgba(163, 190, 140, 0.2);
|
||||
border-color: var(--nord13);
|
||||
}
|
||||
|
||||
.latency-medium {
|
||||
background-color: rgba(208, 135, 112, 0.2); /* --nord12, orange for <=1000ms */
|
||||
background-color: rgba(208, 135, 112, 0.2);
|
||||
border-color: var(--nord12);
|
||||
}
|
||||
|
||||
.latency-high {
|
||||
background-color: rgba(191, 97, 106, 0.2); /* --nord11, red for >1000ms */
|
||||
background-color: rgba(191, 625, 106, 0.2);
|
||||
border-color: var(--nord11);
|
||||
}
|
||||
|
||||
.latency-unavailable {
|
||||
background-color: rgba(129, 161, 193, 0.2); /* --nord9, blue for N/A */
|
||||
background-color: rgba(129, 161, 193, 0.2);
|
||||
border-color: var(--nord9);
|
||||
}
|
||||
|
||||
@ -181,7 +181,7 @@ code {
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 100%;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
margin-left: clamp(-90px, -10vw, -110px);
|
||||
transition: opacity 0.3s;
|
||||
@ -257,6 +257,10 @@ code {
|
||||
color: var(--nord11); /* Red */
|
||||
}
|
||||
|
||||
.log-level-debug { /* Added for potential debug logs */
|
||||
color: var(--nord9); /* Blue */
|
||||
}
|
||||
|
||||
.json-toggle {
|
||||
color: var(--nord8);
|
||||
cursor: pointer;
|
||||
@ -267,9 +271,64 @@ code {
|
||||
background-color: var(--nord1);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--nord5);
|
||||
margin-top: 5px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Filter Container Styles */
|
||||
.filter-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-container label {
|
||||
color: var(--nord5);
|
||||
font-size: 0.9rem;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.filter-container input[type="radio"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.filter-container input[type="datetime-local"], .filter-container button {
|
||||
background-color: var(--nord2);
|
||||
color: var(--nord5);
|
||||
border: 1px solid var(--nord3);
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.filter-container input[type="datetime-local"]:focus, .filter-container button:focus {
|
||||
outline: none;
|
||||
border-color: var(--nord8);
|
||||
}
|
||||
|
||||
.filter-container button {
|
||||
background-color: var(--nord10);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-container button:hover {
|
||||
background-color: var(--nord8);
|
||||
}
|
||||
|
||||
.filter-container fieldset {
|
||||
color: var(--nord5);
|
||||
border: 1px solid var(--nord3);
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.filter-container legend {
|
||||
color: var(--nord6);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
@ -4,19 +4,23 @@
|
||||
<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">
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="header-container">
|
||||
<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>
|
||||
<p>Service UUID: <code>{{ service_uuid }}</code></p> <!-- Always display service UUID -->
|
||||
</div>
|
||||
|
||||
<div id="node-grid-container">
|
||||
<p class="loading-message">Loading node data...</p>
|
||||
</div>
|
||||
|
||||
<script src="/static/script.js"></script>
|
||||
<script>
|
||||
// Pass the base path for API calls if mounted under a subpath
|
||||
window.API_BASE_PATH = "{{ root_path }}";
|
||||
</script>
|
||||
<script src="{{ url_for('static', path='/script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,21 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<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 -->
|
||||
<!-- Removed: <meta http-equiv="refresh" content="5"> -->
|
||||
<title>Node Monitor Logs - {{ service_uuid }}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='/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>
|
||||
<p>Total Logs: <span id="log-count">{{ log_count }}</span> (Auto-refreshing every 5 seconds)</p>
|
||||
<div class="filter-container">
|
||||
<fieldset>
|
||||
<legend>Log Level</legend>
|
||||
<label><input type="radio" name="log-level" value="" checked> All</label>
|
||||
<label><input type="radio" name="log-level" value="INFO"> INFO</label>
|
||||
<label><input type="radio" name="log-level" value="WARNING"> WARNING</label>
|
||||
<label><input type="radio" name="log-level" value="ERROR"> ERROR</label>
|
||||
</fieldset>
|
||||
<label for="since-filter">Since (ISO):</label>
|
||||
<input type="datetime-local" id="since-filter" placeholder="2025-06-11T13:32:00">
|
||||
<button id="apply-filters">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="log-table-container">
|
||||
<p class="loading-message">Loading logs...</p>
|
||||
<div id="log-table-container" data-service-uuid="{{ service_uuid }}">
|
||||
{% if logs %}
|
||||
<table class="log-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Level</th>
|
||||
<th>Message</th>
|
||||
<th>Extra</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td>{{ log.timestamp }}</td>
|
||||
<td class="log-level log-level-{{ (log.level or '').lower() }}">{{ log.level }}</td>
|
||||
<td>{{ log.message }}</td>
|
||||
<td>
|
||||
{% if log.extra %}
|
||||
<div class="json-toggle">Show JSON</div>
|
||||
<pre class="json-content" style="display: none;">{{ log.extra | tojson(indent=2) }}</pre>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="loading-message">No logs available on server.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script src="/static/logs.js"></script>
|
||||
<script>
|
||||
// Pass the base path for API calls if mounted under a subpath
|
||||
window.API_BASE_PATH = "{{ root_path }}";
|
||||
</script>
|
||||
<script src="{{ url_for('static', path='/logs.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Reference in New Issue
Block a user