From 076dca2aef702035ae812d7e4ceb2b0bde61047b Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Sun, 9 Aug 2026 20:14:04 +0200 Subject: [PATCH] Tickets eines entfernten Projekts aus dem Kanban entfernen (Issue #43) Bislang hat RepoConfigController::delete() nur die Projektkonfiguration geloescht - die Tickets des Projekts blieben (nur ohne Projektfarbe, Issue #34) dauerhaft im Kanban sichtbar. Jetzt loescht der Endpunkt zusaetzlich alle Tickets, deren repo-Feld auf das entfernte Projekt zeigt. --- API.md | 2 +- api/src/Application.php | 2 +- api/src/Controller/RepoConfigController.php | 17 +++++++++--- api/src/Repository/RepoConfigRepository.php | 12 +++++++-- api/src/Repository/TicketRepository.php | 13 ++++++++++ .../Controller/RepoConfigControllerTest.php | 26 ++++++++++++++++++- .../Repository/RepoConfigRepositoryTest.php | 6 +++-- .../Unit/Repository/TicketRepositoryTest.php | 13 ++++++++++ 8 files changed, 80 insertions(+), 11 deletions(-) diff --git a/API.md b/API.md index a26fd1e..05828fe 100644 --- a/API.md +++ b/API.md @@ -335,7 +335,7 @@ bisherigen Default-Projekte `fds2610/monitor` (gelb), `fds2610/Steuerkram` (grue | `GET /api/v1/repo-configs` | - | Liste aller konfigurierten Projekte, sortiert nach `sort_order` | | `POST /api/v1/repo-configs` | `X-Admin-Key` | Legt ein Projekt an (`owner`, `repo`, `color` als 6-stelliger Hex-Wert, mit oder ohne `#`) | | `PATCH /api/v1/repo-configs/{id}` | `X-Admin-Key` | Aendert `owner`/`repo`/`color` eines Projekts | -| `DELETE /api/v1/repo-configs/{id}` | `X-Admin-Key` | Entfernt ein Projekt (der Sync liest es danach nicht mehr) | +| `DELETE /api/v1/repo-configs/{id}` | `X-Admin-Key` | Entfernt ein Projekt (der Sync liest es danach nicht mehr) und loescht dessen Tickets aus dem Kanban (Issue #43) | `owner`+`repo` muessen zusammen eindeutig sein (409 -> hier als `422` mit Fehlermeldung, da kein Konflikt-Statuscode im Fehlerformat vorgesehen ist). Der laufende `sync`-Dienst diff --git a/api/src/Application.php b/api/src/Application.php index aa3645a..42f70c9 100644 --- a/api/src/Application.php +++ b/api/src/Application.php @@ -65,7 +65,7 @@ final class Application $eventController = new EventController($events); $systemController = new SystemController($systems, new LegacyStatusFileReader(), $config->statusFilePath()); $kpiController = new KpiController($kpis); - $repoConfigController = new RepoConfigController($repoConfigs); + $repoConfigController = new RepoConfigController($repoConfigs, $tickets); $webhookController = new WebhookController(new SyncTrigger($config->syncTriggerPath()), $hooks); $streamController = new StreamController( new ChangeFeed($tickets, $events, $systems, $kpis, $agents, $hooks), diff --git a/api/src/Controller/RepoConfigController.php b/api/src/Controller/RepoConfigController.php index a1eb820..0afd198 100644 --- a/api/src/Controller/RepoConfigController.php +++ b/api/src/Controller/RepoConfigController.php @@ -8,11 +8,14 @@ use InvalidArgumentException; use Monitor\Api\Http\Request; use Monitor\Api\Http\Response; use Monitor\Api\Repository\RepoConfigRepository; +use Monitor\Api\Repository\TicketRepository; final class RepoConfigController { - public function __construct(private readonly RepoConfigRepository $repoConfigs) - { + public function __construct( + private readonly RepoConfigRepository $repoConfigs, + private readonly TicketRepository $tickets, + ) { } public function index(Request $request): Response @@ -43,10 +46,16 @@ final class RepoConfigController return Response::json(['repo' => $repo]); } - /** @param array $params */ + /** + * Issue #43: Nach dem Entfernen eines Projekts sollen auch dessen Tickets aus dem Kanban + * verschwinden statt (wie zuvor, Issue #34) nur ihre Projektfarbe zu verlieren. + * + * @param array $params + */ public function delete(Request $request, array $params): Response { - $this->repoConfigs->delete((int) $params['id']); + $repo = $this->repoConfigs->delete((int) $params['id']); + $this->tickets->deleteByRepo($repo['owner'] . '/' . $repo['repo']); return Response::json(['deleted' => true]); } diff --git a/api/src/Repository/RepoConfigRepository.php b/api/src/Repository/RepoConfigRepository.php index 0907bd5..3d4649e 100644 --- a/api/src/Repository/RepoConfigRepository.php +++ b/api/src/Repository/RepoConfigRepository.php @@ -84,14 +84,22 @@ final class RepoConfigRepository return $this->find($id); } - public function delete(int $id): void + /** + * @return array - der geloeschte Eintrag, damit der Aufrufer (Issue #43: + * RepoConfigController::delete() raeumt anschliessend die Tickets des Projekts auf) dessen + * owner/repo kennt, ohne ihn selbst erneut nachzuschlagen. + */ + public function delete(int $id): array { - if ($this->find($id) === null) { + $repo = $this->find($id); + if ($repo === null) { throw new NotFoundException('Projekt nicht gefunden: ' . $id); } $stmt = $this->db->pdo()->prepare('DELETE FROM repo_configs WHERE id = :id'); $stmt->execute(['id' => $id]); + + return $repo; } /** diff --git a/api/src/Repository/TicketRepository.php b/api/src/Repository/TicketRepository.php index 07b0c74..d877736 100644 --- a/api/src/Repository/TicketRepository.php +++ b/api/src/Repository/TicketRepository.php @@ -197,6 +197,19 @@ final class TicketRepository $stmt->execute($slugs); } + /** + * Entfernt alle Tickets eines Projekts vollstaendig aus dem Kanban (Issue #43: bislang + * blieben Tickets eines ueber das Projekte-Modal geloeschten Projekts sichtbar, nur die + * Farbe wurde ueber clearColorForReposNotIn() geloescht). Wird von + * RepoConfigController::delete() direkt nach dem Entfernen des repo_configs-Eintrags + * aufgerufen, nicht erst beim naechsten periodischen Sync. + */ + public function deleteByRepo(string $repo): void + { + $stmt = $this->db->pdo()->prepare('DELETE FROM tickets WHERE repo = :repo'); + $stmt->execute(['repo' => $repo]); + } + /** * Billiger "Fingerabdruck" (Anzahl + juengster Aenderungszeitpunkt) fuer den SSE-Endpoint * (Realtime\ChangeFeed, F13) - aendert sich bei jedem Anlegen/Verschieben eines Tickets. diff --git a/api/tests/Unit/Controller/RepoConfigControllerTest.php b/api/tests/Unit/Controller/RepoConfigControllerTest.php index 9c10bb5..6ac3d68 100644 --- a/api/tests/Unit/Controller/RepoConfigControllerTest.php +++ b/api/tests/Unit/Controller/RepoConfigControllerTest.php @@ -8,17 +8,21 @@ use Monitor\Api\Controller\RepoConfigController; use Monitor\Api\Db\Database; use Monitor\Api\Http\Request; use Monitor\Api\Repository\RepoConfigRepository; +use Monitor\Api\Repository\TicketRepository; use PHPUnit\Framework\TestCase; final class RepoConfigControllerTest extends TestCase { private RepoConfigController $controller; + private TicketRepository $tickets; protected function setUp(): void { $db = Database::inMemory(); $db->pdo()->exec('DELETE FROM repo_configs'); - $this->controller = new RepoConfigController(new RepoConfigRepository($db)); + $db->pdo()->exec('DELETE FROM tickets'); + $this->tickets = new TicketRepository($db); + $this->controller = new RepoConfigController(new RepoConfigRepository($db), $this->tickets); } public function testIndexReturnsEmptyListInitially(): void @@ -79,4 +83,24 @@ final class RepoConfigControllerTest extends TestCase self::assertSame(200, $response->status); self::assertTrue($response->body()['deleted']); } + + public function testDeleteRemovesTicketsOfTheProjectFromTheKanban(): void + { + $created = $this->controller->create(Request::create('POST', '/api/v1/repo-configs', body: [ + 'owner' => 'fds2610', 'repo' => 'monitor', 'color' => '#fbca04', + ]))->body()['repo']; + + $orphaned = $this->tickets->create(['title' => 'A', 'repo' => 'fds2610/monitor']); + $unrelated = $this->tickets->create(['title' => 'B', 'repo' => 'fds2610/anderes-projekt']); + $manual = $this->tickets->create(['title' => 'C']); + + $this->controller->delete( + Request::create('DELETE', '/api/v1/repo-configs/' . $created['id']), + ['id' => (string) $created['id']] + ); + + self::assertNull($this->tickets->find((int) $orphaned['id'])); + self::assertNotNull($this->tickets->find((int) $unrelated['id'])); + self::assertNotNull($this->tickets->find((int) $manual['id'])); + } } diff --git a/api/tests/Unit/Repository/RepoConfigRepositoryTest.php b/api/tests/Unit/Repository/RepoConfigRepositoryTest.php index 9d25983..98f12e3 100644 --- a/api/tests/Unit/Repository/RepoConfigRepositoryTest.php +++ b/api/tests/Unit/Repository/RepoConfigRepositoryTest.php @@ -81,13 +81,15 @@ final class RepoConfigRepositoryTest extends TestCase $this->repository->update(999, ['owner' => 'a', 'repo' => 'b', 'color' => '#000000']); } - public function testDeleteRemovesEntry(): void + public function testDeleteRemovesEntryAndReturnsIt(): void { $repo = $this->repository->create(['owner' => 'fds2610', 'repo' => 'monitor', 'color' => '#fbca04']); - $this->repository->delete((int) $repo['id']); + $deleted = $this->repository->delete((int) $repo['id']); self::assertSame([], $this->repository->all()); + self::assertSame('fds2610', $deleted['owner']); + self::assertSame('monitor', $deleted['repo']); } public function testDeleteUnknownIdThrowsNotFound(): void diff --git a/api/tests/Unit/Repository/TicketRepositoryTest.php b/api/tests/Unit/Repository/TicketRepositoryTest.php index f92bf9f..ef2bd48 100644 --- a/api/tests/Unit/Repository/TicketRepositoryTest.php +++ b/api/tests/Unit/Repository/TicketRepositoryTest.php @@ -193,4 +193,17 @@ final class TicketRepositoryTest extends TestCase self::assertNull($this->repository->find((int) $ticket['id'])['color']); } + + public function testDeleteByRepoRemovesOnlyTicketsOfThatRepo(): void + { + $removed = $this->repository->create(['title' => 'A', 'repo' => 'fds2610/altes-projekt']); + $kept = $this->repository->create(['title' => 'B', 'repo' => 'fds2610/monitor']); + $manual = $this->repository->create(['title' => 'C']); + + $this->repository->deleteByRepo('fds2610/altes-projekt'); + + self::assertNull($this->repository->find((int) $removed['id'])); + self::assertNotNull($this->repository->find((int) $kept['id'])); + self::assertNotNull($this->repository->find((int) $manual['id'])); + } }