feat(bugtracker, tokens): Add project search, push_id, target_agent, tags, edit modal, extended severities (idea, wishlist), token actions (copy/toggle/delete), and agent integration guide

This commit is contained in:
Deploymentcenter Bot
2026-08-06 21:53:30 +02:00
parent cb52298c7a
commit c437bdee6f
9 changed files with 682 additions and 177 deletions
+31
View File
@@ -157,6 +157,35 @@ try {
exit;
}
if ($action === 'update' || str_contains($path, '/update')) {
if ($method !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'POST required for update']);
exit;
}
if (!$itemId && !empty($input['id'])) {
$itemId = (int)$input['id'];
}
if (!$itemId) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Missing item ID']);
exit;
}
$author = !empty($input['author']) ? trim($input['author']) : $authorName;
$ok = $repo->updateItemDetails($itemId, $input, $author);
if ($ok) {
echo json_encode(['status' => 'success', 'message' => "Item #{$itemId} updated successfully"]);
} else {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Failed to update item']);
}
exit;
}
// Detail View of a single item
if ($itemId > 0 && $method === 'GET') {
$details = $repo->getItemDetails($itemId);
@@ -176,6 +205,8 @@ try {
'type' => $_GET['type'] ?? 'all',
'status' => $_GET['status'] ?? 'all',
'severity' => $_GET['severity'] ?? 'all',
'push_id' => $_GET['push_id'] ?? '',
'target_agent' => $_GET['target_agent'] ?? $_GET['agent'] ?? '',
'search' => $_GET['search'] ?? $_GET['q'] ?? '',
];
+2 -1
View File
@@ -71,8 +71,9 @@ try {
'error_hash' => $result['error_hash'],
'type' => $result['type'],
'environment' => $result['environment'],
'push_id' => $result['push_id'] ?? null,
'message' => $result['is_new']
? ($result['type'] === 'bug' ? 'New bug reported successfully.' : 'New feature request submitted.')
? ($result['type'] === 'bug' ? 'New bug reported successfully.' : 'New feature request / idea submitted.')
: 'Recurring bug count updated.',
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+264 -50
View File
@@ -437,6 +437,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
if ($action === 'delete_core_token') {
$tokenId = trim($_POST['token_id'] ?? '');
if ($tokenId) {
$coreTokenMgr = new CoreTokenManager($pdo);
$coreTokenMgr->deleteToken($tokenId);
$msg = "Token '{$tokenId}' und abgeleitete Sub-Tokens wurden dauerhaft gelöscht.";
}
}
// Bugtracker & Feature-Tracker Actions
if ($action === 'bt_add_comment') {
$itemId = (int)($_POST['item_id'] ?? 0);
@@ -474,6 +483,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$build = trim($_POST['build_version'] ?? 'v1.0.0');
$env = $_POST['environment'] ?? 'production';
$sev = $_POST['severity'] ?? 'medium';
$pushId = trim($_POST['push_id'] ?? '');
$agent = trim($_POST['target_agent'] ?? '');
$tags = trim($_POST['tags'] ?? '');
$createdBy = trim($_POST['created_by'] ?? $_SESSION['dc_username'] ?? 'admin');
if ($title) {
@@ -488,6 +500,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'build_version' => $build,
'environment' => $env,
'severity' => $sev,
'push_id' => $pushId,
'target_agent' => $agent,
'tags' => $tags,
'created_by' => $createdBy,
]);
$msg = $res['is_new']
@@ -508,6 +523,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$msg = "Status für Item #{$itemId} geändert auf {$status}.";
}
}
if ($action === 'bt_update_item') {
$itemId = (int)($_POST['item_id'] ?? 0);
$status = $_POST['status'] ?? 'open';
$sev = $_POST['severity'] ?? 'medium';
$pushId = trim($_POST['push_id'] ?? '');
$agent = trim($_POST['target_agent'] ?? '');
$tags = trim($_POST['tags'] ?? '');
$notes = trim($_POST['resolution_notes'] ?? '');
$build = trim($_POST['resolved_in_build'] ?? '');
$author = trim($_POST['author'] ?? $_SESSION['dc_username'] ?? 'admin');
if ($itemId > 0) {
$bugRepo = new BugRepo($pdo);
$bugRepo->updateItemDetails($itemId, [
'status' => $status,
'severity' => $sev,
'push_id' => $pushId,
'target_agent' => $agent,
'tags' => $tags,
'resolution_notes' => $notes,
'resolved_in_build' => $build,
], $author);
$msg = "Item #{$itemId} wurde erfolgreich aktualisiert.";
}
}
}
// Fetch All Data
@@ -1838,7 +1879,7 @@ $baseUrl = $protocol . '://' . $host;
<div id="tab-bugtracker" class="tab-content">
<div id="sub-bugtracker-items" class="subtab-content active">
<div class="stats-grid">
<div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));">
<div class="stat-card">
<div class="stat-header">Dev Bugs (Entwicklung)</div>
<div class="stat-value" style="color:#5b9dff;"><?= $bugtrackerStats['open_bugs_dev'] ?></div>
@@ -1851,6 +1892,10 @@ $baseUrl = $protocol . '://' . $host;
<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">💡 Ideen & Gedanken</div>
<div class="stat-value" style="color:#e082ff;"><?= $bugtrackerStats['ideas_count'] ?></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>
@@ -1858,26 +1903,46 @@ $baseUrl = $protocol . '://' . $host;
</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;">
<div class="card-header" style="display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; gap:0.75rem;">
<h2 class="card-title" style="margin:0;">🐛 Bugs, Features & Ideen</h2>
<div style="display:flex; flex-wrap:wrap; gap:0.4rem; align-items:center;">
<select id="btFilterProject" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="filterBugtrackerTable()">
<option value="all">🌐 Alle Projekte</option>
<?php foreach ($projects as $p): ?>
<option value="<?= htmlspecialchars($p['slug']) ?>"><?= htmlspecialchars($p['name']) ?> (<?= htmlspecialchars($p['slug']) ?>)</option>
<?php endforeach; ?>
</select>
<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="all">🌍 Alle Umgebungen</option>
<option value="production">🔴 Produktion (Prod)</option>
<option value="development">🔵 Entwicklung (Dev)</option>
<option value="staging">🟡 Staging</option>
<option value="testing">🧪 Testing</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="btFilterSeverity" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="filterBugtrackerTable()">
<option value="all">🔥 Alle Schweregrade</option>
<option value="idea">💡 Nur Ideen & Gedanken</option>
<option value="wishlist">⭐ Nur Wunschliste</option>
<option value="low">Niedrig</option>
<option value="medium">Mittel</option>
<option value="high">Hoch</option>
<option value="critical">🔥 Kritisch</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="planned">Geplant</option>
<option value="in_progress">In Bearbeitung</option>
<option value="resolved">Gelöst / Umgesetzt</option>
<option value="closed">Geschlossen</option>
<option value="rejected">Abgelehnt</option>
</select>
<input type="text" id="btFilterSearch" class="form-input" style="width:160px; padding:0.3rem 0.6rem; font-size:0.8rem;" placeholder="🔍 Suche..." onkeyup="filterBugtrackerTable()">
</div>
</div>
<table>
@@ -1908,6 +1973,7 @@ $baseUrl = $protocol . '://' . $host;
$statusBadge = match($item['status']) {
'open' => '<span class="badge badge-down">OFFEN</span>',
'planned' => '<span class="badge" style="background:rgba(255,165,0,0.2); color:orange; border:1px solid orange;">GEPLANT</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>',
@@ -1915,22 +1981,38 @@ $baseUrl = $protocol . '://' . $host;
};
$sevBadge = match($item['severity']) {
'idea' => '<span class="badge" style="background:rgba(215,100,255,0.25); color:#e082ff; border:1px solid #e082ff;">💡 IDEE</span>',
'wishlist' => '<span class="badge" style="background:rgba(0,210,255,0.25); color:#5ce1e6; border:1px solid #5ce1e6;">⭐ WUNSCHLISTE</span>',
'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>'
};
$jsonItem = htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8');
?>
<tr class="bt-row"
data-project="<?= htmlspecialchars($item['project_slug']) ?>"
data-env="<?= htmlspecialchars($item['environment']) ?>"
data-type="<?= htmlspecialchars($item['type']) ?>"
data-severity="<?= htmlspecialchars($item['severity']) ?>"
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 style="font-size:0.75rem; color:var(--text-muted); margin-top:0.2rem; display:flex; flex-wrap:wrap; gap:0.4rem; align-items:center;">
<span>Projekt: <code><?= htmlspecialchars($item['project_slug']) ?></code></span>
<span>Von: <?= htmlspecialchars($item['created_by']) ?></span>
<?php if (!empty($item['push_id'])): ?>
<span class="badge" style="background:rgba(255,255,255,0.06); font-family:monospace; font-size:0.7rem;">📲 Push: <?= htmlspecialchars($item['push_id']) ?></span>
<?php endif; ?>
<?php if (!empty($item['target_agent'])): ?>
<span class="badge" style="background:rgba(0,200,100,0.15); color:#66bb6a; font-size:0.7rem;">🤖 <?= htmlspecialchars($item['target_agent']) ?></span>
<?php endif; ?>
<?php if (!empty($item['tags'])): ?>
<span class="badge" style="background:rgba(100,180,255,0.15); color:#90caf9; font-size:0.7rem;">🏷️ <?= htmlspecialchars($item['tags']) ?></span>
<?php endif; ?>
</div>
</td>
<td><code><?= htmlspecialchars($item['build_version'] ?? 'v1.0.0') ?></code></td>
@@ -1942,10 +2024,13 @@ $baseUrl = $protocol . '://' . $host;
</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; ?>
<div style="display:flex; gap:0.3rem;">
<button type="button" class="btn btn-sm btn-secondary" onclick="openEditBugtrackerModal(<?= $jsonItem ?>)">✏️ Edit</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="openBugtrackerModal(<?= $item['id'] ?>)">🔍 Details</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; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
@@ -1957,7 +2042,7 @@ $baseUrl = $protocol . '://' . $host;
<!-- 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>
<div class="card-header"><h2 class="card-title"> Bug, Feature Request oder Idee Erfassen</h2></div>
<form method="POST" action="index.php#tab-bugtracker">
<input type="hidden" name="action" value="bt_create_item">
<div class="form-grid">
@@ -1973,7 +2058,7 @@ $baseUrl = $protocol . '://' . $host;
<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>
<option value="feature_request">💡 Feature Request / Vorschlag / Idee</option>
</select>
</div>
<div class="form-group">
@@ -1988,6 +2073,8 @@ $baseUrl = $protocol . '://' . $host;
<div class="form-group">
<label class="form-label">Schweregrad / Priorität</label>
<select name="severity" class="form-input" required>
<option value="idea">💡 Idee / Gedanke für Später</option>
<option value="wishlist">⭐ Wunschliste / Backlog</option>
<option value="low">Niedrig</option>
<option value="medium" selected>Mittel</option>
<option value="high">Hoch</option>
@@ -1995,9 +2082,23 @@ $baseUrl = $protocol . '://' . $host;
</select>
</div>
</div>
<div class="form-grid" style="margin-top:1rem;">
<div class="form-group">
<label class="form-label">Push-ID (Optional, z. B. Workflow / Notification Token)</label>
<input type="text" name="push_id" class="form-input" placeholder="z. B. push_notify_88192">
</div>
<div class="form-group">
<label class="form-label">Ziel-Agent / Zuweisung (Optional)</label>
<input type="text" name="target_agent" class="form-input" placeholder="z. B. agent:code-fixer-01">
</div>
<div class="form-group">
<label class="form-label">Tags / Kategorien (Kommagetrennt)</label>
<input type="text" name="tags" class="form-input" placeholder="z. B. ui, security, database">
</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">
<input type="text" name="title" class="form-input" required placeholder="z. B. NullReferenceException bei Order-Submit oder Idee für TOTP 2FA">
</div>
<div class="form-group" style="margin-top:1rem;">
<label class="form-label">Build / Version</label>
@@ -2005,14 +2106,14 @@ $baseUrl = $protocol . '://' . $host;
</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>
<textarea name="description" class="form-input" rows="3" placeholder="Was ist passiert? Unter welchen Bedingungen? Oder was soll umgesetzt werden?"></textarea>
</div>
<div class="form-group" style="margin-top:1rem;">
<label class="form-label">Fehlermeldung / Exception Message</label>
<label class="form-label">Fehlermeldung / Exception Message (Nur bei Bugs)</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>
<label class="form-label">Stacktrace / Log Ausschnitt (Nur bei Bugs)</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>
@@ -2068,22 +2169,29 @@ $baseUrl = $protocol . '://' . $host;
<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) ?>">
<code id="tok-core-<?= $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>
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleTokenMask('tok-core-<?= $mTok['token_id'] ?>')">👁️</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="copyTokenValue('tok-core-<?= $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">
<div style="display:flex; gap:0.3rem;">
<?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-warning">Widerrufen</button>
</form>
<?php else: ?>
<span class="badge badge-down">WIDERUFEN</span>
<?php endif; ?>
<form method="POST" action="index.php#tab-tokens" style="display:inline" onsubmit="return confirm('Master-Token und alle Sub-Tokens DAUERHAFT LÖSCHEN?');">
<input type="hidden" name="action" value="delete_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>
<button type="submit" class="btn btn-sm btn-danger">🗑️ Löschen</button>
</form>
<?php else: ?>
<span class="badge badge-down">WIDERUFEN</span>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
@@ -2103,8 +2211,10 @@ $baseUrl = $protocol . '://' . $host;
<th>Bezeichnung / Client</th>
<th>Umgebung</th>
<th>Scopes</th>
<th>Token Key</th>
<th>Zuletzt Genutzt</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
@@ -2112,6 +2222,8 @@ $baseUrl = $protocol . '://' . $host;
$subTokens = array_filter($coreAllTokens, fn($t) => $t['type'] === 'sub');
foreach ($subTokens as $sTok):
$scopesArr = json_decode($sTok['scopes'], true) ?: [];
$sRaw = !empty($sTok['raw_token']) ? $sTok['raw_token'] : $sTok['token_id'];
$sMasked = (strlen($sRaw) > 12) ? substr($sRaw, 0, 12) . '••••••••••••••••' : $sRaw;
?>
<tr>
<td><code><?= htmlspecialchars($sTok['token_id']) ?></code></td>
@@ -2125,8 +2237,31 @@ $baseUrl = $protocol . '://' . $host;
<span class="badge" style="background:rgba(255,255,255,0.06); font-size:0.75rem;"><?= htmlspecialchars($sc) ?></span>
<?php endforeach; ?>
</td>
<td>
<code id="tok-sub-<?= $sTok['token_id'] ?>" data-full="<?= htmlspecialchars($sRaw) ?>" data-masked="<?= htmlspecialchars($sMasked) ?>">
<?= htmlspecialchars($sMasked) ?>
</code>
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleTokenMask('tok-sub-<?= $sTok['token_id'] ?>')">👁️</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="copyTokenValue('tok-sub-<?= $sTok['token_id'] ?>')">📋</button>
</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>
<td>
<div style="display:flex; gap:0.3rem;">
<?php if (!$sTok['revoked']): ?>
<form method="POST" action="index.php#tab-tokens" style="display:inline" onsubmit="return confirm('Sub-Token widerrufen?');">
<input type="hidden" name="action" value="revoke_core_token">
<input type="hidden" name="token_id" value="<?= $sTok['token_id'] ?>">
<button type="submit" class="btn btn-sm btn-warning">Widerrufen</button>
</form>
<?php endif; ?>
<form method="POST" action="index.php#tab-tokens" style="display:inline" onsubmit="return confirm('Sub-Token DAUERHAFT LÖSCHEN?');">
<input type="hidden" name="action" value="delete_core_token">
<input type="hidden" name="token_id" value="<?= $sTok['token_id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">🗑️ Löschen</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
@@ -2393,16 +2528,23 @@ $baseUrl = $protocol . '://' . $host;
// Bugtracker Table Filter JS
function filterBugtrackerTable() {
const project = document.getElementById('btFilterProject').value;
const env = document.getElementById('btFilterEnv').value;
const type = document.getElementById('btFilterType').value;
const severity = document.getElementById('btFilterSeverity').value;
const status = document.getElementById('btFilterStatus').value;
const query = (document.getElementById('btFilterSearch').value || '').toLowerCase().trim();
document.querySelectorAll('#btTableBody .bt-row').forEach(row => {
const matchProject = (project === 'all' || row.getAttribute('data-project') === project);
const matchEnv = (env === 'all' || row.getAttribute('data-env') === env);
const matchType = (type === 'all' || row.getAttribute('data-type') === type);
const matchSeverity = (severity === 'all' || row.getAttribute('data-severity') === severity);
const matchStatus = (status === 'all' || row.getAttribute('data-status') === status);
const textContent = row.innerText.toLowerCase();
const matchQuery = (!query || textContent.includes(query));
if (matchEnv && matchType && matchStatus) {
if (matchProject && matchEnv && matchType && matchSeverity && matchStatus && matchQuery) {
row.style.display = '';
} else {
row.style.display = 'none';
@@ -2410,6 +2552,24 @@ $baseUrl = $protocol . '://' . $host;
});
}
// Open Bugtracker Edit Modal
function openEditBugtrackerModal(item) {
document.getElementById('btEditItemId').value = item.id;
document.getElementById('btEditTitleDisplay').innerText = '#' + item.id + ': ' + item.title;
document.getElementById('btEditStatus').value = item.status;
document.getElementById('btEditSeverity').value = item.severity;
document.getElementById('btEditPushId').value = item.push_id || '';
document.getElementById('btEditTargetAgent').value = item.target_agent || '';
document.getElementById('btEditTags').value = item.tags || '';
document.getElementById('btEditResolvedInBuild').value = item.resolved_in_build || '';
document.getElementById('btEditResolutionNotes').value = item.resolution_notes || '';
document.getElementById('btEditModal').style.display = 'flex';
}
function closeEditBugtrackerModal() {
document.getElementById('btEditModal').style.display = 'none';
}
// Open Bugtracker Item Details & Timeline Modal
function openBugtrackerModal(itemId) {
const modal = document.getElementById('btDetailModal');
@@ -2640,9 +2800,10 @@ $baseUrl = $protocol . '://' . $host;
document.getElementById('editExpiresAt').value = opt.getAttribute('data-exp') || '';
}
// Token Masking Toggle
// Token Masking Toggle & Copy
function toggleTokenMask(tokId) {
const el = document.getElementById('tok-text-' + tokId);
const el = document.getElementById(tokId) || document.getElementById('tok-text-' + tokId) || document.getElementById('tok-core-' + tokId) || document.getElementById('tok-sub-' + tokId);
if (!el) return;
if (el.innerText.includes('••••')) {
el.innerText = el.getAttribute('data-full');
} else {
@@ -2651,10 +2812,18 @@ $baseUrl = $protocol . '://' . $host;
}
function copyTokenValue(tokId) {
const el = document.getElementById('tok-text-' + tokId);
const el = document.getElementById(tokId) || document.getElementById('tok-text-' + tokId) || document.getElementById('tok-core-' + tokId) || document.getElementById('tok-sub-' + tokId);
if (!el) return;
const fullVal = el.getAttribute('data-full');
navigator.clipboard.writeText(fullVal);
alert('Token in Zwischenablage kopiert!');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(fullVal).then(() => {
alert('Token in Zwischenablage kopiert!');
}).catch(() => {
prompt('Token kopieren:', fullVal);
});
} else {
prompt('Token kopieren:', fullVal);
}
}
function generateOfflinePayload() {
@@ -2709,28 +2878,73 @@ $baseUrl = $protocol . '://' . $host;
</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);">
<!-- Bugtracker Edit Modal -->
<div id="btEditModal" 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:650px; 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>
<h3 class="card-title" style="margin:0; color:#fff;">✏️ Item Bearbeiten</h3>
<button type="button" class="btn btn-sm btn-secondary" onclick="closeEditBugtrackerModal()">✕</button>
</div>
<p style="font-size:0.875rem; color:var(--text-muted); margin-bottom:1rem;" id="resolveItemTitle"></p>
<p style="font-size:0.875rem; color:var(--text-muted); margin-bottom:1rem;" id="btEditTitleDisplay"></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">
<input type="hidden" name="action" value="bt_update_item">
<input type="hidden" name="item_id" id="btEditItemId" value="0">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Status Bearbeiten</label>
<select name="status" id="btEditStatus" class="form-input" required>
<option value="open">Offen</option>
<option value="planned">Geplant</option>
<option value="in_progress">In Bearbeitung</option>
<option value="resolved">Gelöst / Umgesetzt</option>
<option value="closed">Geschlossen</option>
<option value="rejected">Abgelehnt</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Schweregrad Bearbeiten</label>
<select name="severity" id="btEditSeverity" class="form-input" required>
<option value="idea">💡 Idee / Gedanke für Später</option>
<option value="wishlist">⭐ Wunschliste / Backlog</option>
<option value="low">Niedrig</option>
<option value="medium">Mittel</option>
<option value="high">Hoch</option>
<option value="critical">🔥 Kritisch</option>
</select>
</div>
</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 class="form-grid" style="margin-top:1rem;">
<div class="form-group">
<label class="form-label">Push-ID (Workflow / Push Notification Token)</label>
<input type="text" name="push_id" id="btEditPushId" class="form-input" placeholder="z. B. push_notify_99182">
</div>
<div class="form-group">
<label class="form-label">Ziel-Agent / Zuweisung</label>
<input type="text" name="target_agent" id="btEditTargetAgent" class="form-input" placeholder="z. B. agent:dev-monitor-01">
</div>
</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 class="form-group" style="margin-top:1rem;">
<label class="form-label">Tags (Kommagetrennt)</label>
<input type="text" name="tags" id="btEditTags" class="form-input" placeholder="z. B. ui, security, database">
</div>
<div class="form-grid" style="margin-top:1rem;">
<div class="form-group">
<label class="form-label">Lösungs-Build (Falls gelöst)</label>
<input type="text" name="resolved_in_build" id="btEditResolvedInBuild" class="form-input" placeholder="v1.4.3">
</div>
<div class="form-group">
<label class="form-label">Lösungs-Notizen / Dokumentation</label>
<input type="text" name="resolution_notes" id="btEditResolutionNotes" class="form-input" placeholder="Note zur Behebung...">
</div>
</div>
<div style="display:flex; justify-content:flex-end; gap:0.5rem; margin-top:1.25rem;">
<button type="button" class="btn btn-secondary" onclick="closeEditBugtrackerModal()">Abbrechen</button>
<button type="submit" class="btn">Änderungen Speichern</button>
</div>
</form>
</div>
+28
View File
@@ -47,6 +47,34 @@ try {
}
}
// Execute migration files in sql/migrations/
$migrationDir = __DIR__ . '/../sql/migrations';
if (is_dir($migrationDir)) {
$files = glob($migrationDir . '/*.sql');
sort($files);
foreach ($files as $mFile) {
$mSql = file_get_contents($mFile);
$mLines = explode("\n", $mSql);
$mClean = [];
foreach ($mLines as $l) {
$t = trim($l);
if (str_starts_with($t, '--') || str_starts_with($t, '#')) continue;
$mClean[] = $l;
}
$mQueries = array_filter(array_map('trim', explode(';', implode("\n", $mClean))));
foreach ($mQueries as $mq) {
if (!empty($mq)) {
try {
$pdo->exec($mq);
$executed++;
} catch (Throwable $e) {
// Ignore harmless duplicate column / migration errors
}
}
}
}
}
// Create / update Admin user: admin / Admin1337!
$adminUsername = 'admin';
$adminPassword = 'Admin1337!';