prepare('SELECT l.*, p.slug as product_slug FROM license_licenses l JOIN dc_projects p ON l.product_id = p.id WHERE l.id = :id');
$stmt->execute([':id' => $licId]);
$lic = $stmt->fetch();
if ($lic) {
$payload = [
'type' => 'offline_license_file',
'issued_at' => time(),
'product' => $lic['product_slug'],
'license_key' => $lic['license_key'],
'customer' => $lic['customer_name'] ?? 'Universal',
'valid_until' => $lic['expires_at'] ? strtotime($lic['expires_at']) : strtotime('+1 year'),
'signature' => 'ED25519_SIG_' . base64_encode(hash('sha256', $lic['license_key'] . 'DC_OFFLINE_SECRET', true))
];
$jsonContent = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$filename = sprintf('%s_%s_offline.lic', $lic['product_slug'], substr($lic['license_key'], 0, 5));
header('Content-Type: application/json');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . strlen($jsonContent));
echo $jsonContent;
exit;
}
}
// Download Handler: Watchdog Agent Installer Script (.ps1 or .sh)
if (isset($_GET['action']) && $_GET['action'] === 'download_agent') {
$source = trim($_GET['source'] ?? 'server-node');
$os = trim($_GET['os'] ?? 'windows');
$token = trim($_GET['token'] ?? 'wd_live_token_default');
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'dc.mhdf.de';
$baseUrl = $protocol . '://' . $host;
if ($os === 'windows') {
$script = "# Deploymentcenter Watchdog Agent Installer (Windows PowerShell)\n"
. "\$WatchdogUrl = \"{$baseUrl}/api/watchdog/v1/ping\"\n"
. "\$Token = \"{$token}\"\n"
. "\$Source = \"{$source}\"\n"
. "Write-Host \"🚀 Initialisiere Watchdog Agent für \$Source...\" -ForegroundColor Cyan\n"
. "\$body = @{ source = \$Source; status = 'ok'; message = 'Heartbeat via PowerShell Task'; interval = 60 } | ConvertTo-Json\n"
. "Invoke-RestMethod -Uri \$WatchdogUrl -Method Post -Body \$body -ContentType 'application/json' -Headers @{ 'X-Agent-Token' = \$Token }\n"
. "Write-Host \"[✔] Heartbeat erfolgreich gesendet!\" -ForegroundColor Green\n";
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=\"watchdog-install-{$source}.ps1\"");
echo $script;
exit;
} else {
$script = "#!/usr/bin/env bash\n"
. "WATCHDOG_URL=\"{$baseUrl}/api/watchdog/v1/ping\"\n"
. "TOKEN=\"{$token}\"\n"
. "SOURCE=\"{$source}\"\n"
. "echo \"🚀 Initialisiere Watchdog Agent für \$SOURCE...\"\n"
. "curl -X POST \"\$WATCHDOG_URL\" -H \"Content-Type: application/json\" -H \"X-Agent-Token: \$TOKEN\" -d '{\"source\": \"'\"\$SOURCE\"'\", \"status\": \"ok\", \"message\": \"Heartbeat via Bash Cron\", \"interval\": 60}'\n"
. "echo \"[✔] Heartbeat gesendet!\"\n";
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=\"watchdog-install-{$source}.sh\"");
echo $script;
exit;
}
}
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
// Create / Edit Project
if ($action === 'save_project') {
$id = (int)($_POST['project_id'] ?? 0);
$slug = trim($_POST['slug'] ?? '');
$name = trim($_POST['name'] ?? '');
$ttl = (int)($_POST['ttl'] ?? 168);
$notes = trim($_POST['notes'] ?? '');
if ($name) {
try {
if ($id > 0) {
$stmt = $pdo->prepare('UPDATE dc_projects SET name = :n, default_cache_ttl_hours = :t, notes = :notes WHERE id = :id');
$stmt->execute([':n' => $name, ':t' => $ttl, ':notes' => $notes, ':id' => $id]);
$msg = "Projekt '{$name}' wurde aktualisiert.";
} else {
if ($slug) {
$stmt = $pdo->prepare('INSERT INTO dc_projects (slug, name, default_cache_ttl_hours, notes) VALUES (:s, :n, :t, :notes)');
$stmt->execute([':s' => $slug, ':n' => $name, ':t' => $ttl, ':notes' => $notes]);
$msg = "Neues Projekt '{$name}' angelegt.";
}
}
} catch (Throwable $e) {
$msg = "Fehler beim Speichern des Projekts: " . $e->getMessage();
$msgType = 'danger';
}
}
}
// Delete Project (Clean transaction cascade to avoid 500 errors)
if ($action === 'delete_project') {
$id = (int)($_POST['project_id'] ?? 0);
$confirmSlug = trim($_POST['confirm_slug'] ?? '');
if ($id > 0) {
try {
$stmt = $pdo->prepare('SELECT slug, name FROM dc_projects WHERE id = :id');
$stmt->execute([':id' => $id]);
$proj = $stmt->fetch();
if ($proj && hash_equals($proj['slug'], $confirmSlug)) {
$pdo->beginTransaction();
$pdo->prepare('DELETE a FROM license_activations a JOIN license_licenses l ON a.license_id = l.id WHERE l.product_id = :id')
->execute([':id' => $id]);
$pdo->prepare('DELETE FROM license_licenses WHERE product_id = :id')
->execute([':id' => $id]);
$pdo->prepare('DELETE FROM updateservice_releases WHERE product_slug = :slug')
->execute([':slug' => $proj['slug']]);
$delStmt = $pdo->prepare('DELETE FROM dc_projects WHERE id = :id');
$delStmt->execute([':id' => $id]);
$pdo->commit();
$msg = "Projekt '{$proj['name']}' ({$proj['slug']}) und zugehörige Daten wurden gelöscht.";
} else {
$msg = "Sicherheitsbestätigung fehlgeschlagen! Der eingegebene Slug stimmte nicht überein.";
$msgType = 'danger';
}
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$msg = "Fehler beim Löschen des Projekts: " . $e->getMessage();
$msgType = 'danger';
}
}
}
// Create License Key
if ($action === 'create_license') {
$productId = (int)($_POST['product_id'] ?? 0);
$customerName = trim($_POST['customer_name'] ?? '');
$customerEmail = trim($_POST['customer_email'] ?? '');
$maxActivations = (int)($_POST['max_activations'] ?? 2);
$expiresAt = !empty($_POST['expires_at']) ? $_POST['expires_at'] . ' 23:59:59' : null;
$notes = trim($_POST['notes'] ?? '');
if ($productId > 0) {
$licenseKey = KeyGen::generateKey();
try {
$stmt = $pdo->prepare('
INSERT INTO license_licenses (product_id, license_key, customer_name, customer_email, max_activations, expires_at, notes)
VALUES (:pid, :key, :cname, :cemail, :max, :exp, :notes)
');
$stmt->execute([
':pid' => $productId,
':key' => $licenseKey,
':cname' => $customerName,
':cemail' => $customerEmail,
':max' => $maxActivations,
':exp' => $expiresAt,
':notes' => $notes
]);
$pdo->prepare('INSERT INTO license_audit_log (actor, action, details) VALUES ("admin", "license.create", :d)')
->execute([':d' => json_encode(['key' => $licenseKey, 'customer' => $customerName])]);
$msg = "Lizenzschlüssel generiert: {$licenseKey}";
} catch (Throwable $e) {
$msg = "Fehler bei Generierung: " . $e->getMessage();
$msgType = 'danger';
}
}
}
// Edit License
if ($action === 'edit_license') {
$id = (int)($_POST['license_id'] ?? 0);
$customerName = trim($_POST['customer_name'] ?? '');
$customerEmail = trim($_POST['customer_email'] ?? '');
$status = $_POST['status'] ?? 'active';
$maxActivations = (int)($_POST['max_activations'] ?? 2);
$expiresAt = !empty($_POST['expires_at']) ? $_POST['expires_at'] . ' 23:59:59' : null;
$notes = trim($_POST['notes'] ?? '');
if ($id > 0) {
$stmt = $pdo->prepare('
UPDATE license_licenses
SET customer_name = :cname, customer_email = :cemail, status = :status, max_activations = :max_act, expires_at = :exp, notes = :notes
WHERE id = :id
');
$stmt->execute([
':cname' => $customerName,
':cemail' => $customerEmail,
':status' => $status,
':max_act' => $maxActivations,
':exp' => $expiresAt,
':notes' => $notes,
':id' => $id
]);
$pdo->prepare('INSERT INTO license_audit_log (actor, action, details) VALUES ("admin", "license.update", :d)')
->execute([':d' => json_encode(['license_id' => $id, 'status' => $status])]);
$msg = "Lizenzdaten wurden erfolgreich aktualisiert.";
}
}
// Revoke / Re-activate License
if ($action === 'toggle_license_status') {
$licId = (int)($_POST['license_id'] ?? 0);
$newStatus = $_POST['new_status'] ?? 'revoked';
if ($licId > 0) {
$stmt = $pdo->prepare('UPDATE license_licenses SET status = :s WHERE id = :id');
$stmt->execute([':s' => $newStatus, ':id' => $licId]);
$msg = "Lizenz-Status geändert zu: " . strtoupper($newStatus);
}
}
// Toggle Hardware Activation Block
if ($action === 'toggle_block_activation') {
$actId = (int)($_POST['activation_id'] ?? 0);
$block = (int)($_POST['block_state'] ?? 0);
if ($actId > 0) {
$stmt = $pdo->prepare('UPDATE license_activations SET is_blocked = :b WHERE id = :id');
$stmt->execute([':b' => $block, ':id' => $actId]);
$msg = $block ? "Hardware-Aktivierung wurde gesperrt." : "Hardware-Aktivierung wurde entsperrt.";
}
}
// Release (Delete) Hardware Activation
if ($action === 'release_activation') {
$actId = (int)($_POST['activation_id'] ?? 0);
if ($actId > 0) {
$actStmt = $pdo->prepare('SELECT a.*, l.license_key FROM license_activations a JOIN license_licenses l ON a.license_id = l.id WHERE a.id = :id');
$actStmt->execute([':id' => $actId]);
$actRow = $actStmt->fetch();
if ($actRow) {
$delStmt = $pdo->prepare('DELETE FROM license_activations WHERE id = :id');
$delStmt->execute([':id' => $actId]);
Audit::log($pdo, $_SESSION['username'] ?? 'admin', 'activation_released', [
'license_id' => $actRow['license_id'],
'license_key' => $actRow['license_key'],
'hardware_id' => $actRow['hardware_id'],
'hostname' => $actRow['hostname']
]);
$msg = "Hardware-Aktivierung wurde freigegeben (gelöscht).";
}
}
}
// Add Watchdog Host Machine / Application
if ($action === 'add_watchdog_monitor') {
$source = trim($_POST['source'] ?? '');
$type = $_POST['type'] ?? 'heartbeat';
$group = trim($_POST['group'] ?? 'Default');
$os = trim($_POST['os'] ?? 'Linux');
$interval = (int)($_POST['interval'] ?? 60);
$parent = trim($_POST['parent_source'] ?? '');
if ($source) {
$stmt = $pdo->prepare('
INSERT INTO watchdog_monitors (source, instance, type, state, expected_interval_sec, group_key, parent_source, os, created_utc, updated_utc)
VALUES (:source, "default", :type, "stopped", :interval, :group, :parent, :os, NOW(), NOW())
ON DUPLICATE KEY UPDATE expected_interval_sec = VALUES(expected_interval_sec), group_key = VALUES(group_key), parent_source = VALUES(parent_source)
');
$stmt->execute([':source' => $source, ':type' => $type, ':interval' => $interval, ':group' => $group, ':parent' => !empty($parent)?$parent:null, ':os' => $os]);
$tokMgr = new TokenManager($pdo);
$tok = $tokMgr->createToken($source, "Token for {$source}");
$msg = "Monitor '{$source}' angelegt. Agent Token: {$tok['raw_token']}";
}
}
// Edit Watchdog Monitor (Rich Edit with Icon Upload & Mute Switch matching screenshot)
if ($action === 'edit_watchdog_monitor') {
$oldSource = trim($_POST['old_source'] ?? $_POST['source'] ?? '');
$newSource = trim($_POST['new_source'] ?? $_POST['source'] ?? '');
$type = trim($_POST['type'] ?? 'host');
$group = trim($_POST['group_key'] ?? $_POST['group'] ?? 'Default');
$parent = trim($_POST['parent_source'] ?? '');
$os = trim($_POST['os'] ?? '');
$notes = trim($_POST['notes'] ?? '');
$url = trim($_POST['url'] ?? '');
$icon = trim($_POST['icon'] ?? '');
$interval = (int)($_POST['expected_interval_sec'] ?? $_POST['interval'] ?? 60);
$isMuted = isset($_POST['is_muted']) ? true : false;
// Custom icon file upload
if (!empty($_FILES['custom_icon_file']['name'])) {
$file = $_FILES['custom_icon_file'];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (in_array($ext, ['svg', 'png', 'jpg', 'jpeg', 'webp'], true)) {
$customDir = __DIR__ . '/assets/icons/custom';
if (!is_dir($customDir)) mkdir($customDir, 0777, true);
$filename = 'icon_' . time() . '_' . preg_replace('/[^a-z0-9]/', '', strtolower(pathinfo($file['name'], PATHINFO_FILENAME))) . '.' . $ext;
if (move_uploaded_file($file['tmp_name'], $customDir . '/' . $filename)) {
$icon = 'custom/' . $filename;
}
}
}
if ($oldSource && $newSource) {
$monRepo = new MonitorRepo($pdo);
$monRepo->updateMonitor($oldSource, $newSource, 'default', $type, $group, $parent, $os, $notes, $url, $interval, $icon, $isMuted);
$msg = "Monitor '{$newSource}' wurde erfolgreich aktualisiert.";
}
}
// Delete Watchdog Monitor
if ($action === 'delete_watchdog_monitor') {
$source = trim($_POST['source'] ?? '');
if ($source) {
$monRepo = new MonitorRepo($pdo);
$monRepo->deleteMonitor($source, 'default');
$msg = "Monitor '{$source}' wurde gelöscht.";
}
}
// Link Entity Parent (Watchdog Hierarchy)
if ($action === 'link_entity') {
$source = trim($_POST['source'] ?? '');
$parent = trim($_POST['parent_source'] ?? '');
if ($source) {
$monRepo = new MonitorRepo($pdo);
$monRepo->setParentSource($source, $parent);
$msg = "Hierarchie gespeichert: {$source} → " . ($parent ?: 'Keine (Top Level)');
}
}
// Create Agent Token (Watchdog)
if ($action === 'create_agent_token') {
$source = trim($_POST['token_source'] ?? '');
$name = trim($_POST['token_name'] ?? '');
if (empty($name)) {
$name = "Token for " . ($source ?: 'General');
}
$tokMgr = new TokenManager($pdo);
$res = $tokMgr->createToken($source, $name);
$msg = "Agent-Token generiert: {$res['raw_token']}";
}
// Revoke Agent Token
if ($action === 'revoke_agent_token') {
$tokId = trim($_POST['token_id'] ?? '');
if ($tokId) {
$stmt = $pdo->prepare('UPDATE watchdog_agent_tokens SET revoked = 1 WHERE token_id = :id');
$stmt->execute([':id' => $tokId]);
$msg = "Agent-Token wurde widerrufen.";
}
}
// Add Update Release
if ($action === 'add_release') {
$productSlug = trim($_POST['product_slug'] ?? '');
$version = trim($_POST['version'] ?? '');
$channel = trim($_POST['channel'] ?? 'prod');
$url = trim($_POST['download_url'] ?? '');
$hash = trim($_POST['sha256_hash'] ?? '');
$gitCommit = trim($_POST['git_commit'] ?? '');
$sizeBytes = (int)($_POST['size_bytes'] ?? 0);
$notes = trim($_POST['release_notes'] ?? '');
$critical = isset($_POST['is_critical']);
if ($productSlug && $version && $url) {
$updMgr = new UpdateManager($pdo);
if ($updMgr->addRelease($productSlug, $version, $channel, $notes, $url, $hash, $gitCommit, $sizeBytes, null, $critical)) {
$msg = "Release v{$version} ({$channel}) für Projekt '{$productSlug}' veröffentlicht.";
} else {
$msg = "Fehler beim Speichern des Releases.";
$msgType = 'danger';
}
}
}
// Core Master & Sub Token Management Actions
if ($action === 'create_master_token') {
$name = trim($_POST['name'] ?? '');
$proj = trim($_POST['project_slug'] ?? '');
$lic = trim($_POST['license_key'] ?? '');
$ownerType = $_POST['owner_type'] ?? 'custom';
$ownerIdentity = trim($_POST['owner_identity'] ?? '');
$scopes = isset($_POST['scopes']) && is_array($_POST['scopes']) ? $_POST['scopes'] : ['*'];
$env = $_POST['environment'] ?? 'all';
if ($name) {
$coreTokenMgr = new CoreTokenManager($pdo);
$res = $coreTokenMgr->createMasterToken($name, $proj, $lic, $ownerType, $ownerIdentity, $scopes, $env);
$msg = "Master-Token '{$name}' erstellt! Raw Master-Token (einmalig kopieren): {$res['raw_token']}";
}
}
if ($action === 'revoke_core_token') {
$tokenId = trim($_POST['token_id'] ?? '');
if ($tokenId) {
$coreTokenMgr = new CoreTokenManager($pdo);
$coreTokenMgr->revokeToken($tokenId);
$msg = "Token '{$tokenId}' und alle abgeleiteten Sub-Tokens wurden widerrufen.";
}
}
if ($action === 'delete_core_token') {
$tokenId = trim($_POST['token_id'] ?? '');
if ($tokenId) {
$coreTokenMgr = new CoreTokenManager($pdo);
$coreTokenMgr->deleteToken($tokenId);
$msg = "Token '{$tokenId}' und abgeleitete Sub-Tokens wurden dauerhaft gelöscht.";
}
}
// Bugtracker & Feature-Tracker Actions
if ($action === 'bt_add_comment') {
$itemId = (int)($_POST['item_id'] ?? 0);
$comment = trim($_POST['comment'] ?? '');
$author = trim($_POST['author'] ?? $_SESSION['dc_username'] ?? 'admin');
$actionTaken = trim($_POST['action_taken'] ?? 'commented');
if ($itemId > 0 && !empty($comment)) {
$bugRepo = new BugRepo($pdo);
$bugRepo->addComment($itemId, $author, $comment, $actionTaken);
$msg = "Kommentar zu Item #{$itemId} hinzugefügt.";
}
}
if ($action === 'bt_resolve') {
$itemId = (int)($_POST['item_id'] ?? 0);
$build = trim($_POST['resolved_in_build'] ?? 'v1.0.0');
$notes = trim($_POST['resolution_notes'] ?? '');
$author = trim($_POST['author'] ?? $_SESSION['dc_username'] ?? 'admin');
if ($itemId > 0 && !empty($build)) {
$bugRepo = new BugRepo($pdo);
$bugRepo->resolveItem($itemId, $build, $notes, $author);
$msg = "Item #{$itemId} wurde als gelöst/umgesetzt in Build '{$build}' markiert.";
}
}
if ($action === 'bt_create_item') {
$proj = trim($_POST['project_slug'] ?? 'default');
$type = $_POST['type'] ?? 'bug';
$title = trim($_POST['title'] ?? '');
$desc = trim($_POST['description'] ?? '');
$errMsg = trim($_POST['error_message'] ?? '');
$trace = trim($_POST['stack_trace'] ?? '');
$build = trim($_POST['build_version'] ?? 'v1.0.0');
$env = $_POST['environment'] ?? 'production';
$sev = $_POST['severity'] ?? 'medium';
$pushId = trim($_POST['push_id'] ?? '');
$agent = trim($_POST['target_agent'] ?? '');
$tags = trim($_POST['tags'] ?? '');
$createdBy = trim($_POST['created_by'] ?? $_SESSION['dc_username'] ?? 'admin');
if ($title) {
$bugRepo = new BugRepo($pdo);
$res = $bugRepo->reportItem([
'project_slug' => $proj,
'type' => $type,
'title' => $title,
'description' => $desc,
'error_message' => $errMsg,
'stack_trace' => $trace,
'build_version' => $build,
'environment' => $env,
'severity' => $sev,
'push_id' => $pushId,
'target_agent' => $agent,
'tags' => $tags,
'created_by' => $createdBy,
]);
$msg = $res['is_new']
? "Neues Item #{$res['id']} ({$res['type']}) erfolgreich in {$env} erfasst."
: "Wiederkehrender Fehler erfasst. Occurrence Count erhöht auf {$res['occurrence_count']}.";
}
}
if ($action === 'bt_change_status') {
$itemId = (int)($_POST['item_id'] ?? 0);
$status = $_POST['status'] ?? 'open';
$notes = trim($_POST['notes'] ?? '');
$author = trim($_POST['author'] ?? $_SESSION['dc_username'] ?? 'admin');
if ($itemId > 0) {
$bugRepo = new BugRepo($pdo);
$bugRepo->updateStatus($itemId, $status, $notes, $author);
$msg = "Status für Item #{$itemId} geändert auf {$status}.";
}
}
if ($action === 'bt_update_item') {
$itemId = (int)($_POST['item_id'] ?? 0);
$status = $_POST['status'] ?? 'open';
$sev = $_POST['severity'] ?? 'medium';
$pushId = trim($_POST['push_id'] ?? '');
$agent = trim($_POST['target_agent'] ?? '');
$tags = trim($_POST['tags'] ?? '');
$notes = trim($_POST['resolution_notes'] ?? '');
$build = trim($_POST['resolved_in_build'] ?? '');
$author = trim($_POST['author'] ?? $_SESSION['dc_username'] ?? 'admin');
if ($itemId > 0) {
$bugRepo = new BugRepo($pdo);
$bugRepo->updateItemDetails($itemId, [
'status' => $status,
'severity' => $sev,
'push_id' => $pushId,
'target_agent' => $agent,
'tags' => $tags,
'resolution_notes' => $notes,
'resolved_in_build' => $build,
], $author);
$msg = "Item #{$itemId} wurde erfolgreich aktualisiert.";
}
}
}
// Fetch All Data
$projects = $pdo->query('SELECT * FROM dc_projects ORDER BY name ASC')->fetchAll();
$licenses = $pdo->query('
SELECT l.*, p.name as product_name, p.slug as product_slug,
(SELECT COUNT(*) FROM license_activations a WHERE a.license_id = l.id) as active_count
FROM license_licenses l
JOIN dc_projects p ON l.product_id = p.id
ORDER BY l.created_at DESC
')->fetchAll();
$activations = $pdo->query('
SELECT a.*, l.license_key, p.slug as product_slug, p.name as product_name
FROM license_activations a
JOIN license_licenses l ON a.license_id = l.id
JOIN dc_projects p ON l.product_id = p.id
ORDER BY a.last_seen DESC
')->fetchAll();
$auditLogs = $pdo->query('SELECT * FROM license_audit_log ORDER BY created_at DESC LIMIT 100')->fetchAll();
$monitorRepo = new MonitorRepo($pdo);
$monitors = $monitorRepo->getAllMonitors();
$tokenManager = new TokenManager($pdo);
$agentTokens = $tokenManager->getAllTokens();
// Map Active Tokens per Monitor Source
$tokensBySource = [];
foreach ($agentTokens as $tok) {
if (!$tok['revoked'] && !empty($tok['monitor_source'])) {
$tokensBySource[$tok['monitor_source']] = $tok;
}
}
// Flatten Tree for Hierarchy Rendering (Up to 3 levels: Hypervisor -> Host -> App)
function buildMonitorTree(array $monitors): array {
$childrenOf = [];
$bySource = [];
foreach ($monitors as $m) {
$bySource[$m['source']] = $m;
$parent = !empty($m['parent_source']) ? $m['parent_source'] : '__ROOT__';
$childrenOf[$parent][] = $m['source'];
}
$flat = [];
$walk = function(string $parentSource, int $depth = 0) use (&$walk, &$flat, $childrenOf, $bySource) {
if ($depth > 3) return;
if (empty($childrenOf[$parentSource])) return;
$nodes = $childrenOf[$parentSource];
$count = count($nodes);
for ($i = 0; $i < $count; $i++) {
$src = $nodes[$i];
if (!isset($bySource[$src])) continue;
$node = $bySource[$src];
$node['depth'] = $depth;
$node['is_last'] = ($i === $count - 1);
$flat[] = $node;
$walk($src, $depth + 1);
}
};
$walk('__ROOT__', 0);
// Append any unvisited monitors as root level
$visited = array_column($flat, 'source');
foreach ($monitors as $m) {
if (!in_array($m['source'], $visited, true)) {
$m['depth'] = 0;
$m['is_last'] = true;
$flat[] = $m;
}
}
return $flat;
}
$hierarchicalMonitors = buildMonitorTree($monitors);
// Calculate Monitor Groups Stats (for Dashboard Group Panel)
$groupStats = [];
$monitorsUp = 0; $monitorsWarning = 0; $monitorsDown = 0;
foreach ($monitors as $m) {
if ($m['state'] === 'up') $monitorsUp++;
elseif ($m['state'] === 'warning') $monitorsWarning++;
else $monitorsDown++;
$gKey = !empty($m['group_key']) ? $m['group_key'] : 'Default';
if (!isset($groupStats[$gKey])) {
$groupStats[$gKey] = ['total' => 0, 'ok' => 0];
}
$groupStats[$gKey]['total']++;
if ($m['state'] === 'up') {
$groupStats[$gKey]['ok']++;
}
}
// Icon Resolver Helper
function getMonitorIconUrl(?string $icon, ?string $source, ?string $os, ?string $type): string {
if (!empty($icon) && $icon !== 'auto') {
if (str_starts_with($icon, 'custom/')) return 'assets/icons/' . $icon;
if (str_starts_with($icon, 'assets/')) return $icon;
return 'assets/icons/' . $icon;
}
$s = strtolower(($source ?? '') . ' ' . ($os ?? '') . ' ' . ($type ?? ''));
if (str_contains($s, 'proxmox') || str_contains($s, 'pve')) return 'assets/icons/proxmox.svg';
if (str_contains($s, 'win')) return 'assets/icons/windows.svg';
if (str_contains($s, 'linux') || str_contains($s, 'ubuntu') || str_contains($s, 'debian')) return 'assets/icons/linux.svg';
if (str_contains($s, 'mysql') || str_contains($s, 'mariadb') || str_contains($s, 'db')) return 'assets/icons/mysql.svg';
if (str_contains($s, 'docker')) return 'assets/icons/docker.svg';
if (str_contains($s, 'nginx')) return 'assets/icons/nginx.svg';
if (str_contains($s, 'redis')) return 'assets/icons/redis.svg';
if (str_contains($s, 'python')) return 'assets/icons/python.svg';
if (str_contains($s, 'php')) return 'assets/icons/php.svg';
if (str_contains($s, 'node')) return 'assets/icons/node.svg';
return 'assets/icons/server.svg';
}
$eventLog = new EventLog($pdo);
$recentEvents = $eventLog->getRecentEvents(100);
$updateMgr = new UpdateManager($pdo);
$releases = $updateMgr->getReleases();
// Core Token Manager Data
$coreTokenMgr = new CoreTokenManager($pdo);
$coreMasterTokens = $coreTokenMgr->getAllMasterTokens();
$coreAllTokens = $coreTokenMgr->getAllTokens();
// Bugtracker Data
$bugRepo = new BugRepo($pdo);
$bugtrackerStats = $bugRepo->getStats();
$bugtrackerItems = $bugRepo->getItems();
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'dc.mhdf.de';
$baseUrl = $protocol . '://' . $host;
?>
Deploymentcenter - Operations Hub
= $msg ?>
= $monitorsUp ?> /
= $monitorsWarning ?> /
= $monitorsDown ?>
| Hierarchie / Entity Source |
Typ |
Status |
Letzte Meldung |
Zuletzt Gesehen |
0);
$iconUrl = getMonitorIconUrl($m['icon']??null, $m['source']??null, $m['os']??null, $m['type']??null);
?>
= $m['is_last'] ? '└─' : '├─' ?>
= htmlspecialchars($m['source']) ?>
(= htmlspecialchars($m['instance']) ?>)
|
= htmlspecialchars($m['type']) ?> |
● = strtoupper($m['state']) ?>
|
= htmlspecialchars($m['last_message'] ?? '-') ?> |
= htmlspecialchars($m['last_seen_utc'] ?? 'Nie') ?> |
| ID |
Slug |
Projekt Name |
Cache TTL |
Verknüpfte Lizenzen |
Verknüpfte Releases |
Aktionen |
$l['product_slug'] === $p['slug']));
$relCount = count(array_filter($releases, fn($r) => $r['product_slug'] === $p['slug']));
?>
| = $p['id'] ?> |
= htmlspecialchars($p['slug']) ?> |
= htmlspecialchars($p['name']) ?> |
= $p['default_cache_ttl_hours'] ?> h (= round($p['default_cache_ttl_hours']/24, 1) ?> Tage) |
= $licCount ?> Lizenzen |
= $relCount ?> Releases |
|
⚠️ Projekt unwiderruflich löschen
Sind Sie sicher, dass Sie das Projekt löschen möchten?
Bitte geben Sie zur Sicherheitsbestätigung den Projekt-Slug ein:
✏️ Monitor & Icon bearbeiten
= count(array_filter($licenses, fn($l) => $l['status'] === 'active')) ?>
| Projekt |
Lizenzschlüssel |
Kunde |
Aktivierungen |
Status |
Ablaufdatum |
Aktionen |
| = htmlspecialchars($l['product_name']) ?> |
= htmlspecialchars($l['license_key']) ?> |
= htmlspecialchars($l['customer_name'] ?? '-') ?> |
= $l['active_count'] ?> / = $l['max_activations'] ?> |
= strtoupper($l['status']) ?> |
= $l['expires_at'] ? htmlspecialchars($l['expires_at']) : 'Unbefristet' ?> |
📥 .lic
|
| Projekt |
Lizenzschlüssel |
Hardware-ID / Quelle |
Plattform / Ver. |
Hostname |
Zuletzt gesehen |
Status |
Aktionen |
| = htmlspecialchars($a['product_name']) ?> |
= htmlspecialchars($a['license_key']) ?> |
= htmlspecialchars($a['hardware_id']) ?>
Quelle: = htmlspecialchars($a['hwid_source']) ?>
|
= htmlspecialchars(strtoupper($a['platform'] ?? 'WIN')) ?> (v= (int)($a['hwid_version'] ?? 1) ?>)
|
= htmlspecialchars($a['hostname'] ?? '-') ?> |
= htmlspecialchars($a['last_seen']) ?> |
= $a['is_blocked'] ? 'GESPERRT' : 'AKTIV' ?>
|
|
Erzeugt eine signierte .lic Offline-Lizenzdatei für Air-Gapped Kundensysteme.
Wählen Sie oben eine Lizenz aus und klicken Sie auf 'Offline Payload Generieren'.
| ID |
Zeitpunkt |
Akteur |
Aktion |
Details |
| = $log['id'] ?> |
= $log['created_at'] ?> |
= htmlspecialchars($log['actor']) ?> |
= htmlspecialchars($log['action']) ?> |
= htmlspecialchars($log['details'] ?? '-') ?> |
$gData): ?>
= htmlspecialchars($gName) ?>
= $gData['ok'] ?>/= $gData['total'] ?> OK
| Name |
Typ |
Status |
Zuletzt gesehen |
Kurz-Info / Message |
Aktionen |
0);
$iconUrl = getMonitorIconUrl($m['icon']??null, $m['source']??null, $m['os']??null, $m['type']??null);
?>
= $m['is_last'] ? '└─' : '├─' ?>
= htmlspecialchars($m['source']) ?>
|
= htmlspecialchars($m['type']) ?> |
= strtoupper($m['state']) ?> |
= htmlspecialchars($m['last_seen_utc'] ?? 'Nie') ?> |
= htmlspecialchars($m['last_message'] ?? '-') ?> |
|
| Zeitpunkt (UTC) |
Source |
Kind |
Severity |
Nachricht |
| = htmlspecialchars($e['at_utc']) ?> |
= htmlspecialchars($e['source']) ?> |
= htmlspecialchars($e['kind']) ?> |
= strtoupper($e['severity']) ?> |
= htmlspecialchars($e['message'] ?? '-') ?> |
| Token ID |
Bezeichnung |
Gebundene Source |
Token Value |
Status |
Aktion |
= htmlspecialchars($tok['token_id']) ?> |
= htmlspecialchars($tok['name']) ?> |
= htmlspecialchars($tok['monitor_source'] ?? 'Alle Sources') ?> |
= htmlspecialchars($masked) ?>
|
= $tok['revoked'] ? 'WIDERUFEN' : 'AKTIV' ?> |
|
| Projekt |
Kanal |
Version |
Git Commit |
Größe |
Release Notes |
Download & SHA256 |
Datum |
'badge-up',
'beta' => 'badge-warning',
'dev' => 'badge-stopped',
default => 'badge-up'
};
$sizeFormatted = !empty($r['size_bytes']) ? round($r['size_bytes'] / (1024 * 1024), 2) . ' MB' : '-';
?>
| = htmlspecialchars($r['product_slug']) ?> |
= strtoupper(htmlspecialchars($r['channel'] ?? 'prod')) ?> |
v= htmlspecialchars($r['version']) ?> KRITISCH |
= htmlspecialchars($r['git_commit'] ?? 'n/a') ?> |
= $sizeFormatted ?> |
= htmlspecialchars($r['release_notes'] ?? '-') ?> |
⬇️ Download Package
SHA: = htmlspecialchars(substr($r['sha256_hash'], 0, 12)) ?>...
|
= htmlspecialchars($r['created_at']) ?> |
= $bugtrackerStats['open_bugs_dev'] ?>
= $bugtrackerStats['open_bugs_prod'] ?>
= $bugtrackerStats['open_features'] ?>
= $bugtrackerStats['ideas_count'] ?>
= $bugtrackerStats['resolved_total'] ?>
| ID / Typ |
Umgebung |
Projekt & Titel |
Build / Version |
Schweregrad |
Anzahl |
Status |
Aktionen |
💡 FEATURE'
: '🐛 BUG';
$envBadge = match($item['environment']) {
'development' => '🔵 DEV',
'production' => '🔴 PROD',
default => '' . strtoupper($item['environment']) . ''
};
$statusBadge = match($item['status']) {
'open' => 'OFFEN',
'planned' => 'GEPLANT',
'in_progress' => 'IN BEARBEITUNG',
'resolved' => 'GELÖST / UMGESETZT',
'rejected' => 'ABGELEHNT',
default => '' . strtoupper($item['status']) . ''
};
$sevBadge = match($item['severity']) {
'idea' => '💡 IDEE',
'wishlist' => '⭐ WUNSCHLISTE',
'critical' => '🔥 KRITISCH',
'high' => 'HOCH',
'medium' => 'MITTEL',
default => 'NIEDRIG'
};
$jsonItem = htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8');
?>
#= $item['id'] ?> = $typeBadge ?> |
= $envBadge ?> |
= htmlspecialchars($item['title']) ?>
Projekt: = htmlspecialchars($item['project_slug']) ?>
Von: = htmlspecialchars($item['created_by']) ?>
📲 Push: = htmlspecialchars($item['push_id']) ?>
🤖 = htmlspecialchars($item['target_agent']) ?>
🏷️ = htmlspecialchars($item['tags']) ?>
|
= htmlspecialchars($item['build_version'] ?? 'v1.0.0') ?> |
= $sevBadge ?> |
= (int)$item['occurrence_count'] ?>x
|
= $statusBadge ?> |
|
| Token ID |
Bezeichnung |
Typ / Identität |
Projekt / Lizenz |
Rechte (Scopes) |
Sub-Tokens |
Token Key |
Aktionen |
= htmlspecialchars($mTok['token_id']) ?> |
= htmlspecialchars($mTok['name']) ?> |
= strtoupper($mTok['owner_type']) ?>
= htmlspecialchars($mTok['owner_identity'] ?? '-') ?>
|
= htmlspecialchars($mTok['project_slug'] ?? '-') ?>
= !empty($mTok['license_key']) ? ' Lic: ' . htmlspecialchars($mTok['license_key']) . '' : '' ?>
|
= htmlspecialchars($sc) ?>
|
= (int)($mTok['sub_token_count'] ?? 0) ?> Sub-Tokens
|
= htmlspecialchars($masked) ?>
|
WIDERUFEN
|
| Token ID |
Parent Master ID |
Bezeichnung / Client |
Umgebung |
Scopes |
Token Key |
Zuletzt Genutzt |
Status |
Aktionen |
$t['type'] === 'sub');
foreach ($subTokens as $sTok):
$scopesArr = json_decode($sTok['scopes'], true) ?: [];
$sRaw = !empty($sTok['raw_token']) ? $sTok['raw_token'] : $sTok['token_id'];
$sMasked = (strlen($sRaw) > 12) ? substr($sRaw, 0, 12) . '••••••••••••••••' : $sRaw;
?>
= htmlspecialchars($sTok['token_id']) ?> |
= htmlspecialchars($sTok['parent_token_id'] ?? '-') ?> |
= htmlspecialchars($sTok['name']) ?> |
= strtoupper($sTok['environment']) ?>
|
= htmlspecialchars($sc) ?>
|
= htmlspecialchars($sMasked) ?>
|
= $sTok['last_used_at'] ? htmlspecialchars($sTok['last_used_at']) : 'Noch nie' ?> |
= $sTok['revoked'] ? 'WIDERUFEN' : 'AKTIV' ?> |
|
Die API-Dokumentation liegt geschützt hinter der Plattform-Authentifizierung.
=== DEPLOYMENTCENTER REST API SPECIFICATION (OpenAPI 3.0) ===
1. LIZENZEN MODULE:
- POST /api/license/v1/validate (Public)
Body: { "product": "myapp", "license_key": "XXXXX-...", "hardware_id": "HWID-..." }
- POST /api/license/v1/deactivate (Geschützt - Requires Bearer / Auth)
Body: { "product": "myapp", "license_key": "XXXXX-...", "hardware_id": "HWID-..." }
2. WATCHDOG MODULE:
- POST /api/watchdog/v1/ping (Header: X-Agent-Token)
Body: { "source": "srv-db-01", "status": "ok", "message": "Heartbeat", "interval": 60 }
- GET /api/watchdog/v1/status
- GET /api/watchdog/v1/events?limit=50
3. UPDATESERVICE MODULE:
- GET /api/updateservice/v1/check?product=myapp&version=1.0.0
- GET /api/updateservice/v1/releases?product=myapp