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:
@@ -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.
|
||||
|
||||
@@ -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'] ?? '',
|
||||
];
|
||||
|
||||
|
||||
@@ -71,8 +71,9 @@ try {
|
||||
'error_hash' => $result['error_hash'],
|
||||
'type' => $result['type'],
|
||||
'environment' => $result['environment'],
|
||||
'push_id' => $result['push_id'] ?? null,
|
||||
'message' => $result['is_new']
|
||||
? ($result['type'] === 'bug' ? 'New bug reported successfully.' : 'New feature request submitted.')
|
||||
? ($result['type'] === 'bug' ? 'New bug reported successfully.' : 'New feature request / idea submitted.')
|
||||
: 'Recurring bug count updated.',
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
|
||||
+264
-50
@@ -437,6 +437,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'delete_core_token') {
|
||||
$tokenId = trim($_POST['token_id'] ?? '');
|
||||
if ($tokenId) {
|
||||
$coreTokenMgr = new CoreTokenManager($pdo);
|
||||
$coreTokenMgr->deleteToken($tokenId);
|
||||
$msg = "Token '{$tokenId}' und abgeleitete Sub-Tokens wurden dauerhaft gelöscht.";
|
||||
}
|
||||
}
|
||||
|
||||
// Bugtracker & Feature-Tracker Actions
|
||||
if ($action === 'bt_add_comment') {
|
||||
$itemId = (int)($_POST['item_id'] ?? 0);
|
||||
@@ -474,6 +483,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$build = trim($_POST['build_version'] ?? 'v1.0.0');
|
||||
$env = $_POST['environment'] ?? 'production';
|
||||
$sev = $_POST['severity'] ?? 'medium';
|
||||
$pushId = trim($_POST['push_id'] ?? '');
|
||||
$agent = trim($_POST['target_agent'] ?? '');
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
$createdBy = trim($_POST['created_by'] ?? $_SESSION['dc_username'] ?? 'admin');
|
||||
|
||||
if ($title) {
|
||||
@@ -488,6 +500,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
'build_version' => $build,
|
||||
'environment' => $env,
|
||||
'severity' => $sev,
|
||||
'push_id' => $pushId,
|
||||
'target_agent' => $agent,
|
||||
'tags' => $tags,
|
||||
'created_by' => $createdBy,
|
||||
]);
|
||||
$msg = $res['is_new']
|
||||
@@ -508,6 +523,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$msg = "Status für Item #{$itemId} geändert auf {$status}.";
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'bt_update_item') {
|
||||
$itemId = (int)($_POST['item_id'] ?? 0);
|
||||
$status = $_POST['status'] ?? 'open';
|
||||
$sev = $_POST['severity'] ?? 'medium';
|
||||
$pushId = trim($_POST['push_id'] ?? '');
|
||||
$agent = trim($_POST['target_agent'] ?? '');
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
$notes = trim($_POST['resolution_notes'] ?? '');
|
||||
$build = trim($_POST['resolved_in_build'] ?? '');
|
||||
$author = trim($_POST['author'] ?? $_SESSION['dc_username'] ?? 'admin');
|
||||
|
||||
if ($itemId > 0) {
|
||||
$bugRepo = new BugRepo($pdo);
|
||||
$bugRepo->updateItemDetails($itemId, [
|
||||
'status' => $status,
|
||||
'severity' => $sev,
|
||||
'push_id' => $pushId,
|
||||
'target_agent' => $agent,
|
||||
'tags' => $tags,
|
||||
'resolution_notes' => $notes,
|
||||
'resolved_in_build' => $build,
|
||||
], $author);
|
||||
$msg = "Item #{$itemId} wurde erfolgreich aktualisiert.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch All Data
|
||||
@@ -1838,7 +1879,7 @@ $baseUrl = $protocol . '://' . $host;
|
||||
<div id="tab-bugtracker" class="tab-content">
|
||||
|
||||
<div id="sub-bugtracker-items" class="subtab-content active">
|
||||
<div class="stats-grid">
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">Dev Bugs (Entwicklung)</div>
|
||||
<div class="stat-value" style="color:#5b9dff;"><?= $bugtrackerStats['open_bugs_dev'] ?></div>
|
||||
@@ -1851,6 +1892,10 @@ $baseUrl = $protocol . '://' . $host;
|
||||
<div class="stat-header">Offene Feature Requests</div>
|
||||
<div class="stat-value" style="color:var(--warning);"><?= $bugtrackerStats['open_features'] ?></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">💡 Ideen & Gedanken</div>
|
||||
<div class="stat-value" style="color:#e082ff;"><?= $bugtrackerStats['ideas_count'] ?></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">Gelöst / Umgesetzt</div>
|
||||
<div class="stat-value" style="color:var(--success);"><?= $bugtrackerStats['resolved_total'] ?></div>
|
||||
@@ -1858,26 +1903,46 @@ $baseUrl = $protocol . '://' . $host;
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h2 class="card-title">🐛 Bugs & Feature Requests</h2>
|
||||
<div style="display:flex; gap:0.5rem;">
|
||||
<div class="card-header" style="display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; gap:0.75rem;">
|
||||
<h2 class="card-title" style="margin:0;">🐛 Bugs, Features & Ideen</h2>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:0.4rem; align-items:center;">
|
||||
<select id="btFilterProject" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="filterBugtrackerTable()">
|
||||
<option value="all">🌐 Alle Projekte</option>
|
||||
<?php foreach ($projects as $p): ?>
|
||||
<option value="<?= htmlspecialchars($p['slug']) ?>"><?= htmlspecialchars($p['name']) ?> (<?= htmlspecialchars($p['slug']) ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select id="btFilterEnv" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="filterBugtrackerTable()">
|
||||
<option value="all">🌐 Alle Umgebungen</option>
|
||||
<option value="all">🌍 Alle Umgebungen</option>
|
||||
<option value="production">🔴 Produktion (Prod)</option>
|
||||
<option value="development">🔵 Entwicklung (Dev)</option>
|
||||
<option value="staging">🟡 Staging</option>
|
||||
<option value="testing">🧪 Testing</option>
|
||||
</select>
|
||||
<select id="btFilterType" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="filterBugtrackerTable()">
|
||||
<option value="all">📂 Alle Typen</option>
|
||||
<option value="bug">🐛 Nur Bugs</option>
|
||||
<option value="feature_request">💡 Nur Feature Requests</option>
|
||||
</select>
|
||||
<select id="btFilterSeverity" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="filterBugtrackerTable()">
|
||||
<option value="all">🔥 Alle Schweregrade</option>
|
||||
<option value="idea">💡 Nur Ideen & Gedanken</option>
|
||||
<option value="wishlist">⭐ Nur Wunschliste</option>
|
||||
<option value="low">Niedrig</option>
|
||||
<option value="medium">Mittel</option>
|
||||
<option value="high">Hoch</option>
|
||||
<option value="critical">🔥 Kritisch</option>
|
||||
</select>
|
||||
<select id="btFilterStatus" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="filterBugtrackerTable()">
|
||||
<option value="all">📌 Alle Status</option>
|
||||
<option value="open">Offen</option>
|
||||
<option value="planned">Geplant</option>
|
||||
<option value="in_progress">In Bearbeitung</option>
|
||||
<option value="resolved">Gelöst / Umgesetzt</option>
|
||||
<option value="closed">Geschlossen</option>
|
||||
<option value="rejected">Abgelehnt</option>
|
||||
</select>
|
||||
<input type="text" id="btFilterSearch" class="form-input" style="width:160px; padding:0.3rem 0.6rem; font-size:0.8rem;" placeholder="🔍 Suche..." onkeyup="filterBugtrackerTable()">
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
@@ -1908,6 +1973,7 @@ $baseUrl = $protocol . '://' . $host;
|
||||
|
||||
$statusBadge = match($item['status']) {
|
||||
'open' => '<span class="badge badge-down">OFFEN</span>',
|
||||
'planned' => '<span class="badge" style="background:rgba(255,165,0,0.2); color:orange; border:1px solid orange;">GEPLANT</span>',
|
||||
'in_progress' => '<span class="badge badge-warning">IN BEARBEITUNG</span>',
|
||||
'resolved' => '<span class="badge badge-up">GELÖST / UMGESETZT</span>',
|
||||
'rejected' => '<span class="badge badge-stopped">ABGELEHNT</span>',
|
||||
@@ -1915,22 +1981,38 @@ $baseUrl = $protocol . '://' . $host;
|
||||
};
|
||||
|
||||
$sevBadge = match($item['severity']) {
|
||||
'idea' => '<span class="badge" style="background:rgba(215,100,255,0.25); color:#e082ff; border:1px solid #e082ff;">💡 IDEE</span>',
|
||||
'wishlist' => '<span class="badge" style="background:rgba(0,210,255,0.25); color:#5ce1e6; border:1px solid #5ce1e6;">⭐ WUNSCHLISTE</span>',
|
||||
'critical' => '<span class="badge badge-down" style="font-weight:bold;">🔥 KRITISCH</span>',
|
||||
'high' => '<span class="badge badge-warning">HOCH</span>',
|
||||
'medium' => '<span class="badge badge-stopped">MITTEL</span>',
|
||||
default => '<span class="badge badge-stopped">NIEDRIG</span>'
|
||||
};
|
||||
|
||||
$jsonItem = htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8');
|
||||
?>
|
||||
<tr class="bt-row"
|
||||
data-project="<?= htmlspecialchars($item['project_slug']) ?>"
|
||||
data-env="<?= htmlspecialchars($item['environment']) ?>"
|
||||
data-type="<?= htmlspecialchars($item['type']) ?>"
|
||||
data-severity="<?= htmlspecialchars($item['severity']) ?>"
|
||||
data-status="<?= htmlspecialchars($item['status']) ?>">
|
||||
<td>#<?= $item['id'] ?><br><?= $typeBadge ?></td>
|
||||
<td><?= $envBadge ?></td>
|
||||
<td>
|
||||
<strong style="color:#fff;"><?= htmlspecialchars($item['title']) ?></strong>
|
||||
<div style="font-size:0.75rem; color:var(--text-muted);">
|
||||
Projekt: <code><?= htmlspecialchars($item['project_slug']) ?></code> | Von: <?= htmlspecialchars($item['created_by']) ?>
|
||||
<div style="font-size:0.75rem; color:var(--text-muted); margin-top:0.2rem; display:flex; flex-wrap:wrap; gap:0.4rem; align-items:center;">
|
||||
<span>Projekt: <code><?= htmlspecialchars($item['project_slug']) ?></code></span>
|
||||
<span>Von: <?= htmlspecialchars($item['created_by']) ?></span>
|
||||
<?php if (!empty($item['push_id'])): ?>
|
||||
<span class="badge" style="background:rgba(255,255,255,0.06); font-family:monospace; font-size:0.7rem;">📲 Push: <?= htmlspecialchars($item['push_id']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($item['target_agent'])): ?>
|
||||
<span class="badge" style="background:rgba(0,200,100,0.15); color:#66bb6a; font-size:0.7rem;">🤖 <?= htmlspecialchars($item['target_agent']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($item['tags'])): ?>
|
||||
<span class="badge" style="background:rgba(100,180,255,0.15); color:#90caf9; font-size:0.7rem;">🏷️ <?= htmlspecialchars($item['tags']) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><code><?= htmlspecialchars($item['build_version'] ?? 'v1.0.0') ?></code></td>
|
||||
@@ -1942,10 +2024,13 @@ $baseUrl = $protocol . '://' . $host;
|
||||
</td>
|
||||
<td><?= $statusBadge ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="openBugtrackerModal(<?= $item['id'] ?>)">🔍 Details & Timeline</button>
|
||||
<?php if ($item['status'] !== 'resolved'): ?>
|
||||
<button type="button" class="btn btn-sm" onclick="openResolveModal(<?= $item['id'] ?>, '<?= htmlspecialchars(addslashes($item['title'])) ?>')">✔ Gelöst</button>
|
||||
<?php endif; ?>
|
||||
<div style="display:flex; gap:0.3rem;">
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="openEditBugtrackerModal(<?= $jsonItem ?>)">✏️ Edit</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="openBugtrackerModal(<?= $item['id'] ?>)">🔍 Details</button>
|
||||
<?php if ($item['status'] !== 'resolved'): ?>
|
||||
<button type="button" class="btn btn-sm" onclick="openResolveModal(<?= $item['id'] ?>, '<?= htmlspecialchars(addslashes($item['title'])) ?>')">✔ Gelöst</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@@ -1957,7 +2042,7 @@ $baseUrl = $protocol . '://' . $host;
|
||||
<!-- Subtab: Item manuell anlegen -->
|
||||
<div id="sub-bugtracker-new" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">➕ Bug oder Feature Request Manuell Erfassen</h2></div>
|
||||
<div class="card-header"><h2 class="card-title">➕ Bug, Feature Request oder Idee Erfassen</h2></div>
|
||||
<form method="POST" action="index.php#tab-bugtracker">
|
||||
<input type="hidden" name="action" value="bt_create_item">
|
||||
<div class="form-grid">
|
||||
@@ -1973,7 +2058,7 @@ $baseUrl = $protocol . '://' . $host;
|
||||
<label class="form-label">Typ</label>
|
||||
<select name="type" class="form-input" required>
|
||||
<option value="bug" selected>🐛 Bug / Fehlerbericht</option>
|
||||
<option value="feature_request">💡 Feature Request / Vorschlag</option>
|
||||
<option value="feature_request">💡 Feature Request / Vorschlag / Idee</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -1988,6 +2073,8 @@ $baseUrl = $protocol . '://' . $host;
|
||||
<div class="form-group">
|
||||
<label class="form-label">Schweregrad / Priorität</label>
|
||||
<select name="severity" class="form-input" required>
|
||||
<option value="idea">💡 Idee / Gedanke für Später</option>
|
||||
<option value="wishlist">⭐ Wunschliste / Backlog</option>
|
||||
<option value="low">Niedrig</option>
|
||||
<option value="medium" selected>Mittel</option>
|
||||
<option value="high">Hoch</option>
|
||||
@@ -1995,9 +2082,23 @@ $baseUrl = $protocol . '://' . $host;
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid" style="margin-top:1rem;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Push-ID (Optional, z. B. Workflow / Notification Token)</label>
|
||||
<input type="text" name="push_id" class="form-input" placeholder="z. B. push_notify_88192">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ziel-Agent / Zuweisung (Optional)</label>
|
||||
<input type="text" name="target_agent" class="form-input" placeholder="z. B. agent:code-fixer-01">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tags / Kategorien (Kommagetrennt)</label>
|
||||
<input type="text" name="tags" class="form-input" placeholder="z. B. ui, security, database">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:1rem;">
|
||||
<label class="form-label">Titel / Zusammenfassung</label>
|
||||
<input type="text" name="title" class="form-input" required placeholder="z. B. NullReferenceException bei Order-Submit">
|
||||
<input type="text" name="title" class="form-input" required placeholder="z. B. NullReferenceException bei Order-Submit oder Idee für TOTP 2FA">
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:1rem;">
|
||||
<label class="form-label">Build / Version</label>
|
||||
@@ -2005,14 +2106,14 @@ $baseUrl = $protocol . '://' . $host;
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:1rem;">
|
||||
<label class="form-label">Detaillierte Beschreibung</label>
|
||||
<textarea name="description" class="form-input" rows="3" placeholder="Was ist passiert? Unter welchen Bedingungen?"></textarea>
|
||||
<textarea name="description" class="form-input" rows="3" placeholder="Was ist passiert? Unter welchen Bedingungen? Oder was soll umgesetzt werden?"></textarea>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:1rem;">
|
||||
<label class="form-label">Fehlermeldung / Exception Message</label>
|
||||
<label class="form-label">Fehlermeldung / Exception Message (Nur bei Bugs)</label>
|
||||
<textarea name="error_message" class="form-input" rows="2" placeholder="Exakte Fehlermeldung aus dem Log..."></textarea>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:1rem;">
|
||||
<label class="form-label">Stacktrace / Log Ausschnitt</label>
|
||||
<label class="form-label">Stacktrace / Log Ausschnitt (Nur bei Bugs)</label>
|
||||
<textarea name="stack_trace" class="form-input" rows="4" style="font-family:monospace;" placeholder="at MyApp.Core.Service.DoWork()..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn" style="margin-top:1rem;">Item Speichern & Anlegen</button>
|
||||
@@ -2068,22 +2169,29 @@ $baseUrl = $protocol . '://' . $host;
|
||||
<span class="badge badge-up"><?= (int)($mTok['sub_token_count'] ?? 0) ?> Sub-Tokens</span>
|
||||
</td>
|
||||
<td>
|
||||
<code id="tok-core-text-<?= $mTok['token_id'] ?>" data-full="<?= htmlspecialchars($rawVal) ?>" data-masked="<?= htmlspecialchars($masked) ?>">
|
||||
<code id="tok-core-<?= $mTok['token_id'] ?>" data-full="<?= htmlspecialchars($rawVal) ?>" data-masked="<?= htmlspecialchars($masked) ?>">
|
||||
<?= htmlspecialchars($masked) ?>
|
||||
</code>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleTokenMask('core-text-<?= $mTok['token_id'] ?>')">👁️</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="copyTokenValue('core-text-<?= $mTok['token_id'] ?>')">📋</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleTokenMask('tok-core-<?= $mTok['token_id'] ?>')">👁️</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="copyTokenValue('tok-core-<?= $mTok['token_id'] ?>')">📋</button>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!$mTok['revoked']): ?>
|
||||
<form method="POST" action="index.php#tab-tokens" style="display:inline" onsubmit="return confirm('Master-Token widerrufen? Alle zugehörigen Sub-Tokens werden sofort kaskadierend mit gesperrt!');">
|
||||
<input type="hidden" name="action" value="revoke_core_token">
|
||||
<div style="display:flex; gap:0.3rem;">
|
||||
<?php if (!$mTok['revoked']): ?>
|
||||
<form method="POST" action="index.php#tab-tokens" style="display:inline" onsubmit="return confirm('Master-Token widerrufen? Alle zugehörigen Sub-Tokens werden sofort kaskadierend mit gesperrt!');">
|
||||
<input type="hidden" name="action" value="revoke_core_token">
|
||||
<input type="hidden" name="token_id" value="<?= $mTok['token_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-warning">Widerrufen</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-down">WIDERUFEN</span>
|
||||
<?php endif; ?>
|
||||
<form method="POST" action="index.php#tab-tokens" style="display:inline" onsubmit="return confirm('Master-Token und alle Sub-Tokens DAUERHAFT LÖSCHEN?');">
|
||||
<input type="hidden" name="action" value="delete_core_token">
|
||||
<input type="hidden" name="token_id" value="<?= $mTok['token_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Master & Subs Widerrufen</button>
|
||||
<button type="submit" class="btn btn-sm btn-danger">🗑️ Löschen</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-down">WIDERUFEN</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@@ -2103,8 +2211,10 @@ $baseUrl = $protocol . '://' . $host;
|
||||
<th>Bezeichnung / Client</th>
|
||||
<th>Umgebung</th>
|
||||
<th>Scopes</th>
|
||||
<th>Token Key</th>
|
||||
<th>Zuletzt Genutzt</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -2112,6 +2222,8 @@ $baseUrl = $protocol . '://' . $host;
|
||||
$subTokens = array_filter($coreAllTokens, fn($t) => $t['type'] === 'sub');
|
||||
foreach ($subTokens as $sTok):
|
||||
$scopesArr = json_decode($sTok['scopes'], true) ?: [];
|
||||
$sRaw = !empty($sTok['raw_token']) ? $sTok['raw_token'] : $sTok['token_id'];
|
||||
$sMasked = (strlen($sRaw) > 12) ? substr($sRaw, 0, 12) . '••••••••••••••••' : $sRaw;
|
||||
?>
|
||||
<tr>
|
||||
<td><code><?= htmlspecialchars($sTok['token_id']) ?></code></td>
|
||||
@@ -2125,8 +2237,31 @@ $baseUrl = $protocol . '://' . $host;
|
||||
<span class="badge" style="background:rgba(255,255,255,0.06); font-size:0.75rem;"><?= htmlspecialchars($sc) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</td>
|
||||
<td>
|
||||
<code id="tok-sub-<?= $sTok['token_id'] ?>" data-full="<?= htmlspecialchars($sRaw) ?>" data-masked="<?= htmlspecialchars($sMasked) ?>">
|
||||
<?= htmlspecialchars($sMasked) ?>
|
||||
</code>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleTokenMask('tok-sub-<?= $sTok['token_id'] ?>')">👁️</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="copyTokenValue('tok-sub-<?= $sTok['token_id'] ?>')">📋</button>
|
||||
</td>
|
||||
<td><?= $sTok['last_used_at'] ? htmlspecialchars($sTok['last_used_at']) : 'Noch nie' ?></td>
|
||||
<td><span class="badge badge-<?= $sTok['revoked'] ? 'down' : 'up' ?>"><?= $sTok['revoked'] ? 'WIDERUFEN' : 'AKTIV' ?></span></td>
|
||||
<td>
|
||||
<div style="display:flex; gap:0.3rem;">
|
||||
<?php if (!$sTok['revoked']): ?>
|
||||
<form method="POST" action="index.php#tab-tokens" style="display:inline" onsubmit="return confirm('Sub-Token widerrufen?');">
|
||||
<input type="hidden" name="action" value="revoke_core_token">
|
||||
<input type="hidden" name="token_id" value="<?= $sTok['token_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-warning">Widerrufen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<form method="POST" action="index.php#tab-tokens" style="display:inline" onsubmit="return confirm('Sub-Token DAUERHAFT LÖSCHEN?');">
|
||||
<input type="hidden" name="action" value="delete_core_token">
|
||||
<input type="hidden" name="token_id" value="<?= $sTok['token_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger">🗑️ Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
@@ -2393,16 +2528,23 @@ $baseUrl = $protocol . '://' . $host;
|
||||
|
||||
// Bugtracker Table Filter JS
|
||||
function filterBugtrackerTable() {
|
||||
const project = document.getElementById('btFilterProject').value;
|
||||
const env = document.getElementById('btFilterEnv').value;
|
||||
const type = document.getElementById('btFilterType').value;
|
||||
const severity = document.getElementById('btFilterSeverity').value;
|
||||
const status = document.getElementById('btFilterStatus').value;
|
||||
const query = (document.getElementById('btFilterSearch').value || '').toLowerCase().trim();
|
||||
|
||||
document.querySelectorAll('#btTableBody .bt-row').forEach(row => {
|
||||
const matchProject = (project === 'all' || row.getAttribute('data-project') === project);
|
||||
const matchEnv = (env === 'all' || row.getAttribute('data-env') === env);
|
||||
const matchType = (type === 'all' || row.getAttribute('data-type') === type);
|
||||
const matchSeverity = (severity === 'all' || row.getAttribute('data-severity') === severity);
|
||||
const matchStatus = (status === 'all' || row.getAttribute('data-status') === status);
|
||||
const textContent = row.innerText.toLowerCase();
|
||||
const matchQuery = (!query || textContent.includes(query));
|
||||
|
||||
if (matchEnv && matchType && matchStatus) {
|
||||
if (matchProject && matchEnv && matchType && matchSeverity && matchStatus && matchQuery) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
@@ -2410,6 +2552,24 @@ $baseUrl = $protocol . '://' . $host;
|
||||
});
|
||||
}
|
||||
|
||||
// Open Bugtracker Edit Modal
|
||||
function openEditBugtrackerModal(item) {
|
||||
document.getElementById('btEditItemId').value = item.id;
|
||||
document.getElementById('btEditTitleDisplay').innerText = '#' + item.id + ': ' + item.title;
|
||||
document.getElementById('btEditStatus').value = item.status;
|
||||
document.getElementById('btEditSeverity').value = item.severity;
|
||||
document.getElementById('btEditPushId').value = item.push_id || '';
|
||||
document.getElementById('btEditTargetAgent').value = item.target_agent || '';
|
||||
document.getElementById('btEditTags').value = item.tags || '';
|
||||
document.getElementById('btEditResolvedInBuild').value = item.resolved_in_build || '';
|
||||
document.getElementById('btEditResolutionNotes').value = item.resolution_notes || '';
|
||||
document.getElementById('btEditModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeEditBugtrackerModal() {
|
||||
document.getElementById('btEditModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Open Bugtracker Item Details & Timeline Modal
|
||||
function openBugtrackerModal(itemId) {
|
||||
const modal = document.getElementById('btDetailModal');
|
||||
@@ -2640,9 +2800,10 @@ $baseUrl = $protocol . '://' . $host;
|
||||
document.getElementById('editExpiresAt').value = opt.getAttribute('data-exp') || '';
|
||||
}
|
||||
|
||||
// Token Masking Toggle
|
||||
// Token Masking Toggle & Copy
|
||||
function toggleTokenMask(tokId) {
|
||||
const el = document.getElementById('tok-text-' + tokId);
|
||||
const el = document.getElementById(tokId) || document.getElementById('tok-text-' + tokId) || document.getElementById('tok-core-' + tokId) || document.getElementById('tok-sub-' + tokId);
|
||||
if (!el) return;
|
||||
if (el.innerText.includes('••••')) {
|
||||
el.innerText = el.getAttribute('data-full');
|
||||
} else {
|
||||
@@ -2651,10 +2812,18 @@ $baseUrl = $protocol . '://' . $host;
|
||||
}
|
||||
|
||||
function copyTokenValue(tokId) {
|
||||
const el = document.getElementById('tok-text-' + tokId);
|
||||
const el = document.getElementById(tokId) || document.getElementById('tok-text-' + tokId) || document.getElementById('tok-core-' + tokId) || document.getElementById('tok-sub-' + tokId);
|
||||
if (!el) return;
|
||||
const fullVal = el.getAttribute('data-full');
|
||||
navigator.clipboard.writeText(fullVal);
|
||||
alert('Token in Zwischenablage kopiert!');
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(fullVal).then(() => {
|
||||
alert('Token in Zwischenablage kopiert!');
|
||||
}).catch(() => {
|
||||
prompt('Token kopieren:', fullVal);
|
||||
});
|
||||
} else {
|
||||
prompt('Token kopieren:', fullVal);
|
||||
}
|
||||
}
|
||||
|
||||
function generateOfflinePayload() {
|
||||
@@ -2709,28 +2878,73 @@ $baseUrl = $protocol . '://' . $host;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bugtracker Resolve Modal -->
|
||||
<div id="btResolveModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.75); backdrop-filter:blur(8px); z-index:9999; align-items:center; justify-content:center; padding:1rem;">
|
||||
<div class="card" style="width:100%; max-width:500px; background:#161c28; border:1px solid var(--border-glass);">
|
||||
<!-- Bugtracker Edit Modal -->
|
||||
<div id="btEditModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.75); backdrop-filter:blur(8px); z-index:9999; align-items:center; justify-content:center; padding:1rem;">
|
||||
<div class="card" style="width:100%; max-width:650px; background:#161c28; border:1px solid var(--border-glass);">
|
||||
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 class="card-title" style="margin:0; color:#fff;">✔ Item als Gelöst / Umgesetzt Markieren</h3>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="closeResolveModal()">✕</button>
|
||||
<h3 class="card-title" style="margin:0; color:#fff;">✏️ Item Bearbeiten</h3>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="closeEditBugtrackerModal()">✕</button>
|
||||
</div>
|
||||
<p style="font-size:0.875rem; color:var(--text-muted); margin-bottom:1rem;" id="resolveItemTitle"></p>
|
||||
<p style="font-size:0.875rem; color:var(--text-muted); margin-bottom:1rem;" id="btEditTitleDisplay"></p>
|
||||
<form method="POST" action="index.php#tab-bugtracker">
|
||||
<input type="hidden" name="action" value="bt_resolve">
|
||||
<input type="hidden" name="item_id" id="resolveItemId" value="0">
|
||||
<div class="form-group" style="margin-bottom:1rem;">
|
||||
<label class="form-label">Lösungs-Build / Version (z. B. v1.4.3)</label>
|
||||
<input type="text" name="resolved_in_build" class="form-input" required value="v1.4.3" placeholder="v1.4.3">
|
||||
<input type="hidden" name="action" value="bt_update_item">
|
||||
<input type="hidden" name="item_id" id="btEditItemId" value="0">
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status Bearbeiten</label>
|
||||
<select name="status" id="btEditStatus" class="form-input" required>
|
||||
<option value="open">Offen</option>
|
||||
<option value="planned">Geplant</option>
|
||||
<option value="in_progress">In Bearbeitung</option>
|
||||
<option value="resolved">Gelöst / Umgesetzt</option>
|
||||
<option value="closed">Geschlossen</option>
|
||||
<option value="rejected">Abgelehnt</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Schweregrad Bearbeiten</label>
|
||||
<select name="severity" id="btEditSeverity" class="form-input" required>
|
||||
<option value="idea">💡 Idee / Gedanke für Später</option>
|
||||
<option value="wishlist">⭐ Wunschliste / Backlog</option>
|
||||
<option value="low">Niedrig</option>
|
||||
<option value="medium">Mittel</option>
|
||||
<option value="high">Hoch</option>
|
||||
<option value="critical">🔥 Kritisch</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:1.25rem;">
|
||||
<label class="form-label">Lösungs-Notizen / Dokumentation</label>
|
||||
<textarea name="resolution_notes" class="form-input" rows="3" placeholder="Kurze Beschreibung, wie das Problem behoben oder das Feature umgesetzt wurde..."></textarea>
|
||||
|
||||
<div class="form-grid" style="margin-top:1rem;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Push-ID (Workflow / Push Notification Token)</label>
|
||||
<input type="text" name="push_id" id="btEditPushId" class="form-input" placeholder="z. B. push_notify_99182">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ziel-Agent / Zuweisung</label>
|
||||
<input type="text" name="target_agent" id="btEditTargetAgent" class="form-input" placeholder="z. B. agent:dev-monitor-01">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:flex-end; gap:0.5rem;">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeResolveModal()">Abbrechen</button>
|
||||
<button type="submit" class="btn">Als Gelöst Speichern</button>
|
||||
|
||||
<div class="form-group" style="margin-top:1rem;">
|
||||
<label class="form-label">Tags (Kommagetrennt)</label>
|
||||
<input type="text" name="tags" id="btEditTags" class="form-input" placeholder="z. B. ui, security, database">
|
||||
</div>
|
||||
|
||||
<div class="form-grid" style="margin-top:1rem;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Lösungs-Build (Falls gelöst)</label>
|
||||
<input type="text" name="resolved_in_build" id="btEditResolvedInBuild" class="form-input" placeholder="v1.4.3">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Lösungs-Notizen / Dokumentation</label>
|
||||
<input type="text" name="resolution_notes" id="btEditResolutionNotes" class="form-input" placeholder="Note zur Behebung...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:flex-end; gap:0.5rem; margin-top:1.25rem;">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeEditBugtrackerModal()">Abbrechen</button>
|
||||
<button type="submit" class="btn">Änderungen Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,39 +90,43 @@ 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()
|
||||
)
|
||||
');
|
||||
|
||||
$ins->execute([
|
||||
':pid' => $projectId,
|
||||
':slug' => $projectSlug,
|
||||
':type' => $type,
|
||||
':title' => $title,
|
||||
':desc' => $description,
|
||||
':err' => $errorMessage,
|
||||
':trace' => $stackTrace,
|
||||
':hash' => $errorHash,
|
||||
':build' => $buildVersion,
|
||||
':env' => $environment,
|
||||
':sev' => $severity,
|
||||
':created_by' => $createdBy,
|
||||
':pid' => $projectId,
|
||||
':slug' => $projectSlug,
|
||||
':type' => $type,
|
||||
':title' => $title,
|
||||
':desc' => $description,
|
||||
':err' => $errorMessage,
|
||||
':trace' => $stackTrace,
|
||||
':hash' => $errorHash,
|
||||
':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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user