Merge pull request 'feat: push-based monitoring with remote agent' (#35) from feat/push-monitoring-agent into master

Reviewed-on: #35
This commit was merged in pull request #35.
This commit is contained in:
2026-06-11 18:31:47 -04:00
12 changed files with 731 additions and 1 deletions

83
bin/agent.sh Normal file
View File

@@ -0,0 +1,83 @@
#!/bin/bash
# ServerManager Monitoring Agent
# This script runs as a systemd service on monitored servers.
set -e
CONFIG_FILE="/etc/servermanager/agent.conf"
load_config() {
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
fi
SERVER_URL="${SERVER_URL:-}"
AGENT_KEY="${AGENT_KEY:-}"
INTERVAL="${INTERVAL:-60}"
}
collect_metrics() {
CPU=$(top -bn1 2>/dev/null | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1)
[ -z "$CPU" ] && CPU=0
RAM=$(free 2>/dev/null | grep Mem | awk '{printf "%.1f", $3/$2 * 100}')
[ -z "$RAM" ] && RAM=0
DISK=$(df / 2>/dev/null | tail -1 | awk '{print $5}' | sed 's/%//')
[ -z "$DISK" ] && DISK=0
LOAD=$(cat /proc/loadavg 2>/dev/null | awk '{print $1}')
[ -z "$LOAD" ] && LOAD=0
UPTIME=$(uptime -p 2>/dev/null | sed 's/^up //')
[ -z "$UPTIME" ] && UPTIME=""
}
push_metrics() {
local payload
payload=$(cat <<EOF
{
"agent_key": "$AGENT_KEY",
"cpu": $CPU,
"ram": $RAM,
"disk": $DISK,
"load": $LOAD,
"uptime": "$UPTIME"
}
EOF
)
curl -s -X POST "$SERVER_URL/api/metrics/push" \
-H "Content-Type: application/json" \
-d "$payload" \
--connect-timeout 10 \
--max-time 30 \
-o /dev/null -w "%{http_code}" 2>/dev/null || echo "000"
}
main() {
load_config
if [ -z "$SERVER_URL" ] || [ -z "$AGENT_KEY" ]; then
echo "ERROR: SERVER_URL and AGENT_KEY must be set in $CONFIG_FILE"
exit 1
fi
echo "ServerManager Agent started"
echo "Server: $SERVER_URL"
echo "Interval: ${INTERVAL}s"
sleep 5
while true; do
collect_metrics
http_code=$(push_metrics)
if [ "$http_code" = "200" ]; then
echo "[$(date)] Metrics pushed successfully"
else
echo "[$(date)] Failed to push metrics (HTTP $http_code)"
fi
sleep "$INTERVAL"
done
}
main

View File

@@ -0,0 +1,9 @@
ALTER TABLE servers
ADD COLUMN agent_key VARCHAR(64) DEFAULT NULL AFTER ssh_key,
ADD COLUMN agent_installed TINYINT(1) NOT NULL DEFAULT 0 AFTER agent_key,
ADD COLUMN agent_install_path VARCHAR(255) DEFAULT NULL AFTER agent_installed,
ADD COLUMN agent_last_seen_at DATETIME DEFAULT NULL AFTER agent_install_path;
CREATE UNIQUE INDEX idx_servers_agent_key ON servers(agent_key);
UPDATE servers SET agent_key = LOWER(HEX(RANDOM_BYTES(32))) WHERE agent_key IS NULL;

136
install/agent-install.sh Normal file
View File

@@ -0,0 +1,136 @@
#!/bin/bash
# ServerManager Agent Installer
# Usage: bash agent-install.sh <SERVER_URL> <AGENT_KEY> [INTERVAL]
set -e
SERVER_URL="${1:-}"
AGENT_KEY="${2:-}"
INTERVAL="${3:-60}"
if [ -z "$SERVER_URL" ] || [ -z "$AGENT_KEY" ]; then
echo "Usage: bash agent-install.sh <SERVER_URL> <AGENT_KEY> [INTERVAL]"
exit 1
fi
AGENT_BIN="/usr/local/bin/servermanager-agent"
CONFIG_DIR="/etc/servermanager"
CONFIG_FILE="$CONFIG_DIR/agent.conf"
SERVICE_FILE="/etc/systemd/system/servermanager-agent.service"
echo "[1/5] Creating config directory..."
mkdir -p "$CONFIG_DIR"
echo "[2/5] Installing agent binary..."
cat > "$AGENT_BIN" << 'AGENTSCRIPT'
#!/bin/bash
set -e
CONFIG_FILE="/etc/servermanager/agent.conf"
load_config() {
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
fi
SERVER_URL="${SERVER_URL:-}"
AGENT_KEY="${AGENT_KEY:-}"
INTERVAL="${INTERVAL:-60}"
}
collect_metrics() {
CPU=$(top -bn1 2>/dev/null | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1)
[ -z "$CPU" ] && CPU=0
RAM=$(free 2>/dev/null | grep Mem | awk '{printf "%.1f", $3/$2 * 100}')
[ -z "$RAM" ] && RAM=0
DISK=$(df / 2>/dev/null | tail -1 | awk '{print $5}' | sed 's/%//')
[ -z "$DISK" ] && DISK=0
LOAD=$(cat /proc/loadavg 2>/dev/null | awk '{print $1}')
[ -z "$LOAD" ] && LOAD=0
UPTIME=$(uptime -p 2>/dev/null | sed 's/^up //')
[ -z "$UPTIME" ] && UPTIME=""
}
push_metrics() {
local payload
payload=$(cat <<EOF
{
"agent_key": "$AGENT_KEY",
"cpu": $CPU,
"ram": $RAM,
"disk": $DISK,
"load": $LOAD,
"uptime": "$UPTIME"
}
EOF
)
curl -s -X POST "$SERVER_URL/api/metrics/push" \
-H "Content-Type: application/json" \
-d "$payload" \
--connect-timeout 10 --max-time 30 \
-o /dev/null -w "%{http_code}" 2>/dev/null || echo "000"
}
main() {
load_config
if [ -z "$SERVER_URL" ] || [ -z "$AGENT_KEY" ]; then
echo "ERROR: SERVER_URL and AGENT_KEY must be set in $CONFIG_FILE"
exit 1
fi
echo "ServerManager Agent started (interval: ${INTERVAL}s)"
sleep 5
while true; do
collect_metrics
http_code=$(push_metrics)
if [ "$http_code" = "200" ]; then
echo "[$(date)] Metrics pushed successfully"
else
echo "[$(date)] Failed to push metrics (HTTP $http_code)"
fi
sleep "$INTERVAL"
done
}
main
AGENTSCRIPT
chmod +x "$AGENT_BIN"
echo "[3/5] Writing config..."
cat > "$CONFIG_FILE" << EOF
SERVER_URL="$SERVER_URL"
AGENT_KEY="$AGENT_KEY"
INTERVAL=$INTERVAL
EOF
chmod 600 "$CONFIG_FILE"
echo "[4/5] Installing systemd service..."
cat > "$SERVICE_FILE" << 'UNIT'
[Unit]
Description=ServerManager Monitoring Agent
Documentation=https://github.com/servermanager
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/servermanager-agent
Restart=always
RestartSec=10
StartLimitIntervalSec=60
StartLimitBurst=5
NoNewPrivileges=yes
ProtectSystem=full
ProtectHome=yes
PrivateDevices=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
UNIT
echo "[5/5] Enabling and starting service..."
systemctl daemon-reload
systemctl enable servermanager-agent
systemctl restart servermanager-agent
echo ""
echo "ServerManager Agent installed successfully!"
echo " Binary: $AGENT_BIN"
echo " Config: $CONFIG_FILE"
echo " Service: $SERVICE_FILE"

View File

@@ -0,0 +1,22 @@
#!/bin/bash
# ServerManager Agent Uninstaller
set -e
AGENT_BIN="/usr/local/bin/servermanager-agent"
CONFIG_DIR="/etc/servermanager"
SERVICE_FILE="/etc/systemd/system/servermanager-agent.service"
echo "[1/3] Stopping and disabling service..."
systemctl stop servermanager-agent 2>/dev/null || true
systemctl disable servermanager-agent 2>/dev/null || true
systemctl daemon-reload
echo "[2/3] Removing files..."
rm -f "$AGENT_BIN"
rm -rf "$CONFIG_DIR"
rm -f "$SERVICE_FILE"
echo "[3/3] Reloading systemd..."
systemctl daemon-reload
echo "ServerManager Agent uninstalled successfully."

View File

@@ -0,0 +1,21 @@
[Unit]
Description=ServerManager Monitoring Agent
Documentation=https://github.com/servermanager
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/servermanager-agent
Restart=always
RestartSec=10
StartLimitIntervalSec=60
StartLimitBurst=5
NoNewPrivileges=yes
ProtectSystem=full
ProtectHome=yes
PrivateDevices=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target

View File

@@ -122,3 +122,8 @@ $router->get('/api/users', [ApiController::class, 'users']);
$router->get('/api/profile', [ApiController::class, 'profile']); $router->get('/api/profile', [ApiController::class, 'profile']);
$router->put('/api/profile', [ApiController::class, 'updateProfile']); $router->put('/api/profile', [ApiController::class, 'updateProfile']);
$router->get('/api/refresh-metrics', [ApiController::class, 'refreshAllMetrics']); $router->get('/api/refresh-metrics', [ApiController::class, 'refreshAllMetrics']);
$router->post('/api/metrics/push', [ApiController::class, 'pushMetrics']);
$router->post('/servers/:id/agent/install', [ServerController::class, 'agentInstall'], [AuthMiddleware::class, CSRFMiddleware::class]);
$router->post('/servers/:id/agent/uninstall', [ServerController::class, 'agentUninstall'], [AuthMiddleware::class, CSRFMiddleware::class]);
$router->post('/servers/:id/agent/regenerate-key', [ServerController::class, 'agentRegenerateKey'], [AuthMiddleware::class, CSRFMiddleware::class]);

View File

@@ -114,6 +114,8 @@ class ApiController
$monitoringService = new MonitoringService(); $monitoringService = new MonitoringService();
$metrics = $monitoringService->getLatestMetrics($id); $metrics = $monitoringService->getLatestMetrics($id);
unset($server['agent_key'], $server['ssh_password'], $server['ssh_key']);
$this->view->json([ $this->view->json([
'success' => true, 'success' => true,
'data' => [ 'data' => [
@@ -296,6 +298,43 @@ class ApiController
]); ]);
} }
public function pushMetrics(): void
{
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['agent_key'])) {
$this->view->json(['error' => 'agent_key is required'], 401);
}
$serverModel = new Server();
$server = $serverModel->findByAgentKey($input['agent_key']);
if (!$server || !$server['is_active']) {
$this->view->json(['error' => 'Invalid agent key'], 401);
}
$cpu = isset($input['cpu']) ? max(0, min(100, (float) $input['cpu'])) : 0;
$ram = isset($input['ram']) ? max(0, min(100, (float) $input['ram'])) : 0;
$disk = isset($input['disk']) ? max(0, min(100, (float) $input['disk'])) : 0;
$load = (float) ($input['load'] ?? 0);
$uptime = (string) ($input['uptime'] ?? '');
$monitoring = new MonitoringService();
$monitoring->saveMetrics([
'server_id' => (int) $server['id'],
'status' => 'online',
'cpu_usage' => $cpu,
'ram_usage' => $ram,
'disk_usage' => $disk,
'load_average' => $load,
'uptime' => $uptime,
]);
$serverModel->updateAgentLastSeen((int) $server['id']);
$this->view->json(['success' => true]);
}
public function refreshAllMetrics(): void public function refreshAllMetrics(): void
{ {
$user = $this->authenticateRequest(); $user = $this->authenticateRequest();

View File

@@ -255,6 +255,40 @@ class ServerController
]); ]);
} }
public function agentInstall(int $id): void
{
$server = $this->requireServerAccess($id, 'manager');
$agentService = new \ServerManager\Services\AgentService();
$result = $agentService->install($id);
$this->view->json($result);
}
public function agentUninstall(int $id): void
{
$server = $this->requireServerAccess($id, 'manager');
$agentService = new \ServerManager\Services\AgentService();
$result = $agentService->uninstall($id);
$this->view->json($result);
}
public function agentRegenerateKey(int $id): void
{
$this->requireServerAccess($id, 'manager');
$newKey = $this->serverModel->regenerateAgentKey($id);
$this->auditService->log('agent_key_regenerated', 'server', $id);
$this->view->json([
'success' => true,
'agent_key' => $newKey,
]);
}
public function testConnection(int $id): void public function testConnection(int $id): void
{ {
$server = $this->requireServerAccess($id, 'viewer'); $server = $this->requireServerAccess($id, 'viewer');

View File

@@ -122,6 +122,10 @@ class Server
$data['ssh_key'] = null; $data['ssh_key'] = null;
} }
if (empty($data['agent_key'])) {
$data['agent_key'] = $this->generateAgentKey();
}
$data['created_at'] = date('Y-m-d H:i:s'); $data['created_at'] = date('Y-m-d H:i:s');
$data['updated_at'] = date('Y-m-d H:i:s'); $data['updated_at'] = date('Y-m-d H:i:s');
@@ -405,4 +409,47 @@ class Server
[$userId] [$userId]
); );
} }
public function findByAgentKey(string $key): ?array
{
$server = $this->db->fetch(
'SELECT * FROM servers WHERE agent_key = ? AND status = \'active\'',
[$key]
);
if ($server) {
$server = $this->encryption->decryptArray($server, $this->encryptedFields);
}
return $server;
}
public function generateAgentKey(): string
{
return bin2hex(random_bytes(32));
}
public function regenerateAgentKey(int $id): string
{
$key = $this->generateAgentKey();
$this->db->update('servers', [
'agent_key' => $key,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$id]);
return $key;
}
public function updateAgentLastSeen(int $id): bool
{
return $this->db->update('servers', [
'agent_last_seen_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$id]) > 0;
}
public function setAgentInstalled(int $id, bool $installed, ?string $path = null): bool
{
return $this->db->update('servers', [
'agent_installed' => $installed ? 1 : 0,
'agent_install_path' => $path,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$id]) > 0;
}
} }

