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