feat: integrate Bugtracker module, UpdateService enhancements & Token hierarchy

This commit is contained in:
Deploymentcenter Bot
2026-08-06 12:45:58 +02:00
parent 70b35f7b8b
commit d71f90cdfc
28 changed files with 3494 additions and 32 deletions
+315
View File
@@ -0,0 +1,315 @@
<?php
namespace Deploymentcenter\Modules\Bugtracker;
use PDO;
class BugRepo
{
private PDO $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
/**
* Ingest a Bug or Feature Request. Automates error-hash deduplication for bugs.
*/
public function reportItem(array $data): array
{
$projectSlug = !empty($data['project_slug']) ? trim($data['project_slug']) : 'default';
$type = (isset($data['type']) && $data['type'] === 'feature_request') ? 'feature_request' : 'bug';
$title = !empty($data['title']) ? trim($data['title']) : ($type === 'bug' ? 'Unhandled Exception' : 'New Feature Request');
$description = $data['description'] ?? null;
$errorMessage = $data['error_message'] ?? null;
$stackTrace = $data['stack_trace'] ?? null;
$buildVersion = $data['build_version'] ?? 'v1.0.0';
$environment = in_array($data['environment'] ?? '', ['production', 'development', 'staging', 'testing'])
? $data['environment']
: 'production';
$severity = in_array($data['severity'] ?? '', ['low', 'medium', 'high', 'critical'])
? $data['severity']
: 'medium';
$createdBy = !empty($data['created_by']) ? trim($data['created_by']) : 'agent';
// Find associated project_id from dc_projects
$projStmt = $this->db->prepare('SELECT id FROM dc_projects WHERE slug = :slug');
$projStmt->execute([':slug' => $projectSlug]);
$projectId = $projStmt->fetchColumn() ?: null;
// Deduplication logic for Bugs
$errorHash = null;
if ($type === 'bug') {
$hashInput = $projectSlug . '|' . ($errorMessage ?: $title) . '|' . substr($stackTrace ?: '', 0, 200) . '|' . $environment;
$errorHash = substr(hash('sha256', $hashInput), 0, 24);
$existingStmt = $this->db->prepare('
SELECT id, occurrence_count
FROM bugtracker_items
WHERE error_hash = :hash
AND environment = :env
AND status IN ("open", "in_progress", "planned")
LIMIT 1
');
$existingStmt->execute([':hash' => $errorHash, ':env' => $environment]);
$existing = $existingStmt->fetch();
if ($existing) {
$newCount = (int)$existing['occurrence_count'] + 1;
$upd = $this->db->prepare('
UPDATE bugtracker_items
SET occurrence_count = :count, last_seen_at = NOW()
WHERE id = :id
');
$upd->execute([':count' => $newCount, ':id' => $existing['id']]);
return [
'id' => (int)$existing['id'],
'is_new' => false,
'occurrence_count' => $newCount,
'error_hash' => $errorHash,
'type' => $type,
'environment' => $environment,
];
}
}
$ins = $this->db->prepare('
INSERT INTO bugtracker_items (
project_id, project_slug, type, title, description,
error_message, stack_trace, error_hash, build_version,
environment, severity, status, occurrence_count,
first_seen_at, last_seen_at, created_by, created_at
) VALUES (
:pid, :slug, :type, :title, :desc,
:err, :trace, :hash, :build,
:env, :sev, "open", 1,
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,
]);
$newItemId = (int)$this->db->lastInsertId();
// Initial comment log
$this->addComment(
$newItemId,
$createdBy,
$type === 'bug' ? 'Bug in System erfasst.' : 'Feature-Request eingereicht.',
'reported'
);
return [
'id' => $newItemId,
'is_new' => true,
'occurrence_count' => 1,
'error_hash' => $errorHash,
'type' => $type,
'environment' => $environment,
];
}
/**
* Get filtered list of Bugs & Feature Requests.
*/
public function getItems(array $filters = []): array
{
$where = [];
$params = [];
if (!empty($filters['project_slug']) && $filters['project_slug'] !== 'all') {
$where[] = 'project_slug = :slug';
$params[':slug'] = $filters['project_slug'];
}
if (!empty($filters['environment']) && $filters['environment'] !== 'all') {
$where[] = 'environment = :env';
$params[':env'] = $filters['environment'];
}
if (!empty($filters['type']) && $filters['type'] !== 'all') {
$where[] = 'type = :type';
$params[':type'] = $filters['type'];
}
if (!empty($filters['status']) && $filters['status'] !== 'all') {
$where[] = 'status = :status';
$params[':status'] = $filters['status'];
}
if (!empty($filters['severity']) && $filters['severity'] !== 'all') {
$where[] = 'severity = :severity';
$params[':severity'] = $filters['severity'];
}
if (!empty($filters['search'])) {
$where[] = '(title LIKE :q OR description LIKE :q OR error_message LIKE :q)';
$params[':q'] = '%' . trim($filters['search']) . '%';
}
$sql = 'SELECT * FROM bugtracker_items';
if (!empty($where)) {
$sql .= ' WHERE ' . implode(' AND ', $where);
}
$sql .= ' ORDER BY last_seen_at DESC, id DESC';
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll() ?: [];
}
/**
* Get single Item details including complete comment history.
*/
public function getItemDetails(int $id): ?array
{
$stmt = $this->db->prepare('SELECT * FROM bugtracker_items WHERE id = :id');
$stmt->execute([':id' => $id]);
$item = $stmt->fetch();
if (!$item) {
return null;
}
$commStmt = $this->db->prepare('SELECT * FROM bugtracker_comments WHERE item_id = :id ORDER BY created_at ASC');
$commStmt->execute([':id' => $id]);
$item['comments'] = $commStmt->fetchAll() ?: [];
return $item;
}
/**
* Add a diagnostic comment or timeline entry to an Item.
*/
public function addComment(
int $itemId,
string $author,
string $comment,
?string $actionTaken = null,
?array $meta = null
): array {
$stmt = $this->db->prepare('
INSERT INTO bugtracker_comments (
item_id, author, comment, action_taken, meta_json, created_at
) VALUES (
:item_id, :author, :comment, :action, :meta, NOW()
)
');
$stmt->execute([
':item_id' => $itemId,
':author' => $author,
':comment' => $comment,
':action' => $actionTaken,
':meta' => !empty($meta) ? json_encode($meta) : null,
]);
return [
'id' => (int)$this->db->lastInsertId(),
'item_id' => $itemId,
'author' => $author,
'created_at' => date('Y-m-d H:i:s'),
];
}
/**
* Update Item status.
*/
public function updateStatus(int $itemId, string $status, ?string $notes = null, string $author = 'agent'): bool
{
$allowed = ['open', 'planned', 'in_progress', 'resolved', 'closed', 'rejected'];
if (!in_array($status, $allowed)) {
return false;
}
$stmt = $this->db->prepare('UPDATE bugtracker_items SET status = :status WHERE id = :id');
$result = $stmt->execute([':status' => $status, ':id' => $itemId]);
if ($result) {
$msg = 'Status geändert auf "' . $status . '"' . ($notes ? ': ' . $notes : '');
$this->addComment($itemId, $author, $msg, 'status_changed');
}
return $result;
}
/**
* Resolve a Bug or complete a Feature Request with build details.
*/
public function resolveItem(
int $itemId,
string $resolvedInBuild,
?string $resolutionNotes = null,
string $author = 'agent'
): bool {
$stmt = $this->db->prepare('
UPDATE bugtracker_items
SET status = "resolved",
resolved_in_build = :build,
resolution_notes = :notes,
resolved_at = NOW()
WHERE id = :id
');
$result = $stmt->execute([
':build' => $resolvedInBuild,
':notes' => $resolutionNotes,
':id' => $itemId,
]);
if ($result) {
$msg = 'Als gelöst/umgesetzt markiert in Build "' . $resolvedInBuild . '"' . ($resolutionNotes ? '. Note: ' . $resolutionNotes : '');
$this->addComment($itemId, $author, $msg, 'marked_resolved');
}
return $result;
}
/**
* Get summary stats for the dashboard.
*/
public function getStats(): array
{
$stats = [
'open_bugs_prod' => 0,
'open_bugs_dev' => 0,
'open_features' => 0,
'resolved_total' => 0,
'critical_bugs' => 0,
];
$res = $this->db->query('
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 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
')->fetch();
if ($res) {
$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['resolved_total'] = (int)($res['resolved_total'] ?? 0);
$stats['critical_bugs'] = (int)($res['critical_bugs'] ?? 0);
}
return $stats;
}
}
+27 -10
View File
@@ -13,14 +13,14 @@ class UpdateManager
$this->db = $db;
}
public function checkUpdate(string $productSlug, string $currentVersion): ?array
public function checkUpdate(string $productSlug, string $currentVersion, string $channel = 'prod'): ?array
{
$stmt = $this->db->prepare('
SELECT * FROM updateservice_releases
WHERE product_slug = :slug AND version > :ver
WHERE product_slug = :slug AND channel = :channel AND version > :ver
ORDER BY created_at DESC LIMIT 1
');
$stmt->execute([':slug' => $productSlug, ':ver' => $currentVersion]);
$stmt->execute([':slug' => $productSlug, ':channel' => $channel, ':ver' => $currentVersion]);
$latest = $stmt->fetch();
return $latest ?: null;
@@ -29,38 +29,55 @@ class UpdateManager
public function addRelease(
string $productSlug,
string $version,
?string $releaseNotes,
string $downloadUrl,
?string $sha256Hash,
string $channel = 'prod',
?string $releaseNotes = null,
string $downloadUrl = '',
?string $sha256Hash = null,
?string $gitCommit = null,
int $sizeBytes = 0,
?string $manifestJson = null,
bool $isCritical = false
): bool {
$stmt = $this->db->prepare('
INSERT INTO updateservice_releases (
product_slug, version, release_notes, download_url, sha256_hash, is_critical
product_slug, version, channel, release_notes, download_url, sha256_hash, git_commit, size_bytes, manifest_json, is_critical
) VALUES (
:slug, :version, :notes, :url, :hash, :critical
:slug, :version, :channel, :notes, :url, :hash, :git, :size, :manifest, :critical
) ON DUPLICATE KEY UPDATE
release_notes = VALUES(release_notes),
download_url = VALUES(download_url),
sha256_hash = VALUES(sha256_hash),
git_commit = VALUES(git_commit),
size_bytes = VALUES(size_bytes),
manifest_json = VALUES(manifest_json),
is_critical = VALUES(is_critical)
');
return $stmt->execute([
':slug' => $productSlug,
':version' => $version,
':channel' => $channel,
':notes' => $releaseNotes,
':url' => $downloadUrl,
':hash' => $sha256Hash,
':git' => $gitCommit,
':size' => $sizeBytes,
':manifest' => $manifestJson,
':critical' => $isCritical ? 1 : 0,
]);
}
public function getReleases(?string $productSlug = null): array
public function getReleases(?string $productSlug = null, ?string $channel = null): array
{
if ($productSlug) {
if ($productSlug && $channel) {
$stmt = $this->db->prepare('SELECT * FROM updateservice_releases WHERE product_slug = :slug AND channel = :channel ORDER BY created_at DESC');
$stmt->execute([':slug' => $productSlug, ':channel' => $channel]);
} elseif ($productSlug) {
$stmt = $this->db->prepare('SELECT * FROM updateservice_releases WHERE product_slug = :slug ORDER BY created_at DESC');
$stmt->execute([':slug' => $productSlug]);
} elseif ($channel) {
$stmt = $this->db->prepare('SELECT * FROM updateservice_releases WHERE channel = :channel ORDER BY created_at DESC');
$stmt->execute([':channel' => $channel]);
} else {
$stmt = $this->db->query('SELECT * FROM updateservice_releases ORDER BY created_at DESC');
}