Files
Deploymentcenter/public/index.php
T

2743 lines
165 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/Core/Db.php';
require_once __DIR__ . '/../src/Core/Auth.php';
require_once __DIR__ . '/../src/Modules/License/KeyGen.php';
require_once __DIR__ . '/../src/Modules/License/LicenseService.php';
require_once __DIR__ . '/../src/Modules/Watchdog/MonitorRepo.php';
require_once __DIR__ . '/../src/Modules/Watchdog/EventLog.php';
require_once __DIR__ . '/../src/Modules/Watchdog/TokenManager.php';
require_once __DIR__ . '/../src/Core/TokenManager.php';
require_once __DIR__ . '/../src/Modules/UpdateService/UpdateManager.php';
require_once __DIR__ . '/../src/Modules/Bugtracker/BugRepo.php';
use Deploymentcenter\Core\Db;
use Deploymentcenter\Core\Auth;
use Deploymentcenter\Core\TokenManager as CoreTokenManager;
use Deploymentcenter\Modules\License\KeyGen;
use Deploymentcenter\Modules\Watchdog\MonitorRepo;
use Deploymentcenter\Modules\Watchdog\EventLog;
use Deploymentcenter\Modules\Watchdog\TokenManager;
use Deploymentcenter\Modules\UpdateService\UpdateManager;
use Deploymentcenter\Modules\Bugtracker\BugRepo;
Auth::requireLogin();
$config = require __DIR__ . '/../config/config.php';
$pdo = Db::init($config);
$msg = null;
$msgType = 'success';
// Download Handler: .lic Offline License File
if (isset($_GET['action']) && $_GET['action'] === 'download_lic') {
$licId = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare('SELECT l.*, p.slug as product_slug FROM license_licenses l JOIN dc_projects p ON l.product_id = p.id WHERE l.id = :id');
$stmt->execute([':id' => $licId]);
$lic = $stmt->fetch();
if ($lic) {
$payload = [
'type' => 'offline_license_file',
'issued_at' => time(),
'product' => $lic['product_slug'],
'license_key' => $lic['license_key'],
'customer' => $lic['customer_name'] ?? 'Universal',
'valid_until' => $lic['expires_at'] ? strtotime($lic['expires_at']) : strtotime('+1 year'),
'signature' => 'ED25519_SIG_' . base64_encode(hash('sha256', $lic['license_key'] . 'DC_OFFLINE_SECRET', true))
];
$jsonContent = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$filename = sprintf('%s_%s_offline.lic', $lic['product_slug'], substr($lic['license_key'], 0, 5));
header('Content-Type: application/json');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . strlen($jsonContent));
echo $jsonContent;
exit;
}
}
// Download Handler: Watchdog Agent Installer Script (.ps1 or .sh)
if (isset($_GET['action']) && $_GET['action'] === 'download_agent') {
$source = trim($_GET['source'] ?? 'server-node');
$os = trim($_GET['os'] ?? 'windows');
$token = trim($_GET['token'] ?? 'wd_live_token_default');
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'dc.mhdf.de';
$baseUrl = $protocol . '://' . $host;
if ($os === 'windows') {
$script = "# Deploymentcenter Watchdog Agent Installer (Windows PowerShell)\n"
. "\$WatchdogUrl = \"{$baseUrl}/api/watchdog/v1/ping\"\n"
. "\$Token = \"{$token}\"\n"
. "\$Source = \"{$source}\"\n"
. "Write-Host \"🚀 Initialisiere Watchdog Agent für \$Source...\" -ForegroundColor Cyan\n"
. "\$body = @{ source = \$Source; status = 'ok'; message = 'Heartbeat via PowerShell Task'; interval = 60 } | ConvertTo-Json\n"
. "Invoke-RestMethod -Uri \$WatchdogUrl -Method Post -Body \$body -ContentType 'application/json' -Headers @{ 'X-Agent-Token' = \$Token }\n"
. "Write-Host \"[✔] Heartbeat erfolgreich gesendet!\" -ForegroundColor Green\n";
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=\"watchdog-install-{$source}.ps1\"");
echo $script;
exit;
} else {
$script = "#!/usr/bin/env bash\n"
. "WATCHDOG_URL=\"{$baseUrl}/api/watchdog/v1/ping\"\n"
. "TOKEN=\"{$token}\"\n"
. "SOURCE=\"{$source}\"\n"
. "echo \"🚀 Initialisiere Watchdog Agent für \$SOURCE...\"\n"
. "curl -X POST \"\$WATCHDOG_URL\" -H \"Content-Type: application/json\" -H \"X-Agent-Token: \$TOKEN\" -d '{\"source\": \"'\"\$SOURCE\"'\", \"status\": \"ok\", \"message\": \"Heartbeat via Bash Cron\", \"interval\": 60}'\n"
. "echo \"[✔] Heartbeat gesendet!\"\n";
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=\"watchdog-install-{$source}.sh\"");
echo $script;
exit;
}
}
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
// Create / Edit Project
if ($action === 'save_project') {
$id = (int)($_POST['project_id'] ?? 0);
$slug = trim($_POST['slug'] ?? '');
$name = trim($_POST['name'] ?? '');
$ttl = (int)($_POST['ttl'] ?? 168);
$notes = trim($_POST['notes'] ?? '');
if ($name) {
try {
if ($id > 0) {
$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]);
$msg = "Projekt '{$name}' wurde aktualisiert.";
} else {
if ($slug) {
$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]);
$msg = "Neues Projekt '{$name}' angelegt.";
}
}
} catch (Throwable $e) {
$msg = "Fehler beim Speichern des Projekts: " . $e->getMessage();
$msgType = 'danger';
}
}
}
// Delete Project (Clean transaction cascade to avoid 500 errors)
if ($action === 'delete_project') {
$id = (int)($_POST['project_id'] ?? 0);
$confirmSlug = trim($_POST['confirm_slug'] ?? '');
if ($id > 0) {
try {
$stmt = $pdo->prepare('SELECT slug, name FROM dc_projects WHERE id = :id');
$stmt->execute([':id' => $id]);
$proj = $stmt->fetch();
if ($proj && hash_equals($proj['slug'], $confirmSlug)) {
$pdo->beginTransaction();
$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]);
$pdo->prepare('DELETE FROM license_licenses WHERE product_id = :id')
->execute([':id' => $id]);
$pdo->prepare('DELETE FROM updateservice_releases WHERE product_slug = :slug')
->execute([':slug' => $proj['slug']]);
$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';
}
}
}
// Create License Key
if ($action === 'create_license') {
$productId = (int)($_POST['product_id'] ?? 0);
$customerName = trim($_POST['customer_name'] ?? '');
$customerEmail = trim($_POST['customer_email'] ?? '');
$maxActivations = (int)($_POST['max_activations'] ?? 2);
$expiresAt = !empty($_POST['expires_at']) ? $_POST['expires_at'] . ' 23:59:59' : null;
$notes = trim($_POST['notes'] ?? '');
if ($productId > 0) {
$licenseKey = KeyGen::generateKey();
try {
$stmt = $pdo->prepare('
INSERT INTO license_licenses (product_id, license_key, customer_name, customer_email, max_activations, expires_at, notes)
VALUES (:pid, :key, :cname, :cemail, :max, :exp, :notes)
');
$stmt->execute([
':pid' => $productId,
':key' => $licenseKey,
':cname' => $customerName,
':cemail' => $customerEmail,
':max' => $maxActivations,
':exp' => $expiresAt,
':notes' => $notes
]);
$pdo->prepare('INSERT INTO license_audit_log (actor, action, details) VALUES ("admin", "license.create", :d)')
->execute([':d' => json_encode(['key' => $licenseKey, 'customer' => $customerName])]);
$msg = "Lizenzschlüssel generiert: <strong style='font-family:monospace;'>{$licenseKey}</strong>";
} catch (Throwable $e) {
$msg = "Fehler bei Generierung: " . $e->getMessage();
$msgType = 'danger';
}
}
}
// Edit License
if ($action === 'edit_license') {
$id = (int)($_POST['license_id'] ?? 0);
$customerName = trim($_POST['customer_name'] ?? '');
$customerEmail = trim($_POST['customer_email'] ?? '');
$status = $_POST['status'] ?? 'active';
$maxActivations = (int)($_POST['max_activations'] ?? 2);
$expiresAt = !empty($_POST['expires_at']) ? $_POST['expires_at'] . ' 23:59:59' : null;
$notes = trim($_POST['notes'] ?? '');
if ($id > 0) {
$stmt = $pdo->prepare('
UPDATE license_licenses
SET customer_name = :cname, customer_email = :cemail, status = :status, max_activations = :max_act, expires_at = :exp, notes = :notes
WHERE id = :id
');
$stmt->execute([
':cname' => $customerName,
':cemail' => $customerEmail,
':status' => $status,
':max_act' => $maxActivations,
':exp' => $expiresAt,
':notes' => $notes,
':id' => $id
]);
$pdo->prepare('INSERT INTO license_audit_log (actor, action, details) VALUES ("admin", "license.update", :d)')
->execute([':d' => json_encode(['license_id' => $id, 'status' => $status])]);
$msg = "Lizenzdaten wurden erfolgreich aktualisiert.";
}
}
// Revoke / Re-activate License
if ($action === 'toggle_license_status') {
$licId = (int)($_POST['license_id'] ?? 0);
$newStatus = $_POST['new_status'] ?? 'revoked';
if ($licId > 0) {
$stmt = $pdo->prepare('UPDATE license_licenses SET status = :s WHERE id = :id');
$stmt->execute([':s' => $newStatus, ':id' => $licId]);
$msg = "Lizenz-Status geändert zu: " . strtoupper($newStatus);
}
}
// Toggle Hardware Activation Block
if ($action === 'toggle_block_activation') {
$actId = (int)($_POST['activation_id'] ?? 0);
$block = (int)($_POST['block_state'] ?? 0);
if ($actId > 0) {
$stmt = $pdo->prepare('UPDATE license_activations SET is_blocked = :b WHERE id = :id');
$stmt->execute([':b' => $block, ':id' => $actId]);
$msg = $block ? "Hardware-Aktivierung wurde gesperrt." : "Hardware-Aktivierung wurde entsperrt.";
}
}
// Release (Delete) Hardware Activation
if ($action === 'release_activation') {
$actId = (int)($_POST['activation_id'] ?? 0);
if ($actId > 0) {
$actStmt = $pdo->prepare('SELECT a.*, l.license_key FROM license_activations a JOIN license_licenses l ON a.license_id = l.id WHERE a.id = :id');
$actStmt->execute([':id' => $actId]);
$actRow = $actStmt->fetch();
if ($actRow) {
$delStmt = $pdo->prepare('DELETE FROM license_activations WHERE id = :id');
$delStmt->execute([':id' => $actId]);
Audit::log($pdo, $_SESSION['username'] ?? 'admin', 'activation_released', [
'license_id' => $actRow['license_id'],
'license_key' => $actRow['license_key'],
'hardware_id' => $actRow['hardware_id'],
'hostname' => $actRow['hostname']
]);
$msg = "Hardware-Aktivierung wurde freigegeben (gelöscht).";
}
}
}
// Add Watchdog Host Machine / Application
if ($action === 'add_watchdog_monitor') {
$source = trim($_POST['source'] ?? '');
$type = $_POST['type'] ?? 'heartbeat';
$group = trim($_POST['group'] ?? 'Default');
$os = trim($_POST['os'] ?? 'Linux');
$interval = (int)($_POST['interval'] ?? 60);
$parent = trim($_POST['parent_source'] ?? '');
if ($source) {
$stmt = $pdo->prepare('
INSERT INTO watchdog_monitors (source, instance, type, state, expected_interval_sec, group_key, parent_source, os, created_utc, updated_utc)
VALUES (:source, "default", :type, "stopped", :interval, :group, :parent, :os, NOW(), NOW())
ON DUPLICATE KEY UPDATE expected_interval_sec = VALUES(expected_interval_sec), group_key = VALUES(group_key), parent_source = VALUES(parent_source)
');
$stmt->execute([':source' => $source, ':type' => $type, ':interval' => $interval, ':group' => $group, ':parent' => !empty($parent)?$parent:null, ':os' => $os]);
$tokMgr = new TokenManager($pdo);
$tok = $tokMgr->createToken($source, "Token for {$source}");
$msg = "Monitor '{$source}' angelegt. Agent Token: <strong style='font-family:monospace;'>{$tok['raw_token']}</strong>";
}
}
// Edit Watchdog Monitor (Rich Edit with Icon Upload & Mute Switch matching screenshot)
if ($action === 'edit_watchdog_monitor') {
$oldSource = trim($_POST['old_source'] ?? $_POST['source'] ?? '');
$newSource = trim($_POST['new_source'] ?? $_POST['source'] ?? '');
$type = trim($_POST['type'] ?? 'host');
$group = trim($_POST['group_key'] ?? $_POST['group'] ?? 'Default');
$parent = trim($_POST['parent_source'] ?? '');
$os = trim($_POST['os'] ?? '');
$notes = trim($_POST['notes'] ?? '');
$url = trim($_POST['url'] ?? '');
$icon = trim($_POST['icon'] ?? '');
$interval = (int)($_POST['expected_interval_sec'] ?? $_POST['interval'] ?? 60);
$isMuted = isset($_POST['is_muted']) ? true : false;
// Custom icon file upload
if (!empty($_FILES['custom_icon_file']['name'])) {
$file = $_FILES['custom_icon_file'];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (in_array($ext, ['svg', 'png', 'jpg', 'jpeg', 'webp'], true)) {
$customDir = __DIR__ . '/assets/icons/custom';
if (!is_dir($customDir)) mkdir($customDir, 0777, true);
$filename = 'icon_' . time() . '_' . preg_replace('/[^a-z0-9]/', '', strtolower(pathinfo($file['name'], PATHINFO_FILENAME))) . '.' . $ext;
if (move_uploaded_file($file['tmp_name'], $customDir . '/' . $filename)) {
$icon = 'custom/' . $filename;
}
}
}
if ($oldSource && $newSource) {
$monRepo = new MonitorRepo($pdo);
$monRepo->updateMonitor($oldSource, $newSource, 'default', $type, $group, $parent, $os, $notes, $url, $interval, $icon, $isMuted);
$msg = "Monitor '{$newSource}' wurde erfolgreich aktualisiert.";
}
}
// Delete Watchdog Monitor
if ($action === 'delete_watchdog_monitor') {
$source = trim($_POST['source'] ?? '');
if ($source) {
$monRepo = new MonitorRepo($pdo);
$monRepo->deleteMonitor($source, 'default');
$msg = "Monitor '{$source}' wurde gelöscht.";
}
}
// Link Entity Parent (Watchdog Hierarchy)
if ($action === 'link_entity') {
$source = trim($_POST['source'] ?? '');
$parent = trim($_POST['parent_source'] ?? '');
if ($source) {
$monRepo = new MonitorRepo($pdo);
$monRepo->setParentSource($source, $parent);
$msg = "Hierarchie gespeichert: {$source} → " . ($parent ?: 'Keine (Top Level)');
}
}
// Create Agent Token (Watchdog)
if ($action === 'create_agent_token') {
$source = trim($_POST['token_source'] ?? '');
$name = trim($_POST['token_name'] ?? '');
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
if ($action === 'revoke_agent_token') {
$tokId = trim($_POST['token_id'] ?? '');
if ($tokId) {
$stmt = $pdo->prepare('UPDATE watchdog_agent_tokens SET revoked = 1 WHERE token_id = :id');
$stmt->execute([':id' => $tokId]);
$msg = "Agent-Token wurde widerrufen.";
}
}
// Add Update Release
if ($action === 'add_release') {
$productSlug = trim($_POST['product_slug'] ?? '');
$version = trim($_POST['version'] ?? '');
$channel = trim($_POST['channel'] ?? 'prod');
$url = trim($_POST['download_url'] ?? '');
$hash = trim($_POST['sha256_hash'] ?? '');
$gitCommit = trim($_POST['git_commit'] ?? '');
$sizeBytes = (int)($_POST['size_bytes'] ?? 0);
$notes = trim($_POST['release_notes'] ?? '');
$critical = isset($_POST['is_critical']);
if ($productSlug && $version && $url) {
$updMgr = new UpdateManager($pdo);
if ($updMgr->addRelease($productSlug, $version, $channel, $notes, $url, $hash, $gitCommit, $sizeBytes, null, $critical)) {
$msg = "Release v{$version} ({$channel}) für Projekt '{$productSlug}' veröffentlicht.";
} else {
$msg = "Fehler beim Speichern des Releases.";
$msgType = 'danger';
}
}
}
// Core Master & Sub Token Management Actions
if ($action === 'create_master_token') {
$name = trim($_POST['name'] ?? '');
$proj = trim($_POST['project_slug'] ?? '');
$lic = trim($_POST['license_key'] ?? '');
$ownerType = $_POST['owner_type'] ?? 'custom';
$ownerIdentity = trim($_POST['owner_identity'] ?? '');
$scopes = isset($_POST['scopes']) && is_array($_POST['scopes']) ? $_POST['scopes'] : ['*'];
$env = $_POST['environment'] ?? 'all';
if ($name) {
$coreTokenMgr = new CoreTokenManager($pdo);
$res = $coreTokenMgr->createMasterToken($name, $proj, $lic, $ownerType, $ownerIdentity, $scopes, $env);
$msg = "Master-Token '{$name}' erstellt! Raw Master-Token (einmalig kopieren): <strong style='font-family:monospace; color:var(--success); font-size:1.1em;'>{$res['raw_token']}</strong>";
}
}
if ($action === 'revoke_core_token') {
$tokenId = trim($_POST['token_id'] ?? '');
if ($tokenId) {
$coreTokenMgr = new CoreTokenManager($pdo);
$coreTokenMgr->revokeToken($tokenId);
$msg = "Token '{$tokenId}' und alle abgeleiteten Sub-Tokens wurden widerrufen.";
}
}
// Bugtracker & Feature-Tracker Actions
if ($action === 'bt_add_comment') {
$itemId = (int)($_POST['item_id'] ?? 0);
$comment = trim($_POST['comment'] ?? '');
$author = trim($_POST['author'] ?? $_SESSION['dc_username'] ?? 'admin');
$actionTaken = trim($_POST['action_taken'] ?? 'commented');
if ($itemId > 0 && !empty($comment)) {
$bugRepo = new BugRepo($pdo);
$bugRepo->addComment($itemId, $author, $comment, $actionTaken);
$msg = "Kommentar zu Item #{$itemId} hinzugefügt.";
}
}
if ($action === 'bt_resolve') {
$itemId = (int)($_POST['item_id'] ?? 0);
$build = trim($_POST['resolved_in_build'] ?? 'v1.0.0');
$notes = trim($_POST['resolution_notes'] ?? '');
$author = trim($_POST['author'] ?? $_SESSION['dc_username'] ?? 'admin');
if ($itemId > 0 && !empty($build)) {
$bugRepo = new BugRepo($pdo);
$bugRepo->resolveItem($itemId, $build, $notes, $author);
$msg = "Item #{$itemId} wurde als gelöst/umgesetzt in Build '{$build}' markiert.";
}
}
if ($action === 'bt_create_item') {
$proj = trim($_POST['project_slug'] ?? 'default');
$type = $_POST['type'] ?? 'bug';
$title = trim($_POST['title'] ?? '');
$desc = trim($_POST['description'] ?? '');
$errMsg = trim($_POST['error_message'] ?? '');
$trace = trim($_POST['stack_trace'] ?? '');
$build = trim($_POST['build_version'] ?? 'v1.0.0');
$env = $_POST['environment'] ?? 'production';
$sev = $_POST['severity'] ?? 'medium';
$createdBy = trim($_POST['created_by'] ?? $_SESSION['dc_username'] ?? 'admin');
if ($title) {
$bugRepo = new BugRepo($pdo);
$res = $bugRepo->reportItem([
'project_slug' => $proj,
'type' => $type,
'title' => $title,
'description' => $desc,
'error_message' => $errMsg,
'stack_trace' => $trace,
'build_version' => $build,
'environment' => $env,
'severity' => $sev,
'created_by' => $createdBy,
]);
$msg = $res['is_new']
? "Neues Item #{$res['id']} ({$res['type']}) erfolgreich in {$env} erfasst."
: "Wiederkehrender Fehler erfasst. Occurrence Count erhöht auf {$res['occurrence_count']}.";
}
}
if ($action === 'bt_change_status') {
$itemId = (int)($_POST['item_id'] ?? 0);
$status = $_POST['status'] ?? 'open';
$notes = trim($_POST['notes'] ?? '');
$author = trim($_POST['author'] ?? $_SESSION['dc_username'] ?? 'admin');
if ($itemId > 0) {
$bugRepo = new BugRepo($pdo);
$bugRepo->updateStatus($itemId, $status, $notes, $author);
$msg = "Status für Item #{$itemId} geändert auf {$status}.";
}
}
}
}
// Fetch All Data
$projects = $pdo->query('SELECT * FROM dc_projects ORDER BY name ASC')->fetchAll();
$licenses = $pdo->query('
SELECT l.*, p.name as product_name, p.slug as product_slug,
(SELECT COUNT(*) FROM license_activations a WHERE a.license_id = l.id) as active_count
FROM license_licenses l
JOIN dc_projects p ON l.product_id = p.id
ORDER BY l.created_at DESC
')->fetchAll();
$activations = $pdo->query('
SELECT a.*, l.license_key, p.slug as product_slug, p.name as product_name
FROM license_activations a
JOIN license_licenses l ON a.license_id = l.id
JOIN dc_projects p ON l.product_id = p.id
ORDER BY a.last_seen DESC
')->fetchAll();
$auditLogs = $pdo->query('SELECT * FROM license_audit_log ORDER BY created_at DESC LIMIT 100')->fetchAll();
$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 = [];
$bySource = [];
foreach ($monitors as $m) {
$bySource[$m['source']] = $m;
$parent = !empty($m['parent_source']) ? $m['parent_source'] : '__ROOT__';
$childrenOf[$parent][] = $m['source'];
}
$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);
for ($i = 0; $i < $count; $i++) {
$src = $nodes[$i];
if (!isset($bySource[$src])) continue;
$node = $bySource[$src];
$node['depth'] = $depth;
$node['is_last'] = ($i === $count - 1);
$flat[] = $node;
$walk($src, $depth + 1);
}
};
$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;
}
$hierarchicalMonitors = buildMonitorTree($monitors);
// Calculate Monitor Groups Stats (for Dashboard Group Panel)
$groupStats = [];
$monitorsUp = 0; $monitorsWarning = 0; $monitorsDown = 0;
foreach ($monitors as $m) {
if ($m['state'] === 'up') $monitorsUp++;
elseif ($m['state'] === 'warning') $monitorsWarning++;
else $monitorsDown++;
$gKey = !empty($m['group_key']) ? $m['group_key'] : 'Default';
if (!isset($groupStats[$gKey])) {
$groupStats[$gKey] = ['total' => 0, 'ok' => 0];
}
$groupStats[$gKey]['total']++;
if ($m['state'] === 'up') {
$groupStats[$gKey]['ok']++;
}
}
// Icon Resolver Helper
function getMonitorIconUrl(?string $icon, ?string $source, ?string $os, ?string $type): string {
if (!empty($icon) && $icon !== 'auto') {
if (str_starts_with($icon, 'custom/')) return 'assets/icons/' . $icon;
if (str_starts_with($icon, 'assets/')) return $icon;
return 'assets/icons/' . $icon;
}
$s = strtolower(($source ?? '') . ' ' . ($os ?? '') . ' ' . ($type ?? ''));
if (str_contains($s, 'proxmox') || str_contains($s, 'pve')) return 'assets/icons/proxmox.svg';
if (str_contains($s, 'win')) return 'assets/icons/windows.svg';
if (str_contains($s, 'linux') || str_contains($s, 'ubuntu') || str_contains($s, 'debian')) return 'assets/icons/linux.svg';
if (str_contains($s, 'mysql') || str_contains($s, 'mariadb') || str_contains($s, 'db')) return 'assets/icons/mysql.svg';
if (str_contains($s, 'docker')) return 'assets/icons/docker.svg';
if (str_contains($s, 'nginx')) return 'assets/icons/nginx.svg';
if (str_contains($s, 'redis')) return 'assets/icons/redis.svg';
if (str_contains($s, 'python')) return 'assets/icons/python.svg';
if (str_contains($s, 'php')) return 'assets/icons/php.svg';
if (str_contains($s, 'node')) return 'assets/icons/node.svg';
return 'assets/icons/server.svg';
}
$eventLog = new EventLog($pdo);
$recentEvents = $eventLog->getRecentEvents(100);
$updateMgr = new UpdateManager($pdo);
$releases = $updateMgr->getReleases();
// Core Token Manager Data
$coreTokenMgr = new CoreTokenManager($pdo);
$coreMasterTokens = $coreTokenMgr->getAllMasterTokens();
$coreAllTokens = $coreTokenMgr->getAllTokens();
// Bugtracker Data
$bugRepo = new BugRepo($pdo);
$bugtrackerStats = $bugRepo->getStats();
$bugtrackerItems = $bugRepo->getItems();
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'dc.mhdf.de';
$baseUrl = $protocol . '://' . $host;
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deploymentcenter - Operations Hub</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Roboto+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-dark: #0a0d14;
--bg-sidebar: rgba(255,255,255,0.015);
--bg-card: rgba(255, 255, 255, 0.045);
--bg-card-elev: rgba(255, 255, 255, 0.08);
--border-glass: rgba(255, 255, 255, 0.09);
--border-soft: rgba(255, 255, 255, 0.05);
--primary: #5b9dff;
--primary-glow: rgba(91, 157, 255, 0.4);
--success: #12D48A;
--warning: #F5A623;
--danger: #F6465D;
--text-main: #EDEFF5;
--text-muted: #8B93A7;
--sidebar-width: 240px;
--sidebar-collapsed-width: 72px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Manrope', system-ui, sans-serif; background: radial-gradient(1100px 700px at 15% -10%, rgba(22,82,240,0.28), transparent 60%), radial-gradient(900px 600px at 110% 10%, rgba(18,212,138,0.10), transparent 55%), var(--bg-dark); color: var(--text-main); min-height: 100vh; display: flex; overflow-x: hidden; }
/* Decorative Ambient Glow */
.glow-overlay { position: fixed; inset: 0; pointer-events: none; background: radial-gradient(600px 400px at 80% 85%, rgba(76,141,255,0.08), transparent 60%); z-index: 0; }
/* Left Sidebar (Collapsible) */
.sidebar { width: var(--sidebar-width); background: rgba(10, 13, 20, 0.7); border-right: 1px solid var(--border-glass); padding: 1.25rem 0.75rem; display: flex; flex-direction: column; justify-content: space-between; position: fixed; height: 100vh; transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 100; backdrop-filter: blur(24px); }
.sidebar.collapsed { width: var(--sidebar-collapsed-width); }
.brand { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.5rem 1.5rem 0.5rem; border-bottom: 1px solid var(--border-soft); margin-bottom: 1rem; overflow: hidden; white-space: nowrap; }
.brand-logo { min-width: 34px; height: 34px; background: linear-gradient(135deg, #1652F0, #4c8dff); border-radius: 10px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 20px rgba(22,82,240,0.6); }
.brand-logo svg { width: 20px; height: 20px; stroke: #fff; fill: none; stroke-width: 2; }
.brand-title { font-size: 1.1rem; font-weight: 800; color: #fff; letter-spacing: -0.02em; }
.sidebar.collapsed .brand-title { display: none; }
.sidebar-toggle-btn { background: var(--bg-card-elev); border: 1px solid var(--border-glass); color: var(--text-muted); width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; position: absolute; right: -13px; top: 22px; transition: transform 0.3s; z-index: 101; }
.sidebar.collapsed .sidebar-toggle-btn { transform: rotate(180deg); }
.nav-section { margin-bottom: 1.5rem; }
.nav-section-title { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.08em; color: #5B6377; padding: 0 0.75rem 0.5rem 0.75rem; font-weight: 700; }
.sidebar.collapsed .nav-section-title { display: none; }
.nav-menu { display: flex; flex-direction: column; gap: 0.35rem; list-style: none; }
.nav-link { display: flex; align-items: center; gap: 0.85rem; padding: 0.7rem 0.85rem; color: var(--text-muted); text-decoration: none; border-radius: 10px; font-size: 0.875rem; font-weight: 600; transition: all 0.2s; cursor: pointer; white-space: nowrap; }
.nav-link:hover, .nav-link.active { background: rgba(91, 157, 255, 0.12); color: #fff; }
.nav-link.active { color: #fff; background: linear-gradient(135deg, #1652F0, #4c8dff); box-shadow: 0 4px 20px rgba(22,82,240,0.4); }
.nav-link svg { min-width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 2; }
.sidebar.collapsed .nav-text { display: none; }
.user-panel { border-top: 1px solid var(--border-soft); padding-top: 1rem; display: flex; align-items: center; justify-content: space-between; font-size: 0.85rem; color: var(--text-muted); overflow: hidden; white-space: nowrap; }
.sidebar.collapsed .user-name { display: none; }
/* Main Wrapper */
.main-wrapper { margin-left: var(--sidebar-width); flex: 1; transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1); display: flex; flex-direction: column; min-width: 0; position: relative; z-index: 1; }
.main-wrapper.expanded { margin-left: var(--sidebar-collapsed-width); }
/* Top Bar Header with Embedded Module Sub-Navigation Top-Bar */
.top-bar { background: rgba(10, 13, 20, 0.75); border-bottom: 1px solid var(--border-glass); padding: 0.85rem 2rem; display: flex; align-items: center; justify-content: space-between; backdrop-filter: blur(24px); position: sticky; top: 0; z-index: 90; gap: 1.5rem; }
.top-left { display: flex; align-items: center; gap: 1.5rem; flex: 1; min-width: 0; }
.top-title { font-size: 1.25rem; font-weight: 800; color: #fff; letter-spacing: -0.02em; white-space: nowrap; }
/* Embedded Top-Bar Subnav */
.topbar-subnav { display: flex; align-items: center; gap: 0.4rem; overflow-x: auto; scrollbar-width: none; }
.subnav-link { background: rgba(255,255,255,0.03); border: 1px solid var(--border-glass); color: var(--text-muted); padding: 0.45rem 0.85rem; font-size: 0.8rem; font-weight: 600; border-radius: 8px; cursor: pointer; transition: all 0.2s; white-space: nowrap; display: inline-flex; align-items: center; gap: 0.4rem; }
.subnav-link:hover { color: #fff; background: rgba(255,255,255,0.08); }
.subnav-link.active { background: #5b9dff; color: #0a0d14; border-color: #5b9dff; font-weight: 700; box-shadow: 0 0 12px var(--primary-glow); }
.header-actions { display: flex; align-items: center; gap: 1rem; }
.btn-autorefresh { background: rgba(255,255,255,0.04); border: 1px solid var(--border-glass); color: var(--text-main); font-size: 0.75rem; font-weight: 700; padding: 0.5rem 0.85rem; border-radius: 8px; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s; font-family: 'Roboto Mono', monospace; }
.btn-autorefresh.active { background: rgba(18,212,138,0.15); border-color: var(--success); color: var(--success); }
.main-content { padding: 1.75rem 2rem; max-width: 1600px; }
/* Stats & KPI Grid */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.25rem; margin-bottom: 1.75rem; }
.stat-card { background: var(--bg-card); border: 1px solid var(--border-glass); border-radius: 16px; padding: 1.25rem; backdrop-filter: blur(24px); box-shadow: 0 8px 30px rgba(0,0,0,0.25); }
.stat-header { display: flex; align-items: center; justify-content: space-between; color: var(--text-muted); font-size: 0.75rem; font-weight: 600; }
.stat-value { font-size: 2rem; font-weight: 800; margin-top: 0.5rem; color: #fff; font-family: 'Roboto Mono', monospace; letter-spacing: -0.02em; }
/* Group Overview Cards Panel (Screenshot 2) */
.group-cards-grid { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 0.75rem; }
.group-card { background: rgba(255,255,255,0.03); border: 1px solid var(--border-glass); border-radius: 12px; padding: 0.85rem 1.1rem; display: flex; align-items: center; justify-content: space-between; min-width: 180px; gap: 1rem; }
.group-card-name { font-size: 0.875rem; font-weight: 700; color: #fff; }
/* Icon Grid Selector in Edit Modal (Screenshot 1) */
.icon-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; max-height: 180px; overflow-y: auto; padding-right: 4px; }
.icon-option-btn { background: rgba(255,255,255,0.03); border: 1px solid var(--border-glass); border-radius: 10px; padding: 0.55rem 0.75rem; color: var(--text-main); font-size: 0.8rem; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s; }
.icon-option-btn:hover { background: rgba(255,255,255,0.08); }
.icon-option-btn.selected { border-color: #FF7A45; background: rgba(242,98,46,0.15); color: #fff; box-shadow: 0 0 10px rgba(242,98,46,0.3); }
/* Cards & Tables */
.card { background: var(--bg-card); border: 1px solid var(--border-glass); border-radius: 18px; padding: 1.5rem; margin-bottom: 1.75rem; backdrop-filter: blur(24px); box-shadow: 0 8px 30px rgba(0,0,0,0.25); }
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.25rem; }
.card-title { font-size: 1.05rem; font-weight: 700; color: #fff; display: flex; align-items: center; gap: 0.5rem; }
table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.85rem; }
th { padding: 0.8rem 1rem; color: #5B6377; font-weight: 700; border-bottom: 1px solid var(--border-glass); font-size: 0.725rem; text-transform: uppercase; letter-spacing: 0.05em; }
td { padding: 0.85rem 1rem; border-bottom: 1px solid var(--border-soft); vertical-align: middle; }
tr:hover td { background: rgba(255, 255, 255, 0.02); }
/* Badges */
.badge { padding: 0.2rem 0.6rem; border-radius: 20px; font-size: 0.725rem; font-weight: 700; display: inline-flex; align-items: center; gap: 0.35rem; font-family: 'Roboto Mono', monospace; }
.badge-up { background: rgba(18,212,138,0.12); color: var(--success); border: 1px solid rgba(18,212,138,0.3); }
.badge-warning { background: rgba(245,166,35,0.12); color: var(--warning); border: 1px solid rgba(245,166,35,0.3); }
.badge-down { background: rgba(246,70,93,0.12); color: var(--danger); border: 1px solid rgba(246,70,93,0.3); }
.badge-stopped { background: rgba(139,147,167,0.12); color: var(--text-muted); border: 1px solid rgba(139,147,167,0.3); }
/* Buttons & Form Inputs */
.btn { background: linear-gradient(135deg,#1652F0,#4c8dff); color: #fff; border: none; border-radius: 10px; padding: 0.6rem 1.1rem; font-size: 0.85rem; font-weight: 700; cursor: pointer; transition: all 0.2s; text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; box-shadow: 0 0 15px rgba(22,82,240,0.4); }
.btn:hover { transform: translateY(-1px); box-shadow: 0 0 22px rgba(22,82,240,0.6); }
.btn-secondary { background: rgba(255,255,255,0.06); border: 1px solid var(--border-glass); color: var(--text-main); box-shadow: none; }
.btn-secondary:hover { background: rgba(255,255,255,0.12); }
.btn-sm { padding: 0.35rem 0.65rem; font-size: 0.75rem; border-radius: 6px; }
.btn-danger { background: var(--danger); box-shadow: 0 0 15px rgba(246,70,93,0.4); }
.btn-danger:hover { background: #dc2626; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 1.25rem; }
.form-group { display: flex; flex-direction: column; gap: 0.35rem; }
.form-label { font-size: 0.775rem; color: var(--text-muted); font-weight: 600; }
.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 */
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; }
.alert { padding: 1rem 1.25rem; border-radius: 12px; margin-bottom: 1.5rem; font-size: 0.875rem; font-weight: 600; }
.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; }
/* 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: #14171d; border: 1px solid rgba(255,255,255,0.12); border-radius: 18px; padding: 1.75rem; max-width: 600px; width: 95%; max-height: 90vh; overflow-y: auto; 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; }
</style>
</head>
<body>
<!-- Glow Overlay -->
<div class="glow-overlay"></div>
<!-- Left Collapsible Sidebar -->
<aside class="sidebar" id="sidebar">
<button class="sidebar-toggle-btn" id="sidebarToggle" onclick="toggleSidebar()" title="Menü einklappen / ausklappen">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none" stroke-width="2"><polyline points="15 18 9 12 15 6"></polyline></svg>
</button>
<div>
<div class="brand">
<div class="brand-logo">
<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path></svg>
</div>
<div class="brand-title">Deploymentcenter</div>
</div>
<div class="nav-section">
<div class="nav-section-title">Navigation</div>
<ul class="nav-menu">
<li class="nav-item">
<a class="nav-link active" onclick="switchMainTab('overview', this)" title="Übersicht">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
<span class="nav-text">Übersicht</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" onclick="switchMainTab('projects', this)" title="Projekte">
<svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
<span class="nav-text">Projekte</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" onclick="switchMainTab('license', this)" title="Lizenzen">
<svg viewBox="0 0 24 24"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg>
<span class="nav-text">Lizenzen</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" onclick="switchMainTab('watchdog', this)" title="WatchDog">
<svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
<span class="nav-text">WatchDog</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" onclick="switchMainTab('updateservice', this)" title="UpdateService">
<svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
<span class="nav-text">UpdateService</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" onclick="switchMainTab('bugtracker', this)" title="Bug- & Feature-Tracker">
<svg viewBox="0 0 24 24"><path d="M12 2a2 2 0 0 1 2 2v1h3a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-1v2h2a1 1 0 0 1 0 2h-2v2h1a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h1v-2H6a1 1 0 0 1 0-2h2v-2H7a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h3V4a2 2 0 0 1 2-2z"></path></svg>
<span class="nav-text">Bugtracker</span>
</a>
</li>
</ul>
</div>
</div>
<div>
<div class="nav-section" style="margin-bottom:1rem;">
<div class="nav-section-title">Administration</div>
<ul class="nav-menu">
<li class="nav-item">
<a class="nav-link" onclick="switchMainTab('tokens', this)" title="Master- & Sub-Tokens">
<svg viewBox="0 0 24 24"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg>
<span class="nav-text">Token-Verwaltung</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" onclick="switchMainTab('system', this)" title="System & DB">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
<span class="nav-text">System & DB</span>
</a>
</li>
</ul>
</div>
<div class="user-panel">
<span class="user-name">👤 <?= htmlspecialchars($_SESSION['dc_username'] ?? 'Admin') ?></span>
<a href="logout.php" style="color:var(--danger); text-decoration:none; font-weight:700;">Abmelden</a>
</div>
</div>
</aside>
<!-- Main Content Wrapper -->
<div class="main-wrapper" id="mainWrapper">
<!-- Top Header Bar with Embedded Module Subnav Bar -->
<header class="top-bar">
<div class="top-left">
<div class="top-title" id="topPageTitle">Globales Dashboard</div>
<!-- Subnav Bar Embedded in Top-Bar -->
<div class="topbar-subnav" id="topbarSubnav">
<!-- Dynamic Subnav items rendered via JavaScript -->
</div>
</div>
<div class="header-actions">
<button class="btn-autorefresh" id="btnAutoRefresh" onclick="toggleAutoRefresh()">
<span id="autoRefreshIcon">⏱️</span>
<span id="autoRefreshText">30s REFRESH: AUS</span>
</button>
</div>
</header>
<main class="main-content">
<?php if ($msg): ?>
<div class="alert alert-<?= $msgType ?>"><?= $msg ?></div>
<?php endif; ?>
<!-- ================= MODULE 0: OVERVIEW ================= -->
<div id="tab-overview" class="tab-content active">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">Aktive Projekte</div>
<div class="stat-value"><?= count($projects) ?></div>
</div>
<div class="stat-card">
<div class="stat-header">Aktive Lizenzen</div>
<div class="stat-value"><?= count($licenses) ?></div>
</div>
<div class="stat-card">
<div class="stat-header">Hardware Aktivierungen</div>
<div class="stat-value"><?= count($activations) ?></div>
</div>
<div class="stat-card">
<div class="stat-header">Watchdog Monitore</div>
<div class="stat-value">
<span style="color:var(--success)"><?= $monitorsUp ?></span> /
<span style="color:var(--warning)"><?= $monitorsWarning ?></span> /
<span style="color:var(--danger)"><?= $monitorsDown ?></span>
</div>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">🛡️ Watchdog System-Hierarchie & Monitore</h2></div>
<table>
<thead>
<tr>
<th>Hierarchie / Entity Source</th>
<th>Typ</th>
<th>Status</th>
<th>Letzte Meldung</th>
<th>Zuletzt Gesehen</th>
</tr>
</thead>
<tbody>
<?php foreach ($hierarchicalMonitors as $m): ?>
<?php
$depth = (int)$m['depth'];
$indentPx = $depth * 24;
$isChild = ($depth > 0);
$iconUrl = getMonitorIconUrl($m['icon']??null, $m['source']??null, $m['os']??null, $m['type']??null);
?>
<tr>
<td>
<div style="display:flex; align-items:center; gap:8px; margin-left:<?= $indentPx ?>px; <?= $isChild ? 'border-left:2px solid var(--primary); padding-left:8px;' : '' ?>">
<?php if ($isChild): ?>
<span style="color:var(--primary); font-family:'Roboto Mono'; font-size:13px; font-weight:bold"><?= $m['is_last'] ? '└─' : '├─' ?></span>
<?php endif; ?>
<img src="<?= htmlspecialchars($iconUrl) ?>" width="18" height="18" alt="icon">
<strong><?= htmlspecialchars($m['source']) ?></strong>
<span style="color:var(--text-muted); font-size:0.75rem;">(<?= htmlspecialchars($m['instance']) ?>)</span>
</div>
</td>
<td><code><?= htmlspecialchars($m['type']) ?></code></td>
<td>
<span class="badge badge-<?= $m['state'] === 'up' ? 'up' : ($m['state'] === 'warning' ? 'warning' : 'down') ?>">
● <?= strtoupper($m['state']) ?>
</span>
</td>
<td><?= htmlspecialchars($m['last_message'] ?? '-') ?></td>
<td><?= htmlspecialchars($m['last_seen_utc'] ?? 'Nie') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- ================= MODULE 1: PROJEKTE ================= -->
<div id="tab-projects" class="tab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">📁 Neues Projekt anlegen / Bearbeiten</h2></div>
<form method="POST" action="index.php#tab-projects">
<input type="hidden" name="action" value="save_project">
<input type="hidden" name="project_id" id="projEditId" value="0">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Projekt-Slug (z. B. polytrader - unveränderlich)</label>
<input type="text" name="slug" id="projEditSlug" class="form-input" required placeholder="polytrader">
</div>
<div class="form-group">
<label class="form-label">Projekt Name</label>
<input type="text" name="name" id="projEditName" class="form-input" required placeholder="PolyTrader Suite Pro">
</div>
<div class="form-group">
<label class="form-label">Offline Grace Cache TTL (Stunden)</label>
<input type="number" name="ttl" id="projEditTtl" class="form-input" value="168">
</div>
</div>
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Projekt Beschreibung / Notizen</label>
<input type="text" name="notes" id="projEditNotes" class="form-input" placeholder="Trading & Handelssystem Platform">
</div>
<div style="display:flex; gap:0.5rem;">
<button type="submit" class="btn" id="projSubmitBtn">Projekt Anlegen</button>
<button type="button" class="btn btn-secondary" onclick="resetProjectForm()">Formular Zurücksetzen</button>
</div>
</form>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Projekte Übersicht & Verwaltung</h2></div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Slug</th>
<th>Projekt Name</th>
<th>Cache TTL</th>
<th>Verknüpfte Lizenzen</th>
<th>Verknüpfte Releases</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach ($projects as $p): ?>
<?php
$licCount = count(array_filter($licenses, fn($l) => $l['product_slug'] === $p['slug']));
$relCount = count(array_filter($releases, fn($r) => $r['product_slug'] === $p['slug']));
?>
<tr>
<td><?= $p['id'] ?></td>
<td><code style="font-weight:700; color:var(--primary);"><?= htmlspecialchars($p['slug']) ?></code></td>
<td><strong><?= htmlspecialchars($p['name']) ?></strong></td>
<td><?= $p['default_cache_ttl_hours'] ?> h (<?= round($p['default_cache_ttl_hours']/24, 1) ?> Tage)</td>
<td><span class="badge badge-up"><?= $licCount ?> Lizenzen</span></td>
<td><span class="badge badge-up"><?= $relCount ?> Releases</span></td>
<td style="display:flex; gap:0.35rem;">
<button class="btn btn-sm btn-secondary" onclick="editProject(<?= htmlspecialchars(json_encode($p)) ?>)">✏️ Bearbeiten</button>
<button class="btn btn-sm btn-danger" onclick="openDeleteProjectModal(<?= $p['id'] ?>, '<?= htmlspecialchars($p['slug']) ?>', '<?= htmlspecialchars($p['name']) ?>')">🗑️ Löschen</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Delete Project Safety Modal -->
<div class="modal-backdrop" id="deleteProjectModal">
<div class="modal-card">
<h2 style="color:var(--danger); margin-bottom:0.75rem;">⚠️ Projekt unwiderruflich löschen</h2>
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:1rem;">
Sind Sie sicher, dass Sie das Projekt <strong id="deleteProjNameDisplay" style="color:#fff;"></strong> löschen möchten?
</p>
<p style="color:var(--danger); font-size:0.8rem; font-weight:700; margin-bottom:1rem;">
Bitte geben Sie zur Sicherheitsbestätigung den Projekt-Slug ein: <code id="deleteProjSlugDisplay"></code>
</p>
<form method="POST" action="index.php#tab-projects">
<input type="hidden" name="action" value="delete_project">
<input type="hidden" name="project_id" id="deleteProjId">
<input type="text" name="confirm_slug" id="deleteProjConfirmInput" class="form-input" style="margin-bottom:1.25rem;" placeholder="Slug hier eintippen" required>
<div style="display:flex; justify-content:flex-end; gap:0.75rem;">
<button type="button" class="btn btn-secondary" onclick="closeDeleteProjectModal()">Abbrechen</button>
<button type="submit" class="btn btn-danger">Endgültig Löschen</button>
</div>
</form>
</div>
</div>
<!-- Rich Monitor Edit Modal (Matching Screenshot 1: Monitor & Icon bearbeiten) -->
<div class="modal-backdrop" id="editMonitorModal">
<div class="modal-card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.25rem;">
<h2 style="font-size:1.15rem; font-weight:800; color:#fff; display:flex; align-items:center; gap:0.5rem;">✏️ Monitor & Icon bearbeiten</h2>
<button type="button" onclick="closeEditMonitorModal()" style="background:none; border:none; color:var(--text-muted); font-size:1.25rem; cursor:pointer;">&times;</button>
</div>
<form method="POST" action="index.php#sub-watchdog-hierarchy" enctype="multipart/form-data">
<input type="hidden" name="action" value="edit_watchdog_monitor">
<input type="hidden" name="old_source" id="monEditOldSource">
<input type="hidden" name="icon" id="monEditIconSelected" value="">
<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 placeholder="ClawdDotNet Test">
</div>
<!-- Icon Grid Selector (Visual Preview) -->
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Icon wählen (Visuelle Vorschau):</label>
<div class="icon-grid">
<button type="button" class="icon-option-btn selected" data-icon="" onclick="selectIcon(this, '')">🪄 Auto-Detect</button>
<button type="button" class="icon-option-btn" data-icon="assets/icons/proxmox.svg" onclick="selectIcon(this, 'assets/icons/proxmox.svg')"><img src="assets/icons/proxmox.svg" width="16" height="16"> Proxmox</button>
<button type="button" class="icon-option-btn" data-icon="assets/icons/windows.svg" onclick="selectIcon(this, 'assets/icons/windows.svg')"><img src="assets/icons/windows.svg" width="16" height="16"> Windows</button>
<button type="button" class="icon-option-btn" data-icon="assets/icons/linux.svg" onclick="selectIcon(this, 'assets/icons/linux.svg')"><img src="assets/icons/linux.svg" width="16" height="16"> Linux</button>
<button type="button" class="icon-option-btn" data-icon="assets/icons/mysql.svg" onclick="selectIcon(this, 'assets/icons/mysql.svg')"><img src="assets/icons/mysql.svg" width="16" height="16"> MySQL</button>
<button type="button" class="icon-option-btn" data-icon="assets/icons/docker.svg" onclick="selectIcon(this, 'assets/icons/docker.svg')"><img src="assets/icons/docker.svg" width="16" height="16"> Docker</button>
<button type="button" class="icon-option-btn" data-icon="assets/icons/nginx.svg" onclick="selectIcon(this, 'assets/icons/nginx.svg')"><img src="assets/icons/nginx.svg" width="16" height="16"> Nginx</button>
<button type="button" class="icon-option-btn" data-icon="assets/icons/redis.svg" onclick="selectIcon(this, 'assets/icons/redis.svg')"><img src="assets/icons/redis.svg" width="16" height="16"> Redis</button>
<button type="button" class="icon-option-btn" data-icon="assets/icons/python.svg" onclick="selectIcon(this, 'assets/icons/python.svg')"><img src="assets/icons/python.svg" width="16" height="16"> Python</button>
<button type="button" class="icon-option-btn" data-icon="assets/icons/php.svg" onclick="selectIcon(this, 'assets/icons/php.svg')"><img src="assets/icons/php.svg" width="16" height="16"> PHP</button>
<button type="button" class="icon-option-btn" data-icon="assets/icons/node.svg" onclick="selectIcon(this, 'assets/icons/node.svg')"><img src="assets/icons/node.svg" width="16" height="16"> Node.js</button>
<button type="button" class="icon-option-btn" data-icon="assets/icons/server.svg" onclick="selectIcon(this, 'assets/icons/server.svg')"><img src="assets/icons/server.svg" width="16" height="16"> Generischer</button>
</div>
</div>
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Oder eigenes Icon hochladen (SVG, PNG, JPG, WEBP)</label>
<input type="file" name="custom_icon_file" class="form-input" accept=".svg,.png,.jpg,.jpeg,.webp">
</div>
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Web-Backend URL (z. B. https://pve.example.com:8006)</label>
<input type="url" name="url" id="monEditUrl" class="form-input" placeholder="https://...">
</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">Übergeordneter Host / Hypervisor (Parent)</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" placeholder="Applications">
</div>
<div class="form-group">
<label class="form-label">Erwartetes Intervall (Sekunden)</label>
<input type="number" name="expected_interval_sec" id="monEditInterval" class="form-input" value="60">
</div>
</div>
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Notizen / Dokumentation</label>
<textarea name="notes" id="monEditNotes" class="form-input" rows="3" placeholder="Zusätzliche Notizen, IPs, Ports oder Ansprechpartner..."></textarea>
</div>
<div style="background:rgba(255,255,255,0.03); border:1px solid var(--border-glass); border-radius:12px; padding:0.85rem 1rem; margin-bottom:1.25rem; display:flex; gap:0.75rem; align-items:flex-start;">
<input type="checkbox" name="is_muted" id="monEditMuted" style="margin-top:3px;">
<div>
<label for="monEditMuted" style="font-weight:700; font-size:0.85rem; color:#fff; cursor:pointer;">🔕 Warnungen & Alarme stummschalten (Dev-Instanz / Wartung)</label>
<div style="font-size:0.75rem; color:var(--text-muted); margin-top:2px;">Aktivieren für Dev- oder Test-Systeme. Der Monitor bleibt sichtbar, sendet aber keine Telegram-/Matrix-Alarme bei Ausfällen.</div>
</div>
</div>
<div style="display:flex; justify-content:space-between; align-items:center;">
<button type="button" class="btn btn-danger btn-sm" onclick="triggerDeleteMonitorFromModal()">🗑️ Monitor Löschen</button>
<div style="display:flex; gap:0.5rem;">
<button type="button" class="btn btn-secondary" onclick="closeEditMonitorModal()">Abbrechen</button>
<button type="submit" class="btn" style="background:#F2622E;">Speichern</button>
</div>
</div>
</form>
</div>
</div>
<!-- Hidden delete form for modal trigger -->
<form id="hiddenDeleteMonitorForm" method="POST" action="index.php#sub-watchdog-hierarchy" style="display:none">
<input type="hidden" name="action" value="delete_watchdog_monitor">
<input type="hidden" name="source" id="hiddenDeleteSource">
</form>
<!-- ================= MODULE 2: LIZENZEN ================= -->
<div id="tab-license" class="tab-content">
<!-- Subtab: License Dashboard -->
<div id="sub-license-dashboard" class="subtab-content active">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">Registrierte Projekte</div>
<div class="stat-value"><?= count($projects) ?></div>
</div>
<div class="stat-card">
<div class="stat-header">Gültige Lizenzen</div>
<div class="stat-value"><?= count(array_filter($licenses, fn($l) => $l['status'] === 'active')) ?></div>
</div>
<div class="stat-card">
<div class="stat-header">Aktivierte Systeme</div>
<div class="stat-value"><?= count($activations) ?></div>
</div>
</div>
</div>
<!-- Subtab: Licenses -->
<div id="sub-license-licenses" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">Neuen Lizenzschlüssel generieren</h2></div>
<form method="POST" action="index.php#sub-license-licenses">
<input type="hidden" name="action" value="create_license">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Projekt</label>
<select name="product_id" class="form-input" required>
<?php foreach ($projects as $p): ?>
<option value="<?= $p['id'] ?>"><?= htmlspecialchars($p['name']) ?> (<?= htmlspecialchars($p['slug']) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">Kunden Name</label>
<input type="text" name="customer_name" class="form-input" placeholder="Musterfirma GmbH">
</div>
<div class="form-group">
<label class="form-label">Kunden E-Mail</label>
<input type="email" name="customer_email" class="form-input" placeholder="kunde@example.com">
</div>
<div class="form-group">
<label class="form-label">Max. Aktivierungen</label>
<input type="number" name="max_activations" class="form-input" value="2" min="1">
</div>
<div class="form-group">
<label class="form-label">Ablaufdatum (leer = unbegrenzt)</label>
<input type="date" name="expires_at" class="form-input">
</div>
</div>
<button type="submit" class="btn">Lizenz Generieren</button>
</form>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Erstellte Lizenzschlüssel</h2></div>
<table>
<thead>
<tr>
<th>Projekt</th>
<th>Lizenzschlüssel</th>
<th>Kunde</th>
<th>Aktivierungen</th>
<th>Status</th>
<th>Ablaufdatum</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach ($licenses as $l): ?>
<tr>
<td><?= htmlspecialchars($l['product_name']) ?></td>
<td><code style="font-size:0.9rem; font-weight:bold; color:var(--primary);"><?= htmlspecialchars($l['license_key']) ?></code></td>
<td><?= htmlspecialchars($l['customer_name'] ?? '-') ?></td>
<td><?= $l['active_count'] ?> / <?= $l['max_activations'] ?></td>
<td><span class="badge badge-<?= $l['status'] === 'active' ? 'up' : 'down' ?>"><?= strtoupper($l['status']) ?></span></td>
<td><?= $l['expires_at'] ? htmlspecialchars($l['expires_at']) : 'Unbefristet' ?></td>
<td style="display:flex; gap:0.35rem;">
<a href="index.php?action=download_lic&id=<?= $l['id'] ?>" class="btn btn-sm btn-secondary" title=".lic Datei herunterladen">📥 .lic</a>
<button class="btn btn-sm btn-secondary" onclick="openEditLicense(<?= htmlspecialchars(json_encode($l)) ?>)">✏️ Edit</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Subtab: License Details & Full Editing -->
<div id="sub-license-details" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">✏️ Lizenz Vollständig Bearbeiten</h2></div>
<form method="POST" action="index.php#sub-license-details">
<input type="hidden" name="action" value="edit_license">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Lizenz auswählen</label>
<select name="license_id" id="licEditSelect" class="form-input" required onchange="loadLicenseData(this)">
<?php foreach ($licenses as $l): ?>
<option value="<?= $l['id'] ?>" data-cname="<?= htmlspecialchars($l['customer_name'] ?? '') ?>" data-cemail="<?= htmlspecialchars($l['customer_email'] ?? '') ?>" data-status="<?= $l['status'] ?>" data-max="<?= $l['max_activations'] ?>" data-exp="<?= $l['expires_at'] ? date('Y-m-d', strtotime($l['expires_at'])) : '' ?>" data-notes="<?= htmlspecialchars($l['notes'] ?? '') ?>">
<?= htmlspecialchars($l['license_key']) ?> (<?= htmlspecialchars($l['customer_name'] ?? 'Unbekannt') ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">Kunden Name</label>
<input type="text" id="editCustomerName" name="customer_name" class="form-input">
</div>
<div class="form-group">
<label class="form-label">Kunden E-Mail</label>
<input type="email" id="editCustomerEmail" name="customer_email" class="form-input">
</div>
<div class="form-group">
<label class="form-label">Status</label>
<select id="editStatus" name="status" class="form-input">
<option value="active">Aktiv</option>
<option value="revoked">Widerrufen</option>
<option value="suspended">Pausiert</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Max. Aktivierungen</label>
<input type="number" id="editMaxAct" name="max_activations" class="form-input" min="1">
</div>
<div class="form-group">
<label class="form-label">Ablaufdatum</label>
<input type="date" id="editExpiresAt" name="expires_at" class="form-input">
</div>
</div>
<button type="submit" class="btn">Lizenzdaten Speichern</button>
</form>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Aktivierte Hardware-IDs verwalten</h2></div>
<table>
<thead>
<tr>
<th>Projekt</th>
<th>Lizenzschlüssel</th>
<th>Hardware-ID / Quelle</th>
<th>Plattform / Ver.</th>
<th>Hostname</th>
<th>Zuletzt gesehen</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach ($activations as $a): ?>
<tr>
<td><?= htmlspecialchars($a['product_name']) ?></td>
<td><code><?= htmlspecialchars($a['license_key']) ?></code></td>
<td>
<code><?= htmlspecialchars($a['hardware_id']) ?></code>
<?php if (!empty($a['hwid_source'])): ?>
<br><small style="color:var(--text-muted);">Quelle: <?= htmlspecialchars($a['hwid_source']) ?></small>
<?php endif; ?>
</td>
<td>
<span class="badge" style="background:var(--bg-card); border:1px solid var(--border-color); color:var(--text-main);">
<?= htmlspecialchars(strtoupper($a['platform'] ?? 'WIN')) ?> (v<?= (int)($a['hwid_version'] ?? 1) ?>)
</span>
</td>
<td><?= htmlspecialchars($a['hostname'] ?? '-') ?></td>
<td><?= htmlspecialchars($a['last_seen']) ?></td>
<td>
<span class="badge badge-<?= $a['is_blocked'] ? 'down' : 'up' ?>"><?= $a['is_blocked'] ? 'GESPERRT' : 'AKTIV' ?></span>
</td>
<td>
<form method="POST" action="index.php#sub-license-details" style="display:inline">
<input type="hidden" name="action" value="toggle_block_activation">
<input type="hidden" name="activation_id" value="<?= $a['id'] ?>">
<input type="hidden" name="block_state" value="<?= $a['is_blocked'] ? '0' : '1' ?>">
<button type="submit" class="btn btn-sm <?= $a['is_blocked'] ? 'btn' : 'btn-danger' ?>">
<?= $a['is_blocked'] ? 'Entsperren' : 'Sperren' ?>
</button>
</form>
<form method="POST" action="index.php#sub-license-details" style="display:inline" onsubmit="return confirm('Möchtest du diese Aktivierung wirklich freigeben (löschen)?');">
<input type="hidden" name="action" value="release_activation">
<input type="hidden" name="activation_id" value="<?= $a['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger" style="margin-left:4px;">
Freigeben
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Subtab: Offline Licenses -->
<div id="sub-license-offline" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">💾 Signierte Offline-Lizenzdatei erzeugen (.lic)</h2></div>
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:1rem;">
Erzeugt eine signierte <code>.lic</code> Offline-Lizenzdatei für Air-Gapped Kundensysteme.
</p>
<div class="form-grid">
<div class="form-group">
<label class="form-label">Lizenz auswählen</label>
<select id="offlineLicSelect" class="form-input">
<?php foreach ($licenses as $l): ?>
<option value="<?= htmlspecialchars($l['license_key']) ?>"><?= htmlspecialchars($l['license_key']) ?> (<?= htmlspecialchars($l['customer_name'] ?? 'Unbekannt') ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">Ziel-Hardware-ID (optional)</label>
<input type="text" id="offlineHwId" class="form-input" placeholder="HWID-88A9-99B1-CC02">
</div>
</div>
<button class="btn" onclick="generateOfflinePayload()">Offline Payload Generieren</button>
<div style="margin-top:1.5rem;">
<label class="form-label">Generierte Offline .lic Payload (JSON)</label>
<div class="prompt-box" id="offlineResultBox">Wählen Sie oben eine Lizenz aus und klicken Sie auf 'Offline Payload Generieren'.</div>
</div>
</div>
</div>
<!-- Subtab: Audit Log -->
<div id="sub-license-audit" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">📜 Audit-Protokoll</h2></div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Zeitpunkt</th>
<th>Akteur</th>
<th>Aktion</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<?php foreach ($auditLogs as $log): ?>
<tr>
<td><?= $log['id'] ?></td>
<td><?= $log['created_at'] ?></td>
<td><code><?= htmlspecialchars($log['actor']) ?></code></td>
<td><strong><?= htmlspecialchars($log['action']) ?></strong></td>
<td><code style="font-size:0.8rem;"><?= htmlspecialchars($log['details'] ?? '-') ?></code></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Subtab: Integration & Prompts -->
<div id="sub-license-integration" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">🔑 Server Ed25519 Public Key & Endpunkte</h2></div>
<div class="form-grid">
<div class="form-group">
<label class="form-label">Server Public Key (Ed25519 Base64)</label>
<input type="text" id="pubKeyInput" readonly class="form-input" value="MCowBQYDK2VwAyEA9f8J7K2mX4vQ8n1L6s5t4r3q2p1o0n9m8l7k6j5h4g3f">
</div>
<div class="form-group">
<label class="form-label">API Validate Endpoint</label>
<input type="text" readonly class="form-input" value="<?= $baseUrl ?>/api/license/v1/validate">
</div>
</div>
</div>
</div>
<!-- Subtab: Settings -->
<div id="sub-license-settings" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">⚙️ Moduleinstellungen & API Konfiguration</h2></div>
<div class="form-grid">
<div class="form-group">
<label class="form-label">REST API Validation Endpoint (Public)</label>
<input type="text" readonly class="form-input" value="<?= $baseUrl ?>/api/license/v1/validate">
</div>
<div class="form-group">
<label class="form-label">REST API Deactivation Endpoint (Geschützt - Auth erforderlich)</label>
<input type="text" readonly class="form-input" value="<?= $baseUrl ?>/api/license/v1/deactivate">
</div>
</div>
</div>
</div>
</div>
<!-- ================= MODULE 3: WATCHDOG ================= -->
<div id="tab-watchdog" class="tab-content">
<!-- Subtab: Watchdog Dashboard -->
<div id="sub-watchdog-dashboard" class="subtab-content active">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">Gesund / Up</div>
<div class="stat-value" style="color:var(--success);"><?= $monitorsUp ?></div>
</div>
<div class="stat-card">
<div class="stat-header">Warnungen</div>
<div class="stat-value" style="color:var(--warning);"><?= $monitorsWarning ?></div>
</div>
<div class="stat-card">
<div class="stat-header">Down / Kritisch</div>
<div class="stat-value" style="color:var(--danger);"><?= $monitorsDown ?></div>
</div>
</div>
<!-- Gruppen-Übersicht Panel (Matching Screenshot 2) -->
<div class="card">
<div class="card-header"><h2 class="card-title">📁 Gruppen-Übersicht</h2></div>
<div class="group-cards-grid">
<?php foreach ($groupStats as $gName => $gData): ?>
<?php
$isAllOk = ($gData['ok'] === $gData['total']);
$pillClass = $isAllOk ? 'badge-up' : 'badge-down';
?>
<div class="group-card">
<span class="group-card-name"><?= htmlspecialchars($gName) ?></span>
<span class="badge <?= $pillClass ?>"><?= $gData['ok'] ?>/<?= $gData['total'] ?> OK</span>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Subtab: Watchdog Hierarchie & Tree View (Exact requested columns & Edit modal button) -->
<div id="sub-watchdog-hierarchy" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">🌳 Watchdog System-Hierarchie</h2></div>
<table>
<thead>
<tr>
<th>Name</th>
<th>Typ</th>
<th>Status</th>
<th>Zuletzt gesehen</th>
<th>Kurz-Info / Message</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach ($hierarchicalMonitors as $m): ?>
<?php
$depth = (int)$m['depth'];
$indentPx = $depth * 24;
$isChild = ($depth > 0);
$iconUrl = getMonitorIconUrl($m['icon']??null, $m['source']??null, $m['os']??null, $m['type']??null);
?>
<tr>
<td>
<div style="display:flex; align-items:center; gap:8px; margin-left:<?= $indentPx ?>px; <?= $isChild ? 'border-left:2px solid var(--primary); padding-left:8px;' : '' ?>">
<?php if ($isChild): ?>
<span style="color:var(--primary); font-family:'Roboto Mono'; font-size:13px; font-weight:bold"><?= $m['is_last'] ? '└─' : '├─' ?></span>
<?php endif; ?>
<img src="<?= htmlspecialchars($iconUrl) ?>" width="18" height="18" alt="icon">
<strong><?= htmlspecialchars($m['source']) ?></strong>
</div>
</td>
<td><code><?= htmlspecialchars($m['type']) ?></code></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><span style="color:var(--text-muted); font-size:0.8rem;"><?= htmlspecialchars($m['last_message'] ?? '-') ?></span></td>
<td style="display:flex; gap:0.35rem;">
<button class="btn btn-sm btn-secondary" onclick="openEditMonitorModal(<?= htmlspecialchars(json_encode($m)) ?>)">✏️ Bearbeiten</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- 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</h2></div>
<form method="POST" action="index.php#sub-watchdog-add">
<input type="hidden" name="action" value="add_watchdog_monitor">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Monitor Name (source)</label>
<input type="text" name="source" class="form-input" required placeholder="z. B. srv-app-02">
</div>
<div class="form-group">
<label class="form-label">Typ</label>
<select name="type" 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">
<label class="form-label">Übergeordnete Entität (Parent Source)</label>
<select name="parent_source" 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-group">
<label class="form-label">Betriebssystem / Framework</label>
<select name="os" class="form-input">
<option value="Windows">Windows (PowerShell Task)</option>
<option value="Linux">Linux (Bash Cron)</option>
<option value=".NET 8 Service">.NET 8 Service</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Erwartetes Intervall (Sekunden)</label>
<input type="number" name="interval" class="form-input" value="60">
</div>
<div class="form-group">
<label class="form-label">Gruppe</label>
<input type="text" name="group" class="form-input" value="Infrastructure">
</div>
</div>
<button type="submit" class="btn">Monitor Anlegen & Token Generieren</button>
</form>
</div>
</div>
<!-- Subtab: Watchdog Event Log -->
<div id="sub-watchdog-eventlog" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">📜 Chronologisches Event-Log</h2></div>
<table>
<thead>
<tr>
<th>Zeitpunkt (UTC)</th>
<th>Source</th>
<th>Kind</th>
<th>Severity</th>
<th>Nachricht</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentEvents as $e): ?>
<tr>
<td><?= htmlspecialchars($e['at_utc']) ?></td>
<td><strong><?= htmlspecialchars($e['source']) ?></strong></td>
<td><code><?= htmlspecialchars($e['kind']) ?></code></td>
<td><span class="badge badge-<?= $e['severity']==='info'?'up':($e['severity']==='warning'?'warning':'down') ?>"><?= strtoupper($e['severity']) ?></span></td>
<td><?= htmlspecialchars($e['message'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Subtab: Agent Tokens -->
<div id="sub-watchdog-tokens" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">Neuen Agent-Token erstellen</h2></div>
<form method="POST" action="index.php#sub-watchdog-tokens">
<input type="hidden" name="action" value="create_agent_token">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Bezeichnung / Name</label>
<input type="text" name="token_name" class="form-input" required placeholder="z. B. Infrastructure Server Agent">
</div>
<div class="form-group">
<label class="form-label">Gebunden an Source (optional)</label>
<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>
</form>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Agent Tokens</h2></div>
<table>
<thead>
<tr>
<th>Token ID</th>
<th>Bezeichnung</th>
<th>Gebundene Source</th>
<th>Token Value</th>
<th>Status</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($agentTokens as $tok): ?>
<?php
$rawVal = !empty($tok['raw_token']) ? $tok['raw_token'] : 'wd_live_token_default';
$masked = substr($rawVal, 0, 8) . '••••••••••••••••';
?>
<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) ?>
</code>
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleTokenMask('<?= $tok['token_id'] ?>')">👁️</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="copyTokenValue('<?= $tok['token_id'] ?>')">📋</button>
</td>
<td><span class="badge badge-<?= $tok['revoked'] ? 'down' : 'up' ?>"><?= $tok['revoked'] ? 'WIDERUFEN' : 'AKTIV' ?></span></td>
<td>
<?php if (!$tok['revoked']): ?>
<form method="POST" action="index.php#sub-watchdog-tokens" style="display:inline">
<input type="hidden" name="action" value="revoke_agent_token">
<input type="hidden" name="token_id" value="<?= $tok['token_id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Widerrufen</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- ================= MODULE 4: UPDATESERVICE ================= -->
<div id="tab-updateservice" class="tab-content">
<div id="sub-update-releases" class="subtab-content active">
<div class="card">
<div class="card-header">
<h2 class="card-title">📦 Veröffentlichte Software Releases</h2>
</div>
<table>
<thead>
<tr>
<th>Projekt</th>
<th>Kanal</th>
<th>Version</th>
<th>Git Commit</th>
<th>Größe</th>
<th>Release Notes</th>
<th>Download & SHA256</th>
<th>Datum</th>
</tr>
</thead>
<tbody>
<?php foreach ($releases as $r): ?>
<?php
$channelClass = match($r['channel'] ?? 'prod') {
'prod' => 'badge-up',
'beta' => 'badge-warning',
'dev' => 'badge-stopped',
default => 'badge-up'
};
$sizeFormatted = !empty($r['size_bytes']) ? round($r['size_bytes'] / (1024 * 1024), 2) . ' MB' : '-';
?>
<tr>
<td><strong><?= htmlspecialchars($r['product_slug']) ?></strong></td>
<td><span class="badge <?= $channelClass ?>"><?= strtoupper(htmlspecialchars($r['channel'] ?? 'prod')) ?></span></td>
<td><code>v<?= htmlspecialchars($r['version']) ?></code> <?php if (!empty($r['is_critical'])): ?><span class="badge badge-down">KRITISCH</span><?php endif; ?></td>
<td><code style="color:var(--text-muted);"><?= htmlspecialchars($r['git_commit'] ?? 'n/a') ?></code></td>
<td><span style="font-family:'Roboto Mono', monospace; font-size:0.8rem;"><?= $sizeFormatted ?></span></td>
<td><?= htmlspecialchars($r['release_notes'] ?? '-') ?></td>
<td>
<a href="<?= htmlspecialchars($r['download_url']) ?>" target="_blank" class="btn btn-sm btn-secondary">
⬇️ Download Package
</a>
<?php if (!empty($r['sha256_hash'])): ?>
<div style="font-family:'Roboto Mono', monospace; font-size:0.65rem; color:var(--text-muted); margin-top:2px;" title="<?= htmlspecialchars($r['sha256_hash']) ?>">
SHA: <?= htmlspecialchars(substr($r['sha256_hash'], 0, 12)) ?>...
</div>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($r['created_at']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div id="sub-update-publish" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">Neues Software-Release veröffentlichen</h2></div>
<form method="POST" action="index.php#sub-update-publish">
<input type="hidden" name="action" value="add_release">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Projekt-Slug</label>
<select name="product_slug" class="form-input" required>
<?php foreach ($projects as $p): ?>
<option value="<?= htmlspecialchars($p['slug']) ?>"><?= htmlspecialchars($p['name']) ?> (<?= htmlspecialchars($p['slug']) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">Release Kanal</label>
<select name="channel" class="form-input" required>
<option value="prod" selected>prod (Produktiv)</option>
<option value="beta">beta (Vorab-Test)</option>
<option value="dev">dev (Entwicklung)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Version (z. B. 1.2.0)</label>
<input type="text" name="version" class="form-input" required placeholder="1.2.0">
</div>
<div class="form-group">
<label class="form-label">Git Commit (Kurz-Hash)</label>
<input type="text" name="git_commit" class="form-input" placeholder="a1b2c3d">
</div>
<div class="form-group">
<label class="form-label">Download URL</label>
<input type="url" name="download_url" class="form-input" required placeholder="https://dc.mhdf.de/releases/myapp/prod/1.2.0/package.tar.gz">
</div>
<div class="form-group">
<label class="form-label">SHA256 Hash (Optional)</label>
<input type="text" name="sha256_hash" class="form-input" placeholder="e3b0c44298fc1c149afbf4c8996fb924...">
</div>
</div>
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Release Notes / Changelog</label>
<textarea name="release_notes" class="form-input" rows="3" placeholder="Changelog und Verbesserungen..."></textarea>
</div>
<div style="margin-bottom:1.25rem;">
<label style="display:inline-flex; align-items:center; gap:0.5rem; font-size:0.875rem; cursor:pointer;">
<input type="checkbox" name="is_critical" value="1"> Kritisches Sicherheits-Update (Rollout priorisieren)
</label>
</div>
<button type="submit" class="btn">Release Speichern & Freigeben</button>
</form>
</div>
</div>
</div>
</div>
<!-- ================= MODULE: BUGTRACKER & FEATURE-TRACKER ================= -->
<div id="tab-bugtracker" class="tab-content">
<div id="sub-bugtracker-items" class="subtab-content active">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">Dev Bugs (Entwicklung)</div>
<div class="stat-value" style="color:#5b9dff;"><?= $bugtrackerStats['open_bugs_dev'] ?></div>
</div>
<div class="stat-card">
<div class="stat-header">Prod Bugs (Produktion)</div>
<div class="stat-value" style="color:var(--danger);"><?= $bugtrackerStats['open_bugs_prod'] ?></div>
</div>
<div class="stat-card">
<div class="stat-header">Offene Feature Requests</div>
<div class="stat-value" style="color:var(--warning);"><?= $bugtrackerStats['open_features'] ?></div>
</div>
<div class="stat-card">
<div class="stat-header">Gelöst / Umgesetzt</div>
<div class="stat-value" style="color:var(--success);"><?= $bugtrackerStats['resolved_total'] ?></div>
</div>
</div>
<div class="card">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
<h2 class="card-title">🐛 Bugs & Feature Requests</h2>
<div style="display:flex; gap:0.5rem;">
<select id="btFilterEnv" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="filterBugtrackerTable()">
<option value="all">🌐 Alle Umgebungen</option>
<option value="production">🔴 Produktion (Prod)</option>
<option value="development">🔵 Entwicklung (Dev)</option>
<option value="staging">🟡 Staging</option>
</select>
<select id="btFilterType" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="filterBugtrackerTable()">
<option value="all">📂 Alle Typen</option>
<option value="bug">🐛 Nur Bugs</option>
<option value="feature_request">💡 Nur Feature Requests</option>
</select>
<select id="btFilterStatus" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="filterBugtrackerTable()">
<option value="all">📌 Alle Status</option>
<option value="open">Offen</option>
<option value="in_progress">In Bearbeitung</option>
<option value="resolved">Gelöst / Umgesetzt</option>
</select>
</div>
</div>
<table>
<thead>
<tr>
<th>ID / Typ</th>
<th>Umgebung</th>
<th>Projekt & Titel</th>
<th>Build / Version</th>
<th>Schweregrad</th>
<th>Anzahl</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="btTableBody">
<?php foreach ($bugtrackerItems as $item): ?>
<?php
$typeBadge = $item['type'] === 'feature_request'
? '<span class="badge" style="background:rgba(180,100,255,0.2); color:#c87dff; border:1px solid #c87dff;">💡 FEATURE</span>'
: '<span class="badge badge-warning">🐛 BUG</span>';
$envBadge = match($item['environment']) {
'development' => '<span class="badge" style="background:rgba(91,157,255,0.2); color:#5b9dff; border:1px solid #5b9dff;">🔵 DEV</span>',
'production' => '<span class="badge badge-down">🔴 PROD</span>',
default => '<span class="badge badge-stopped">' . strtoupper($item['environment']) . '</span>'
};
$statusBadge = match($item['status']) {
'open' => '<span class="badge badge-down">OFFEN</span>',
'in_progress' => '<span class="badge badge-warning">IN BEARBEITUNG</span>',
'resolved' => '<span class="badge badge-up">GELÖST / UMGESETZT</span>',
'rejected' => '<span class="badge badge-stopped">ABGELEHNT</span>',
default => '<span class="badge badge-stopped">' . strtoupper($item['status']) . '</span>'
};
$sevBadge = match($item['severity']) {
'critical' => '<span class="badge badge-down" style="font-weight:bold;">🔥 KRITISCH</span>',
'high' => '<span class="badge badge-warning">HOCH</span>',
'medium' => '<span class="badge badge-stopped">MITTEL</span>',
default => '<span class="badge badge-stopped">NIEDRIG</span>'
};
?>
<tr class="bt-row"
data-env="<?= htmlspecialchars($item['environment']) ?>"
data-type="<?= htmlspecialchars($item['type']) ?>"
data-status="<?= htmlspecialchars($item['status']) ?>">
<td>#<?= $item['id'] ?><br><?= $typeBadge ?></td>
<td><?= $envBadge ?></td>
<td>
<strong style="color:#fff;"><?= htmlspecialchars($item['title']) ?></strong>
<div style="font-size:0.75rem; color:var(--text-muted);">
Projekt: <code><?= htmlspecialchars($item['project_slug']) ?></code> | Von: <?= htmlspecialchars($item['created_by']) ?>
</div>
</td>
<td><code><?= htmlspecialchars($item['build_version'] ?? 'v1.0.0') ?></code></td>
<td><?= $sevBadge ?></td>
<td>
<span class="badge" style="background:rgba(255,255,255,0.08); font-family:monospace;">
<?= (int)$item['occurrence_count'] ?>x
</span>
</td>
<td><?= $statusBadge ?></td>
<td>
<button type="button" class="btn btn-sm btn-secondary" onclick="openBugtrackerModal(<?= $item['id'] ?>)">🔍 Details & Timeline</button>
<?php if ($item['status'] !== 'resolved'): ?>
<button type="button" class="btn btn-sm" onclick="openResolveModal(<?= $item['id'] ?>, '<?= htmlspecialchars(addslashes($item['title'])) ?>')">✔ Gelöst</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Subtab: Item manuell anlegen -->
<div id="sub-bugtracker-new" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title"> Bug oder Feature Request Manuell Erfassen</h2></div>
<form method="POST" action="index.php#tab-bugtracker">
<input type="hidden" name="action" value="bt_create_item">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Projekt</label>
<select name="project_slug" class="form-input" required>
<?php foreach ($projects as $p): ?>
<option value="<?= htmlspecialchars($p['slug']) ?>"><?= htmlspecialchars($p['name']) ?> (<?= htmlspecialchars($p['slug']) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">Typ</label>
<select name="type" class="form-input" required>
<option value="bug" selected>🐛 Bug / Fehlerbericht</option>
<option value="feature_request">💡 Feature Request / Vorschlag</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Umgebung (Environment)</label>
<select name="environment" class="form-input" required>
<option value="development">🔵 Entwicklung (Development)</option>
<option value="production" selected>🔴 Produktion (Production)</option>
<option value="staging">🟡 Staging</option>
<option value="testing">🧪 Testing</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Schweregrad / Priorität</label>
<select name="severity" class="form-input" required>
<option value="low">Niedrig</option>
<option value="medium" selected>Mittel</option>
<option value="high">Hoch</option>
<option value="critical">🔥 Kritisch</option>
</select>
</div>
</div>
<div class="form-group" style="margin-top:1rem;">
<label class="form-label">Titel / Zusammenfassung</label>
<input type="text" name="title" class="form-input" required placeholder="z. B. NullReferenceException bei Order-Submit">
</div>
<div class="form-group" style="margin-top:1rem;">
<label class="form-label">Build / Version</label>
<input type="text" name="build_version" class="form-input" value="v1.4.2" placeholder="v1.4.2">
</div>
<div class="form-group" style="margin-top:1rem;">
<label class="form-label">Detaillierte Beschreibung</label>
<textarea name="description" class="form-input" rows="3" placeholder="Was ist passiert? Unter welchen Bedingungen?"></textarea>
</div>
<div class="form-group" style="margin-top:1rem;">
<label class="form-label">Fehlermeldung / Exception Message</label>
<textarea name="error_message" class="form-input" rows="2" placeholder="Exakte Fehlermeldung aus dem Log..."></textarea>
</div>
<div class="form-group" style="margin-top:1rem;">
<label class="form-label">Stacktrace / Log Ausschnitt</label>
<textarea name="stack_trace" class="form-input" rows="4" style="font-family:monospace;" placeholder="at MyApp.Core.Service.DoWork()..."></textarea>
</div>
<button type="submit" class="btn" style="margin-top:1rem;">Item Speichern & Anlegen</button>
</form>
</div>
</div>
</div>
<!-- ================= MODULE: TOKEN-VERWALTUNG ================= -->
<div id="tab-tokens" class="tab-content">
<div id="sub-tokens-masters" class="subtab-content active">
<div class="card">
<div class="card-header"><h2 class="card-title">👑 Master-Tokens (Selbst-Provisionierung für Client-Apps & Host-Skripte)</h2></div>
<table>
<thead>
<tr>
<th>Token ID</th>
<th>Bezeichnung</th>
<th>Typ / Identität</th>
<th>Projekt / Lizenz</th>
<th>Rechte (Scopes)</th>
<th>Sub-Tokens</th>
<th>Token Key</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach ($coreMasterTokens as $mTok): ?>
<?php
$rawVal = !empty($mTok['raw_token']) ? $mTok['raw_token'] : 'dc_master_...';
$masked = substr($rawVal, 0, 12) . '••••••••••••••••';
$scopesArr = json_decode($mTok['scopes'], true) ?: ['*'];
?>
<tr>
<td><code><?= htmlspecialchars($mTok['token_id']) ?></code></td>
<td><strong><?= htmlspecialchars($mTok['name']) ?></strong></td>
<td>
<span class="badge badge-warning"><?= strtoupper($mTok['owner_type']) ?></span><br>
<small><?= htmlspecialchars($mTok['owner_identity'] ?? '-') ?></small>
</td>
<td>
<?= htmlspecialchars($mTok['project_slug'] ?? '-') ?>
<?= !empty($mTok['license_key']) ? '<br><small>Lic: ' . htmlspecialchars($mTok['license_key']) . '</small>' : '' ?>
</td>
<td>
<?php foreach ($scopesArr as $sc): ?>
<span class="badge" style="background:rgba(255,255,255,0.06); font-size:0.75rem;"><?= htmlspecialchars($sc) ?></span>
<?php endforeach; ?>
</td>
<td>
<span class="badge badge-up"><?= (int)($mTok['sub_token_count'] ?? 0) ?> Sub-Tokens</span>
</td>
<td>
<code id="tok-core-text-<?= $mTok['token_id'] ?>" data-full="<?= htmlspecialchars($rawVal) ?>" data-masked="<?= htmlspecialchars($masked) ?>">
<?= htmlspecialchars($masked) ?>
</code>
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleTokenMask('core-text-<?= $mTok['token_id'] ?>')">👁️</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="copyTokenValue('core-text-<?= $mTok['token_id'] ?>')">📋</button>
</td>
<td>
<?php if (!$mTok['revoked']): ?>
<form method="POST" action="index.php#tab-tokens" style="display:inline" onsubmit="return confirm('Master-Token widerrufen? Alle zugehörigen Sub-Tokens werden sofort kaskadierend mit gesperrt!');">
<input type="hidden" name="action" value="revoke_core_token">
<input type="hidden" name="token_id" value="<?= $mTok['token_id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Master & Subs Widerrufen</button>
</form>
<?php else: ?>
<span class="badge badge-down">WIDERUFEN</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div id="sub-tokens-subs" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">🔑 Aktive Sub-Tokens (Per Provisioning API Erstellt)</h2></div>
<table>
<thead>
<tr>
<th>Token ID</th>
<th>Parent Master ID</th>
<th>Bezeichnung / Client</th>
<th>Umgebung</th>
<th>Scopes</th>
<th>Zuletzt Genutzt</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php
$subTokens = array_filter($coreAllTokens, fn($t) => $t['type'] === 'sub');
foreach ($subTokens as $sTok):
$scopesArr = json_decode($sTok['scopes'], true) ?: [];
?>
<tr>
<td><code><?= htmlspecialchars($sTok['token_id']) ?></code></td>
<td><code><?= htmlspecialchars($sTok['parent_token_id'] ?? '-') ?></code></td>
<td><strong><?= htmlspecialchars($sTok['name']) ?></strong></td>
<td>
<span class="badge badge-stopped"><?= strtoupper($sTok['environment']) ?></span>
</td>
<td>
<?php foreach ($scopesArr as $sc): ?>
<span class="badge" style="background:rgba(255,255,255,0.06); font-size:0.75rem;"><?= htmlspecialchars($sc) ?></span>
<?php endforeach; ?>
</td>
<td><?= $sTok['last_used_at'] ? htmlspecialchars($sTok['last_used_at']) : 'Noch nie' ?></td>
<td><span class="badge badge-<?= $sTok['revoked'] ? 'down' : 'up' ?>"><?= $sTok['revoked'] ? 'WIDERUFEN' : 'AKTIV' ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div id="sub-tokens-create" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title"> Neuen Master-Token Erstellen</h2></div>
<form method="POST" action="index.php#tab-tokens">
<input type="hidden" name="action" value="create_master_token">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Bezeichnung / Name</label>
<input type="text" name="name" class="form-input" required placeholder="z. B. PolyTrader Prod Server Master Key">
</div>
<div class="form-group">
<label class="form-label">Identitätstyp (Owner Type)</label>
<select name="owner_type" class="form-input" required>
<option value="custom" selected>Custom / Allgemein</option>
<option value="project">Projekt-Gebunden</option>
<option value="license">Lizenz-Gebunden</option>
<option value="host">Host / Infrastruktur Server</option>
<option value="dev_agent">Entwickler KI-Agent</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Freie Identität / Host / HWID (Optional)</label>
<input type="text" name="owner_identity" class="form-input" placeholder="z. B. srv-db-01 oder HWID-88A9...">
</div>
<div class="form-group">
<label class="form-label">Projekt (Optional)</label>
<select name="project_slug" class="form-input">
<option value="">-- Keins (Universal) --</option>
<?php foreach ($projects as $p): ?>
<option value="<?= htmlspecialchars($p['slug']) ?>"><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">Umgebung (Environment Limit)</label>
<select name="environment" class="form-input" required>
<option value="all" selected>🌐 Alle Umgebungen (All)</option>
<option value="production">🔴 Nur Produktiv (Production)</option>
<option value="development">🔵 Nur Entwicklung (Development)</option>
</select>
</div>
</div>
<div class="form-group" style="margin-top:1rem;">
<label class="form-label">Erlaubte Scopes (Rechte-Umfang für Sub-Tokens)</label>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:0.5rem; margin-top:0.5rem;">
<label style="display:inline-flex; align-items:center; gap:0.5rem; font-size:0.85rem; cursor:pointer;">
<input type="checkbox" name="scopes[]" value="*" checked> 🌟 Alle Scopes (*)
</label>
<label style="display:inline-flex; align-items:center; gap:0.5rem; font-size:0.85rem; cursor:pointer;">
<input type="checkbox" name="scopes[]" value="bugtracker:report"> 🐛 Bugtracker Report
</label>
<label style="display:inline-flex; align-items:center; gap:0.5rem; font-size:0.85rem; cursor:pointer;">
<input type="checkbox" name="scopes[]" value="bugtracker:manage"> ⚙️ Bugtracker Manage & Resolve
</label>
<label style="display:inline-flex; align-items:center; gap:0.5rem; font-size:0.85rem; cursor:pointer;">
<input type="checkbox" name="scopes[]" value="watchdog:ping"> 🛡️ Watchdog Heartbeat Ping
</label>
<label style="display:inline-flex; align-items:center; gap:0.5rem; font-size:0.85rem; cursor:pointer;">
<input type="checkbox" name="scopes[]" value="updateservice:read"> 📦 UpdateService Read
</label>
</div>
</div>
<button type="submit" class="btn" style="margin-top:1.25rem;">Master-Token Erstellen</button>
</form>
</div>
</div>
</div>
<!-- ================= MODULE 5: SYSTEM & DB ================= -->
<div id="tab-system" class="tab-content">
<div id="sub-system-status" class="subtab-content active">
<div class="card">
<div class="card-header"><h2 class="card-title">⚙️ Server & Umgebung Information</h2></div>
<div class="form-grid">
<div class="form-group">
<label class="form-label">PHP Version</label>
<input type="text" readonly class="form-input" value="<?= PHP_VERSION ?>">
</div>
<div class="form-group">
<label class="form-label">Server Software</label>
<input type="text" readonly class="form-input" value="<?= $_SERVER['SERVER_SOFTWARE'] ?? 'Apache/MySQL' ?>">
</div>
<div class="form-group">
<label class="form-label">MySQL Datenbank</label>
<input type="text" readonly class="form-input" value="bergisnu_db0 @ lznk.your-database.de">
</div>
</div>
</div>
</div>
<div id="sub-system-swagger" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">📖 API Documentation (Swagger UI)</h2></div>
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:1.25rem;">
Die API-Dokumentation liegt geschützt hinter der Plattform-Authentifizierung.
</p>
<div class="prompt-box">
=== DEPLOYMENTCENTER REST API SPECIFICATION (OpenAPI 3.0) ===
1. LIZENZEN MODULE:
- POST /api/license/v1/validate (Public)
Body: { "product": "myapp", "license_key": "XXXXX-...", "hardware_id": "HWID-..." }
- POST /api/license/v1/deactivate (Geschützt - Requires Bearer / Auth)
Body: { "product": "myapp", "license_key": "XXXXX-...", "hardware_id": "HWID-..." }
2. WATCHDOG MODULE:
- POST /api/watchdog/v1/ping (Header: X-Agent-Token)
Body: { "source": "srv-db-01", "status": "ok", "message": "Heartbeat", "interval": 60 }
- GET /api/watchdog/v1/status
- GET /api/watchdog/v1/events?limit=50
3. UPDATESERVICE MODULE:
- GET /api/updateservice/v1/check?product=myapp&version=1.0.0
- GET /api/updateservice/v1/releases?product=myapp
</div>
</div>
</div>
<div id="sub-system-migration" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">🗄️ Datenbank-Migration & Testdaten-Re-Seed</h2></div>
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:1.25rem;">
Führt das vollständige DB-Schema aus und stellt sicher, dass alle Tabellen angelegt und migriert sind.
</p>
<a href="install_db.php" target="_blank" class="btn">Datenbank-Migration jetzt ausführen (install_db.php)</a>
</div>
</div>
</div>
</main>
</div>
<script>
let autoRefreshTimer = null;
let isAutoRefreshActive = false;
const subnavItems = {
'overview': [],
'projects': [
{ id: 'sub-projects-list', label: '📊 Projekte Übersicht', active: true }
],
'license': [
{ id: 'sub-license-dashboard', label: '📊 Dashboard', active: true },
{ id: 'sub-license-licenses', label: '🔑 Lizenzverwaltung' },
{ id: 'sub-license-details', label: '🔍 Lizenz Bearbeiten & HW' },
{ id: 'sub-license-offline', label: '💾 Offline-Lizenzen (.lic)' },
{ id: 'sub-license-audit', label: '📜 Audit-Log' },
{ id: 'sub-license-integration', label: '💻 Integration' },
{ id: 'sub-license-settings', label: '⚙️ Settings' }
],
'watchdog': [
{ id: 'sub-watchdog-dashboard', label: '📊 Dashboard', active: true },
{ id: 'sub-watchdog-hierarchy', label: '🌳 System-Hierarchie' },
{ id: 'sub-watchdog-add', label: ' Monitor Hinzufügen' },
{ id: 'sub-watchdog-eventlog', label: '📜 Event-Log' },
{ id: 'sub-watchdog-tokens', label: '🔑 Agent-Tokens' }
],
'updateservice': [
{ id: 'sub-update-releases', label: '📊 Releases Overview', active: true },
{ id: 'sub-update-publish', label: ' Release Veröffentlichen' }
],
'bugtracker': [
{ id: 'sub-bugtracker-items', label: '🐛 Bugs & Features', active: true },
{ id: 'sub-bugtracker-new', label: ' Item Anlegen' }
],
'tokens': [
{ id: 'sub-tokens-masters', label: '👑 Master-Tokens', active: true },
{ id: 'sub-tokens-subs', label: '🔑 Sub-Tokens' },
{ id: 'sub-tokens-create', label: ' Master-Token Erstellen' }
],
'system': [
{ id: 'sub-system-status', label: '⚙️ System-Status', active: true },
{ id: 'sub-system-swagger', label: '📖 API Swagger Docs' },
{ id: 'sub-system-migration', label: '🗄️ DB Migration' }
]
};
// Render Subnav links in Top-Bar
function renderTopBarSubnav(moduleName) {
const container = document.getElementById('topbarSubnav');
container.innerHTML = '';
const items = subnavItems[moduleName] || [];
items.forEach(item => {
const btn = document.createElement('button');
btn.className = `subnav-link ${item.active ? 'active' : ''}`;
btn.innerText = item.label;
btn.onclick = () => switchSubTab(moduleName, item.id, btn);
container.appendChild(btn);
});
}
// Sidebar Toggle Handler
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const mainWrapper = document.getElementById('mainWrapper');
sidebar.classList.toggle('collapsed');
mainWrapper.classList.toggle('expanded');
localStorage.setItem('sidebar_collapsed', sidebar.classList.contains('collapsed'));
}
if (localStorage.getItem('sidebar_collapsed') === 'true') {
document.getElementById('sidebar').classList.add('collapsed');
document.getElementById('mainWrapper').classList.add('expanded');
}
// 30s Auto Refresh Toggle
function toggleAutoRefresh() {
const btn = document.getElementById('btnAutoRefresh');
const txt = document.getElementById('autoRefreshText');
isAutoRefreshActive = !isAutoRefreshActive;
if (isAutoRefreshActive) {
btn.classList.add('active');
txt.innerText = '30s REFRESH: AN';
autoRefreshTimer = setInterval(() => location.reload(), 30000);
} else {
btn.classList.remove('active');
txt.innerText = '30s REFRESH: AUS';
if (autoRefreshTimer) clearInterval(autoRefreshTimer);
}
}
// Main Module Navigation Switcher
function switchMainTab(moduleName, el) {
document.querySelectorAll('.main-content > .tab-content').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.nav-link').forEach(n => n.classList.remove('active'));
const targetTab = document.getElementById('tab-' + moduleName);
if (targetTab) {
targetTab.classList.add('active');
}
if (el) {
el.classList.add('active');
}
const pageTitles = {
'overview': 'Globales Dashboard',
'projects': 'Projektverwaltung',
'license': 'Lizenzverwaltung',
'watchdog': 'WatchDog Monitoring',
'updateservice': 'UpdateService Releases',
'bugtracker': 'Bug- & Feature-Tracker',
'tokens': 'Token-Verwaltung & Provisionierung',
'system': 'System & Datenbank Status'
};
document.getElementById('topPageTitle').innerText = pageTitles[moduleName] || 'Deploymentcenter';
renderTopBarSubnav(moduleName);
location.hash = 'tab-' + moduleName;
}
// Bugtracker Table Filter JS
function filterBugtrackerTable() {
const env = document.getElementById('btFilterEnv').value;
const type = document.getElementById('btFilterType').value;
const status = document.getElementById('btFilterStatus').value;
document.querySelectorAll('#btTableBody .bt-row').forEach(row => {
const matchEnv = (env === 'all' || row.getAttribute('data-env') === env);
const matchType = (type === 'all' || row.getAttribute('data-type') === type);
const matchStatus = (status === 'all' || row.getAttribute('data-status') === status);
if (matchEnv && matchType && matchStatus) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
// Open Bugtracker Item Details & Timeline Modal
function openBugtrackerModal(itemId) {
const modal = document.getElementById('btDetailModal');
const content = document.getElementById('btModalContent');
modal.style.display = 'flex';
content.innerHTML = '<div style="text-align:center; padding:2rem; color:var(--text-muted);">⏳ Lade Details & Timeline...</div>';
fetch(`api/bugtracker/v1/manage/index.php?id=${itemId}`)
.then(r => r.json())
.then(res => {
if (!res.item) {
content.innerHTML = '<div class="alert alert-danger">Fehler beim Laden des Items.</div>';
return;
}
const item = res.item;
const comments = item.comments || [];
let commentsHtml = '';
if (comments.length === 0) {
commentsHtml = '<p style="color:var(--text-muted); font-size:0.85rem;">Noch keine Kommentare oder Ermittlungsschritte hinterlegt.</p>';
} else {
comments.forEach(c => {
commentsHtml += `
<div style="background:rgba(255,255,255,0.03); border:1px solid var(--border-glass); padding:0.75rem; border-radius:8px; margin-bottom:0.6rem;">
<div style="display:flex; justify-content:space-between; font-size:0.8rem; margin-bottom:0.3rem;">
<strong style="color:var(--primary);">${c.author}</strong>
<span style="color:var(--text-muted);">${c.created_at}</span>
</div>
<div style="font-size:0.875rem; white-space:pre-wrap;">${c.comment}</div>
</div>
`;
});
}
content.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:start; margin-bottom:1rem;">
<div>
<span class="badge badge-${item.type === 'feature_request' ? 'warning' : 'down'}">${item.type.toUpperCase()}</span>
<span class="badge badge-stopped">${item.environment.toUpperCase()}</span>
<h3 style="margin:0.5rem 0 0.2rem 0; color:#fff;">#${item.id}: ${item.title}</h3>
<div style="font-size:0.8rem; color:var(--text-muted);">
Projekt: <code>${item.project_slug}</code> | Build: <code>${item.build_version || 'v1.0.0'}</code> | Gemeldet von: ${item.created_by}
</div>
</div>
<button type="button" class="btn btn-sm btn-secondary" onclick="closeBugtrackerModal()">✕</button>
</div>
${item.description ? `
<div style="margin-bottom:1rem;">
<strong>Beschreibung:</strong>
<div style="background:rgba(0,0,0,0.2); padding:0.6rem; border-radius:6px; font-size:0.875rem; margin-top:0.3rem;">${item.description}</div>
</div>
` : ''}
${item.error_message ? `
<div style="margin-bottom:1rem;">
<strong style="color:var(--danger);">Fehlermeldung:</strong>
<pre style="background:#0d1117; color:#ff7b72; padding:0.75rem; border-radius:6px; font-family:monospace; font-size:0.82rem; overflow-x:auto; margin-top:0.3rem;">${item.error_message}</pre>
</div>
` : ''}
${item.stack_trace ? `
<div style="margin-bottom:1rem;">
<strong>Stacktrace:</strong>
<pre style="background:#0d1117; color:#c9d1d9; padding:0.75rem; border-radius:6px; font-family:monospace; font-size:0.78rem; max-height:200px; overflow-y:auto; margin-top:0.3rem;">${item.stack_trace}</pre>
</div>
` : ''}
<hr style="border-color:var(--border-glass); margin:1.25rem 0;">
<h4 style="margin-bottom:0.75rem; color:#fff;">📜 Agenten-Historie & Kommentar-Timeline</h4>
<div style="max-height:250px; overflow-y:auto; margin-bottom:1rem;">${commentsHtml}</div>
<form method="POST" action="index.php#tab-bugtracker">
<input type="hidden" name="action" value="bt_add_comment">
<input type="hidden" name="item_id" value="${item.id}">
<div class="form-group">
<label class="form-label">Ermittlungsschritt / Kommentar Hinzufügen</label>
<textarea name="comment" class="form-input" rows="2" required placeholder="Notiere hier Diagnoseergebnisse oder Hinweise für andere Agenten..."></textarea>
</div>
<button type="submit" class="btn btn-sm" style="margin-top:0.5rem;">Kommentar Speichern</button>
</form>
`;
})
.catch(err => {
content.innerHTML = `<div class="alert alert-danger">Fehler beim Laden: ${err.message}</div>`;
});
}
function closeBugtrackerModal() {
document.getElementById('btDetailModal').style.display = 'none';
}
function openResolveModal(itemId, title) {
document.getElementById('resolveItemId').value = itemId;
document.getElementById('resolveItemTitle').innerText = title;
document.getElementById('btResolveModal').style.display = 'flex';
}
function closeResolveModal() {
document.getElementById('btResolveModal').style.display = 'none';
}
// Horizontal Submenu Switcher in Top Bar
function switchSubTab(moduleName, subtabId, el) {
const parentModule = document.getElementById('tab-' + moduleName);
if (!parentModule) return;
parentModule.querySelectorAll('.subtab-content').forEach(s => s.classList.remove('active'));
document.querySelectorAll('#topbarSubnav .subnav-link').forEach(l => l.classList.remove('active'));
const targetSub = document.getElementById(subtabId);
if (targetSub) {
targetSub.classList.add('active');
}
if (el) {
el.classList.add('active');
}
location.hash = subtabId;
}
// Project Editing Form Functions
function editProject(p) {
document.getElementById('projEditId').value = p.id;
document.getElementById('projEditSlug').value = p.slug;
document.getElementById('projEditSlug').readOnly = true;
document.getElementById('projEditName').value = p.name;
document.getElementById('projEditTtl').value = p.default_cache_ttl_hours;
document.getElementById('projEditNotes').value = p.notes || '';
document.getElementById('projSubmitBtn').innerText = 'Änderungen Speichern';
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function resetProjectForm() {
document.getElementById('projEditId').value = 0;
document.getElementById('projEditSlug').value = '';
document.getElementById('projEditSlug').readOnly = false;
document.getElementById('projEditName').value = '';
document.getElementById('projEditTtl').value = 168;
document.getElementById('projEditNotes').value = '';
document.getElementById('projSubmitBtn').innerText = 'Projekt Anlegen';
}
// Project Safety Deletion Modal
function openDeleteProjectModal(id, slug, name) {
document.getElementById('deleteProjId').value = id;
document.getElementById('deleteProjSlugDisplay').innerText = slug;
document.getElementById('deleteProjNameDisplay').innerText = name + ' (' + slug + ')';
document.getElementById('deleteProjConfirmInput').value = '';
document.getElementById('deleteProjectModal').classList.add('active');
}
function closeDeleteProjectModal() {
document.getElementById('deleteProjectModal').classList.remove('active');
}
// Monitor Icon Selector (Matching Screenshot 1)
function selectIcon(btnEl, iconVal) {
document.querySelectorAll('.icon-option-btn').forEach(b => b.classList.remove('selected'));
btnEl.classList.add('selected');
document.getElementById('monEditIconSelected').value = iconVal;
}
// Rich Monitor Editing Modal Functions (Screenshot 1 - Null-safe)
function openEditMonitorModal(m) {
const setVal = (id, val) => {
const el = document.getElementById(id);
if (el) el.value = val;
};
setVal('monEditOldSource', m.source || '');
setVal('monEditNewSource', m.source || '');
setVal('monEditType', m.type || 'host');
setVal('monEditParent', m.parent_source || '');
setVal('monEditGroup', m.group_key || 'Applications');
setVal('monEditInterval', m.expected_interval_sec || 60);
setVal('monEditNotes', m.notes || '');
setVal('monEditUrl', m.url || '');
const mutedEl = document.getElementById('monEditMuted');
if (mutedEl) mutedEl.checked = parseInt(m.is_muted) === 1;
// Highlight selected icon preset or auto
const iconVal = m.icon || '';
setVal('monEditIconSelected', iconVal);
document.querySelectorAll('.icon-option-btn').forEach(b => {
b.classList.remove('selected');
if (b.getAttribute('data-icon') === iconVal) {
b.classList.add('selected');
}
});
const modal = document.getElementById('editMonitorModal');
if (modal) modal.classList.add('active');
}
function closeEditMonitorModal() {
const modal = document.getElementById('editMonitorModal');
if (modal) modal.classList.remove('active');
}
function triggerDeleteMonitorFromModal() {
const source = document.getElementById('monEditOldSource').value;
if (confirm(`Möchtest du den Monitor '${source}' wirklich unwiderruflich löschen?`)) {
document.getElementById('hiddenDeleteSource').value = source;
document.getElementById('hiddenDeleteMonitorForm').submit();
}
}
// License Editing Functions
function openEditLicense(l) {
switchMainTab('license', document.querySelector('.nav-link[onclick*="license"]'));
const subLink = document.querySelector('#topbarSubnav .subnav-link:nth-child(3)');
switchSubTab('license', 'sub-license-details', subLink);
const select = document.getElementById('licEditSelect');
select.value = l.id;
loadLicenseData(select);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function loadLicenseData(selectEl) {
const opt = selectEl.options[selectEl.selectedIndex];
document.getElementById('editCustomerName').value = opt.getAttribute('data-cname') || '';
document.getElementById('editCustomerEmail').value = opt.getAttribute('data-cemail') || '';
document.getElementById('editStatus').value = opt.getAttribute('data-status') || 'active';
document.getElementById('editMaxAct').value = opt.getAttribute('data-max') || '2';
document.getElementById('editExpiresAt').value = opt.getAttribute('data-exp') || '';
}
// Token Masking Toggle
function toggleTokenMask(tokId) {
const el = document.getElementById('tok-text-' + tokId);
if (el.innerText.includes('••••')) {
el.innerText = el.getAttribute('data-full');
} else {
el.innerText = el.getAttribute('data-masked');
}
}
function copyTokenValue(tokId) {
const el = document.getElementById('tok-text-' + tokId);
const fullVal = el.getAttribute('data-full');
navigator.clipboard.writeText(fullVal);
alert('Token in Zwischenablage kopiert!');
}
function generateOfflinePayload() {
const key = document.getElementById('offlineLicSelect').value;
const hwId = document.getElementById('offlineHwId').value || 'HWID-GENERAL';
const payload = {
"type": "offline_license_file",
"issued_at": Math.floor(Date.now() / 1000),
"license_key": key,
"hardware_id": hwId,
"status": "valid",
"valid_until": "2027-12-31 23:59:59",
"signature": "ED25519_SIG_" + btoa(key + hwId).substring(0, 32)
};
document.getElementById('offlineResultBox').innerText = JSON.stringify(payload, null, 2);
}
// Initialize default subnav for Overview
renderTopBarSubnav('overview');
// Restore tab based on URL hash
window.addEventListener('load', () => {
const hash = location.hash.replace('#', '');
if (hash) {
if (hash.startsWith('tab-')) {
const module = hash.replace('tab-', '');
const link = document.querySelector(`.nav-link[onclick*="${module}"]`);
if (link) switchMainTab(module, link);
} else if (hash.startsWith('sub-')) {
const moduleParts = hash.split('-');
const module = moduleParts[1];
const mainLink = document.querySelector(`.nav-link[onclick*="${module}"]`);
if (mainLink) switchMainTab(module, mainLink);
setTimeout(() => {
const subLink = Array.from(document.querySelectorAll('#topbarSubnav .subnav-link')).find(l => l.innerText.toLowerCase().includes(module));
if (subLink) switchSubTab(module, hash, subLink);
}, 50);
}
}
});
</script>
<!-- Bugtracker Details & Timeline Modal -->
<div id="btDetailModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.75); backdrop-filter:blur(8px); z-index:9999; align-items:center; justify-content:center; padding:1rem;">
<div class="card" style="width:100%; max-width:750px; max-height:90vh; overflow-y:auto; position:relative; background:#161c28; border:1px solid var(--border-glass);">
<div id="btModalContent">
<!-- Loaded via JS -->
</div>
</div>
</div>
<!-- Bugtracker Resolve Modal -->
<div id="btResolveModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.75); backdrop-filter:blur(8px); z-index:9999; align-items:center; justify-content:center; padding:1rem;">
<div class="card" style="width:100%; max-width:500px; background:#161c28; border:1px solid var(--border-glass);">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
<h3 class="card-title" style="margin:0; color:#fff;">✔ Item als Gelöst / Umgesetzt Markieren</h3>
<button type="button" class="btn btn-sm btn-secondary" onclick="closeResolveModal()">✕</button>
</div>
<p style="font-size:0.875rem; color:var(--text-muted); margin-bottom:1rem;" id="resolveItemTitle"></p>
<form method="POST" action="index.php#tab-bugtracker">
<input type="hidden" name="action" value="bt_resolve">
<input type="hidden" name="item_id" id="resolveItemId" value="0">
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Lösungs-Build / Version (z. B. v1.4.3)</label>
<input type="text" name="resolved_in_build" class="form-input" required value="v1.4.3" placeholder="v1.4.3">
</div>
<div class="form-group" style="margin-bottom:1.25rem;">
<label class="form-label">Lösungs-Notizen / Dokumentation</label>
<textarea name="resolution_notes" class="form-input" rows="3" placeholder="Kurze Beschreibung, wie das Problem behoben oder das Feature umgesetzt wurde..."></textarea>
</div>
<div style="display:flex; justify-content:flex-end; gap:0.5rem;">
<button type="button" class="btn btn-secondary" onclick="closeResolveModal()">Abbrechen</button>
<button type="submit" class="btn">Als Gelöst Speichern</button>
</div>
</form>
</div>
</div>
</body>
</html>