Files
Deploymentcenter/public/index.php
T

1302 lines
76 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/Modules/UpdateService/UpdateManager.php';
use Deploymentcenter\Core\Db;
use Deploymentcenter\Core\Auth;
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;
Auth::requireLogin();
$config = require __DIR__ . '/../config/config.php';
$pdo = Db::init($config);
$msg = null;
$msgType = 'success';
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
// Create Product
if ($action === 'create_product') {
$slug = trim($_POST['slug'] ?? '');
$name = trim($_POST['name'] ?? '');
$ttl = (int)($_POST['ttl'] ?? 168);
$notes = trim($_POST['notes'] ?? '');
if ($slug && $name) {
try {
$stmt = $pdo->prepare('INSERT INTO license_products (slug, name, default_cache_ttl_hours, notes) VALUES (:s, :n, :t, :notes)');
$stmt->execute([':s' => $slug, ':n' => $name, ':t' => $ttl, ':notes' => $notes]);
$msg = "Produkt '{$name}' (Slug: {$slug}) wurde erfolgreich angelegt.";
} catch (Throwable $e) {
$msg = "Fehler beim Erstellen des Produkts: " . $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
]);
// Audit log
$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 erfolgreich generiert: <strong style='font-family:monospace; font-size:1.1rem;'>{$licenseKey}</strong>";
} catch (Throwable $e) {
$msg = "Fehler bei Generierung: " . $e->getMessage();
$msgType = 'danger';
}
}
}
// Revoke License
if ($action === 'revoke_license') {
$licId = (int)($_POST['license_id'] ?? 0);
if ($licId > 0) {
$stmt = $pdo->prepare('UPDATE license_licenses SET status = "revoked" WHERE id = :id');
$stmt->execute([':id' => $licId]);
$pdo->prepare('INSERT INTO license_audit_log (actor, action, details) VALUES ("admin", "license.revoke", :d)')
->execute([':d' => json_encode(['license_id' => $licId])]);
$msg = "Lizenz wurde widerrufen.";
}
}
// Block / Unblock Hardware Activation
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.";
}
}
// Extend License Expiration
if ($action === 'extend_license') {
$licId = (int)($_POST['license_id'] ?? 0);
$newExp = !empty($_POST['new_expires_at']) ? $_POST['new_expires_at'] . ' 23:59:59' : null;
if ($licId > 0) {
$stmt = $pdo->prepare('UPDATE license_licenses SET expires_at = :exp, status = "active" WHERE id = :id');
$stmt->execute([':exp' => $newExp, ':id' => $licId]);
$msg = "Ablaufdatum der Lizenz wurde aktualisiert.";
}
}
// Create Agent Token (Watchdog)
if ($action === 'create_agent_token') {
$source = trim($_POST['token_source'] ?? '');
$name = trim($_POST['token_name'] ?? '');
if ($name) {
$tokMgr = new TokenManager($pdo);
$res = $tokMgr->createToken($source, $name);
$msg = "Agent-Token für '{$name}' generiert: <strong style='font-family:monospace;'>{$res['raw_token']}</strong> (Bitte sicher aufbewahren!)";
}
}
// 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'] ?? '');
$url = trim($_POST['download_url'] ?? '');
$hash = trim($_POST['sha256_hash'] ?? '');
$notes = trim($_POST['release_notes'] ?? '');
$critical = isset($_POST['is_critical']);
if ($productSlug && $version && $url) {
$updMgr = new UpdateManager($pdo);
if ($updMgr->addRelease($productSlug, $version, $notes, $url, $hash, $critical)) {
$msg = "Release v{$version} für '{$productSlug}' gespeichert.";
} else {
$msg = "Fehler beim Speichern des Releases.";
$msgType = 'danger';
}
}
}
}
// Fetch all Data
$products = $pdo->query('SELECT * FROM license_products 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 license_products 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 license_products 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();
$monitorsUp = 0; $monitorsWarning = 0; $monitorsDown = 0;
foreach ($monitors as $m) {
if ($m['state'] === 'up') $monitorsUp++;
elseif ($m['state'] === 'warning') $monitorsWarning++;
else $monitorsDown++;
}
$eventLog = new EventLog($pdo);
$recentEvents = $eventLog->getRecentEvents(100);
$agentTokens = $pdo->query('SELECT * FROM watchdog_agent_tokens ORDER BY created_at_utc DESC')->fetchAll();
$updateMgr = new UpdateManager($pdo);
$releases = $updateMgr->getReleases();
$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 - Central Platform</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=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-main: #0e1013;
--bg-sidebar: #12151b;
--bg-card: rgba(22, 25, 31, 0.85);
--bg-card-elev: #1c2027;
--border-card: #262b33;
--border-soft: rgba(255, 255, 255, 0.08);
--primary: #f2622e; /* Brand Orange */
--primary-hover: #d95323;
--accent-weak: rgba(242, 98, 46, 0.14);
--success: #35c46a;
--warning: #f5a623;
--danger: #ef4e4e;
--text-main: #e6e8eb;
--text-muted: #8a9099;
--sidebar-width: 260px;
--sidebar-collapsed-width: 72px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', system-ui, sans-serif; background: var(--bg-main); color: var(--text-main); min-height: 100vh; display: flex; overflow-x: hidden; }
/* Sidebar Collapsible */
.sidebar { width: var(--sidebar-width); background: var(--bg-sidebar); border-right: 1px solid var(--border-card); 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; }
.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: 38px; height: 38px; background: linear-gradient(135deg, var(--primary), #a855f7); border-radius: 10px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(242,98,46,0.3); }
.brand-logo svg { width: 22px; height: 22px; stroke: #fff; fill: none; stroke-width: 2; }
.brand-title { font-size: 1.1rem; font-weight: 700; 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-card); color: var(--text-muted); width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; position: absolute; right: -14px; top: 22px; transition: transform 0.3s; z-index: 101; }
.sidebar.collapsed .sidebar-toggle-btn { transform: rotate(180deg); }
.nav-menu { display: flex; flex-direction: column; gap: 0.35rem; list-style: none; }
.nav-item { position: relative; }
.nav-link { display: flex; align-items: center; gap: 0.85rem; padding: 0.75rem 0.85rem; color: var(--text-muted); text-decoration: none; border-radius: 10px; font-size: 0.9rem; font-weight: 500; transition: all 0.2s; cursor: pointer; white-space: nowrap; }
.nav-link:hover, .nav-link.active { background: var(--accent-weak); color: #fff; }
.nav-link.active { color: var(--primary); border-left: 3px solid var(--primary); }
.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 Content */
.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; }
.main-wrapper.expanded { margin-left: var(--sidebar-collapsed-width); }
.main-content { padding: 1.75rem 2rem; max-width: 1500px; }
/* Horizontal Submenu Top Bar */
.module-subnav { display: flex; align-items: center; gap: 0.5rem; background: var(--bg-card); border: 1px solid var(--border-card); padding: 0.5rem 0.75rem; border-radius: 14px; margin-bottom: 1.75rem; overflow-x: auto; scrollbar-width: none; }
.subnav-link { background: transparent; border: none; color: var(--text-muted); padding: 0.6rem 1rem; font-size: 0.85rem; font-weight: 600; border-radius: 8px; cursor: pointer; transition: all 0.2s; white-space: nowrap; display: flex; align-items: center; gap: 0.5rem; }
.subnav-link:hover { color: #fff; background: rgba(255,255,255,0.04); }
.subnav-link.active { background: var(--primary); color: #fff; box-shadow: 0 4px 12px rgba(242,98,46,0.3); }
/* Stats 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-card); border-radius: 14px; padding: 1.25rem; }
.stat-header { display: flex; align-items: center; justify-content: space-between; color: var(--text-muted); font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
.stat-value { font-size: 1.9rem; font-weight: 700; margin-top: 0.5rem; color: #fff; display: flex; align-items: center; gap: 0.5rem; }
/* Segment Bars (Resource Usage) */
.segment-bar-container { margin-top: 0.5rem; }
.segment-bar-label { display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.25rem; }
.segment-bar { display: flex; gap: 2px; height: 10px; background: rgba(0,0,0,0.3); padding: 2px; border-radius: 4px; }
.segment { flex: 1; border-radius: 1px; background: rgba(255,255,255,0.06); }
.segment.filled-ok { background: var(--success); }
.segment.filled-warn { background: var(--warning); }
.segment.filled-crit { background: var(--danger); }
/* Cards & Tables */
.card { background: var(--bg-card); border: 1px solid var(--border-card); border-radius: 14px; padding: 1.5rem; margin-bottom: 1.75rem; }
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.25rem; }
.card-title { font-size: 1.1rem; font-weight: 600; color: #fff; display: flex; align-items: center; gap: 0.5rem; }
table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.875rem; }
th { padding: 0.75rem 1rem; color: var(--text-muted); font-weight: 600; border-bottom: 1px solid var(--border-card); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; }
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.25rem 0.65rem; border-radius: 20px; font-size: 0.75rem; font-weight: 600; display: inline-flex; align-items: center; gap: 0.35rem; }
.badge-up { background: rgba(53, 196, 106, 0.15); color: var(--success); border: 1px solid rgba(53, 196, 106, 0.3); }
.badge-warning { background: rgba(245, 166, 35, 0.15); color: var(--warning); border: 1px solid rgba(245, 166, 35, 0.3); }
.badge-down { background: rgba(239, 78, 78, 0.15); color: var(--danger); border: 1px solid rgba(239, 78, 78, 0.3); }
.badge-stopped { background: rgba(138, 144, 153, 0.15); color: var(--text-muted); border: 1px solid rgba(138, 144, 153, 0.3); }
/* Buttons & Controls */
.btn { background: var(--primary); color: #fff; border: none; border-radius: 8px; padding: 0.6rem 1.1rem; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: all 0.2s; text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; }
.btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
.btn-secondary { background: var(--bg-card-elev); border: 1px solid var(--border-card); color: var(--text-main); }
.btn-secondary:hover { background: #262b33; }
.btn-sm { padding: 0.35rem 0.7rem; font-size: 0.75rem; }
.btn-danger { background: var(--danger); }
.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.8rem; color: var(--text-muted); font-weight: 500; }
.form-input { background: #0e1013; border: 1px solid var(--border-card); border-radius: 8px; padding: 0.65rem 0.85rem; color: #fff; font-size: 0.875rem; outline: none; transition: border-color 0.2s; }
.form-input:focus { border-color: var(--primary); }
.prompt-box { background: #0b0d10; border: 1px solid var(--border-card); border-radius: 10px; padding: 1.1rem; font-family: 'Consolas', 'Courier New', monospace; font-size: 0.85rem; 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: 10px; margin-bottom: 1.5rem; font-size: 0.9rem; }
.alert-success { background: rgba(53, 196, 106, 0.15); border: 1px solid rgba(53, 196, 106, 0.3); color: #a7f3d0; }
.alert-danger { background: rgba(239, 78, 78, 0.15); border: 1px solid rgba(239, 78, 78, 0.3); color: #fca5a5; }
.tab-content, .subtab-content { display: none; }
.tab-content.active, .subtab-content.active { display: block; }
</style>
</head>
<body>
<!-- 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>
<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('license', this)" title="LicenseLabrador">
<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">LicenseLabrador</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('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:600;">Abmelden</a>
</div>
</aside>
<!-- Main Content Area -->
<div class="main-wrapper" id="mainWrapper">
<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 style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.5rem;">
<h1 style="font-size:1.6rem; font-weight:700;">Globales Plattform Dashboard</h1>
<span style="font-size:0.85rem; color:var(--text-muted);">Letzte Aktualisierung: <?= date('H:i:s') ?> Uhr</span>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">Produkte</div>
<div class="stat-value"><?= count($products) ?></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">Monitore Status</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 Monitore Status</h2></div>
<table>
<thead>
<tr>
<th>Source / Instanz</th>
<th>Typ</th>
<th>Gruppe</th>
<th>Status</th>
<th>Letzte Meldung</th>
<th>Zuletzt Gesehen</th>
</tr>
</thead>
<tbody>
<?php foreach ($monitors as $m): ?>
<tr>
<td><strong><?= htmlspecialchars($m['source']) ?></strong> (<?= htmlspecialchars($m['instance']) ?>)</td>
<td><code><?= htmlspecialchars($m['type']) ?></code></td>
<td><?= htmlspecialchars($m['group_key'] ?? 'Default') ?></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: LICENSELABRADOR ================= -->
<div id="tab-license" class="tab-content">
<!-- Module Sub-Navigation Topbar -->
<div class="module-subnav">
<button class="subnav-link active" onclick="switchSubTab('license', 'sub-license-dashboard', this)">📊 Dashboard</button>
<button class="subnav-link" onclick="switchSubTab('license', 'sub-license-products', this)">📦 Produkte</button>
<button class="subnav-link" onclick="switchSubTab('license', 'sub-license-licenses', this)">🔑 Lizenzen</button>
<button class="subnav-link" onclick="switchSubTab('license', 'sub-license-details', this)">🔍 Lizenz-Details</button>
<button class="subnav-link" onclick="switchSubTab('license', 'sub-license-offline', this)">💾 Offline-Lizenzen</button>
<button class="subnav-link" onclick="switchSubTab('license', 'sub-license-audit', this)">📜 Audit-Log</button>
<button class="subnav-link" onclick="switchSubTab('license', 'sub-license-integration', this)">💻 Integration & Prompts</button>
<button class="subnav-link" onclick="switchSubTab('license', 'sub-license-settings', this)">⚙️ Einstellungen</button>
</div>
<!-- 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 Produkte</div>
<div class="stat-value"><?= count($products) ?></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: Products -->
<div id="sub-license-products" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">Neues Produkt anlegen</h2></div>
<form method="POST">
<input type="hidden" name="action" value="create_product">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Produkt-Slug (z. B. myapp)</label>
<input type="text" name="slug" class="form-input" required placeholder="myapp">
</div>
<div class="form-group">
<label class="form-label">Produkt Name</label>
<input type="text" name="name" class="form-input" required placeholder="My Application Pro">
</div>
<div class="form-group">
<label class="form-label">Offline Grace Cache TTL (Stunden)</label>
<input type="number" name="ttl" class="form-input" value="168">
</div>
</div>
<button type="submit" class="btn">Produkt Speichern</button>
</form>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Produktübersicht</h2></div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Slug</th>
<th>Produkt Name</th>
<th>Cache TTL</th>
<th>Erstellt am</th>
</tr>
</thead>
<tbody>
<?php foreach ($products as $p): ?>
<tr>
<td><?= $p['id'] ?></td>
<td><code><?= 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><?= $p['created_at'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</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">
<input type="hidden" name="action" value="create_license">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Produkt</label>
<select name="product_id" class="form-input" required>
<?php foreach ($products 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">Lizenzschlüssel Verwaltung</h2></div>
<table>
<thead>
<tr>
<th>Produkt</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.95rem; 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>
<?php if ($l['status'] === 'active'): ?>
<form method="POST" style="display:inline">
<input type="hidden" name="action" value="revoke_license">
<input type="hidden" name="license_id" value="<?= $l['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Lizenz wirklich widerrufen?')">Widerrufen</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Subtab: License Details -->
<div id="sub-license-details" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">Hardware-Aktivierungen verwalten</h2></div>
<table>
<thead>
<tr>
<th>Produkt</th>
<th>Lizenzschlüssel</th>
<th>Hardware-ID</th>
<th>Hostname</th>
<th>App-Version</th>
<th>Zuletzt gesehen</th>
<th>Sperr-Status</th>
<th>Aktion</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></td>
<td><?= htmlspecialchars($a['hostname'] ?? '-') ?></td>
<td><?= htmlspecialchars($a['app_version'] ?? '-') ?></td>
<td><?= htmlspecialchars($a['last_seen']) ?></td>
<td>
<?php if ($a['is_blocked']): ?>
<span class="badge badge-down">GESPERRT</span>
<?php else: ?>
<span class="badge badge-up">AKTIV</span>
<?php endif; ?>
</td>
<td>
<form method="POST" 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>
</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</h2></div>
<p style="color:var(--text-muted); font-size:0.9rem; margin-bottom:1rem;">
Generiert eine verschlüsselte/signierte <code>.lic</code> Offline-Lizenzdatei für Air-Gapped Kundensysteme ohne Internetverbindung.
</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</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">📜 System 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">🤖 KI-Prompts zur Modul-Integration in Ihre Softwareprojekte</h2></div>
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:1rem;">
Kopieren Sie diese vorgefertigten Prompts und übergeben Sie sie Ihrem KI-Coding-Assistenten (GitHub Copilot, Antigravity, ChatGPT), um die Lizenzprüfung in Ihre Anwendungen einzubauen:
</p>
<div style="margin-bottom:1.5rem;">
<label class="form-label">C# / .NET Project Prompt</label>
<div class="prompt-box" id="prompt-cs">Ich möchte mein C# / .NET Projekt mit dem LicenseLabrador Modul des Deploymentcenters verbinden.
Server-Konfiguration:
- Base API URL: <?= $baseUrl ?>/api/license/v1
- Endpunkte: /validate (POST), /deactivate (POST)
- Produkt-Slug: myapp
Anforderungen:
1. Erstelle eine C# Klasse `LicenseValidator.cs`.
2. Sende beim Anwendungsstart einen POST-Request an `<?= $baseUrl ?>/api/license/v1/validate` mit JSON:
{ "product": "myapp", "license_key": "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", "hardware_id": getHardwareId(), "nonce": Guid.NewGuid().ToString() }
3. Werte den Status aus ("valid", "revoked", "expired", "activation_limit").
4. Bei Server-Unerreichbarkeit: Erlaube Offline-Nutzung innerhalb des Cache-TTL Zeitraums.</div>
<button class="btn btn-secondary btn-sm" onclick="copyText('prompt-cs')">📋 Prompt Kopieren (C#)</button>
</div>
<div style="margin-bottom:1.5rem;">
<label class="form-label">Python Project Prompt</label>
<div class="prompt-box" id="prompt-py">Ich möchte meine Python-Anwendung mit dem LicenseLabrador Modul des Deploymentcenters verbinden.
Server URL: <?= $baseUrl ?>/api/license/v1/validate
Anforderungen:
- Erstelle ein Python-Skript `license_checker.py`.
- Generiere die Machine-GUID / Hardware-ID.
- Sende einen HTTP POST im JSON-Format mit `product`, `license_key`, `hardware_id`, `nonce`.
- Binde eine lokale Fallback-Logik bei Offline-Zeiten ein.</div>
<button class="btn btn-secondary btn-sm" onclick="copyText('prompt-py')">📋 Prompt Kopieren (Python)</button>
</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</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</label>
<input type="text" readonly class="form-input" value="<?= $baseUrl ?>/api/license/v1/deactivate">
</div>
</div>
</div>
</div>
</div>
<!-- ================= MODULE 2: WATCHDOG ================= -->
<div id="tab-watchdog" class="tab-content">
<!-- Module Sub-Navigation Topbar -->
<div class="module-subnav">
<button class="subnav-link active" onclick="switchSubTab('watchdog', 'sub-watchdog-dashboard', this)">📊 Dashboard</button>
<button class="subnav-link" onclick="switchSubTab('watchdog', 'sub-watchdog-uptime', this)">⏱️ Uptime Monitor</button>
<button class="subnav-link" onclick="switchSubTab('watchdog', 'sub-watchdog-services', this)">⚙️ Services</button>
<button class="subnav-link" onclick="switchSubTab('watchdog', 'sub-watchdog-applications', this)">💻 Applications</button>
<button class="subnav-link" onclick="switchSubTab('watchdog', 'sub-watchdog-machines', this)">🖥️ Machines</button>
<button class="subnav-link" onclick="switchSubTab('watchdog', 'sub-watchdog-hypervisors', this)">☁️ Hypervisors</button>
<button class="subnav-link" onclick="switchSubTab('watchdog', 'sub-watchdog-eventlog', this)">📜 Event-Log</button>
<button class="subnav-link" onclick="switchSubTab('watchdog', 'sub-watchdog-tokens', this)">🔑 Agent-Tokens</button>
<button class="subnav-link" onclick="switchSubTab('watchdog', 'sub-watchdog-integration', this)">💻 Integration & Prompts</button>
</div>
<!-- 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>
<!-- Resource Usage Segment Bars -->
<div class="card">
<div class="card-header"><h2 class="card-title">🖥️ Server-Ressourcen Momentanwerte (Snapshot)</h2></div>
<?php foreach ($monitors as $m): ?>
<?php if ($m['type'] === 'host' && !empty($m['metrics_json'])): ?>
<?php $metrics = json_decode($m['metrics_json'], true); ?>
<div style="margin-bottom:1.25rem;">
<div style="display:flex; justify-content:space-between; font-weight:600; margin-bottom:0.35rem;">
<span><?= htmlspecialchars($m['source']) ?> (<?= htmlspecialchars($m['os'] ?? 'Host') ?>)</span>
<span class="badge badge-<?= $m['state'] === 'up' ? 'up' : 'down' ?>"><?= strtoupper($m['state']) ?></span>
</div>
<div class="segment-bar-container">
<div class="segment-bar-label"><span>CPU Auslastung</span><span><?= $metrics['cpu'] ?? 0 ?>%</span></div>
<div class="segment-bar">
<?php for ($i=1; $i<=40; $i++): ?>
<?php $pct = ($i/40)*100; $fill = ($metrics['cpu'] ?? 0) >= $pct ? (($metrics['cpu']??0)>85?'filled-crit':(($metrics['cpu']??0)>70?'filled-warn':'filled-ok')) : ''; ?>
<div class="segment <?= $fill ?>"></div>
<?php endfor; ?>
</div>
</div>
<div class="segment-bar-container" style="margin-top:0.5rem;">
<div class="segment-bar-label"><span>RAM Auslastung</span><span><?= $metrics['ram'] ?? 0 ?>%</span></div>
<div class="segment-bar">
<?php for ($i=1; $i<=40; $i++): ?>
<?php $pct = ($i/40)*100; $fill = ($metrics['ram'] ?? 0) >= $pct ? (($metrics['ram']??0)>85?'filled-crit':(($metrics['ram']??0)>70?'filled-warn':'filled-ok')) : ''; ?>
<div class="segment <?= $fill ?>"></div>
<?php endfor; ?>
</div>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<!-- Subtab: Watchdog Uptime -->
<div id="sub-watchdog-uptime" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">⏱️ Uptime & Liveness Matrix</h2></div>
<table>
<thead>
<tr>
<th>Monitor</th>
<th>Gruppe</th>
<th>Erwartetes Intervall</th>
<th>Status</th>
<th>Zuletzt Gesehen</th>
</tr>
</thead>
<tbody>
<?php foreach ($monitors as $m): ?>
<tr>
<td><strong><?= htmlspecialchars($m['source']) ?></strong></td>
<td><?= htmlspecialchars($m['group_key'] ?? 'Default') ?></td>
<td>Alle <?= $m['expected_interval_sec'] ?>s</td>
<td><span class="badge badge-<?= $m['state'] === 'up' ? 'up' : ($m['state']==='warning'?'warning':'down') ?>"><?= strtoupper($m['state']) ?></span></td>
<td><?= htmlspecialchars($m['last_seen_utc'] ?? 'Nie') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Subtab: Services -->
<div id="sub-watchdog-services" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">⚙️ Überwachte Backend-Dienste (type=heartbeat / Services)</h2></div>
<table>
<thead>
<tr>
<th>Dienst-Name</th>
<th>Gruppe</th>
<th>Status</th>
<th>Letzte Meldung</th>
</tr>
</thead>
<tbody>
<?php foreach (array_filter($monitors, fn($m) => $m['group_key'] === 'Services') as $m): ?>
<tr>
<td><strong><?= htmlspecialchars($m['source']) ?></strong></td>
<td><?= htmlspecialchars($m['group_key']) ?></td>
<td><span class="badge badge-<?= $m['state'] === 'up' ? 'up' : 'warning' ?>"><?= strtoupper($m['state']) ?></span></td>
<td><?= htmlspecialchars($m['last_message'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Subtab: Applications -->
<div id="sub-watchdog-applications" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">💻 Überwachte Software-Anwendungen</h2></div>
<table>
<thead>
<tr>
<th>Anwendung</th>
<th>Umgebung / OS</th>
<th>Status</th>
<th>Letztes Signal</th>
</tr>
</thead>
<tbody>
<?php foreach (array_filter($monitors, fn($m) => $m['group_key'] === 'Applications') as $m): ?>
<tr>
<td><strong><?= htmlspecialchars($m['source']) ?></strong></td>
<td><?= htmlspecialchars($m['os'] ?? '.NET') ?></td>
<td><span class="badge badge-up">ONLINE</span></td>
<td><?= htmlspecialchars($m['last_seen_utc']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Subtab: Machines -->
<div id="sub-watchdog-machines" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">🖥️ Server & Physische / Virtuelle Maschinen (type=host)</h2></div>
<table>
<thead>
<tr>
<th>Host Name</th>
<th>Betriebssystem</th>
<th>Status</th>
<th>CPU / RAM Snapshot</th>
</tr>
</thead>
<tbody>
<?php foreach (array_filter($monitors, fn($m) => $m['type'] === 'host') as $m): ?>
<?php $metrics = json_decode($m['metrics_json'] ?? '{}', true); ?>
<tr>
<td><strong><?= htmlspecialchars($m['source']) ?></strong></td>
<td><?= htmlspecialchars($m['os'] ?? 'Linux/Windows') ?></td>
<td><span class="badge badge-up">ONLINE</span></td>
<td>CPU: <strong><?= $metrics['cpu'] ?? 0 ?>%</strong> | RAM: <strong><?= $metrics['ram'] ?? 0 ?>%</strong></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Subtab: Hypervisors -->
<div id="sub-watchdog-hypervisors" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">☁️ Proxmox Hypervisor & VM Guests</h2></div>
<table>
<thead>
<tr>
<th>Node / Guest</th>
<th>Typ</th>
<th>Zugehöriger Node</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach (array_filter($monitors, fn($m) => in_array($m['type'], ['hypervisor_node', 'guest'])) as $m): ?>
<tr>
<td><strong><?= htmlspecialchars($m['source']) ?></strong></td>
<td><code><?= htmlspecialchars($m['type']) ?></code></td>
<td><?= htmlspecialchars($m['group_key'] ?? '-') ?></td>
<td><span class="badge badge-<?= $m['state']==='up'?'up':'stopped' ?>"><?= strtoupper($m['state']) ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</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">
<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>
<input type="text" name="token_source" class="form-input" placeholder="srv-db-01">
</div>
</div>
<button type="submit" class="btn">Token Generieren</button>
</form>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Aktive Agent Tokens</h2></div>
<table>
<thead>
<tr>
<th>Token ID</th>
<th>Bezeichnung</th>
<th>Gebundene Source</th>
<th>Erstellt am</th>
<th>Status</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($agentTokens as $tok): ?>
<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><?= htmlspecialchars($tok['created_at_utc']) ?></td>
<td><span class="badge badge-<?= $tok['revoked'] ? 'down' : 'up' ?>"><?= $tok['revoked'] ? 'WIDERUFEN' : 'AKTIV' ?></span></td>
<td>
<?php if (!$tok['revoked']): ?>
<form method="POST" 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>
<!-- Subtab: Watchdog Integration & Prompts -->
<div id="sub-watchdog-integration" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">🤖 Watchdog KI-Prompts & Code-Snippets</h2></div>
<div style="margin-bottom:1.5rem;">
<label class="form-label">C# Background Service Heartbeat Prompt</label>
<div class="prompt-box" id="prompt-wd-cs">Ich möchte in meinem C# .NET Worker Service einen Watchdog-Heartbeat einbauen.
Ping-URL: <?= $baseUrl ?>/api/watchdog/v1/ping
HTTP Method: POST
JSON-Payload:
{
"source": "MyWorkerService",
"status": "ok",
"message": "Processing queue",
"expected_interval_sec": 60
}
Bitte schreibe einen IHostedService in C#, der alle 30 Sekunden automatisch diesen Heartbeat-Ping an das Deploymentcenter sendet.</div>
<button class="btn btn-secondary btn-sm" onclick="copyText('prompt-wd-cs')">📋 Prompt Kopieren (C# Worker)</button>
</div>
</div>
</div>
</div>
<!-- ================= MODULE 3: UPDATESERVICE ================= -->
<div id="tab-updateservice" class="tab-content">
<div class="module-subnav">
<button class="subnav-link active" onclick="switchSubTab('updateservice', 'sub-update-releases', this)">📊 Releases Overview</button>
<button class="subnav-link" onclick="switchSubTab('updateservice', 'sub-update-publish', this)"> Release Veröffentlichen</button>
<button class="subnav-link" onclick="switchSubTab('updateservice', 'sub-update-integration', this)">💻 Integration & Prompts</button>
</div>
<div id="sub-update-releases" class="subtab-content active">
<div class="card">
<div class="card-header"><h2 class="card-title">📦 Veröffentlichte Releases</h2></div>
<table>
<thead>
<tr>
<th>Produkt</th>
<th>Version</th>
<th>Release Notes</th>
<th>Download URL</th>
<th>Datum</th>
</tr>
</thead>
<tbody>
<?php foreach ($releases as $r): ?>
<tr>
<td><strong><?= htmlspecialchars($r['product_slug']) ?></strong></td>
<td><code>v<?= htmlspecialchars($r['version']) ?></code></td>
<td><?= htmlspecialchars($r['release_notes'] ?? '-') ?></td>
<td><a href="<?= htmlspecialchars($r['download_url']) ?>" target="_blank" style="color:var(--primary);"><?= htmlspecialchars($r['download_url']) ?></a></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">
<input type="hidden" name="action" value="add_release">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Produkt-Slug</label>
<select name="product_slug" class="form-input" required>
<?php foreach ($products 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">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">Download URL</label>
<input type="url" name="download_url" class="form-input" required placeholder="https://dc.mhdf.de/downloads/myapp-1.2.0.zip">
</div>
</div>
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Release Notes</label>
<textarea name="release_notes" class="form-input" rows="3" placeholder="Changelog und Verbesserungen..."></textarea>
</div>
<button type="submit" class="btn">Release Speichern & Freigeben</button>
</form>
</div>
</div>
<div id="sub-update-integration" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">🤖 UpdateService KI-Prompts</h2></div>
<div class="prompt-box" id="prompt-upd">Ich möchte in meiner Client-Anwendung eine automatische Update-Prüfung einbauen.
Update-Check URL: <?= $baseUrl ?>/api/updateservice/v1/check?product=myapp&version=1.0.0
Bitte erstelle ein Modul, das beim Start prüft, ob `update_available` true ist, und dem Nutzer den Download-Link sowie die Release Notes anzeigt.</div>
<button class="btn btn-secondary btn-sm" onclick="copyText('prompt-upd')">📋 Prompt Kopieren</button>
</div>
</div>
</div>
<!-- ================= MODULE 4: SYSTEM & DB ================= -->
<div id="tab-system" class="tab-content">
<div class="module-subnav">
<button class="subnav-link active" onclick="switchSubTab('system', 'sub-system-status', this)">⚙️ System-Status</button>
<button class="subnav-link" onclick="switchSubTab('system', 'sub-system-migration', this)">🗄️ Daten-Migration & Repair</button>
</div>
<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-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.9rem; margin-bottom:1.25rem;">
Führt das vollständige DB-Schema aus und stellt sicher, dass alle Tabellen (<code>license_*</code>, <code>watchdog_*</code>, <code>updateservice_*</code>, <code>dc_*</code>) angelegt sind und Testdaten für alle Module migriert werden.
</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>
// Sidebar Toggle Handler with localStorage persistence
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'));
}
// Restore sidebar state
if (localStorage.getItem('sidebar_collapsed') === 'true') {
document.getElementById('sidebar').classList.add('collapsed');
document.getElementById('mainWrapper').classList.add('expanded');
}
// 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');
}
}
// Horizontal Submenu Switcher
function switchSubTab(moduleName, subtabId, el) {
const parentModule = document.getElementById('tab-' + moduleName);
if (!parentModule) return;
parentModule.querySelectorAll('.subtab-content').forEach(s => s.classList.remove('active'));
parentModule.querySelectorAll('.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');
}
}
// Helper to copy text to clipboard
function copyText(id) {
const text = document.getElementById(id).innerText;
navigator.clipboard.writeText(text);
alert('Prompt / Code in Zwischenablage kopiert!');
}
// Generate Offline Payload
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);
}
</script>
</body>
</html>