Fix project deletion 500 error, select dropdown option styling, 3-level watchdog hierarchy traversal, and filter out heartbeat workers as parent candidates
This commit is contained in:
+79
-41
@@ -111,12 +111,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if ($name) {
|
if ($name) {
|
||||||
try {
|
try {
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
// Update: Slug is immutable!
|
|
||||||
$stmt = $pdo->prepare('UPDATE dc_projects SET name = :n, default_cache_ttl_hours = :t, notes = :notes WHERE id = :id');
|
$stmt = $pdo->prepare('UPDATE dc_projects SET name = :n, default_cache_ttl_hours = :t, notes = :notes WHERE id = :id');
|
||||||
$stmt->execute([':n' => $name, ':t' => $ttl, ':notes' => $notes, ':id' => $id]);
|
$stmt->execute([':n' => $name, ':t' => $ttl, ':notes' => $notes, ':id' => $id]);
|
||||||
$msg = "Projekt '{$name}' wurde aktualisiert.";
|
$msg = "Projekt '{$name}' wurde aktualisiert.";
|
||||||
} else {
|
} else {
|
||||||
// New Project
|
|
||||||
if ($slug) {
|
if ($slug) {
|
||||||
$stmt = $pdo->prepare('INSERT INTO dc_projects (slug, name, default_cache_ttl_hours, notes) VALUES (:s, :n, :t, :notes)');
|
$stmt = $pdo->prepare('INSERT INTO dc_projects (slug, name, default_cache_ttl_hours, notes) VALUES (:s, :n, :t, :notes)');
|
||||||
$stmt->execute([':s' => $slug, ':n' => $name, ':t' => $ttl, ':notes' => $notes]);
|
$stmt->execute([':s' => $slug, ':n' => $name, ':t' => $ttl, ':notes' => $notes]);
|
||||||
@@ -130,22 +128,47 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete Project (with Safety Confirmation)
|
// Delete Project (Clean transaction cascade to avoid 500 errors)
|
||||||
if ($action === 'delete_project') {
|
if ($action === 'delete_project') {
|
||||||
$id = (int)($_POST['project_id'] ?? 0);
|
$id = (int)($_POST['project_id'] ?? 0);
|
||||||
$confirmSlug = trim($_POST['confirm_slug'] ?? '');
|
$confirmSlug = trim($_POST['confirm_slug'] ?? '');
|
||||||
|
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
$stmt = $pdo->prepare('SELECT slug, name FROM dc_projects WHERE id = :id');
|
try {
|
||||||
$stmt->execute([':id' => $id]);
|
$stmt = $pdo->prepare('SELECT slug, name FROM dc_projects WHERE id = :id');
|
||||||
$proj = $stmt->fetch();
|
$stmt->execute([':id' => $id]);
|
||||||
|
$proj = $stmt->fetch();
|
||||||
|
|
||||||
if ($proj && hash_equals($proj['slug'], $confirmSlug)) {
|
if ($proj && hash_equals($proj['slug'], $confirmSlug)) {
|
||||||
$delStmt = $pdo->prepare('DELETE FROM dc_projects WHERE id = :id');
|
$pdo->beginTransaction();
|
||||||
$delStmt->execute([':id' => $id]);
|
|
||||||
$msg = "Projekt '{$proj['name']}' ({$proj['slug']}) wurde unwiderruflich gelöscht.";
|
// 1. Delete associated activations
|
||||||
} else {
|
$pdo->prepare('DELETE a FROM license_activations a JOIN license_licenses l ON a.license_id = l.id WHERE l.product_id = :id')
|
||||||
$msg = "Sicherheitsbestätigung fehlgeschlagen! Der eingegebene Slug stimmte nicht überein.";
|
->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]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
$msg = "Projekt '{$proj['name']}' ({$proj['slug']}) und zugehörige Daten wurden gelöscht.";
|
||||||
|
} else {
|
||||||
|
$msg = "Sicherheitsbestätigung fehlgeschlagen! Der eingegebene Slug stimmte nicht überein.";
|
||||||
|
$msgType = 'danger';
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
$msg = "Fehler beim Löschen des Projekts: " . $e->getMessage();
|
||||||
$msgType = 'danger';
|
$msgType = 'danger';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,41 +392,47 @@ $auditLogs = $pdo->query('SELECT * FROM license_audit_log ORDER BY created_at DE
|
|||||||
$monitorRepo = new MonitorRepo($pdo);
|
$monitorRepo = new MonitorRepo($pdo);
|
||||||
$monitors = $monitorRepo->getAllMonitors();
|
$monitors = $monitorRepo->getAllMonitors();
|
||||||
|
|
||||||
// Flatten Tree for Hierarchy Rendering
|
// Flatten Tree for Hierarchy Rendering (Up to 3 levels: Hypervisor -> Host -> App)
|
||||||
function buildMonitorTree(array $monitors): array {
|
function buildMonitorTree(array $monitors): array {
|
||||||
$bySource = [];
|
$childrenOf = [];
|
||||||
|
$bySource = [];
|
||||||
|
|
||||||
foreach ($monitors as $m) {
|
foreach ($monitors as $m) {
|
||||||
$bySource[$m['source']] = $m;
|
$bySource[$m['source']] = $m;
|
||||||
$bySource[$m['source']]['children'] = [];
|
$parent = !empty($m['parent_source']) ? $m['parent_source'] : '__ROOT__';
|
||||||
|
$childrenOf[$parent][] = $m['source'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$tree = [];
|
|
||||||
foreach ($bySource as $source => &$m) {
|
|
||||||
$parent = $m['parent_source'] ?? null;
|
|
||||||
if (!empty($parent) && isset($bySource[$parent])) {
|
|
||||||
$bySource[$parent]['children'][] = &$m;
|
|
||||||
} else {
|
|
||||||
$tree[] = &$m;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unset($m);
|
|
||||||
|
|
||||||
$flat = [];
|
$flat = [];
|
||||||
$walk = function(array $nodes, int $depth = 0) use (&$walk, &$flat) {
|
$walk = function(string $parentSource, int $depth = 0) use (&$walk, &$flat, $childrenOf, $bySource) {
|
||||||
|
if ($depth > 3) return;
|
||||||
|
if (empty($childrenOf[$parentSource])) return;
|
||||||
|
|
||||||
|
$nodes = $childrenOf[$parentSource];
|
||||||
$count = count($nodes);
|
$count = count($nodes);
|
||||||
for ($i = 0; $i < $count; $i++) {
|
for ($i = 0; $i < $count; $i++) {
|
||||||
$node = $nodes[$i];
|
$src = $nodes[$i];
|
||||||
|
if (!isset($bySource[$src])) continue;
|
||||||
|
$node = $bySource[$src];
|
||||||
$node['depth'] = $depth;
|
$node['depth'] = $depth;
|
||||||
$node['is_last'] = ($i === $count - 1);
|
$node['is_last'] = ($i === $count - 1);
|
||||||
$children = $node['children'] ?? [];
|
|
||||||
unset($node['children']);
|
|
||||||
$flat[] = $node;
|
$flat[] = $node;
|
||||||
if (!empty($children)) {
|
|
||||||
$walk($children, $depth + 1);
|
$walk($src, $depth + 1);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
$walk($tree, 0);
|
|
||||||
|
$walk('__ROOT__', 0);
|
||||||
|
|
||||||
|
// Append any unvisited monitors as root level
|
||||||
|
$visited = array_column($flat, 'source');
|
||||||
|
foreach ($monitors as $m) {
|
||||||
|
if (!in_array($m['source'], $visited, true)) {
|
||||||
|
$m['depth'] = 0;
|
||||||
|
$m['is_last'] = true;
|
||||||
|
$flat[] = $m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $flat;
|
return $flat;
|
||||||
}
|
}
|
||||||
@@ -550,6 +579,12 @@ $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 { 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); }
|
.form-input:focus { border-color: var(--primary); }
|
||||||
|
|
||||||
|
/* Dropdown Option List Styling Fix (Readable text across all browsers) */
|
||||||
|
select.form-input option {
|
||||||
|
background-color: #0e121c;
|
||||||
|
color: #EDEFF5;
|
||||||
|
}
|
||||||
|
|
||||||
.prompt-box { background: rgba(0,0,0,0.5); border: 1px solid var(--border-glass); border-radius: 10px; padding: 1.1rem; font-family: 'Roboto Mono', monospace; font-size: 0.825rem; color: #cbd5e1; white-space: pre-wrap; word-break: break-all; margin-bottom: 0.75rem; position: relative; max-height: 380px; overflow-y: auto; }
|
.prompt-box { background: rgba(0,0,0,0.5); border: 1px solid var(--border-glass); border-radius: 10px; padding: 1.1rem; font-family: 'Roboto Mono', monospace; font-size: 0.825rem; color: #cbd5e1; white-space: pre-wrap; word-break: break-all; margin-bottom: 0.75rem; position: relative; max-height: 380px; overflow-y: auto; }
|
||||||
|
|
||||||
.alert { padding: 1rem 1.25rem; border-radius: 12px; margin-bottom: 1.5rem; font-size: 0.875rem; font-weight: 600; }
|
.alert { padding: 1rem 1.25rem; border-radius: 12px; margin-bottom: 1.5rem; font-size: 0.875rem; font-weight: 600; }
|
||||||
@@ -1124,10 +1159,10 @@ $baseUrl = $protocol . '://' . $host;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Subtab: Watchdog Hierarchie & Tree View -->
|
<!-- Subtab: Watchdog Hierarchie & Tree View (3 Ebenen Support) -->
|
||||||
<div id="sub-watchdog-hierarchy" class="subtab-content">
|
<div id="sub-watchdog-hierarchy" class="subtab-content">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header"><h2 class="card-title">🌳 Proxmox & Host System-Hierarchie</h2></div>
|
<div class="card-header"><h2 class="card-title">🌳 Proxmox, Host & Worker System-Hierarchie (3 Ebenen)</h2></div>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -1135,7 +1170,7 @@ $baseUrl = $protocol . '://' . $host;
|
|||||||
<th>Typ</th>
|
<th>Typ</th>
|
||||||
<th>Parent Entity</th>
|
<th>Parent Entity</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Verknüpfen (Parent)</th>
|
<th>Parent Verknüpfen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -1164,9 +1199,10 @@ $baseUrl = $protocol . '://' . $host;
|
|||||||
<select name="parent_source" class="form-input" style="padding:0.25rem 0.5rem; font-size:0.75rem;" onchange="this.form.submit()">
|
<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>
|
<option value="">-- Keine (Top Level) --</option>
|
||||||
<?php foreach ($monitors as $pCandidate): ?>
|
<?php foreach ($monitors as $pCandidate): ?>
|
||||||
<?php if ($pCandidate['source'] !== $m['source']): ?>
|
<!-- 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' : '' ?>>
|
<option value="<?= htmlspecialchars($pCandidate['source']) ?>" <?= $m['parent_source'] === $pCandidate['source'] ? 'selected' : '' ?>>
|
||||||
<?= htmlspecialchars($pCandidate['source']) ?>
|
<?= htmlspecialchars($pCandidate['source']) ?> (<?= htmlspecialchars($pCandidate['type']) ?>)
|
||||||
</option>
|
</option>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -1205,7 +1241,9 @@ $baseUrl = $protocol . '://' . $host;
|
|||||||
<select name="parent_source" class="form-input">
|
<select name="parent_source" class="form-input">
|
||||||
<option value="">-- Keine (Top Level) --</option>
|
<option value="">-- Keine (Top Level) --</option>
|
||||||
<?php foreach ($monitors as $pm): ?>
|
<?php foreach ($monitors as $pm): ?>
|
||||||
<option value="<?= htmlspecialchars($pm['source']) ?>"><?= htmlspecialchars($pm['source']) ?></option>
|
<?php if ($pm['type'] !== 'heartbeat'): ?>
|
||||||
|
<option value="<?= htmlspecialchars($pm['source']) ?>"><?= htmlspecialchars($pm['source']) ?> (<?= htmlspecialchars($pm['type']) ?>)</option>
|
||||||
|
<?php endif; ?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -1519,7 +1557,7 @@ $baseUrl = $protocol . '://' . $host;
|
|||||||
],
|
],
|
||||||
'watchdog': [
|
'watchdog': [
|
||||||
{ id: 'sub-watchdog-dashboard', label: '📊 Dashboard', active: true },
|
{ id: 'sub-watchdog-dashboard', label: '📊 Dashboard', active: true },
|
||||||
{ id: 'sub-watchdog-hierarchy', label: '🌳 System-Hierarchie' },
|
{ id: 'sub-watchdog-hierarchy', label: '🌳 System-Hierarchie (3 Ebenen)' },
|
||||||
{ id: 'sub-watchdog-add', label: '➕ Monitor Hinzufügen' },
|
{ id: 'sub-watchdog-add', label: '➕ Monitor Hinzufügen' },
|
||||||
{ id: 'sub-watchdog-uptime', label: '⏱️ Uptime & CRUD' },
|
{ id: 'sub-watchdog-uptime', label: '⏱️ Uptime & CRUD' },
|
||||||
{ id: 'sub-watchdog-eventlog', label: '📜 Event-Log' },
|
{ id: 'sub-watchdog-eventlog', label: '📜 Event-Log' },
|
||||||
|
|||||||
Reference in New Issue
Block a user