Files
Deploymentcenter/public/index.php
T

1638 lines
96 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';
// 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 ($slug && $name) {
try {
if ($id > 0) {
$stmt = $pdo->prepare('UPDATE dc_projects SET slug = :s, name = :n, default_cache_ttl_hours = :t, notes = :notes WHERE id = :id');
$stmt->execute([':s' => $slug, ':n' => $name, ':t' => $ttl, ':notes' => $notes, ':id' => $id]);
$msg = "Projekt '{$name}' wurde aktualisiert.";
} else {
$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';
}
}
}
// 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.";
}
}
// 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);
if ($source) {
$stmt = $pdo->prepare('
INSERT INTO watchdog_monitors (source, instance, type, state, expected_interval_sec, group_key, os, created_utc, updated_utc)
VALUES (:source, "default", :type, "stopped", :interval, :group, :os, NOW(), NOW())
ON DUPLICATE KEY UPDATE expected_interval_sec = VALUES(expected_interval_sec), group_key = VALUES(group_key)
');
$stmt->execute([':source' => $source, ':type' => $type, ':interval' => $interval, ':group' => $group, ':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>";
}
}
// 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>";
}
}
// 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 Projekt '{$productSlug}' veröffentlicht.";
} else {
$msg = "Fehler beim Speichern des Releases.";
$msgType = 'danger';
}
}
}
}
// 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();
$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);
$tokenManager = new TokenManager($pdo);
$agentTokens = $tokenManager->getAllTokens();
$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;
$pubKeyB64 = 'Fehlt (Ed25519 Key Server Default)';
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deploymentcenter - Unified Operations Center</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;800&display=swap" rel="stylesheet">
<style>
:root {
--bg-main: #0b0d17;
--bg-sidebar: #111424;
--bg-card: rgba(22, 27, 46, 0.75);
--bg-card-elev: #1a1f36;
--border-card: #212845;
--border-soft: rgba(255, 255, 255, 0.08);
--primary: #6366f1;
--primary-hover: #4f46e5;
--accent-glow: rgba(99, 102, 241, 0.25);
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--sidebar-width: 250px;
--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; }
/* Left 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; backdrop-filter: blur(16px); }
.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, #6366f1, #a855f7); border-radius: 10px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 15px var(--accent-glow); }
.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-section { margin-bottom: 1.5rem; }
.nav-section-title { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); 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.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: rgba(99, 102, 241, 0.15); color: #fff; }
.nav-link.active { color: #fff; background: linear-gradient(135deg, var(--primary), #4f46e5); box-shadow: 0 4px 15px var(--accent-glow); }
.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; }
.main-wrapper.expanded { margin-left: var(--sidebar-collapsed-width); }
/* Top Header Bar */
.top-bar { background: rgba(17, 20, 36, 0.8); border-bottom: 1px solid var(--border-card); padding: 1rem 2rem; display: flex; align-items: center; justify-content: space-between; backdrop-filter: blur(12px); position: sticky; top: 0; z-index: 90; }
.top-title { font-size: 1.3rem; font-weight: 700; color: #fff; }
.header-actions { display: flex; align-items: center; gap: 1rem; }
.btn-autorefresh { background: var(--bg-card-elev); border: 1px solid var(--border-card); color: var(--text-main); font-size: 0.8rem; font-weight: 600; padding: 0.5rem 0.85rem; border-radius: 8px; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s; }
.btn-autorefresh.active { background: rgba(16, 185, 129, 0.15); border-color: var(--success); color: #a7f3d0; }
.main-content { padding: 1.75rem 2rem; max-width: 1500px; }
/* Horizontal Submenu Top Bar (Pill style matching image) */
.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: 12px; margin-bottom: 1.75rem; overflow-x: auto; scrollbar-width: none; backdrop-filter: blur(12px); }
.subnav-link { background: transparent; border: none; color: var(--text-muted); padding: 0.55rem 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.05); }
.subnav-link.active { background: var(--primary); color: #fff; box-shadow: 0 4px 14px var(--accent-glow); }
/* 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-card); border-radius: 14px; padding: 1.25rem; backdrop-filter: blur(12px); }
.stat-header { display: flex; align-items: center; justify-content: space-between; color: var(--text-muted); font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
.stat-value { font-size: 1.9rem; font-weight: 800; margin-top: 0.5rem; color: #fff; display: flex; align-items: center; gap: 0.5rem; }
/* Segment Bars (Resource Usage) */
.segment-bar-container { margin-top: 0.6rem; }
.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.4); 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; backdrop-filter: blur(12px); box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.25rem; }
.card-title { font-size: 1.1rem; 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.875rem; }
th { padding: 0.8rem 1rem; color: var(--text-muted); font-weight: 700; border-bottom: 1px solid var(--border-card); font-size: 0.75rem; 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.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(16, 185, 129, 0.15); color: var(--success); border: 1px solid rgba(16, 185, 129, 0.3); }
.badge-warning { background: rgba(245, 158, 11, 0.15); color: var(--warning); border: 1px solid rgba(245, 158, 11, 0.3); }
.badge-down { background: rgba(239, 68, 68, 0.15); color: var(--danger); border: 1px solid rgba(239, 68, 68, 0.3); }
.badge-stopped { background: rgba(148, 163, 184, 0.15); color: var(--text-muted); border: 1px solid rgba(148, 163, 184, 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; box-shadow: 0 4px 12px var(--accent-glow); }
.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); box-shadow: none; }
.btn-secondary:hover { background: #262c4a; }
.btn-sm { padding: 0.35rem 0.7rem; font-size: 0.75rem; }
.btn-danger { background: var(--danger); box-shadow: 0 4px 12px rgba(239,68,68,0.3); }
.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: 600; }
.form-input { background: #0b0d17; 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: #07080f; 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(16, 185, 129, 0.15); border: 1px solid rgba(16, 185, 129, 0.3); color: #a7f3d0; }
.alert-danger { background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 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>
<div class="nav-section">
<div class="nav-section-title">Operations</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>
</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('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>
</div>
</aside>
<!-- Main Content Wrapper -->
<div class="main-wrapper" id="mainWrapper">
<!-- Top Header Bar -->
<header class="top-bar">
<div class="top-title" id="topPageTitle">Globales Dashboard</div>
<div class="header-actions">
<button class="btn-autorefresh" id="btnAutoRefresh" onclick="toggleAutoRefresh()">
<span id="autoRefreshIcon">⏱️</span>
<span id="autoRefreshText">30s Auto-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 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: PROJEKTE ================= -->
<div id="tab-projects" class="tab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">📁 Neues Projekt anlegen</h2></div>
<form method="POST" action="index.php#tab-projects">
<input type="hidden" name="action" value="save_project">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Projekt-Slug (z. B. polytrader)</label>
<input type="text" name="slug" class="form-input" required placeholder="polytrader">
</div>
<div class="form-group">
<label class="form-label">Projekt Name</label>
<input type="text" name="name" 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" 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" class="form-input" placeholder="Trading & Handelssystem Platform">
</div>
<button type="submit" class="btn">Projekt Anlegen</button>
</form>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Projekte Übersicht</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>Erstellt am</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><?= 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><?= $p['created_at'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- ================= MODULE 2: LIZENZEN ================= -->
<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-licenses', this)">🔑 Lizenzverwaltung</button>
<button class="subnav-link" onclick="switchSubTab('license', 'sub-license-details', this)">🔍 Lizenz-Details & Hardware</button>
<button class="subnav-link" onclick="switchSubTab('license', 'sub-license-offline', this)">💾 Offline-Lizenzen (.lic)</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 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.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 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>
<form method="POST" action="index.php#sub-license-licenses" style="display:inline">
<input type="hidden" name="action" value="toggle_license_status">
<input type="hidden" name="license_id" value="<?= $l['id'] ?>">
<input type="hidden" name="new_status" value="<?= $l['status'] === 'active' ? 'revoked' : 'active' ?>">
<button type="submit" class="btn btn-sm <?= $l['status'] === 'active' ? 'btn-danger' : 'btn' ?>">
<?= $l['status'] === 'active' ? 'Widerrufen' : 'Aktivieren' ?>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Subtab: License Details & Editing -->
<div id="sub-license-details" class="subtab-content">
<div class="card">
<div class="card-header"><h2 class="card-title">✏️ Lizenz 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" 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">Änderungen 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</th>
<th>Hostname</th>
<th>App-Version</th>
<th>Zuletzt gesehen</th>
<th>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" 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>
</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.9rem; 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 class="card">
<div class="card-header"><h2 class="card-title">🤖 KI-Prompts zur Integration</h2></div>
<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 Lizenzen-Modul des Deploymentcenters verbinden.
Server-Konfiguration:
- Base API URL: <?= $baseUrl ?>/api/license/v1
- Endpunkte: /validate (POST), /deactivate (POST)
- Public Key: MCowBQYDK2VwAyEA9f8J7K2mX4vQ8n1L6s5t4r3q2p1o0n9m8l7k6j5h4g3f
- 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>
</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 3: 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-add', this)"> Monitor Hinzufügen</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>
<?php endif; ?>
<?php endforeach; ?>
</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">🖥️ Neuen Host oder Dienst-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">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 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>
<th>Skript Download</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>
<td>
<a href="index.php?action=download_agent&source=<?= urlencode($m['source']) ?>&os=windows" class="btn btn-sm btn-secondary">📥 .ps1 (Windows)</a>
<a href="index.php?action=download_agent&source=<?= urlencode($m['source']) ?>&os=linux" class="btn btn-sm btn-secondary">📥 .sh (Linux)</a>
</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</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 (Unmaskable) -->
<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>
<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">Agent Tokens (Jederzeit einsehbar & kopierbar)</h2></div>
<table>
<thead>
<tr>
<th>Token ID</th>
<th>Bezeichnung</th>
<th>Token Value</th>
<th>Gebundene Source</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>
<code id="tok-text-<?= $tok['token_id'] ?>" data-full="<?= htmlspecialchars($rawVal) ?>" data-masked="<?= htmlspecialchars($masked) ?>" style="font-family:monospace;">
<?= 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><?= htmlspecialchars($tok['monitor_source'] ?? 'Alle Sources') ?></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>
<!-- 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 4: 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 Software Releases</h2></div>
<table>
<thead>
<tr>
<th>Projekt</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); font-weight:600;"><?= 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" 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">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 5: 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>
let autoRefreshTimer = null;
let isAutoRefreshActive = false;
// 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 Auto-Refresh: AN (Aktiv)';
autoRefreshTimer = setInterval(() => {
location.reload();
}, 30000);
} else {
btn.classList.remove('active');
txt.innerText = '30s Auto-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',
'system': 'System & Datenbank Status'
};
document.getElementById('topPageTitle').innerText = pageTitles[moduleName] || 'Deploymentcenter';
location.hash = 'tab-' + moduleName;
}
// 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');
}
location.hash = subtabId;
}
// Restore tab based on 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);
const subLink = document.querySelector(`.subnav-link[onclick*="${hash}"]`);
if (subLink) switchSubTab(module, hash, subLink);
}
}
});
// Load data into edit license form
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 copyText(id) {
const text = document.getElementById(id).innerText;
navigator.clipboard.writeText(text);
alert('Prompt / Code 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);
}
</script>
</body>
</html>