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.
This commit is contained in:
fds2610-bot 2026-08-09 20:14:04 +02:00
parent e51b373f07
commit 076dca2aef
8 changed files with 80 additions and 11 deletions

2
API.md
View file

@ -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

View file

@ -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),

View file

@ -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<string, string> $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<string, string> $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]);
}

View file

@ -84,14 +84,22 @@ final class RepoConfigRepository
return $this->find($id);
}
public function delete(int $id): void
/**
* @return array<string, mixed> - 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;
}
/**

View file

@ -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.

View file

@ -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']));
}
}

View file

@ -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

View file

@ -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']));
}
}