Add full monitor edit modal, direct token creation per monitor row, and cascade deletion/renaming in Watchdog

This commit is contained in:
Deploymentcenter Bot
2026-08-05 22:58:04 +02:00
parent 9808d143f7
commit 21275d1a1f
2 changed files with 173 additions and 32 deletions
+150 -32
View File
@@ -142,19 +142,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($proj && hash_equals($proj['slug'], $confirmSlug)) {
$pdo->beginTransaction();
// 1. Delete associated activations
$pdo->prepare('DELETE a FROM license_activations a JOIN license_licenses l ON a.license_id = l.id WHERE l.product_id = :id')
->execute([':id' => $id]);
// 2. Delete associated licenses
$pdo->prepare('DELETE FROM license_licenses WHERE product_id = :id')
->execute([':id' => $id]);
// 3. Delete associated releases
$pdo->prepare('DELETE FROM updateservice_releases WHERE product_slug = :slug')
->execute([':slug' => $proj['slug']]);
// 4. Delete project
$delStmt = $pdo->prepare('DELETE FROM dc_projects WHERE id = :id');
$delStmt->execute([':id' => $id]);
@@ -289,19 +285,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
// Edit Watchdog Monitor
// Edit Watchdog Monitor (Full Update)
if ($action === 'edit_watchdog_monitor') {
$oldSource = trim($_POST['old_source'] ?? '');
$newSource = trim($_POST['new_source'] ?? $oldSource);
$type = trim($_POST['type'] ?? 'host');
$group = trim($_POST['group_key'] ?? 'Default');
$parent = trim($_POST['parent_source'] ?? '');
$os = trim($_POST['os'] ?? '');
$interval = (int)($_POST['interval'] ?? 60);
$notes = trim($_POST['notes'] ?? '');
if ($oldSource && $newSource) {
$monRepo = new MonitorRepo($pdo);
$monRepo->updateMonitor($oldSource, $newSource, 'default', $group, $parent, $notes, null, $interval);
$msg = "Monitor '{$newSource}' wurde aktualisiert.";
$monRepo->updateMonitor($oldSource, $newSource, 'default', $type, $group, $parent, $os, $notes, null, $interval);
$msg = "Monitor '{$newSource}' wurde erfolgreich aktualisiert.";
}
}
@@ -330,11 +328,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'create_agent_token') {
$source = trim($_POST['token_source'] ?? '');
$name = trim($_POST['token_name'] ?? '');
if ($name) {
$tokMgr = new TokenManager($pdo);
$res = $tokMgr->createToken($source, $name);
$msg = "Agent-Token für '{$name}' generiert: <strong style='font-family:monospace;'>{$res['raw_token']}</strong>";
if (empty($name)) {
$name = "Token for " . ($source ?: 'General');
}
$tokMgr = new TokenManager($pdo);
$res = $tokMgr->createToken($source, $name);
$msg = "Agent-Token generiert: <strong style='font-family:monospace;'>{$res['raw_token']}</strong>";
}
// Revoke Agent Token
@@ -392,6 +391,17 @@ $auditLogs = $pdo->query('SELECT * FROM license_audit_log ORDER BY created_at DE
$monitorRepo = new MonitorRepo($pdo);
$monitors = $monitorRepo->getAllMonitors();
$tokenManager = new TokenManager($pdo);
$agentTokens = $tokenManager->getAllTokens();
// Map Active Tokens per Monitor Source
$tokensBySource = [];
foreach ($agentTokens as $tok) {
if (!$tok['revoked'] && !empty($tok['monitor_source'])) {
$tokensBySource[$tok['monitor_source']] = $tok;
}
}
// Flatten Tree for Hierarchy Rendering (Up to 3 levels: Hypervisor -> Host -> App)
function buildMonitorTree(array $monitors): array {
$childrenOf = [];
@@ -449,9 +459,6 @@ foreach ($monitors as $m) {
$eventLog = new EventLog($pdo);
$recentEvents = $eventLog->getRecentEvents(100);
$tokenManager = new TokenManager($pdo);
$agentTokens = $tokenManager->getAllTokens();
$updateMgr = new UpdateManager($pdo);
$releases = $updateMgr->getReleases();
@@ -579,7 +586,7 @@ $baseUrl = $protocol . '://' . $host;
.form-input { background: rgba(255,255,255,0.04); border: 1px solid var(--border-glass); border-radius: 10px; padding: 0.65rem 0.85rem; color: #fff; font-size: 0.875rem; outline: none; font-family: 'Manrope', system-ui; transition: border-color 0.2s; }
.form-input:focus { border-color: var(--primary); }
/* Dropdown Option List Styling Fix (Readable text across all browsers) */
/* Dropdown Option List Styling Fix (Readable dark text/bg across all operating systems & browsers) */
select.form-input option {
background-color: #0e121c;
color: #EDEFF5;
@@ -591,10 +598,10 @@ $baseUrl = $protocol . '://' . $host;
.alert-success { background: rgba(18,212,138,0.15); border: 1px solid rgba(18,212,138,0.3); color: #a7f3d0; }
.alert-danger { background: rgba(246,70,93,0.15); border: 1px solid rgba(246,70,93,0.3); color: #fca5a5; }
/* Safety Modal */
/* Modals */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(10px); display: none; align-items: center; justify-content: center; z-index: 200; }
.modal-backdrop.active { display: flex; }
.modal-card { background: #0e121c; border: 1px solid rgba(246,70,93,0.3); border-radius: 18px; padding: 2rem; max-width: 500px; width: 90%; box-shadow: 0 20px 50px rgba(0,0,0,0.8); }
.modal-card { background: #0e121c; border: 1px solid var(--border-glass); border-radius: 18px; padding: 2rem; max-width: 550px; width: 90%; box-shadow: 0 20px 50px rgba(0,0,0,0.8); }
.tab-content, .subtab-content { display: none; }
.tab-content.active, .subtab-content.active { display: block; }
@@ -865,6 +872,59 @@ $baseUrl = $protocol . '://' . $host;
</div>
</div>
<!-- Edit Watchdog Monitor Modal -->
<div class="modal-backdrop" id="editMonitorModal">
<div class="modal-card">
<h2 style="color:var(--primary); margin-bottom:0.75rem;">✏️ Monitor Bearbeiten</h2>
<form method="POST" action="index.php#sub-watchdog-uptime">
<input type="hidden" name="action" value="edit_watchdog_monitor">
<input type="hidden" name="old_source" id="monEditOldSource">
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Monitor Name (source)</label>
<input type="text" name="new_source" id="monEditNewSource" class="form-input" required>
</div>
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Typ</label>
<select name="type" id="monEditType" class="form-input">
<option value="host">Host / Server (Windows / Linux)</option>
<option value="heartbeat">Dienst / Worker (Heartbeat)</option>
<option value="hypervisor_node">Hypervisor Node (Proxmox)</option>
<option value="guest">Guest VM / Container</option>
</select>
</div>
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Übergeordnete Entität (Parent Source)</label>
<select name="parent_source" id="monEditParent" class="form-input">
<option value="">-- Keine (Top Level) --</option>
<?php foreach ($monitors as $pm): ?>
<?php if ($pm['type'] !== 'heartbeat'): ?>
<option value="<?= htmlspecialchars($pm['source']) ?>"><?= htmlspecialchars($pm['source']) ?> (<?= htmlspecialchars($pm['type']) ?>)</option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label">Gruppe</label>
<input type="text" name="group_key" id="monEditGroup" class="form-input">
</div>
<div class="form-group">
<label class="form-label">Betriebssystem / OS</label>
<input type="text" name="os" id="monEditOs" class="form-input">
</div>
<div class="form-group">
<label class="form-label">Intervall (Sekunden)</label>
<input type="number" name="interval" id="monEditInterval" class="form-input" value="60">
</div>
</div>
<div style="display:flex; justify-content:flex-end; gap:0.75rem; margin-top:1.25rem;">
<button type="button" class="btn btn-secondary" onclick="closeEditMonitorModal()">Abbrechen</button>
<button type="submit" class="btn">Änderungen Speichern</button>
</div>
</form>
</div>
</div>
<!-- ================= MODULE 2: LIZENZEN ================= -->
<div id="tab-license" class="tab-content">
@@ -1159,7 +1219,7 @@ $baseUrl = $protocol . '://' . $host;
</div>
</div>
<!-- Subtab: Watchdog Hierarchie & Tree View (3 Ebenen Support) -->
<!-- Subtab: Watchdog Hierarchie & Tree View (3 Ebenen Support & Direct Editing) -->
<div id="sub-watchdog-hierarchy" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">🌳 Proxmox, Host & Worker System-Hierarchie (3 Ebenen)</h2></div>
@@ -1169,8 +1229,9 @@ $baseUrl = $protocol . '://' . $host;
<th>Hierarchie / Entity Source</th>
<th>Typ</th>
<th>Parent Entity</th>
<th>Agent Token</th>
<th>Status</th>
<th>Parent Verknüpfen</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
@@ -1179,6 +1240,8 @@ $baseUrl = $protocol . '://' . $host;
$depth = (int)$m['depth'];
$indentPx = $depth * 24;
$isChild = ($depth > 0);
$hasToken = isset($tokensBySource[$m['source']]);
$tokenData = $hasToken ? $tokensBySource[$m['source']] : null;
?>
<tr>
<td>
@@ -1190,8 +1253,6 @@ $baseUrl = $protocol . '://' . $host;
</div>
</td>
<td><code><?= htmlspecialchars($m['type']) ?></code></td>
<td><?= htmlspecialchars($m['parent_source'] ?? 'Keine (Top Level)') ?></td>
<td><span class="badge badge-<?= $m['state'] === 'up' ? 'up' : 'down' ?>"><?= strtoupper($m['state']) ?></span></td>
<td>
<form method="POST" action="index.php#sub-watchdog-hierarchy" style="display:flex; gap:0.35rem;">
<input type="hidden" name="action" value="link_entity">
@@ -1199,7 +1260,6 @@ $baseUrl = $protocol . '://' . $host;
<select name="parent_source" class="form-input" style="padding:0.25rem 0.5rem; font-size:0.75rem;" onchange="this.form.submit()">
<option value="">-- Keine (Top Level) --</option>
<?php foreach ($monitors as $pCandidate): ?>
<!-- Dienst/Worker (heartbeat) kann NIEMALS Parent sein! -->
<?php if ($pCandidate['source'] !== $m['source'] && $pCandidate['type'] !== 'heartbeat'): ?>
<option value="<?= htmlspecialchars($pCandidate['source']) ?>" <?= $m['parent_source'] === $pCandidate['source'] ? 'selected' : '' ?>>
<?= htmlspecialchars($pCandidate['source']) ?> (<?= htmlspecialchars($pCandidate['type']) ?>)
@@ -1209,6 +1269,27 @@ $baseUrl = $protocol . '://' . $host;
</select>
</form>
</td>
<td>
<?php if ($hasToken): ?>
<code style="font-size:0.75rem; color:var(--success);"><?= htmlspecialchars(substr($tokenData['raw_token'], 0, 10)) ?>...</code>
<?php else: ?>
<form method="POST" action="index.php#sub-watchdog-hierarchy" style="display:inline">
<input type="hidden" name="action" value="create_agent_token">
<input type="hidden" name="token_source" value="<?= htmlspecialchars($m['source']) ?>">
<input type="hidden" name="token_name" value="Token for <?= htmlspecialchars($m['source']) ?>">
<button type="submit" class="btn btn-sm btn-secondary">🔑 Token Erstellen</button>
</form>
<?php endif; ?>
</td>
<td><span class="badge badge-<?= $m['state'] === 'up' ? 'up' : 'down' ?>"><?= strtoupper($m['state']) ?></span></td>
<td style="display:flex; gap:0.35rem;">
<button class="btn btn-sm btn-secondary" onclick="openEditMonitorModal(<?= htmlspecialchars(json_encode($m)) ?>)">✏️ Edit</button>
<form method="POST" action="index.php#sub-watchdog-hierarchy" onsubmit="return confirm('Monitor \'<?= htmlspecialchars($m['source']) ?>\' wirklich löschen?');" style="display:inline">
<input type="hidden" name="action" value="delete_watchdog_monitor">
<input type="hidden" name="source" value="<?= htmlspecialchars($m['source']) ?>">
<button type="submit" class="btn btn-sm btn-danger">🗑️</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
@@ -1216,10 +1297,10 @@ $baseUrl = $protocol . '://' . $host;
</div>
</div>
<!-- Subtab: Add / Edit Monitor Wizard -->
<!-- Subtab: Add Monitor Wizard -->
<div id="sub-watchdog-add" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">🖥️ Monitor Anlegen / Bearbeiten</h2></div>
<div class="card-header"><h2 class="card-title">🖥️ Monitor Anlegen</h2></div>
<form method="POST" action="index.php#sub-watchdog-add">
<input type="hidden" name="action" value="add_watchdog_monitor">
<div class="form-grid">
@@ -1272,29 +1353,43 @@ $baseUrl = $protocol . '://' . $host;
<!-- Subtab: Watchdog Uptime & Full CRUD -->
<div id="sub-watchdog-uptime" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">⏱️ Uptime Monitor & CRUD Verwaltung</h2></div>
<div class="card-header"><h2 class="card-title">⏱️ Uptime Monitor & Vollständige Verwaltung</h2></div>
<table>
<thead>
<tr>
<th>Monitor Source</th>
<th>Typ</th>
<th>Parent</th>
<th>Gruppe</th>
<th>Intervall</th>
<th>Agent Token</th>
<th>Status</th>
<th>Zuletzt Gesehen</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach ($monitors as $m): ?>
<?php
$hasToken = isset($tokensBySource[$m['source']]);
$tokenData = $hasToken ? $tokensBySource[$m['source']] : null;
?>
<tr>
<td><strong><?= htmlspecialchars($m['source']) ?></strong></td>
<td><code><?= htmlspecialchars($m['type']) ?></code></td>
<td><?= htmlspecialchars($m['parent_source'] ?? '-') ?></td>
<td><?= htmlspecialchars($m['group_key'] ?? 'Default') ?></td>
<td><?= $m['expected_interval_sec'] ?>s</td>
<td>
<?php if ($hasToken): ?>
<code style="font-size:0.75rem; color:var(--success);"><?= htmlspecialchars(substr($tokenData['raw_token'], 0, 10)) ?>...</code>
<?php else: ?>
<form method="POST" action="index.php#sub-watchdog-uptime" style="display:inline">
<input type="hidden" name="action" value="create_agent_token">
<input type="hidden" name="token_source" value="<?= htmlspecialchars($m['source']) ?>">
<input type="hidden" name="token_name" value="Token for <?= htmlspecialchars($m['source']) ?>">
<button type="submit" class="btn btn-sm btn-secondary">🔑 Token Erstellen</button>
</form>
<?php endif; ?>
</td>
<td><span class="badge badge-<?= $m['state'] === 'up' ? 'up' : 'down' ?>"><?= strtoupper($m['state']) ?></span></td>
<td><?= htmlspecialchars($m['last_seen_utc'] ?? 'Nie') ?></td>
<td style="display:flex; gap:0.35rem;">
<button class="btn btn-sm btn-secondary" onclick="openEditMonitorModal(<?= htmlspecialchars(json_encode($m)) ?>)">✏️ Bearbeiten</button>
<a href="index.php?action=download_agent&source=<?= urlencode($m['source']) ?>&os=windows" class="btn btn-sm btn-secondary">.ps1</a>
<a href="index.php?action=download_agent&source=<?= urlencode($m['source']) ?>&os=linux" class="btn btn-sm btn-secondary">.sh</a>
@@ -1353,7 +1448,12 @@ $baseUrl = $protocol . '://' . $host;
</div>
<div class="form-group">
<label class="form-label">Gebunden an Source (optional)</label>
<input type="text" name="token_source" class="form-input" placeholder="srv-db-01">
<select name="token_source" class="form-input">
<option value="">-- Alle Sources (Universal) --</option>
<?php foreach ($monitors as $m): ?>
<option value="<?= htmlspecialchars($m['source']) ?>"><?= htmlspecialchars($m['source']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<button type="submit" class="btn">Token Generieren</button>
@@ -1367,6 +1467,7 @@ $baseUrl = $protocol . '://' . $host;
<tr>
<th>Token ID</th>
<th>Bezeichnung</th>
<th>Gebundene Source</th>
<th>Token Value</th>
<th>Status</th>
<th>Aktion</th>
@@ -1381,6 +1482,7 @@ $baseUrl = $protocol . '://' . $host;
<tr>
<td><code><?= htmlspecialchars($tok['token_id']) ?></code></td>
<td><strong><?= htmlspecialchars($tok['name']) ?></strong></td>
<td><?= htmlspecialchars($tok['monitor_source'] ?? 'Alle Sources') ?></td>
<td>
<code id="tok-text-<?= $tok['token_id'] ?>" data-full="<?= htmlspecialchars($rawVal) ?>" data-masked="<?= htmlspecialchars($masked) ?>">
<?= htmlspecialchars($masked) ?>
@@ -1699,6 +1801,22 @@ $baseUrl = $protocol . '://' . $host;
document.getElementById('deleteProjectModal').classList.remove('active');
}
// Monitor Editing Functions
function openEditMonitorModal(m) {
document.getElementById('monEditOldSource').value = m.source;
document.getElementById('monEditNewSource').value = m.source;
document.getElementById('monEditType').value = m.type || 'host';
document.getElementById('monEditParent').value = m.parent_source || '';
document.getElementById('monEditGroup').value = m.group_key || 'Default';
document.getElementById('monEditOs').value = m.os || '';
document.getElementById('monEditInterval').value = m.expected_interval_sec || 60;
document.getElementById('editMonitorModal').classList.add('active');
}
function closeEditMonitorModal() {
document.getElementById('editMonitorModal').classList.remove('active');
}
// License Editing Functions
function openEditLicense(l) {
switchMainTab('license', document.querySelector('.nav-link[onclick*="license"]'));
+23
View File
@@ -81,19 +81,31 @@ class MonitorRepo
string $oldSource,
string $newSource,
string $instance,
?string $type,
?string $groupKey,
?string $parentSource,
?string $os,
?string $notes,
?string $url,
?int $intervalSec,
?string $icon = null,
bool $isMuted = false
): bool {
// Cascade source renaming to children & agent tokens
if ($oldSource !== $newSource) {
$this->db->prepare('UPDATE watchdog_monitors SET parent_source = :new WHERE parent_source = :old')
->execute([':new' => $newSource, ':old' => $oldSource]);
$this->db->prepare('UPDATE watchdog_agent_tokens SET monitor_source = :new WHERE monitor_source = :old')
->execute([':new' => $newSource, ':old' => $oldSource]);
}
$stmt = $this->db->prepare('
UPDATE watchdog_monitors SET
source = :new_source,
type = COALESCE(:type, type),
group_key = :group_key,
parent_source = :parent_source,
os = :os,
notes = :notes,
url = :url,
expected_interval_sec = COALESCE(:interval, expected_interval_sec),
@@ -105,8 +117,10 @@ class MonitorRepo
return $stmt->execute([
':new_source' => $newSource,
':type' => !empty($type) ? $type : null,
':group_key' => !empty($groupKey) ? $groupKey : null,
':parent_source' => !empty($parentSource) ? $parentSource : null,
':os' => !empty($os) ? $os : null,
':notes' => !empty($notes) ? $notes : null,
':url' => !empty($url) ? $url : null,
':interval' => $intervalSec,
@@ -138,6 +152,15 @@ class MonitorRepo
public function deleteMonitor(string $source, string $instance = 'default'): bool
{
// 1. Unlink any children
$this->db->prepare('UPDATE watchdog_monitors SET parent_source = NULL WHERE parent_source = :s')
->execute([':s' => $source]);
// 2. Revoke agent tokens for this monitor
$this->db->prepare('UPDATE watchdog_agent_tokens SET revoked = 1 WHERE monitor_source = :s')
->execute([':s' => $source]);
// 3. Delete monitor
$stmt = $this->db->prepare('DELETE FROM watchdog_monitors WHERE source = :s AND instance = :i');
return $stmt->execute([':s' => $source, ':i' => $instance]);
}