feat: integrate Bugtracker module, UpdateService enhancements & Token hierarchy
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user