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
+155 -97
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
> **Zweck**: Anleitung zur automatisierten Registrierung, Fehlererfassung, Feature-Einreichung und Behebung über das Deployment Center.
This guide defines the standardized protocol and API specifications for autonomous AI Developer Agents interacting with the **Deployment Center Bugtracker & Token Provisioning System**.
---
## 1. Token-Architektur & Autonome Sub-Token-Erstellung
## 📌 Executive Overview for AI Agents
Das Deployment Center nutzt eine hierarchische Token-Struktur:
- **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.
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.
### Sub-Token Anfordern (`POST /api/tokens/v1/provision`)
Sendet einen HTTP POST Request mit dem Master-Token im `X-Master-Token` Header.
### Core Capabilities:
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",
"instance_id": "DEV-WORKSTATION-01",
"parent_token": "dc_master_myapp_dev_agent_001",
"name": "Codebase Refactoring Agent Token",
"environment": "development",
"scopes": ["bugtracker:report", "bugtracker:manage"],
"environment": "development"
"expires_in_hours": 24
}
```
#### JSON Antwort:
#### Response:
```json
{
"status": "success",
"sub_token": "dc_sub_5f8a2c1...",
"token_id": "tok_s_89a1b2c3",
"name": "Dev Workstation Agent #4",
"token_id": "tok_s_8912ab",
"raw_token": "dc_sub_myapp_refactor_agent_991",
"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
{
"project_slug": "myapp",
"type": "bug", // "bug" oder "feature_request"
"environment": "development", // "production", "development", "staging", "testing"
"severity": "high", // "low", "medium", "high", "critical"
"type": "bug",
"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.",
"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",
"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:
- **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.
### B. Submitting a Feature Request or Quick Reminder Idea (`severity: "idea"`)
```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`)
Filter-Parameter: `environment` (`development`/`production`), `type` (`bug`/`feature_request`), `status` (`open`/`in_progress`/`resolved`).
### A. Fetching Open Items Assigned to an Agent
```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`)
### 3.3 Ermittlungsschritt / Kommentar Hinzufügen (`POST /api/bugtracker/v1/manage/index.php?action=comment&id=123`)
### B. Updating Status & Details (`POST ?action=update`)
```json
{
"comment": "Log-Analyse gestartet. Der Fehler tritt auf, wenn $_SESSION['dc_user_id'] nicht gesetzt ist.",
"author": "agent:code-fixer-01",
"action_taken": "investigated"
"id": 4,
"status": "in_progress",
"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
{
"resolved_in_build": "v1.4.3",
"resolution_notes": "Null-Check für Session-Variable hinzugefügt.",
"id": 4,
"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"
}
```
---
## 4. C# (.NET 8) Implementierungsbeispiel für KI-Agenten
## 💻 4. Code Implementation Examples for Agents
```csharp
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
### Python Example: Automatic Error Reporter Decorator
```python
import requests
import traceback
import sys
public class DeploymentCenterBugtrackerClient
{
private static readonly HttpClient HttpClient = new HttpClient();
private const string BaseUrl = "https://dc.mhdf.de";
DC_API_URL = "https://dc.mhdf.de/api/bugtracker/v1/report.php"
AGENT_TOKEN = "dc_sub_myapp_agent_live_001"
public static async Task IngestDevBugAsync(string masterToken, string title, string errorMessage, string stackTrace)
{
// 1. Sub-Token anfordern
var provisionReq = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/api/tokens/v1/provision")
{
Content = new StringContent(JsonSerializer.Serialize(new
{
client_name = "Dev Agent Client",
scopes = new[] { "bugtracker:report" },
environment = "development"
}), Encoding.UTF8, "application/json")
};
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}");
def report_exception_to_dc(project_slug: str, exc: Exception, env: str = "production", push_id: str = None):
payload = {
"project_slug": project_slug,
"type": "bug",
"title": f"{type(exc).__name__}: {str(exc)}",
"error_message": str(exc),
"stack_trace": traceback.format_exc(),
"build_version": "v1.4.2",
"environment": env,
"severity": "high",
"push_id": push_id,
"created_by": "agent:python-runner"
}
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;
}
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);
+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
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>
<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>
<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-danger">Master & Subs Widerrufen</button>
<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">🗑️ Löschen</button>
</form>
</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);
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" 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>
<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 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>
<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!';
@@ -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,
build_version VARCHAR(64) NULL,
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',
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,
last_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME NULL,
@@ -225,7 +228,8 @@ CREATE TABLE IF NOT EXISTS bugtracker_items (
created_at DATETIME NOT NULL DEFAULT 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_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;
CREATE TABLE IF NOT EXISTS bugtracker_comments (
+9
View File
@@ -206,6 +206,15 @@ class TokenManager
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.
*/
+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
{
@@ -28,9 +28,12 @@ class BugRepo
$environment = in_array($data['environment'] ?? '', ['production', 'development', 'staging', 'testing'])
? $data['environment']
: 'production';
$severity = in_array($data['severity'] ?? '', ['low', 'medium', 'high', 'critical'])
$severity = in_array($data['severity'] ?? '', ['idea', 'wishlist', 'low', 'medium', 'high', 'critical'])
? $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';
// Find associated project_id from dc_projects
@@ -59,10 +62,16 @@ class BugRepo
$newCount = (int)$existing['occurrence_count'] + 1;
$upd = $this->db->prepare('
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
');
$upd->execute([':count' => $newCount, ':id' => $existing['id']]);
$upd->execute([
':count' => $newCount,
':push_id' => $pushId,
':id' => $existing['id'],
]);
return [
'id' => (int)$existing['id'],
@@ -71,6 +80,7 @@ class BugRepo
'error_hash' => $errorHash,
'type' => $type,
'environment' => $environment,
'push_id' => $pushId,
];
}
}
@@ -80,11 +90,13 @@ class BugRepo
project_id, project_slug, type, title, description,
error_message, stack_trace, error_hash, build_version,
environment, severity, status, occurrence_count,
push_id, target_agent, tags,
first_seen_at, last_seen_at, created_by, created_at
) VALUES (
:pid, :slug, :type, :title, :desc,
:err, :trace, :hash, :build,
:env, :sev, "open", 1,
:push_id, :target_agent, :tags,
NOW(), NOW(), :created_by, NOW()
)
');
@@ -101,18 +113,20 @@ class BugRepo
':build' => $buildVersion,
':env' => $environment,
':sev' => $severity,
':push_id' => $pushId,
':target_agent' => $targetAgent,
':tags' => $tags,
':created_by' => $createdBy,
]);
$newItemId = (int)$this->db->lastInsertId();
// Initial comment log
$this->addComment(
$newItemId,
$createdBy,
$type === 'bug' ? 'Bug in System erfasst.' : 'Feature-Request eingereicht.',
'reported'
);
$initialNote = ($type === 'bug')
? 'Bug in System erfasst.'
: ($severity === 'idea' ? '💡 Neue Idee / Gedanke hinterlegt.' : 'Feature-Request eingereicht.');
$this->addComment($newItemId, $createdBy, $initialNote, 'reported');
return [
'id' => $newItemId,
@@ -121,11 +135,12 @@ class BugRepo
'error_hash' => $errorHash,
'type' => $type,
'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
{
@@ -157,8 +172,18 @@ class BugRepo
$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'])) {
$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']) . '%';
}
@@ -248,6 +273,83 @@ class BugRepo
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.
*/
@@ -288,6 +390,7 @@ class BugRepo
'open_bugs_prod' => 0,
'open_bugs_dev' => 0,
'open_features' => 0,
'ideas_count' => 0,
'resolved_total' => 0,
'critical_bugs' => 0,
];
@@ -296,7 +399,8 @@ class BugRepo
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 = "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 type = "bug" AND severity = "critical" AND status IN ("open", "in_progress") THEN 1 ELSE 0 END) as critical_bugs
FROM bugtracker_items
@@ -306,6 +410,7 @@ class BugRepo
$stats['open_bugs_prod'] = (int)($res['open_bugs_prod'] ?? 0);
$stats['open_bugs_dev'] = (int)($res['open_bugs_dev'] ?? 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['critical_bugs'] = (int)($res['critical_bugs'] ?? 0);
}