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
+156 -98
View File
@@ -1,156 +1,214 @@
# Deploymentcenter Bugtracker, Feature-Tracker & Token Provisioning Guide # 🤖 AI Agent Integration Guide: Deployment Center Bugtracker & Provisioning API
> **Zielgruppe**: KI-Agenten & Softwareentwickler This guide defines the standardized protocol and API specifications for autonomous AI Developer Agents interacting with the **Deployment Center Bugtracker & Token Provisioning System**.
> **Zweck**: Anleitung zur automatisierten Registrierung, Fehlererfassung, Feature-Einreichung und Behebung über das Deployment Center.
--- ---
## 1. Token-Architektur & Autonome Sub-Token-Erstellung ## 📌 Executive Overview for AI Agents
Das Deployment Center nutzt eine hierarchische Token-Struktur: As an autonomous AI agent working on software repositories, you MUST actively track all runtime errors, stack traces, planned features, and backlog items in the central Deployment Center.
- **Master-Token**: Wird im Deployment Center Admin-Dashboard pro Identität (z. B. Entwickler-Agent, Client-Anwendung, Host-Server) erstellt.
- **Sub-Token**: Wird von Anwendungen oder KI-Agenten autonom über die Provisioning-API angefordert und für API-Aufrufe genutzt.
### Sub-Token Anfordern (`POST /api/tokens/v1/provision`) ### Core Capabilities:
Sendet einen HTTP POST Request mit dem Master-Token im `X-Master-Token` Header. 1. **Sub-Token Auto-Provisioning**: Generate restricted sub-tokens for scoped agent tasks.
2. **Automated Error Ingestion**: Report unhandled exceptions with stack traces & automatic SHA-256 deduplication.
3. **Feature & Idea Backlog**: Submit roadmap ideas (`severity: "idea"`) or backlog items (`severity: "wishlist"`).
4. **Active Workflow Management**: Fetch active bugs assigned to your agent ID, update status (`in_progress`, `resolved`), and append diagnostic comments.
```http ---
POST /api/tokens/v1/provision HTTP/1.1
Host: dc.mhdf.de
Content-Type: application/json
X-Master-Token: dc_master_myapp_dev_agent_001
## 🔑 1. Token Provisioning API
Agents authenticate using a **Master Token** or auto-provisioned **Sub-Token**.
### Endpoint: `POST /api/tokens/v1/provision`
Header: `Authorization: Bearer <MASTER_TOKEN>`
#### Request Payload:
```json
{ {
"name": "Dev Workstation Agent #4", "parent_token": "dc_master_myapp_dev_agent_001",
"instance_id": "DEV-WORKSTATION-01", "name": "Codebase Refactoring Agent Token",
"environment": "development",
"scopes": ["bugtracker:report", "bugtracker:manage"], "scopes": ["bugtracker:report", "bugtracker:manage"],
"environment": "development" "expires_in_hours": 24
} }
``` ```
#### JSON Antwort: #### Response:
```json ```json
{ {
"status": "success", "status": "success",
"sub_token": "dc_sub_5f8a2c1...", "token_id": "tok_s_8912ab",
"token_id": "tok_s_89a1b2c3", "raw_token": "dc_sub_myapp_refactor_agent_991",
"name": "Dev Workstation Agent #4",
"scopes": ["bugtracker:report", "bugtracker:manage"], "scopes": ["bugtracker:report", "bugtracker:manage"],
"environment": "development" "environment": "development",
"expires_at": "2026-08-07 21:00:00"
} }
``` ```
--- ---
## 2. Bug & Feature Ingest API (`POST /api/bugtracker/v1/report`) ## 🐛 2. Reporting Bugs, Features & Ideas
Wird von Überwachungs-Agenten im laufenden Betrieb oder Entwickler-Agenten auf der Workstation genutzt. ### Endpoint: `POST /api/bugtracker/v1/report.php`
### Request Schema: Header: `Authorization: Bearer <AGENT_TOKEN>`
### A. Reporting an Unhandled Exception / Bug
```json ```json
{ {
"project_slug": "myapp", "project_slug": "myapp",
"type": "bug", // "bug" oder "feature_request" "type": "bug",
"environment": "development", // "production", "development", "staging", "testing"
"severity": "high", // "low", "medium", "high", "critical"
"title": "NullReferenceException in UserAuthService.cs line 42", "title": "NullReferenceException in UserAuthService.cs line 42",
"description": "Beim Login ohne gesetzte Session ist ein Unerwarteter Nullpointer-Fehler aufgetreten.", "description": "Triggered when user logs in without an active session object.",
"error_message": "NullReferenceException: Object reference not set to an instance of an object.", "error_message": "NullReferenceException: Object reference not set to an instance of an object.",
"stack_trace": "at MyApp.Core.UserAuthService.ValidateToken(String token)...", "stack_trace": "at MyApp.Core.UserAuthService.ValidateToken(String token) in UserAuthService.cs:line 42\nat MyApp.Controllers.AuthController.Login() in AuthController.cs:line 18",
"build_version": "v1.4.2-dev", "build_version": "v1.4.2-dev",
"created_by": "agent:dev-monitor-01" "environment": "development",
"severity": "high",
"push_id": "push_wf_8912",
"target_agent": "agent:code-fixer-01",
"tags": "auth, security, csharp",
"created_by": "agent:watchdog-monitor"
} }
``` ```
#### Besonderheiten: ### B. Submitting a Feature Request or Quick Reminder Idea (`severity: "idea"`)
- **Deduplizierung**: Bei Bugs berechnet das System automatisch einen Error-Hash. Tritt derselbe Fehler in derselben Umgebung erneut auf, wird kein neuer Bug angelegt, sondern `occurrence_count` hochgezählt. ```json
{
"project_slug": "myapp",
"type": "feature_request",
"title": "Automatische Datenbank-Backups vor FTP Deployments",
"description": "Gedanke für später: Vor jedem FTP-Deployment automatisch mysqldump ausführen und im Server-Archiv ablegen.",
"build_version": "v1.6.0-roadmap",
"environment": "development",
"severity": "idea",
"push_id": "push_wf_9910",
"target_agent": "agent:db-optimizer",
"tags": "database, automation, backup",
"created_by": "agent:planner"
}
```
#### Response:
```json
{
"status": "success",
"item_id": 4,
"is_new": true,
"occurrence_count": 1,
"error_hash": "e2c918a514d89a42f",
"type": "bug",
"environment": "development",
"push_id": "push_wf_8912",
"message": "New bug reported successfully."
}
```
--- ---
## 3. Geschützte Management & Resolution API (`/api/bugtracker/v1/manage`) ## 📌 3. Managing Items (Fetching, Updating & Commenting)
Schnittstelle für KI-Behebungs-Agenten zum Abrufen offener Bugs, Schreiben von Diagnose-Notizen und Markieren als gelöst. ### Base Endpoint: `/api/bugtracker/v1/manage/index.php`
**Header**: `Authorization: Bearer <dein_sub_token>` (Benötigt Scope `bugtracker:manage`). Header: `Authorization: Bearer <AGENT_TOKEN>`
### 3.1 Offene Items Abfragen (`GET /api/bugtracker/v1/manage/index.php`) ### A. Fetching Open Items Assigned to an Agent
Filter-Parameter: `environment` (`development`/`production`), `type` (`bug`/`feature_request`), `status` (`open`/`in_progress`/`resolved`). ```http
GET /api/bugtracker/v1/manage/index.php?project_slug=myapp&status=open&agent=agent:code-fixer-01
```
### 3.2 Item-Details & Kommentar-Historie (`GET /api/bugtracker/v1/manage/index.php?id=123`) ### B. Updating Status & Details (`POST ?action=update`)
### 3.3 Ermittlungsschritt / Kommentar Hinzufügen (`POST /api/bugtracker/v1/manage/index.php?action=comment&id=123`)
```json ```json
{ {
"comment": "Log-Analyse gestartet. Der Fehler tritt auf, wenn $_SESSION['dc_user_id'] nicht gesetzt ist.", "id": 4,
"author": "agent:code-fixer-01", "status": "in_progress",
"action_taken": "investigated" "severity": "high",
"push_id": "push_wf_8912",
"target_agent": "agent:code-fixer-01",
"tags": "auth, fixed_pending_test",
"author": "agent:code-fixer-01"
} }
``` ```
### 3.4 Item als Gelöst / Umgesetzt Markieren (`POST /api/bugtracker/v1/manage/index.php?action=resolve&id=123`) ### C. Appending Diagnostic Timeline Comments (`POST ?action=comment`)
```json ```json
{ {
"resolved_in_build": "v1.4.3", "id": 4,
"resolution_notes": "Null-Check für Session-Variable hinzugefügt.", "comment": "Ursache identifiziert: $_SESSION['user'] war Null in line 42. Null-Check und Safe Navigation Operator wurden hinzugefügt.",
"action_taken": "code_patched",
"author": "agent:code-fixer-01"
}
```
### D. Marking as Resolved (`POST ?action=resolve`)
```json
{
"id": 4,
"resolved_in_build": "v1.4.3-dev",
"resolution_notes": "Unit tests hinzugefügt und Null-Check in ValidateToken() integriert.",
"author": "agent:code-fixer-01" "author": "agent:code-fixer-01"
} }
``` ```
--- ---
## 4. C# (.NET 8) Implementierungsbeispiel für KI-Agenten ## 💻 4. Code Implementation Examples for Agents
```csharp ### Python Example: Automatic Error Reporter Decorator
using System; ```python
using System.Net.Http; import requests
using System.Text; import traceback
using System.Text.Json; import sys
using System.Threading.Tasks;
public class DeploymentCenterBugtrackerClient DC_API_URL = "https://dc.mhdf.de/api/bugtracker/v1/report.php"
{ AGENT_TOKEN = "dc_sub_myapp_agent_live_001"
private static readonly HttpClient HttpClient = new HttpClient();
private const string BaseUrl = "https://dc.mhdf.de";
public static async Task IngestDevBugAsync(string masterToken, string title, string errorMessage, string stackTrace) def report_exception_to_dc(project_slug: str, exc: Exception, env: str = "production", push_id: str = None):
{ payload = {
// 1. Sub-Token anfordern "project_slug": project_slug,
var provisionReq = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/api/tokens/v1/provision") "type": "bug",
{ "title": f"{type(exc).__name__}: {str(exc)}",
Content = new StringContent(JsonSerializer.Serialize(new "error_message": str(exc),
{ "stack_trace": traceback.format_exc(),
client_name = "Dev Agent Client", "build_version": "v1.4.2",
scopes = new[] { "bugtracker:report" }, "environment": env,
environment = "development" "severity": "high",
}), Encoding.UTF8, "application/json") "push_id": push_id,
}; "created_by": "agent:python-runner"
provisionReq.Headers.Add("X-Master-Token", masterToken);
var provResponse = await HttpClient.SendAsync(provisionReq);
var provJson = await provResponse.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(provJson);
string subToken = doc.RootElement.GetProperty("sub_token").GetString();
// 2. Bug melden
var reportReq = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/api/bugtracker/v1/report")
{
Content = new StringContent(JsonSerializer.Serialize(new
{
project_slug = "myapp",
type = "bug",
environment = "development",
severity = "high",
title = title,
error_message = errorMessage,
stack_trace = stackTrace,
build_version = "v1.4.2-dev",
created_by = "agent:dev-agent-01"
}), Encoding.UTF8, "application/json")
};
reportReq.Headers.Add("Authorization", $"Bearer {subToken}");
var reportResponse = await HttpClient.SendAsync(reportReq);
Console.WriteLine($"Report Response: {reportResponse.StatusCode}");
} }
} headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {AGENT_TOKEN}"
}
try:
r = requests.post(DC_API_URL, json=payload, headers=headers, timeout=5)
return r.json()
except Exception as e:
print(f"Failed to report to Deployment Center: {e}", file=sys.stderr)
``` ```
### cURL Example: Submit Feature Request / Idea
```bash
curl -X POST "https://dc.mhdf.de/api/bugtracker/v1/report.php" \
-H "Authorization: Bearer dc_sub_myapp_agent_live_001" \
-H "Content-Type: application/json" \
-d '{
"project_slug": "myapp",
"type": "feature_request",
"title": "Erweiterte Filterung im WebUI Dashboard",
"severity": "idea",
"push_id": "push_task_1029",
"tags": "ui, dashboard",
"created_by": "agent:dev-assistant"
}'
```
---
## 🎯 Best Practices for Developer Agents
1. **Always set `push_id`**: When executing automated pipelines, pass a `push_id` so all updates can be traced back to the specific execution run.
2. **Use `severity: "idea"` for thoughts**: When noticing potential refactorings or future improvements during coding, log them immediately as ideas.
3. **Comment before resolving**: Before calling `action=resolve`, write a diagnostic comment explaining **why** and **how** the fix was performed.
+31
View File
@@ -157,6 +157,35 @@ try {
exit; 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 // Detail View of a single item
if ($itemId > 0 && $method === 'GET') { if ($itemId > 0 && $method === 'GET') {
$details = $repo->getItemDetails($itemId); $details = $repo->getItemDetails($itemId);
@@ -176,6 +205,8 @@ try {
'type' => $_GET['type'] ?? 'all', 'type' => $_GET['type'] ?? 'all',
'status' => $_GET['status'] ?? 'all', 'status' => $_GET['status'] ?? 'all',
'severity' => $_GET['severity'] ?? 'all', 'severity' => $_GET['severity'] ?? 'all',
'push_id' => $_GET['push_id'] ?? '',
'target_agent' => $_GET['target_agent'] ?? $_GET['agent'] ?? '',
'search' => $_GET['search'] ?? $_GET['q'] ?? '', 'search' => $_GET['search'] ?? $_GET['q'] ?? '',
]; ];
+2 -1
View File
@@ -71,8 +71,9 @@ try {
'error_hash' => $result['error_hash'], 'error_hash' => $result['error_hash'],
'type' => $result['type'], 'type' => $result['type'],
'environment' => $result['environment'], 'environment' => $result['environment'],
'push_id' => $result['push_id'] ?? null,
'message' => $result['is_new'] '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.', : 'Recurring bug count updated.',
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+253 -39
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 // Bugtracker & Feature-Tracker Actions
if ($action === 'bt_add_comment') { if ($action === 'bt_add_comment') {
$itemId = (int)($_POST['item_id'] ?? 0); $itemId = (int)($_POST['item_id'] ?? 0);
@@ -474,6 +483,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$build = trim($_POST['build_version'] ?? 'v1.0.0'); $build = trim($_POST['build_version'] ?? 'v1.0.0');
$env = $_POST['environment'] ?? 'production'; $env = $_POST['environment'] ?? 'production';
$sev = $_POST['severity'] ?? 'medium'; $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'); $createdBy = trim($_POST['created_by'] ?? $_SESSION['dc_username'] ?? 'admin');
if ($title) { if ($title) {
@@ -488,6 +500,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'build_version' => $build, 'build_version' => $build,
'environment' => $env, 'environment' => $env,
'severity' => $sev, 'severity' => $sev,
'push_id' => $pushId,
'target_agent' => $agent,
'tags' => $tags,
'created_by' => $createdBy, 'created_by' => $createdBy,
]); ]);
$msg = $res['is_new'] $msg = $res['is_new']
@@ -508,6 +523,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$msg = "Status für Item #{$itemId} geändert auf {$status}."; $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 // Fetch All Data
@@ -1838,7 +1879,7 @@ $baseUrl = $protocol . '://' . $host;
<div id="tab-bugtracker" class="tab-content"> <div id="tab-bugtracker" class="tab-content">
<div id="sub-bugtracker-items" class="subtab-content active"> <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-card">
<div class="stat-header">Dev Bugs (Entwicklung)</div> <div class="stat-header">Dev Bugs (Entwicklung)</div>
<div class="stat-value" style="color:#5b9dff;"><?= $bugtrackerStats['open_bugs_dev'] ?></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-header">Offene Feature Requests</div>
<div class="stat-value" style="color:var(--warning);"><?= $bugtrackerStats['open_features'] ?></div> <div class="stat-value" style="color:var(--warning);"><?= $bugtrackerStats['open_features'] ?></div>
</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-card">
<div class="stat-header">Gelöst / Umgesetzt</div> <div class="stat-header">Gelöst / Umgesetzt</div>
<div class="stat-value" style="color:var(--success);"><?= $bugtrackerStats['resolved_total'] ?></div> <div class="stat-value" style="color:var(--success);"><?= $bugtrackerStats['resolved_total'] ?></div>
@@ -1858,26 +1903,46 @@ $baseUrl = $protocol . '://' . $host;
</div> </div>
<div class="card"> <div class="card">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;"> <div class="card-header" style="display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; gap:0.75rem;">
<h2 class="card-title">🐛 Bugs & Feature Requests</h2> <h2 class="card-title" style="margin:0;">🐛 Bugs, Features & Ideen</h2>
<div style="display:flex; gap:0.5rem;"> <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()"> <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="production">🔴 Produktion (Prod)</option>
<option value="development">🔵 Entwicklung (Dev)</option> <option value="development">🔵 Entwicklung (Dev)</option>
<option value="staging">🟡 Staging</option> <option value="staging">🟡 Staging</option>
<option value="testing">🧪 Testing</option>
</select> </select>
<select id="btFilterType" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="filterBugtrackerTable()"> <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="all">📂 Alle Typen</option>
<option value="bug">🐛 Nur Bugs</option> <option value="bug">🐛 Nur Bugs</option>
<option value="feature_request">💡 Nur Feature Requests</option> <option value="feature_request">💡 Nur Feature Requests</option>
</select> </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()"> <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="all">📌 Alle Status</option>
<option value="open">Offen</option> <option value="open">Offen</option>
<option value="planned">Geplant</option>
<option value="in_progress">In Bearbeitung</option> <option value="in_progress">In Bearbeitung</option>
<option value="resolved">Gelöst / Umgesetzt</option> <option value="resolved">Gelöst / Umgesetzt</option>
<option value="closed">Geschlossen</option>
<option value="rejected">Abgelehnt</option>
</select> </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>
</div> </div>
<table> <table>
@@ -1908,6 +1973,7 @@ $baseUrl = $protocol . '://' . $host;
$statusBadge = match($item['status']) { $statusBadge = match($item['status']) {
'open' => '<span class="badge badge-down">OFFEN</span>', '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>', 'in_progress' => '<span class="badge badge-warning">IN BEARBEITUNG</span>',
'resolved' => '<span class="badge badge-up">GELÖST / UMGESETZT</span>', 'resolved' => '<span class="badge badge-up">GELÖST / UMGESETZT</span>',
'rejected' => '<span class="badge badge-stopped">ABGELEHNT</span>', 'rejected' => '<span class="badge badge-stopped">ABGELEHNT</span>',
@@ -1915,22 +1981,38 @@ $baseUrl = $protocol . '://' . $host;
}; };
$sevBadge = match($item['severity']) { $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>', 'critical' => '<span class="badge badge-down" style="font-weight:bold;">🔥 KRITISCH</span>',
'high' => '<span class="badge badge-warning">HOCH</span>', 'high' => '<span class="badge badge-warning">HOCH</span>',
'medium' => '<span class="badge badge-stopped">MITTEL</span>', 'medium' => '<span class="badge badge-stopped">MITTEL</span>',
default => '<span class="badge badge-stopped">NIEDRIG</span>' default => '<span class="badge badge-stopped">NIEDRIG</span>'
}; };
$jsonItem = htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8');
?> ?>
<tr class="bt-row" <tr class="bt-row"
data-project="<?= htmlspecialchars($item['project_slug']) ?>"
data-env="<?= htmlspecialchars($item['environment']) ?>" data-env="<?= htmlspecialchars($item['environment']) ?>"
data-type="<?= htmlspecialchars($item['type']) ?>" data-type="<?= htmlspecialchars($item['type']) ?>"
data-severity="<?= htmlspecialchars($item['severity']) ?>"
data-status="<?= htmlspecialchars($item['status']) ?>"> data-status="<?= htmlspecialchars($item['status']) ?>">
<td>#<?= $item['id'] ?><br><?= $typeBadge ?></td> <td>#<?= $item['id'] ?><br><?= $typeBadge ?></td>
<td><?= $envBadge ?></td> <td><?= $envBadge ?></td>
<td> <td>
<strong style="color:#fff;"><?= htmlspecialchars($item['title']) ?></strong> <strong style="color:#fff;"><?= htmlspecialchars($item['title']) ?></strong>
<div style="font-size:0.75rem; color:var(--text-muted);"> <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;">
Projekt: <code><?= htmlspecialchars($item['project_slug']) ?></code> | Von: <?= htmlspecialchars($item['created_by']) ?> <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> </div>
</td> </td>
<td><code><?= htmlspecialchars($item['build_version'] ?? 'v1.0.0') ?></code></td> <td><code><?= htmlspecialchars($item['build_version'] ?? 'v1.0.0') ?></code></td>
@@ -1942,10 +2024,13 @@ $baseUrl = $protocol . '://' . $host;
</td> </td>
<td><?= $statusBadge ?></td> <td><?= $statusBadge ?></td>
<td> <td>
<button type="button" class="btn btn-sm btn-secondary" onclick="openBugtrackerModal(<?= $item['id'] ?>)">🔍 Details & Timeline</button> <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'): ?> <?php if ($item['status'] !== 'resolved'): ?>
<button type="button" class="btn btn-sm" onclick="openResolveModal(<?= $item['id'] ?>, '<?= htmlspecialchars(addslashes($item['title'])) ?>')">✔ Gelöst</button> <button type="button" class="btn btn-sm" onclick="openResolveModal(<?= $item['id'] ?>, '<?= htmlspecialchars(addslashes($item['title'])) ?>')">✔ Gelöst</button>
<?php endif; ?> <?php endif; ?>
</div>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -1957,7 +2042,7 @@ $baseUrl = $protocol . '://' . $host;
<!-- Subtab: Item manuell anlegen --> <!-- Subtab: Item manuell anlegen -->
<div id="sub-bugtracker-new" class="subtab-content"> <div id="sub-bugtracker-new" class="subtab-content">
<div class="card"> <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"> <form method="POST" action="index.php#tab-bugtracker">
<input type="hidden" name="action" value="bt_create_item"> <input type="hidden" name="action" value="bt_create_item">
<div class="form-grid"> <div class="form-grid">
@@ -1973,7 +2058,7 @@ $baseUrl = $protocol . '://' . $host;
<label class="form-label">Typ</label> <label class="form-label">Typ</label>
<select name="type" class="form-input" required> <select name="type" class="form-input" required>
<option value="bug" selected>🐛 Bug / Fehlerbericht</option> <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> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -1988,6 +2073,8 @@ $baseUrl = $protocol . '://' . $host;
<div class="form-group"> <div class="form-group">
<label class="form-label">Schweregrad / Priorität</label> <label class="form-label">Schweregrad / Priorität</label>
<select name="severity" class="form-input" required> <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="low">Niedrig</option>
<option value="medium" selected>Mittel</option> <option value="medium" selected>Mittel</option>
<option value="high">Hoch</option> <option value="high">Hoch</option>
@@ -1995,9 +2082,23 @@ $baseUrl = $protocol . '://' . $host;
</select> </select>
</div> </div>
</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;"> <div class="form-group" style="margin-top:1rem;">
<label class="form-label">Titel / Zusammenfassung</label> <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>
<div class="form-group" style="margin-top:1rem;"> <div class="form-group" style="margin-top:1rem;">
<label class="form-label">Build / Version</label> <label class="form-label">Build / Version</label>
@@ -2005,14 +2106,14 @@ $baseUrl = $protocol . '://' . $host;
</div> </div>
<div class="form-group" style="margin-top:1rem;"> <div class="form-group" style="margin-top:1rem;">
<label class="form-label">Detaillierte Beschreibung</label> <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>
<div class="form-group" style="margin-top:1rem;"> <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> <textarea name="error_message" class="form-input" rows="2" placeholder="Exakte Fehlermeldung aus dem Log..."></textarea>
</div> </div>
<div class="form-group" style="margin-top:1rem;"> <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> <textarea name="stack_trace" class="form-input" rows="4" style="font-family:monospace;" placeholder="at MyApp.Core.Service.DoWork()..."></textarea>
</div> </div>
<button type="submit" class="btn" style="margin-top:1rem;">Item Speichern & Anlegen</button> <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> <span class="badge badge-up"><?= (int)($mTok['sub_token_count'] ?? 0) ?> Sub-Tokens</span>
</td> </td>
<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) ?> <?= htmlspecialchars($masked) ?>
</code> </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="toggleTokenMask('tok-core-<?= $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="copyTokenValue('tok-core-<?= $mTok['token_id'] ?>')">📋</button>
</td> </td>
<td> <td>
<div style="display:flex; gap:0.3rem;">
<?php if (!$mTok['revoked']): ?> <?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!');"> <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="action" value="revoke_core_token">
<input type="hidden" name="token_id" value="<?= $mTok['token_id'] ?>"> <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-warning">Widerrufen</button>
</form> </form>
<?php else: ?> <?php else: ?>
<span class="badge badge-down">WIDERUFEN</span> <span class="badge badge-down">WIDERUFEN</span>
<?php endif; ?> <?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">🗑️ Löschen</button>
</form>
</div>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -2103,8 +2211,10 @@ $baseUrl = $protocol . '://' . $host;
<th>Bezeichnung / Client</th> <th>Bezeichnung / Client</th>
<th>Umgebung</th> <th>Umgebung</th>
<th>Scopes</th> <th>Scopes</th>
<th>Token Key</th>
<th>Zuletzt Genutzt</th> <th>Zuletzt Genutzt</th>
<th>Status</th> <th>Status</th>
<th>Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -2112,6 +2222,8 @@ $baseUrl = $protocol . '://' . $host;
$subTokens = array_filter($coreAllTokens, fn($t) => $t['type'] === 'sub'); $subTokens = array_filter($coreAllTokens, fn($t) => $t['type'] === 'sub');
foreach ($subTokens as $sTok): foreach ($subTokens as $sTok):
$scopesArr = json_decode($sTok['scopes'], true) ?: []; $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> <tr>
<td><code><?= htmlspecialchars($sTok['token_id']) ?></code></td> <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> <span class="badge" style="background:rgba(255,255,255,0.06); font-size:0.75rem;"><?= htmlspecialchars($sc) ?></span>
<?php endforeach; ?> <?php endforeach; ?>
</td> </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><?= $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><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> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
@@ -2393,16 +2528,23 @@ $baseUrl = $protocol . '://' . $host;
// Bugtracker Table Filter JS // Bugtracker Table Filter JS
function filterBugtrackerTable() { function filterBugtrackerTable() {
const project = document.getElementById('btFilterProject').value;
const env = document.getElementById('btFilterEnv').value; const env = document.getElementById('btFilterEnv').value;
const type = document.getElementById('btFilterType').value; const type = document.getElementById('btFilterType').value;
const severity = document.getElementById('btFilterSeverity').value;
const status = document.getElementById('btFilterStatus').value; const status = document.getElementById('btFilterStatus').value;
const query = (document.getElementById('btFilterSearch').value || '').toLowerCase().trim();
document.querySelectorAll('#btTableBody .bt-row').forEach(row => { 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 matchEnv = (env === 'all' || row.getAttribute('data-env') === env);
const matchType = (type === 'all' || row.getAttribute('data-type') === type); 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 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 = ''; row.style.display = '';
} else { } else {
row.style.display = 'none'; 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 // Open Bugtracker Item Details & Timeline Modal
function openBugtrackerModal(itemId) { function openBugtrackerModal(itemId) {
const modal = document.getElementById('btDetailModal'); const modal = document.getElementById('btDetailModal');
@@ -2640,9 +2800,10 @@ $baseUrl = $protocol . '://' . $host;
document.getElementById('editExpiresAt').value = opt.getAttribute('data-exp') || ''; document.getElementById('editExpiresAt').value = opt.getAttribute('data-exp') || '';
} }
// Token Masking Toggle // Token Masking Toggle & Copy
function toggleTokenMask(tokId) { 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('••••')) { if (el.innerText.includes('••••')) {
el.innerText = el.getAttribute('data-full'); el.innerText = el.getAttribute('data-full');
} else { } else {
@@ -2651,10 +2812,18 @@ $baseUrl = $protocol . '://' . $host;
} }
function copyTokenValue(tokId) { 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'); const fullVal = el.getAttribute('data-full');
navigator.clipboard.writeText(fullVal); if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(fullVal).then(() => {
alert('Token in Zwischenablage kopiert!'); alert('Token in Zwischenablage kopiert!');
}).catch(() => {
prompt('Token kopieren:', fullVal);
});
} else {
prompt('Token kopieren:', fullVal);
}
} }
function generateOfflinePayload() { function generateOfflinePayload() {
@@ -2709,28 +2878,73 @@ $baseUrl = $protocol . '://' . $host;
</div> </div>
</div> </div>
<!-- Bugtracker Resolve Modal --> <!-- Bugtracker Edit 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 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:500px; background:#161c28; border:1px solid var(--border-glass);"> <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;"> <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> <h3 class="card-title" style="margin:0; color:#fff;">✏️ Item Bearbeiten</h3>
<button type="button" class="btn btn-sm btn-secondary" onclick="closeResolveModal()">✕</button> <button type="button" class="btn btn-sm btn-secondary" onclick="closeEditBugtrackerModal()">✕</button>
</div> </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"> <form method="POST" action="index.php#tab-bugtracker">
<input type="hidden" name="action" value="bt_resolve"> <input type="hidden" name="action" value="bt_update_item">
<input type="hidden" name="item_id" id="resolveItemId" value="0"> <input type="hidden" name="item_id" id="btEditItemId" value="0">
<div class="form-group" style="margin-bottom:1rem;">
<label class="form-label">Lösungs-Build / Version (z. B. v1.4.3)</label> <div class="form-grid">
<input type="text" name="resolved_in_build" class="form-input" required value="v1.4.3" placeholder="v1.4.3"> <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>
<div class="form-group" style="margin-bottom:1.25rem;"> <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-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 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> <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> <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;"> </div>
<button type="button" class="btn btn-secondary" onclick="closeResolveModal()">Abbrechen</button>
<button type="submit" class="btn">Als Gelöst Speichern</button> <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> </div>
</form> </form>
</div> </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! // Create / update Admin user: admin / Admin1337!
$adminUsername = 'admin'; $adminUsername = 'admin';
$adminPassword = 'Admin1337!'; $adminPassword = 'Admin1337!';
@@ -0,0 +1,55 @@
-- Migration 005: Bugtracker Push ID, Target Agent, Tags, and Extended Severities (idea, wishlist)
-- Non-destructive migration
SET FOREIGN_KEY_CHECKS = 0;
-- 1. Extend ENUM severity column if needed or modify column definition
ALTER TABLE bugtracker_items
MODIFY COLUMN severity ENUM('idea', 'wishlist', 'low', 'medium', 'high', 'critical') NOT NULL DEFAULT 'medium';
-- 2. Add push_id column if not exists
SET @exist_push_id := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'bugtracker_items'
AND COLUMN_NAME = 'push_id'
);
SET @sql_push_id := IF(@exist_push_id = 0, 'ALTER TABLE bugtracker_items ADD COLUMN push_id VARCHAR(128) NULL AFTER occurrence_count, ADD KEY ix_bt_push_id (push_id);', 'SELECT 1;');
PREPARE stmt FROM @sql_push_id;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 3. Add target_agent column if not exists
SET @exist_target_agent := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'bugtracker_items'
AND COLUMN_NAME = 'target_agent'
);
SET @sql_target_agent := IF(@exist_target_agent = 0, 'ALTER TABLE bugtracker_items ADD COLUMN target_agent VARCHAR(100) NULL AFTER push_id;', 'SELECT 1;');
PREPARE stmt FROM @sql_target_agent;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 4. Add tags column if not exists
SET @exist_tags := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'bugtracker_items'
AND COLUMN_NAME = 'tags'
);
SET @sql_tags := IF(@exist_tags = 0, 'ALTER TABLE bugtracker_items ADD COLUMN tags VARCHAR(255) NULL AFTER target_agent;', 'SELECT 1;');
PREPARE stmt FROM @sql_tags;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET FOREIGN_KEY_CHECKS = 1;
-- Seed Sample Feature Idea
INSERT IGNORE INTO bugtracker_items (
id, project_slug, type, title, description, error_message, stack_trace, error_hash, build_version, environment, severity, status, occurrence_count, push_id, target_agent, tags, created_by
) VALUES
(4, 'myapp', 'feature_request', 'Automatische Datenbank-Backups vor Deployments', 'Idee für später: Vor jedem FTP-Deployment automatisch einen Mysqldump ausführen und im Server-Archiv ablegen.', NULL, NULL, NULL, 'v1.6.0-idea', 'development', 'idea', 'open', 1, 'push_notify_99182', 'agent:dev-monitor-01', 'database,automation', 'user:richard');
+6 -2
View File
@@ -213,9 +213,12 @@ CREATE TABLE IF NOT EXISTS bugtracker_items (
error_hash VARCHAR(64) NULL, error_hash VARCHAR(64) NULL,
build_version VARCHAR(64) NULL, build_version VARCHAR(64) NULL,
environment ENUM('production', 'development', 'staging', 'testing') NOT NULL DEFAULT 'production', environment ENUM('production', 'development', 'staging', 'testing') NOT NULL DEFAULT 'production',
severity ENUM('low', 'medium', 'high', 'critical') NOT NULL DEFAULT 'medium', severity ENUM('idea', 'wishlist', 'low', 'medium', 'high', 'critical') NOT NULL DEFAULT 'medium',
status ENUM('open', 'planned', 'in_progress', 'resolved', 'closed', 'rejected') NOT NULL DEFAULT 'open', status ENUM('open', 'planned', 'in_progress', 'resolved', 'closed', 'rejected') NOT NULL DEFAULT 'open',
occurrence_count INT NOT NULL DEFAULT 1, occurrence_count INT NOT NULL DEFAULT 1,
push_id VARCHAR(128) NULL,
target_agent VARCHAR(100) NULL,
tags VARCHAR(255) NULL,
first_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, first_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, last_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME NULL, resolved_at DATETIME NULL,
@@ -225,7 +228,8 @@ CREATE TABLE IF NOT EXISTS bugtracker_items (
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY ix_bt_proj_env (project_slug, environment, type, status), KEY ix_bt_proj_env (project_slug, environment, type, status),
KEY ix_bt_hash (error_hash) KEY ix_bt_hash (error_hash),
KEY ix_bt_push_id (push_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS bugtracker_comments ( CREATE TABLE IF NOT EXISTS bugtracker_comments (
+9
View File
@@ -206,6 +206,15 @@ class TokenManager
return $stmt->execute([':id' => $tokenId]); return $stmt->execute([':id' => $tokenId]);
} }
/**
* Permanently delete a Token (Master or Sub). If Master, child Sub-Tokens are deleted via cascade.
*/
public function deleteToken(string $tokenId): bool
{
$stmt = $this->db->prepare('DELETE FROM dc_tokens WHERE token_id = :id OR parent_token_id = :id');
return $stmt->execute([':id' => $tokenId]);
}
/** /**
* Get all Master Tokens with child count. * Get all Master Tokens with child count.
*/ */
+119 -14
View File
@@ -14,7 +14,7 @@ class BugRepo
} }
/** /**
* Ingest a Bug or Feature Request. Automates error-hash deduplication for bugs. * Ingest a Bug or Feature Request / Idea. Automates error-hash deduplication for bugs.
*/ */
public function reportItem(array $data): array public function reportItem(array $data): array
{ {
@@ -28,9 +28,12 @@ class BugRepo
$environment = in_array($data['environment'] ?? '', ['production', 'development', 'staging', 'testing']) $environment = in_array($data['environment'] ?? '', ['production', 'development', 'staging', 'testing'])
? $data['environment'] ? $data['environment']
: 'production'; : 'production';
$severity = in_array($data['severity'] ?? '', ['low', 'medium', 'high', 'critical']) $severity = in_array($data['severity'] ?? '', ['idea', 'wishlist', 'low', 'medium', 'high', 'critical'])
? $data['severity'] ? $data['severity']
: 'medium'; : ($type === 'feature_request' ? 'medium' : 'medium');
$pushId = !empty($data['push_id']) ? trim($data['push_id']) : null;
$targetAgent = !empty($data['target_agent']) ? trim($data['target_agent']) : null;
$tags = !empty($data['tags']) ? trim($data['tags']) : null;
$createdBy = !empty($data['created_by']) ? trim($data['created_by']) : 'agent'; $createdBy = !empty($data['created_by']) ? trim($data['created_by']) : 'agent';
// Find associated project_id from dc_projects // Find associated project_id from dc_projects
@@ -59,10 +62,16 @@ class BugRepo
$newCount = (int)$existing['occurrence_count'] + 1; $newCount = (int)$existing['occurrence_count'] + 1;
$upd = $this->db->prepare(' $upd = $this->db->prepare('
UPDATE bugtracker_items UPDATE bugtracker_items
SET occurrence_count = :count, last_seen_at = NOW() SET occurrence_count = :count,
last_seen_at = NOW(),
push_id = COALESCE(:push_id, push_id)
WHERE id = :id WHERE id = :id
'); ');
$upd->execute([':count' => $newCount, ':id' => $existing['id']]); $upd->execute([
':count' => $newCount,
':push_id' => $pushId,
':id' => $existing['id'],
]);
return [ return [
'id' => (int)$existing['id'], 'id' => (int)$existing['id'],
@@ -71,6 +80,7 @@ class BugRepo
'error_hash' => $errorHash, 'error_hash' => $errorHash,
'type' => $type, 'type' => $type,
'environment' => $environment, 'environment' => $environment,
'push_id' => $pushId,
]; ];
} }
} }
@@ -80,11 +90,13 @@ class BugRepo
project_id, project_slug, type, title, description, project_id, project_slug, type, title, description,
error_message, stack_trace, error_hash, build_version, error_message, stack_trace, error_hash, build_version,
environment, severity, status, occurrence_count, environment, severity, status, occurrence_count,
push_id, target_agent, tags,
first_seen_at, last_seen_at, created_by, created_at first_seen_at, last_seen_at, created_by, created_at
) VALUES ( ) VALUES (
:pid, :slug, :type, :title, :desc, :pid, :slug, :type, :title, :desc,
:err, :trace, :hash, :build, :err, :trace, :hash, :build,
:env, :sev, "open", 1, :env, :sev, "open", 1,
:push_id, :target_agent, :tags,
NOW(), NOW(), :created_by, NOW() NOW(), NOW(), :created_by, NOW()
) )
'); ');
@@ -101,18 +113,20 @@ class BugRepo
':build' => $buildVersion, ':build' => $buildVersion,
':env' => $environment, ':env' => $environment,
':sev' => $severity, ':sev' => $severity,
':push_id' => $pushId,
':target_agent' => $targetAgent,
':tags' => $tags,
':created_by' => $createdBy, ':created_by' => $createdBy,
]); ]);
$newItemId = (int)$this->db->lastInsertId(); $newItemId = (int)$this->db->lastInsertId();
// Initial comment log // Initial comment log
$this->addComment( $initialNote = ($type === 'bug')
$newItemId, ? 'Bug in System erfasst.'
$createdBy, : ($severity === 'idea' ? '💡 Neue Idee / Gedanke hinterlegt.' : 'Feature-Request eingereicht.');
$type === 'bug' ? 'Bug in System erfasst.' : 'Feature-Request eingereicht.',
'reported' $this->addComment($newItemId, $createdBy, $initialNote, 'reported');
);
return [ return [
'id' => $newItemId, 'id' => $newItemId,
@@ -121,11 +135,12 @@ class BugRepo
'error_hash' => $errorHash, 'error_hash' => $errorHash,
'type' => $type, 'type' => $type,
'environment' => $environment, 'environment' => $environment,
'push_id' => $pushId,
]; ];
} }
/** /**
* Get filtered list of Bugs & Feature Requests. * Get filtered list of Bugs, Features, & Ideas.
*/ */
public function getItems(array $filters = []): array public function getItems(array $filters = []): array
{ {
@@ -157,8 +172,18 @@ class BugRepo
$params[':severity'] = $filters['severity']; $params[':severity'] = $filters['severity'];
} }
if (!empty($filters['push_id'])) {
$where[] = 'push_id = :push_id';
$params[':push_id'] = trim($filters['push_id']);
}
if (!empty($filters['target_agent'])) {
$where[] = 'target_agent = :agent';
$params[':agent'] = trim($filters['target_agent']);
}
if (!empty($filters['search'])) { if (!empty($filters['search'])) {
$where[] = '(title LIKE :q OR description LIKE :q OR error_message LIKE :q)'; $where[] = '(title LIKE :q OR description LIKE :q OR error_message LIKE :q OR push_id LIKE :q OR target_agent LIKE :q OR tags LIKE :q)';
$params[':q'] = '%' . trim($filters['search']) . '%'; $params[':q'] = '%' . trim($filters['search']) . '%';
} }
@@ -248,6 +273,83 @@ class BugRepo
return $result; return $result;
} }
/**
* Comprehensive Item Edit (Status, Severity, Push ID, Target Agent, Tags, Notes).
*/
public function updateItemDetails(int $itemId, array $updates, string $author = 'agent'): bool
{
$existing = $this->getItemDetails($itemId);
if (!$existing) {
return false;
}
$fields = [];
$params = [':id' => $itemId];
$changes = [];
if (isset($updates['status']) && in_array($updates['status'], ['open', 'planned', 'in_progress', 'resolved', 'closed', 'rejected'])) {
$fields[] = 'status = :status';
$params[':status'] = $updates['status'];
if ($existing['status'] !== $updates['status']) {
$changes[] = "Status: {$existing['status']}{$updates['status']}";
}
}
if (isset($updates['severity']) && in_array($updates['severity'], ['idea', 'wishlist', 'low', 'medium', 'high', 'critical'])) {
$fields[] = 'severity = :severity';
$params[':severity'] = $updates['severity'];
if ($existing['severity'] !== $updates['severity']) {
$changes[] = "Schweregrad: {$existing['severity']}{$updates['severity']}";
}
}
if (array_key_exists('push_id', $updates)) {
$fields[] = 'push_id = :push_id';
$params[':push_id'] = !empty($updates['push_id']) ? trim($updates['push_id']) : null;
if ($existing['push_id'] !== $updates['push_id']) {
$changes[] = "Push-ID hinterlegt: {$updates['push_id']}";
}
}
if (array_key_exists('target_agent', $updates)) {
$fields[] = 'target_agent = :target_agent';
$params[':target_agent'] = !empty($updates['target_agent']) ? trim($updates['target_agent']) : null;
if ($existing['target_agent'] !== $updates['target_agent']) {
$changes[] = "Ziel-Agent: {$updates['target_agent']}";
}
}
if (array_key_exists('tags', $updates)) {
$fields[] = 'tags = :tags';
$params[':tags'] = !empty($updates['tags']) ? trim($updates['tags']) : null;
}
if (array_key_exists('resolved_in_build', $updates)) {
$fields[] = 'resolved_in_build = :build';
$params[':build'] = !empty($updates['resolved_in_build']) ? trim($updates['resolved_in_build']) : null;
}
if (array_key_exists('resolution_notes', $updates)) {
$fields[] = 'resolution_notes = :notes';
$params[':notes'] = !empty($updates['resolution_notes']) ? trim($updates['resolution_notes']) : null;
}
if (empty($fields)) {
return true;
}
$sql = 'UPDATE bugtracker_items SET ' . implode(', ', $fields) . ' WHERE id = :id';
$stmt = $this->db->prepare($sql);
$result = $stmt->execute($params);
if ($result && !empty($changes)) {
$changeMsg = 'Item aktualisiert: ' . implode(' | ', $changes);
$this->addComment($itemId, $author, $changeMsg, 'item_updated');
}
return $result;
}
/** /**
* Resolve a Bug or complete a Feature Request with build details. * Resolve a Bug or complete a Feature Request with build details.
*/ */
@@ -288,6 +390,7 @@ class BugRepo
'open_bugs_prod' => 0, 'open_bugs_prod' => 0,
'open_bugs_dev' => 0, 'open_bugs_dev' => 0,
'open_features' => 0, 'open_features' => 0,
'ideas_count' => 0,
'resolved_total' => 0, 'resolved_total' => 0,
'critical_bugs' => 0, 'critical_bugs' => 0,
]; ];
@@ -296,7 +399,8 @@ class BugRepo
SELECT SELECT
SUM(CASE WHEN type = "bug" AND environment = "production" AND status IN ("open", "in_progress") THEN 1 ELSE 0 END) as open_bugs_prod, SUM(CASE WHEN type = "bug" AND environment = "production" AND status IN ("open", "in_progress") THEN 1 ELSE 0 END) as open_bugs_prod,
SUM(CASE WHEN type = "bug" AND environment = "development" AND status IN ("open", "in_progress") THEN 1 ELSE 0 END) as open_bugs_dev, SUM(CASE WHEN type = "bug" AND environment = "development" AND status IN ("open", "in_progress") THEN 1 ELSE 0 END) as open_bugs_dev,
SUM(CASE WHEN type = "feature_request" AND status IN ("open", "planned", "in_progress") THEN 1 ELSE 0 END) as open_features, SUM(CASE WHEN type = "feature_request" AND severity NOT IN ("idea") AND status IN ("open", "planned", "in_progress") THEN 1 ELSE 0 END) as open_features,
SUM(CASE WHEN severity = "idea" AND status IN ("open", "planned") THEN 1 ELSE 0 END) as ideas_count,
SUM(CASE WHEN status = "resolved" THEN 1 ELSE 0 END) as resolved_total, SUM(CASE WHEN status = "resolved" THEN 1 ELSE 0 END) as resolved_total,
SUM(CASE WHEN type = "bug" AND severity = "critical" AND status IN ("open", "in_progress") THEN 1 ELSE 0 END) as critical_bugs SUM(CASE WHEN type = "bug" AND severity = "critical" AND status IN ("open", "in_progress") THEN 1 ELSE 0 END) as critical_bugs
FROM bugtracker_items FROM bugtracker_items
@@ -306,6 +410,7 @@ class BugRepo
$stats['open_bugs_prod'] = (int)($res['open_bugs_prod'] ?? 0); $stats['open_bugs_prod'] = (int)($res['open_bugs_prod'] ?? 0);
$stats['open_bugs_dev'] = (int)($res['open_bugs_dev'] ?? 0); $stats['open_bugs_dev'] = (int)($res['open_bugs_dev'] ?? 0);
$stats['open_features'] = (int)($res['open_features'] ?? 0); $stats['open_features'] = (int)($res['open_features'] ?? 0);
$stats['ideas_count'] = (int)($res['ideas_count'] ?? 0);
$stats['resolved_total'] = (int)($res['resolved_total'] ?? 0); $stats['resolved_total'] = (int)($res['resolved_total'] ?? 0);
$stats['critical_bugs'] = (int)($res['critical_bugs'] ?? 0); $stats['critical_bugs'] = (int)($res['critical_bugs'] ?? 0);
} }