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();
// 1. Delete associated activations
$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]);
// 2. Delete associated licenses
$pdo->prepare('DELETE FROM license_licenses WHERE product_id = :id')
->execute([':id' => $id]);
// 3. Delete associated releases
$pdo->prepare('DELETE FROM updateservice_releases WHERE product_slug = :slug')
->execute([':slug' => $proj['slug']]);
// 4. Delete project
$delStmt = $pdo->prepare('DELETE FROM dc_projects WHERE id = :id');
$delStmt->execute([':id' => $id]);
$pdo->commit();
$msg = "Projekt '{$proj['name']}' ({$proj['slug']}) und zugehörige Daten wurden gelöscht.";
} else {
$msg = "Sicherheitsbestätigung fehlgeschlagen! Der eingegebene Slug stimmte nicht überein.";
$msgType = 'danger';
}
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$msg = "Fehler beim Löschen des Projekts: " . $e->getMessage();
$msgType = 'danger';
}
}
}
// 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.";
}
}
// 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
if ($action === 'edit_watchdog_monitor') {
$oldSource = trim($_POST['old_source'] ?? '');
$newSource = trim($_POST['new_source'] ?? $oldSource);
$group = trim($_POST['group_key'] ?? 'Default');
$parent = trim($_POST['parent_source'] ?? '');
$interval = (int)($_POST['interval'] ?? 60);
$notes = trim($_POST['notes'] ?? '');
if ($oldSource && $newSource) {
$monRepo = new MonitorRepo($pdo);
$monRepo->updateMonitor($oldSource, $newSource, 'default', $group, $parent, $notes, null, $interval);
$msg = "Monitor '{$newSource}' wurde 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 ($name) {
$tokMgr = new TokenManager($pdo);
$res = $tokMgr->createToken($source, $name);
$msg = "Agent-Token für '{$name}' 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'] ?? '');
$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();
// 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);
$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;
?>
Deploymentcenter - Operations Hub
= $msg ?>
= $monitorsUp ?> /
= $monitorsWarning ?> /
= $monitorsDown ?>
| Hierarchie / Entity Source |
Typ |
Status |
Letzte Meldung |
Zuletzt Gesehen |
0);
?>
|
= $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:
= 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 |
Hostname |
Zuletzt gesehen |
Status |
Aktion |
| = htmlspecialchars($a['product_name']) ?> |
= htmlspecialchars($a['license_key']) ?> |
= htmlspecialchars($a['hardware_id']) ?> |
= 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'] ?? '-') ?> |
| Hierarchie / Entity Source |
Typ |
Parent Entity |
Status |
Parent Verknüpfen |
0);
?>
|
= $m['is_last'] ? '└─' : '├─' ?>
= htmlspecialchars($m['source']) ?>
|
= htmlspecialchars($m['type']) ?> |
= htmlspecialchars($m['parent_source'] ?? 'Keine (Top Level)') ?> |
= strtoupper($m['state']) ?> |
|
| Monitor Source |
Parent |
Gruppe |
Intervall |
Status |
Zuletzt Gesehen |
Aktionen |
| = htmlspecialchars($m['source']) ?> |
= htmlspecialchars($m['parent_source'] ?? '-') ?> |
= htmlspecialchars($m['group_key'] ?? 'Default') ?> |
= $m['expected_interval_sec'] ?>s |
= strtoupper($m['state']) ?> |
= htmlspecialchars($m['last_seen_utc'] ?? 'Nie') ?> |
.ps1
.sh
|
| 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 |
Token Value |
Status |
Aktion |
= htmlspecialchars($tok['token_id']) ?> |
= htmlspecialchars($tok['name']) ?> |
= htmlspecialchars($masked) ?>
|
= $tok['revoked'] ? 'WIDERUFEN' : 'AKTIV' ?> |
|
| Projekt |
Version |
Release Notes |
Download URL |
Datum |
| = htmlspecialchars($r['product_slug']) ?> |
v= htmlspecialchars($r['version']) ?> |
= htmlspecialchars($r['release_notes'] ?? '-') ?> |
= htmlspecialchars($r['download_url']) ?> |
= htmlspecialchars($r['created_at']) ?> |
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