feat: push-based monitoring with remote agent
- Add database migration 006 for agent_key, agent_installed, agent_install_path, agent_last_seen_at columns - Add Server model methods: findByAgentKey, generateAgentKey, regenerateAgentKey, updateAgentLastSeen, setAgentInstalled - Auto-generate agent_key on server creation - Create AgentService for SSH-based install/uninstall of remote agent - Create bin/agent.sh - bash agent script for remote servers - Create install/agent-install.sh and install/agent-uninstall.sh - Create install/servermanager-agent.service systemd unit - Add POST /api/metrics/push endpoint (auth via agent_key) - Add web routes for agent install/uninstall/regenerate-key - Add agent status section to server detail view with install/uninstall modals - Make MonitoringService::saveMetrics() public for reuse
This commit is contained in:
@@ -139,6 +139,58 @@ $canManage = $server_role === 'owner' || $server_role === 'manager';
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<div class="card-header">
|
||||
<h2><i class="fas fa-wifi"></i> Monitoring Agent</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($server['agent_key'])): ?>
|
||||
<dl class="details-list">
|
||||
<dt>Agent Key</dt>
|
||||
<dd>
|
||||
<code id="agentKeyDisplay"><?= substr($server['agent_key'], 0, 12) ?>...<?= substr($server['agent_key'], -4) ?></code>
|
||||
<button class="btn btn-sm btn-secondary" onclick="copyAgentKey()" title="Copy full key">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<input type="hidden" id="agentKeyFull" value="<?= htmlspecialchars($server['agent_key']) ?>">
|
||||
</dd>
|
||||
<dt>Status</dt>
|
||||
<dd>
|
||||
<?php if ($server['agent_installed'] ?? false): ?>
|
||||
<span class="badge badge-success">Installed</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary">Not installed</span>
|
||||
<?php endif; ?>
|
||||
</dd>
|
||||
<?php if (!empty($server['agent_install_path'])): ?>
|
||||
<dt>Install Path</dt>
|
||||
<dd><code><?= htmlspecialchars($server['agent_install_path']) ?></code></dd>
|
||||
<?php endif; ?>
|
||||
<dt>Last Seen</dt>
|
||||
<dd><?= $server['agent_last_seen_at'] ?? 'Never' ?></dd>
|
||||
</dl>
|
||||
<div class="form-actions" style="margin-top:1rem">
|
||||
<?php if ($canManage): ?>
|
||||
<?php if ($server['agent_installed'] ?? false): ?>
|
||||
<button onclick="showAgentModal('uninstall')" class="btn btn-danger">
|
||||
<i class="fas fa-minus-circle"></i> Uninstall Agent
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<button onclick="showAgentModal('install')" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle"></i> Install Agent
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button onclick="regenerateAgentKey()" class="btn btn-secondary" title="Regenerate agent key">
|
||||
<i class="fas fa-key"></i> Regenerate Key
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="text-muted">No agent key configured.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -195,6 +247,28 @@ $canManage = $server_role === 'owner' || $server_role === 'manager';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="agentConfirmModal" class="modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-header">
|
||||
<h3 id="agentModalTitle">Install Monitoring Agent</h3>
|
||||
<button type="button" class="modal-close" onclick="closeAgentModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="agentModalMessage">
|
||||
This will install the monitoring agent on <strong><?= htmlspecialchars($server['name'] ?? '') ?></strong> via SSH.
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
The agent will run as a systemd service, collecting and sending metrics every 60 seconds.
|
||||
It will automatically restart if the server reboots.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="agentModalConfirmBtn" class="btn btn-primary" onclick="confirmAgentAction()">Install</button>
|
||||
<button class="btn btn-secondary" onclick="closeAgentModal()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="csrfForm" style="display:none">
|
||||
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||
</form>
|
||||
@@ -315,6 +389,107 @@ if (historicalData.length > 0) {
|
||||
initChart(historicalData);
|
||||
}
|
||||
|
||||
let pendingAgentAction = null;
|
||||
|
||||
function showAgentModal(action) {
|
||||
pendingAgentAction = action;
|
||||
const modal = document.getElementById('agentConfirmModal');
|
||||
const title = document.getElementById('agentModalTitle');
|
||||
const msg = document.getElementById('agentModalMessage');
|
||||
const btn = document.getElementById('agentModalConfirmBtn');
|
||||
|
||||
if (action === 'install') {
|
||||
title.textContent = 'Install Monitoring Agent';
|
||||
msg.innerHTML = 'This will install the monitoring agent on <strong><?= htmlspecialchars($server['name'] ?? '') ?></strong> via SSH.';
|
||||
btn.textContent = 'Install';
|
||||
btn.className = 'btn btn-primary';
|
||||
} else {
|
||||
title.textContent = 'Uninstall Monitoring Agent';
|
||||
msg.innerHTML = 'This will uninstall the monitoring agent from <strong><?= htmlspecialchars($server['name'] ?? '') ?></strong> via SSH. The service will be stopped and all agent files removed.';
|
||||
btn.textContent = 'Uninstall';
|
||||
btn.className = 'btn btn-danger';
|
||||
}
|
||||
|
||||
modal.style.display = 'flex';
|
||||
modal.onclick = function(e) {
|
||||
if (e.target === modal) closeAgentModal();
|
||||
};
|
||||
}
|
||||
|
||||
function closeAgentModal() {
|
||||
document.getElementById('agentConfirmModal').style.display = 'none';
|
||||
pendingAgentAction = null;
|
||||
}
|
||||
|
||||
function confirmAgentAction() {
|
||||
if (!pendingAgentAction) return;
|
||||
|
||||
const action = pendingAgentAction;
|
||||
const btn = document.getElementById('agentModalConfirmBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = action === 'install' ? 'Installing...' : 'Uninstalling...';
|
||||
|
||||
closeAgentModal();
|
||||
|
||||
fetch('/servers/<?= $server['id'] ?>/agent/' + action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': getCsrfToken(),
|
||||
},
|
||||
body: '_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast('success', action === 'install' ? 'Agent installed successfully.' : 'Agent uninstalled successfully.');
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showToast('error', data.error || (action === 'install' ? 'Installation failed.' : 'Uninstallation failed.'));
|
||||
if (data.output) console.log('Output:', data.output);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
showToast('error', 'Request failed: ' + err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = action === 'install' ? 'Install' : 'Uninstall';
|
||||
});
|
||||
}
|
||||
|
||||
function copyAgentKey() {
|
||||
const key = document.getElementById('agentKeyFull').value;
|
||||
navigator.clipboard.writeText(key).then(() => {
|
||||
showToast('success', 'Agent key copied to clipboard.');
|
||||
}).catch(() => {
|
||||
showToast('error', 'Failed to copy agent key.');
|
||||
});
|
||||
}
|
||||
|
||||
function regenerateAgentKey() {
|
||||
if (!confirm('Regenerate agent key? The current key will stop working immediately.')) return;
|
||||
|
||||
fetch('/servers/<?= $server['id'] ?>/agent/regenerate-key', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': getCsrfToken(),
|
||||
},
|
||||
body: '_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast('success', 'Agent key regenerated.');
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showToast('error', data.error || 'Failed to regenerate key.');
|
||||
}
|
||||
})
|
||||
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
||||
}
|
||||
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user