View File

@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace ServerManager\Services;
use ServerManager\Core\App;
use ServerManager\Core\Database;
use ServerManager\Core\Logger;
class AgentService
{
private Database $db;
private Logger $logger;
private string $basePath;
public function __construct()
{
$this->db = Database::getInstance();
$this->logger = Logger::getInstance();
$this->basePath = dirname(__DIR__, 2);
}
public function install(int $serverId): array
{
$server = $this->db->fetch(
'SELECT * FROM servers WHERE id = ? AND status = \'active\' AND is_active = 1',
[$serverId]
);
if (!$server) {
return ['success' => false, 'error' => 'Server not found or inactive'];
}
$server = App::getEncryption()->decryptArray($server, ['ssh_password', 'ssh_key']);
$agentKey = $server['agent_key'];
$serverUrl = rtrim((App::getConfig()['app']['url'] ?? ''), '/');
if (empty($serverUrl)) {
$serverUrl = ($_SERVER['HTTPS'] ?? '' === 'on' ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? 'localhost');
}
$scriptPath = $this->basePath . '/install/agent-install.sh';
if (!file_exists($scriptPath)) {
return ['success' => false, 'error' => 'Agent install script not found'];
}
$script = file_get_contents($scriptPath);
$remoteCommand = sprintf(
'echo %s | base64 -d | bash -s -- %s %s 2>&1',
escapeshellarg(base64_encode($script)),
escapeshellarg($serverUrl),
escapeshellarg($agentKey)
);
try {
$ssh = new SSHService();
if (!$ssh->connect($server, true)) {
return ['success' => false, 'error' => 'SSH connection failed'];
}
$result = $ssh->exec($remoteCommand, 120);
$ssh->disconnect();
$output = trim($result['output']);
$exitStatus = $result['exit_status'] ?? -1;
if ($exitStatus !== 0) {
return [
'success' => false,
'error' => 'Agent installation failed on remote server',
'output' => $output,
];
}
$path = '/usr/local/bin/servermanager-agent';
$serverModel = new \ServerManager\Models\Server();
$serverModel->setAgentInstalled($serverId, true, $path);
$auditService = new AuditService();
$auditService->log('agent_installed', 'server', $serverId, [
'name' => $server['name'],
'path' => $path,
]);
return ['success' => true, 'path' => $path, 'output' => $output];
} catch (\Throwable $e) {
$this->logger->error("Agent install failed for server {$serverId}: " . $e->getMessage());
return ['success' => false, 'error' => $e->getMessage()];
}
}
public function uninstall(int $serverId): array
{
$server = $this->db->fetch(
'SELECT * FROM servers WHERE id = ? AND status = \'active\' AND is_active = 1',
[$serverId]
);
if (!$server) {
return ['success' => false, 'error' => 'Server not found or inactive'];
}
$server = App::getEncryption()->decryptArray($server, ['ssh_password', 'ssh_key']);
$scriptPath = $this->basePath . '/install/agent-uninstall.sh';
if (!file_exists($scriptPath)) {
return ['success' => false, 'error' => 'Agent uninstall script not found'];
}
$script = file_get_contents($scriptPath);
$remoteCommand = sprintf(
'echo %s | base64 -d | bash 2>&1',
escapeshellarg(base64_encode($script))
);
try {
$ssh = new SSHService();
if (!$ssh->connect($server, true)) {
return ['success' => false, 'error' => 'SSH connection failed'];
}
$result = $ssh->exec($remoteCommand, 60);
$ssh->disconnect();
$output = trim($result['output']);
$exitStatus = $result['exit_status'] ?? -1;
$serverModel = new \ServerManager\Models\Server();
$serverModel->setAgentInstalled($serverId, false, null);
$auditService = new AuditService();
$auditService->log('agent_uninstalled', 'server', $serverId, [
'name' => $server['name'],
]);
if ($exitStatus !== 0) {
return [
'success' => false,
'error' => 'Agent uninstallation may have failed on remote server',
'output' => $output,
];
}
return ['success' => true, 'output' => $output];
} catch (\Throwable $e) {
$this->logger->error("Agent uninstall failed for server {$serverId}: " . $e->getMessage());
$serverModel = new \ServerManager\Models\Server();
$serverModel->setAgentInstalled($serverId, false, null);
return ['success' => false, 'error' => $e->getMessage()];
}
}
}

View File

@@ -111,7 +111,7 @@ class MonitoringService
return trim(str_replace('up ', '', $result['output'])); return trim(str_replace('up ', '', $result['output']));
} }
private function saveMetrics(array $metrics): void public function saveMetrics(array $metrics): void
{ {
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');

View File

@@ -139,6 +139,58 @@ $canManage = $server_role === 'owner' || $server_role === 'manager';
</dl> </dl>
</div> </div>
</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>
<div class="card"> <div class="card">
@@ -195,6 +247,28 @@ $canManage = $server_role === 'owner' || $server_role === 'manager';
</div> </div>
</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()">&times;</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"> <form id="csrfForm" style="display:none">
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>"> <input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
</form> </form>
@@ -315,6 +389,107 @@ if (historicalData.length > 0) {
initChart(historicalData); 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() { function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content || ''; return document.querySelector('meta[name="csrf-token"]')?.content || '';
} }