feat: integrate Bugtracker module, UpdateService enhancements & Token hierarchy

This commit is contained in:
Deploymentcenter Bot
2026-08-06 12:45:58 +02:00
parent 70b35f7b8b
commit d71f90cdfc
28 changed files with 3494 additions and 32 deletions
@@ -0,0 +1,9 @@
# Bugtracker Protected Management API (.htaccess Security Layer)
# Allows authenticated API token requests or session-authenticated admin users
Satisfy Any
Allow from all
<IfModule mod_authz_core.c>
Require all granted
</IfModule>
+193
View File
@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../../../src/Core/Auth.php';
require_once __DIR__ . '/../../../../../src/Core/Db.php';
require_once __DIR__ . '/../../../../../src/Core/TokenManager.php';
require_once __DIR__ . '/../../../../../src/Modules/Bugtracker/BugRepo.php';
use Deploymentcenter\Core\Auth;
use Deploymentcenter\Core\Db;
use Deploymentcenter\Core\TokenManager;
use Deploymentcenter\Modules\Bugtracker\BugRepo;
header('Content-Type: application/json; charset=utf-8');
try {
$config = require __DIR__ . '/../../../../../config/config.php';
$db = Db::connect($config['db']);
// Authenticate Request (Session OR Token)
$isAuthenticated = false;
$authorName = 'admin';
if (Auth::isLoggedIn()) {
$isAuthenticated = true;
$authorName = $_SESSION['dc_username'] ?? 'admin';
} else {
$headers = getallheaders();
$token = $headers['X-Agent-Token'] ?? $headers['x-agent-token'] ?? null;
if (!$token && !empty($headers['Authorization'])) {
if (preg_match('/Bearer\s+(.+)/i', $headers['Authorization'], $matches)) {
$token = trim($matches[1]);
}
}
if ($token) {
$tokenMgr = new TokenManager($db);
$tokenInfo = $tokenMgr->validateToken($token, 'bugtracker:manage');
if ($tokenInfo) {
$isAuthenticated = true;
$authorName = 'agent:' . ($tokenInfo['name'] ?? $tokenInfo['token_id']);
}
}
}
if (!$isAuthenticated) {
http_response_code(401);
echo json_encode(['status' => 'error', 'message' => 'Unauthorized: Valid Session or Bearer Token with scope bugtracker:manage required']);
exit;
}
$repo = new BugRepo($db);
$uri = $_SERVER['REQUEST_URI'];
$method = $_SERVER['REQUEST_METHOD'];
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true) ?: $_POST;
// Parse sub-route if any
$path = parse_url($uri, PHP_URL_PATH);
$action = $_GET['action'] ?? null;
// Handle Item Detail/Comment/Resolve via ID in URL or query params
$itemId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if (!$itemId && preg_match('/\/manage\/items\/(\d+)/', $path, $m)) {
$itemId = (int)$m[1];
}
// Sub-actions
if ($action === 'stats' || str_ends_with($path, '/stats')) {
echo json_encode(['status' => 'success', 'stats' => $repo->getStats()], JSON_PRETTY_PRINT);
exit;
}
if ($action === 'resolve' || str_contains($path, '/resolve')) {
if ($method !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'POST required for resolve']);
exit;
}
if (!$itemId) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Missing item ID']);
exit;
}
$build = !empty($input['resolved_in_build']) ? trim($input['resolved_in_build']) : 'v1.0.0';
$notes = !empty($input['resolution_notes']) ? trim($input['resolution_notes']) : null;
$author = !empty($input['author']) ? trim($input['author']) : $authorName;
$ok = $repo->resolveItem($itemId, $build, $notes, $author);
if ($ok) {
echo json_encode(['status' => 'success', 'message' => "Item #{$itemId} resolved in build {$build}"]);
} else {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Failed to resolve item']);
}
exit;
}
if ($action === 'comment' || str_contains($path, '/comments')) {
if ($method !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'POST required for comment']);
exit;
}
if (!$itemId) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Missing item ID']);
exit;
}
$comment = !empty($input['comment']) ? trim($input['comment']) : '';
if (empty($comment)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Comment cannot be empty']);
exit;
}
$author = !empty($input['author']) ? trim($input['author']) : $authorName;
$actionTaken = !empty($input['action_taken']) ? trim($input['action_taken']) : 'commented';
$meta = isset($input['meta']) && is_array($input['meta']) ? $input['meta'] : null;
$comm = $repo->addComment($itemId, $author, $comment, $actionTaken, $meta);
echo json_encode(['status' => 'success', 'comment' => $comm]);
exit;
}
if ($action === 'status' || str_contains($path, '/status')) {
if ($method !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'POST required for status change']);
exit;
}
if (!$itemId) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Missing item ID']);
exit;
}
$status = !empty($input['status']) ? trim($input['status']) : 'open';
$notes = !empty($input['notes']) ? trim($input['notes']) : null;
$author = !empty($input['author']) ? trim($input['author']) : $authorName;
$ok = $repo->updateStatus($itemId, $status, $notes, $author);
if ($ok) {
echo json_encode(['status' => 'success', 'message' => "Status for #{$itemId} updated to {$status}"]);
} else {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Invalid status']);
}
exit;
}
// Detail View of a single item
if ($itemId > 0 && $method === 'GET') {
$details = $repo->getItemDetails($itemId);
if (!$details) {
http_response_code(404);
echo json_encode(['status' => 'error', 'message' => 'Item not found']);
exit;
}
echo json_encode(['status' => 'success', 'item' => $details], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit;
}
// Default: List Items
$filters = [
'project_slug' => $_GET['project_slug'] ?? $_GET['project'] ?? 'all',
'environment' => $_GET['environment'] ?? $_GET['env'] ?? 'all',
'type' => $_GET['type'] ?? 'all',
'status' => $_GET['status'] ?? 'all',
'severity' => $_GET['severity'] ?? 'all',
'search' => $_GET['search'] ?? $_GET['q'] ?? '',
];
$items = $repo->getItems($filters);
echo json_encode([
'status' => 'success',
'count' => count($items),
'filters' => $filters,
'items' => $items,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (Throwable $t) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Manage API Error: ' . $t->getMessage()]);
}
+82
View File
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../../src/Core/Db.php';
require_once __DIR__ . '/../../../../src/Core/TokenManager.php';
require_once __DIR__ . '/../../../../src/Modules/Bugtracker/BugRepo.php';
use Deploymentcenter\Core\Db;
use Deploymentcenter\Core\TokenManager;
use Deploymentcenter\Modules\Bugtracker\BugRepo;
header('Content-Type: application/json; charset=utf-8');
// Allow CORS for public ingest
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Agent-Token');
header('Access-Control-Allow-Methods: POST, OPTIONS');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method Not Allowed']);
exit;
}
$rawInput = file_get_contents('php://input');
$data = json_decode($rawInput, true) ?: $_POST;
if (empty($data)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Empty request body or invalid JSON']);
exit;
}
try {
$config = require __DIR__ . '/../../../../config/config.php';
$db = Db::connect($config['db']);
// Optional Token Verification (if provided)
$headers = getallheaders();
$token = $headers['X-Agent-Token'] ?? $headers['x-agent-token'] ?? null;
if (!$token && !empty($headers['Authorization'])) {
if (preg_match('/Bearer\s+(.+)/i', $headers['Authorization'], $matches)) {
$token = trim($matches[1]);
}
}
if ($token) {
$tokenMgr = new TokenManager($db);
$valid = $tokenMgr->validateToken($token, 'bugtracker:report', $data['environment'] ?? null);
if (!$valid) {
http_response_code(401);
echo json_encode(['status' => 'error', 'message' => 'Invalid, revoked or unauthorized Token for bugtracker:report']);
exit;
}
}
$repo = new BugRepo($db);
$result = $repo->reportItem($data);
echo json_encode([
'status' => 'success',
'item_id' => $result['id'],
'is_new' => $result['is_new'],
'occurrence_count' => $result['occurrence_count'],
'error_hash' => $result['error_hash'],
'type' => $result['type'],
'environment' => $result['environment'],
'message' => $result['is_new']
? ($result['type'] === 'bug' ? 'New bug reported successfully.' : 'New feature request submitted.')
: 'Recurring bug count updated.',
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (Throwable $t) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Failed to log report: ' . $t->getMessage()]);
}
+77
View File
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../../src/Core/Db.php';
require_once __DIR__ . '/../../../../src/Core/TokenManager.php';
use Deploymentcenter\Core\Db;
use Deploymentcenter\Core\TokenManager;
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method Not Allowed']);
exit;
}
// Extract Master Token from Headers
$headers = getallheaders();
$masterToken = $headers['X-Master-Token'] ?? $headers['x-master-token'] ?? null;
if (!$masterToken && !empty($headers['Authorization'])) {
if (preg_match('/Bearer\s+(.+)/i', $headers['Authorization'], $matches)) {
$masterToken = trim($matches[1]);
}
}
$rawInput = file_get_contents('php://input');
$data = json_decode($rawInput, true) ?: $_POST;
if (!$masterToken && !empty($data['master_token'])) {
$masterToken = trim($data['master_token']);
}
if (!$masterToken) {
http_response_code(401);
echo json_encode(['status' => 'error', 'message' => 'Missing Master Token in X-Master-Token header or Authorization Bearer header']);
exit;
}
try {
$config = require __DIR__ . '/../../../../config/config.php';
$db = Db::connect($config['db']);
$tokenMgr = new TokenManager($db);
$name = !empty($data['client_name']) ? trim($data['client_name']) : (!empty($data['name']) ? trim($data['name']) : 'Auto-Provisioned Agent Sub-Token');
$instanceIdentity = !empty($data['instance_id']) ? trim($data['instance_id']) : (!empty($data['hostname']) ? trim($data['hostname']) : null);
$requestedScopes = isset($data['scopes']) && is_array($data['scopes']) ? $data['scopes'] : [];
$environment = !empty($data['environment']) ? trim($data['environment']) : 'all';
$subTokenData = $tokenMgr->provisionSubToken(
$masterToken,
$name,
$instanceIdentity,
$requestedScopes,
$environment
);
echo json_encode([
'status' => 'success',
'sub_token' => $subTokenData['raw_token'],
'token_id' => $subTokenData['token_id'],
'name' => $subTokenData['name'],
'scopes' => $subTokenData['scopes'],
'environment' => $subTokenData['environment'],
'type' => 'sub',
'created_at' => date('Y-m-d H:i:s'),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
} catch (Throwable $t) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Internal server error: ' . $t->getMessage()]);
}
+39 -2
View File
@@ -25,15 +25,51 @@ try {
$updateMgr = new UpdateManager($pdo);
// Read JSON body for POST requests if available
$inputData = [];
if ($method === 'POST') {
$raw = file_get_contents('php://input');
if (!empty($raw)) {
$inputData = json_decode($raw, true) ?? [];
}
}
$action = $_REQUEST['action'] ?? $inputData['action'] ?? '';
// Action: Publish Release (from Packager CLI)
if ($action === 'publish_release' && $method === 'POST') {
$product = $inputData['product_slug'] ?? $_POST['product_slug'] ?? '';
$version = $inputData['version'] ?? $_POST['version'] ?? '';
$channel = $inputData['channel'] ?? $_POST['channel'] ?? 'prod';
$url = $inputData['download_url'] ?? $_POST['download_url'] ?? '';
$hash = $inputData['sha256_hash'] ?? $_POST['sha256_hash'] ?? null;
$gitCommit = $inputData['git_commit'] ?? $_POST['git_commit'] ?? null;
$sizeBytes = (int)($inputData['size_bytes'] ?? $_POST['size_bytes'] ?? 0);
$notes = $inputData['release_notes'] ?? $_POST['release_notes'] ?? null;
$isCritical= !empty($inputData['is_critical']) || !empty($_POST['is_critical']);
if (empty($product) || empty($version) || empty($url)) {
sendResponse(['error' => 'Bad Request', 'message' => 'Missing required fields: product_slug, version, download_url'], 400);
}
$ok = $updateMgr->addRelease($product, $version, $channel, $notes, $url, $hash, $gitCommit, $sizeBytes, null, $isCritical);
if ($ok) {
sendResponse(['status' => 'success', 'message' => "Release v{$version} published for {$product} ({$channel})."]);
} else {
sendResponse(['error' => 'Database Error', 'message' => 'Failed to store release.'], 500);
}
}
if (str_ends_with($uri, '/check') && ($method === 'GET' || $method === 'POST')) {
$product = $_REQUEST['product'] ?? $_REQUEST['product_slug'] ?? '';
$version = $_REQUEST['version'] ?? $_REQUEST['current_version'] ?? '0.0.0';
$channel = $_REQUEST['channel'] ?? 'prod';
if (empty($product)) {
sendResponse(['error' => 'Bad Request', 'message' => 'Parameter "product" is required.'], 400);
}
$latest = $updateMgr->checkUpdate($product, $version);
$latest = $updateMgr->checkUpdate($product, $version, $channel);
if ($latest) {
sendResponse([
'update_available' => true,
@@ -49,7 +85,8 @@ try {
if (str_ends_with($uri, '/releases') && $method === 'GET') {
$product = $_GET['product'] ?? null;
$releases = $updateMgr->getReleases($product);
$channel = $_GET['channel'] ?? null;
$releases = $updateMgr->getReleases($product, $channel);
sendResponse(['count' => count($releases), 'releases' => $releases]);
}
+725 -8
View File
@@ -9,15 +9,19 @@ 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();
@@ -388,21 +392,123 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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, $notes, $url, $hash, $critical)) {
$msg = "Release v{$version} für Projekt '{$productSlug}' veröffentlicht.";
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
@@ -532,6 +638,16 @@ $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;
@@ -740,6 +856,12 @@ $baseUrl = $protocol . '://' . $host;
<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>
@@ -748,6 +870,12 @@ $baseUrl = $protocol . '://' . $host;
<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>
@@ -1601,24 +1729,50 @@ $baseUrl = $protocol . '://' . $host;
<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>
<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 URL</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><code>v<?= htmlspecialchars($r['version']) ?></code></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" style="color:var(--primary); font-weight:700;"><?= htmlspecialchars($r['download_url']) ?></a></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; ?>
@@ -1641,25 +1795,419 @@ $baseUrl = $protocol . '://' . $host;
<?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/downloads/myapp-1.2.0.zip">
<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</label>
<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>
</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">
@@ -1756,6 +2304,15 @@ $baseUrl = $protocol . '://' . $host;
{ 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' },
@@ -1828,6 +2385,8 @@ $baseUrl = $protocol . '://' . $host;
'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';
@@ -1835,6 +2394,128 @@ $baseUrl = $protocol . '://' . $host;
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);
@@ -2021,5 +2702,41 @@ $baseUrl = $protocol . '://' . $host;
}
});
</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>