feat: implement real-time updates and enhance monitoring system
- Add structured logging with slog throughout application - Implement real-time updates using Server-Sent Events and HTMX - Add broadcaster system for instant UI updates when agents report stats - Replace meta refresh with HTMX-powered seamless updates - Add new API endpoints for HTMX fragments and SSE events - Update templates to use HTMX for instant data refresh - Enhance README with real-time features and updated documentation - Remove obsolete template generation file
This commit is contained in:
@@ -9,9 +9,13 @@ templ AgentDetail(agent *store.AgentStats) {
|
||||
@BaseLayout(agent.Hostname, agentDetailContent(agent))
|
||||
}
|
||||
|
||||
templ AgentDetailContent(agent *store.AgentStats) {
|
||||
@agentDetailContent(agent)
|
||||
}
|
||||
|
||||
templ agentDetailContent(agent *store.AgentStats) {
|
||||
<a href="/" class="back-link">← Back to Dashboard</a>
|
||||
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
|
||||
<h2 style="margin: 0;">{ agent.Hostname }</h2>
|
||||
@AgentStatusBadge(agent.LastSeen)
|
||||
@@ -21,14 +25,16 @@ templ agentDetailContent(agent *store.AgentStats) {
|
||||
<div class="grid" style="grid-template-columns: 1fr;">
|
||||
<div class="card">
|
||||
<div class="card-title">CPU Usage</div>
|
||||
<div class="metric-value" style="font-size: 2rem; margin: 1rem 0;">
|
||||
{ FormatPercent(agent.CPUUsage) }
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class={ "progress-fill", calcProgressClass(agent.CPUUsage/100) } style={ fmt.Sprintf("width: %.1f%%", agent.CPUUsage) }></div>
|
||||
</div>
|
||||
<div style="margin-top: 1rem; font-size: 0.875rem; color: #94a3b8;">
|
||||
Last updated: { FormatTime(agent.LastSeen) }
|
||||
<div id="cpu-stats" hx-get={ templ.SafeURL("/api/agents/" + agent.ID + "/stats") } hx-trigger="stats-update" hx-swap="innerHTML">
|
||||
<div class="metric-value" style="font-size: 2rem; margin: 1rem 0;">
|
||||
{ FormatPercent(agent.CPUUsage) }
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" class={ "progress-fill", calcProgressClass(agent.CPUUsage/100) } style={ fmt.Sprintf("width: %.1f%%", agent.CPUUsage) }></div>
|
||||
</div>
|
||||
<div style="margin-top: 1rem; font-size: 0.875rem; color: #94a3b8;">
|
||||
Last updated: { FormatTime(agent.LastSeen) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,74 +8,80 @@ templ Dashboard(agents []*store.AgentStats, staleAgents []*store.AgentStats) {
|
||||
@BaseLayout("Dashboard", dashboardContent(agents, staleAgents))
|
||||
}
|
||||
|
||||
templ DashboardContent(agents []*store.AgentStats, staleAgents []*store.AgentStats) {
|
||||
@dashboardContent(agents, staleAgents)
|
||||
}
|
||||
|
||||
templ dashboardContent(agents []*store.AgentStats, staleAgents []*store.AgentStats) {
|
||||
<h2 style="margin-bottom: 1rem;">Agent Status Overview</h2>
|
||||
|
||||
@StaleAgentAlert(len(staleAgents), nil)
|
||||
|
||||
if len(agents) == 0 {
|
||||
<div class="alert alert-info">
|
||||
<strong>ℹ️ No agents connected</strong>
|
||||
<p style="margin-top: 0.5rem; font-size: 0.875rem;">
|
||||
Start an agent to see its statistics appear here.
|
||||
</p>
|
||||
</div>
|
||||
} else {
|
||||
<div class="card">
|
||||
<div class="card-title">Active Agents</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>CPU Usage</th>
|
||||
<th>Memory</th>
|
||||
<th>Disk</th>
|
||||
<th>Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, agent := range agents {
|
||||
@AgentRow(agent.ID, agent.Hostname, agent.CPUUsage, agent.RAMUsage, agent.RAMTotal, agent.DiskUsage, agent.DiskTotal, agent.LastSeen)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
if len(staleAgents) > 0 {
|
||||
<div class="card" style="margin-top: 2rem; border-color: #d97706;">
|
||||
<div class="card-title" style="color: #f59e0b;">
|
||||
Stale Agents ({ len(staleAgents) })
|
||||
</div>
|
||||
<form method="POST" action="/api/agents/remove-stale" style="margin-top: 1rem;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30px;"><input type="checkbox" id="select-all" onchange="toggleAll(this)" /></th>
|
||||
<th>Hostname</th>
|
||||
<th>Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, agent := range staleAgents {
|
||||
<tr>
|
||||
<td><input type="checkbox" name="agent_ids" value={ agent.ID } class="agent-checkbox" /></td>
|
||||
<td>{ agent.Hostname }</td>
|
||||
<td class="timestamp">{ FormatTime(agent.LastSeen) }</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn btn-danger" style="margin-top: 1rem;">Remove Selected</button>
|
||||
|
||||
@StaleAgentAlert(len(staleAgents), nil)
|
||||
|
||||
<div id="agent-table-container" hx-get="/api/dashboard/table" hx-trigger="stats-update" hx-swap="innerHTML">
|
||||
if len(agents) == 0 {
|
||||
<div class="alert alert-info">
|
||||
<strong>ℹ️ No agents connected</strong>
|
||||
<p style="margin-top: 0.5rem; font-size: 0.875rem;">
|
||||
Start an agent to see its statistics appear here.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
} else {
|
||||
<div class="card">
|
||||
<div class="card-title">Active Agents</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>CPU Usage</th>
|
||||
<th>Memory</th>
|
||||
<th>Disk</th>
|
||||
<th>Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, agent := range agents {
|
||||
@AgentRow(agent.ID, agent.Hostname, agent.CPUUsage, agent.RAMUsage, agent.RAMTotal, agent.DiskUsage, agent.DiskTotal, agent.LastSeen)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
function toggleAll(checkbox) {
|
||||
const checkboxes = document.querySelectorAll('.agent-checkbox');
|
||||
checkboxes.forEach(cb => cb.checked = checkbox.checked);
|
||||
|
||||
if len(staleAgents) > 0 {
|
||||
<div class="card" style="margin-top: 2rem; border-color: #d97706;">
|
||||
<div class="card-title" style="color: #f59e0b;">
|
||||
Stale Agents ({ len(staleAgents) })
|
||||
</div>
|
||||
<form method="POST" action="/api/agents/remove-stale" style="margin-top: 1rem;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30px;"><input type="checkbox" id="select-all" onchange="toggleAll(this)" /></th>
|
||||
<th>Hostname</th>
|
||||
<th>Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, agent := range staleAgents {
|
||||
<tr>
|
||||
<td><input type="checkbox" name="agent_ids" value={ agent.ID } class="agent-checkbox" /></td>
|
||||
<td>{ agent.Hostname }</td>
|
||||
<td class="timestamp">{ FormatTime(agent.LastSeen) }</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn btn-danger" style="margin-top: 1rem;">Remove Selected</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function toggleAll(checkbox) {
|
||||
const checkboxes = document.querySelectorAll('.agent-checkbox');
|
||||
checkboxes.forEach(cb => cb.checked = checkbox.checked);
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
//go:generate templ generate
|
||||
|
||||
package views
|
||||
@@ -6,8 +6,31 @@ templ BaseLayout(title string, content templ.Component) {
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="refresh" content="5" />
|
||||
<title>{ title } - Nerd Monitor</title>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Connect to Server-Sent Events for real-time updates
|
||||
const eventSource = new EventSource('/api/events');
|
||||
eventSource.onmessage = function(event) {
|
||||
console.log('SSE message received:', event.data);
|
||||
};
|
||||
eventSource.addEventListener('stats-update', function(event) {
|
||||
console.log('Stats update event:', event.data);
|
||||
// Trigger HTMX updates using HTMX's API
|
||||
const agentTable = document.getElementById('agent-table-container');
|
||||
const cpuStats = document.getElementById('cpu-stats');
|
||||
if (agentTable) htmx.trigger(agentTable, 'stats-update');
|
||||
if (cpuStats) htmx.trigger(cpuStats, 'stats-update');
|
||||
});
|
||||
eventSource.addEventListener('connected', function(event) {
|
||||
console.log('SSE connected');
|
||||
});
|
||||
eventSource.onerror = function(event) {
|
||||
console.log('SSE error:', event);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
|
||||
Reference in New Issue
Block a user