diff --git a/api/.phpunit.result.cache b/api/.phpunit.result.cache new file mode 100644 index 0000000..2e98ac9 --- /dev/null +++ b/api/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":2,"defects":{"Monitor\\Api\\Tests\\Controller\\StreamControllerTest::testStreamSendsHeartbeatsWhenNothingChanges":8,"Monitor\\Api\\Tests\\Controller\\StreamControllerTest::testStreamEmitsUpdateEventWhenAResourceChangesBetweenPolls":8,"Monitor\\Api\\Tests\\Controller\\StreamControllerTest::testStreamStopsAfterMaxIterations":8,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testInactiveProjectIsNotSynced":7,"Monitor\\Api\\Tests\\ApplicationTest::testFullFlowCreateUpdateAndDeleteRepoConfigWithAdminKey":7},"times":{"Monitor\\Api\\Tests\\ApplicationTest::testUnknownRouteReturns404":0.069,"Monitor\\Api\\Tests\\ApplicationTest::testWrongMethodReturns405":0.001,"Monitor\\Api\\Tests\\ApplicationTest::testWritingWithoutAgentKeyReturns401":0,"Monitor\\Api\\Tests\\ApplicationTest::testRegisterAgentRequiresAdminKey":0,"Monitor\\Api\\Tests\\ApplicationTest::testFullFlowRegisterAgentThenCreateTicket":0.001,"Monitor\\Api\\Tests\\ApplicationTest::testActivityWithUnknownTicketReturns422WithoutInternals":0.001,"Monitor\\Api\\Tests\\ApplicationTest::testPatchUnknownTicketReturns404":0.001,"Monitor\\Api\\Tests\\ApplicationTest::testStreamRouteIsPublicAndReturnsAStreamResponse":0,"Monitor\\Api\\Tests\\Auth\\AdminAuthenticatorTest::testAuthenticateAcceptsCorrectKey":0,"Monitor\\Api\\Tests\\Auth\\AdminAuthenticatorTest::testAuthenticateRejectsWrongKey":0,"Monitor\\Api\\Tests\\Auth\\AdminAuthenticatorTest::testAuthenticateRejectsWhenNoAdminKeyConfigured":0,"Monitor\\Api\\Tests\\Auth\\AdminAuthenticatorTest::testAuthenticateRejectsMissingHeader":0,"Monitor\\Api\\Tests\\Auth\\AgentAuthenticatorTest::testAuthenticateReturnsAgentForValidKey":0.001,"Monitor\\Api\\Tests\\Auth\\AgentAuthenticatorTest::testAuthenticateReturnsNullWithoutHeader":0.001,"Monitor\\Api\\Tests\\Auth\\AgentAuthenticatorTest::testAuthenticateReturnsNullForWrongKey":0.001,"Monitor\\Api\\Tests\\Auth\\ApiKeyGeneratorTest::testGenerateProducesUnique64CharHexKeys":0,"Monitor\\Api\\Tests\\Auth\\ApiKeyGeneratorTest::testHashIsDeterministicSha256":0,"Monitor\\Api\\Tests\\ConfigTest::testDefaultsAreUsedWhenNothingConfigured":0,"Monitor\\Api\\Tests\\ConfigTest::testExplicitEnvArrayDoesNotFallBackToProcessEnvironment":0,"Monitor\\Api\\Tests\\ConfigTest::testValuesCanBeOverridden":0,"Monitor\\Api\\Tests\\Controller\\AgentControllerTest::testRegisterRejectsMissingDisplayName":0,"Monitor\\Api\\Tests\\Controller\\AgentControllerTest::testRegisterRejectsUnknownRole":0,"Monitor\\Api\\Tests\\Controller\\AgentControllerTest::testRegisterReturns201WithApiKey":0,"Monitor\\Api\\Tests\\Controller\\AgentControllerTest::testActivityRejectsMismatchedAgentId":0,"Monitor\\Api\\Tests\\Controller\\AgentControllerTest::testActivityRejectsMissingState":0,"Monitor\\Api\\Tests\\Controller\\AgentControllerTest::testActivityUpdatesOwnState":0,"Monitor\\Api\\Tests\\Controller\\AgentControllerTest::testIndexListsAgents":0,"Monitor\\Api\\Tests\\Controller\\EventControllerTest::testCreateRejectsMissingFields":0,"Monitor\\Api\\Tests\\Controller\\EventControllerTest::testCreateRejectsInvalidSeverity":0,"Monitor\\Api\\Tests\\Controller\\EventControllerTest::testCreateReturns201":0,"Monitor\\Api\\Tests\\Controller\\EventControllerTest::testIndexReturnsEvents":0,"Monitor\\Api\\Tests\\Controller\\KpiControllerTest::testReportRejectsMissingValue":0,"Monitor\\Api\\Tests\\Controller\\KpiControllerTest::testReportRejectsNonNumericValue":0,"Monitor\\Api\\Tests\\Controller\\KpiControllerTest::testReportReturns201WithKpi":0,"Monitor\\Api\\Tests\\Controller\\KpiControllerTest::testIndexListsKpis":0,"Monitor\\Api\\Tests\\Controller\\StreamControllerTest::testStreamSendsHeartbeatsWhenNothingChanges":0,"Monitor\\Api\\Tests\\Controller\\StreamControllerTest::testStreamEmitsUpdateEventWhenAResourceChangesBetweenPolls":0,"Monitor\\Api\\Tests\\Controller\\StreamControllerTest::testStreamStopsAfterMaxIterations":0,"Monitor\\Api\\Tests\\Controller\\SystemControllerTest::testIndexImportsLegacyStatusFile":0,"Monitor\\Api\\Tests\\Controller\\SystemControllerTest::testReportStatusRejectsInvalidStatus":0,"Monitor\\Api\\Tests\\Controller\\SystemControllerTest::testReportStatusReturnsUpdatedSystem":0,"Monitor\\Api\\Tests\\Controller\\TicketControllerTest::testIndexReturnsAllTickets":0,"Monitor\\Api\\Tests\\Controller\\TicketControllerTest::testCreateRejectsMissingTitle":0,"Monitor\\Api\\Tests\\Controller\\TicketControllerTest::testCreateReturns201WithTicket":0,"Monitor\\Api\\Tests\\Controller\\TicketControllerTest::testCreateRejectsInvalidColumnWith422":0,"Monitor\\Api\\Tests\\Controller\\TicketControllerTest::testUpdateMovesTicket":0,"Monitor\\Api\\Tests\\Controller\\TicketControllerTest::testUpdatePropagatesNotFoundForUnknownTicket":0,"Monitor\\Api\\Tests\\Db\\DatabaseTest::testMigrateCreatesAllTables":0.001,"Monitor\\Api\\Tests\\Db\\DatabaseTest::testMigrateIsIdempotent":0.001,"Monitor\\Api\\Tests\\Db\\DatabaseTest::testNowReturnsIso8601Utc":0,"Monitor\\Api\\Tests\\Http\\RequestTest::testCreateNormalizesTrailingSlash":0,"Monitor\\Api\\Tests\\Http\\RequestTest::testCreateKeepsRootPath":0,"Monitor\\Api\\Tests\\Http\\RequestTest::testHeaderLookupIsCaseInsensitive":0,"Monitor\\Api\\Tests\\Http\\RequestTest::testHeaderReturnsNullWhenMissing":0,"Monitor\\Api\\Tests\\Http\\RequestTest::testCreateStoresQueryAndBody":0,"Monitor\\Api\\Tests\\Http\\ResponseTest::testJsonResponseCarriesStatusAndBody":0,"Monitor\\Api\\Tests\\Http\\ResponseTest::testErrorResponseDefaultsToGivenStatus":0,"Monitor\\Api\\Tests\\Http\\ResponseTest::testJsonResponseIsNotAStream":0,"Monitor\\Api\\Tests\\Http\\ResponseTest::testStreamResponseExposesTheWriterWithoutBody":0,"Monitor\\Api\\Tests\\Http\\RouterTest::testMatchesStaticRoute":0,"Monitor\\Api\\Tests\\Http\\RouterTest::testExtractsPathParams":0,"Monitor\\Api\\Tests\\Http\\RouterTest::testReturnsNullForUnknownPath":0,"Monitor\\Api\\Tests\\Http\\RouterTest::testThrowsMethodNotAllowedForKnownPathWrongMethod":0,"Monitor\\Api\\Tests\\Http\\RouterTest::testCarriesAuthRequirement":0,"Monitor\\Api\\Tests\\Realtime\\ChangeFeedTest::testCurrentFingerprintCoversAllFiveResources":0,"Monitor\\Api\\Tests\\Realtime\\ChangeFeedTest::testDiffIsEmptyWithoutChanges":0,"Monitor\\Api\\Tests\\Realtime\\ChangeFeedTest::testDiffReportsOnlyTheChangedResource":0,"Monitor\\Api\\Tests\\Realtime\\ChangeFeedTest::testDiffReportsMultipleChangedResources":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testRegisterAssignsLowestFreeDesk":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testUserRoleNeverGetsADesk":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testOverflowAgentGetsNoDeskWhenAllOccupied":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testRegisterRejectsUnknownRole":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testFindByApiKeyReturnsMatchingAgent":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testFindByApiKeyReturnsNullForUnknownKey":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testUpdateActivityChangesStateAndTicket":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testUpdateActivityRejectsUnknownTicket":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testUpdateActivityThrowsForUnknownAgent":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testAllListsAgentsWithoutApiKeyHash":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testFingerprintChangesWhenAnAgentIsRegistered":0,"Monitor\\Api\\Tests\\Repository\\EventRepositoryTest::testCreateStoresEvent":0,"Monitor\\Api\\Tests\\Repository\\EventRepositoryTest::testCreateRejectsInvalidSeverity":0,"Monitor\\Api\\Tests\\Repository\\EventRepositoryTest::testLatestReturnsNewestFirst":0,"Monitor\\Api\\Tests\\Repository\\EventRepositoryTest::testLatestRespectsDefaultLimitOfFive":0,"Monitor\\Api\\Tests\\Repository\\EventRepositoryTest::testLatestClampsLimitToAtLeastOne":0,"Monitor\\Api\\Tests\\Repository\\EventRepositoryTest::testFingerprintChangesWhenAnEventIsCreated":0,"Monitor\\Api\\Tests\\Repository\\KpiRepositoryTest::testReportCreatesKpiWithCurrentValue":0,"Monitor\\Api\\Tests\\Repository\\KpiRepositoryTest::testReportAppendsHistoryAndUpdatesCurrentValue":0,"Monitor\\Api\\Tests\\Repository\\KpiRepositoryTest::testHistoryIsLimited":0,"Monitor\\Api\\Tests\\Repository\\KpiRepositoryTest::testFindReturnsNullForUnknownKey":0,"Monitor\\Api\\Tests\\Repository\\KpiRepositoryTest::testAllListsAllKpis":0,"Monitor\\Api\\Tests\\Repository\\KpiRepositoryTest::testFingerprintChangesWhenAValueIsReported":0,"Monitor\\Api\\Tests\\Repository\\LegacyStatusFileReaderTest::testReadsValidLines":0,"Monitor\\Api\\Tests\\Repository\\LegacyStatusFileReaderTest::testSkipsMalformedLines":0,"Monitor\\Api\\Tests\\Repository\\LegacyStatusFileReaderTest::testReturnsEmptyArrayForMissingFile":0,"Monitor\\Api\\Tests\\Repository\\SystemRepositoryTest::testReportStatusInsertsSystem":0,"Monitor\\Api\\Tests\\Repository\\SystemRepositoryTest::testReportStatusRejectsInvalidStatus":0,"Monitor\\Api\\Tests\\Repository\\SystemRepositoryTest::testReportStatusUpdatesExistingSystem":0,"Monitor\\Api\\Tests\\Repository\\SystemRepositoryTest::testImportLegacyRowsDoesNotOverwriteApiSourcedSystem":0,"Monitor\\Api\\Tests\\Repository\\SystemRepositoryTest::testImportLegacyRowsCreatesNewLegacySystem":0,"Monitor\\Api\\Tests\\Repository\\SystemRepositoryTest::testAllOrdersErrorsFirstAndLimitsToNineTiles":0,"Monitor\\Api\\Tests\\Repository\\SystemRepositoryTest::testFingerprintChangesWhenStatusIsReported":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testCreateDefaultsToBacklogColumn":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testCreateRejectsUnknownColumn":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testUpdateMovesTicketToAnotherColumn":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testUpdateRejectsUnknownColumn":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testUpdateThrowsNotFoundForMissingTicket":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testAllFiltersByColumn":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testAllOrdersByColumnThenCreatedAt":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testUpsertFromCodebergCreatesNewTicket":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testUpsertFromCodebergUpdatesExistingBacklogTicket":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testUpsertFromCodebergDoesNotResetManuallyMovedTicket":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testFingerprintChangesWhenATicketIsCreated":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testFingerprintIsStableWithoutChanges":0,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSyncCreatesBacklogAndDoneTickets":0.004,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSyncNormalizesLabelColorRegardlessOfLeadingHash":0,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSyncDoesNotResetManuallyMovedTicket":0.001,"Monitor\\Api\\Tests\\Sync\\CurlCodebergClientTest::testListIssuesParsesSinglePage":0.003,"Monitor\\Api\\Tests\\Sync\\CurlCodebergClientTest::testListIssuesStopsWhenPageEmpty":0,"Monitor\\Api\\Tests\\Sync\\CurlCodebergClientTest::testListIssuesSendsAuthorizationHeaderWhenTokenGiven":0,"Monitor\\Api\\Tests\\Sync\\CurlCodebergClientTest::testThrowsOnHttpError":0,"Monitor\\Api\\Tests\\ApplicationTest::testCodebergWebhookWithoutSignatureReturns401":0,"Monitor\\Api\\Tests\\ApplicationTest::testCodebergWebhookWithValidSignatureIsAccepted":0.001,"Monitor\\Api\\Tests\\Auth\\WebhookAuthenticatorTest::testAcceptsForgejoSignature":0,"Monitor\\Api\\Tests\\Auth\\WebhookAuthenticatorTest::testAcceptsGiteaSignatureHeaderForCompatibility":0,"Monitor\\Api\\Tests\\Auth\\WebhookAuthenticatorTest::testRejectsSignatureCreatedWithAnotherSecret":0,"Monitor\\Api\\Tests\\Auth\\WebhookAuthenticatorTest::testRejectsSignatureForDifferentPayload":0,"Monitor\\Api\\Tests\\Auth\\WebhookAuthenticatorTest::testRejectsRequestWithoutSignature":0,"Monitor\\Api\\Tests\\Auth\\WebhookAuthenticatorTest::testRejectsEverythingWhenNoSecretIsConfigured":0,"Monitor\\Api\\Tests\\Controller\\WebhookControllerTest::testIssueEventRequestsASync":0,"Monitor\\Api\\Tests\\Controller\\WebhookControllerTest::testPingIsAnsweredWithoutRequestingASync":0,"Monitor\\Api\\Tests\\Controller\\WebhookControllerTest::testUnknownEventStillRequestsASync":0,"Monitor\\Api\\Tests\\Sync\\SyncTriggerTest::testConsumeReportsNothingWhenNoSyncWasRequested":0,"Monitor\\Api\\Tests\\Sync\\SyncTriggerTest::testConsumeReportsRequestedSyncExactlyOnce":0,"Monitor\\Api\\Tests\\Sync\\SyncTriggerTest::testRepeatedRequestsCollapseIntoOneSync":0,"Monitor\\Api\\Tests\\Sync\\SyncTriggerTest::testRequestCreatesMissingDirectory":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testRegisterAndUpdateActivityReportFreshAgentAsPresent":0,"Monitor\\Api\\Tests\\Repository\\AgentRepositoryTest::testFindByIdReportsAgentAsAbsentAfterPresenceTimeout":0,"Monitor\\Api\\Tests\\Repository\\TicketRepositoryTest::testUpsertFromCodebergMovesTicketBetweenSyncManagedColumns":0,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSyncMapsOpenIssueLabelToBoardColumn#automate -> bereit":0.001,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSyncMapsOpenIssueLabelToBoardColumn#wip -> in_arbeit":0.001,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSyncMapsOpenIssueLabelToBoardColumn#testing -> testing":0.001,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSyncMovesTicketBetweenAutomatedColumnsWhenLabelChanges":0.002,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSyncClosesTicketFromAnyAutomatedColumnIntoDone":0.002,"Monitor\\Api\\Tests\\Sync\\SyncScheduleTest::testUsesTheRegularIntervalWhileEverythingWorks":0.001,"Monitor\\Api\\Tests\\Sync\\SyncScheduleTest::testRetriesQuicklyAfterAFailure":0,"Monitor\\Api\\Tests\\Sync\\SyncScheduleTest::testBacksOffExponentiallyWhileFailuresContinue":0,"Monitor\\Api\\Tests\\Sync\\SyncScheduleTest::testNeverWaitsLongerThanTheRegularInterval":0,"Monitor\\Api\\Tests\\Sync\\SyncScheduleTest::testSurvivesAnAbsurdNumberOfFailures":0,"Monitor\\Api\\Tests\\Sync\\SyncScheduleTest::testShortIntervalIsNotStretchedByTheRetryBase":0,"Monitor\\Api\\Tests\\ApplicationTest::testProjectListIsPublic":0,"Monitor\\Api\\Tests\\ApplicationTest::testProjectWritesRequireAdminKey#anlegen":0,"Monitor\\Api\\Tests\\ApplicationTest::testProjectWritesRequireAdminKey#aendern":0,"Monitor\\Api\\Tests\\ApplicationTest::testProjectWritesRequireAdminKey#loeschen":0,"Monitor\\Api\\Tests\\ApplicationTest::testProjectLifecycleWithAdminKey":0.001,"Monitor\\Api\\Tests\\ApplicationTest::testTicketsOfDeactivatedProjectDisappearFromTheBoard":0.001,"Monitor\\Api\\Tests\\ApplicationTest::testPreflightIsAnsweredWithoutAuth":0,"Monitor\\Api\\Tests\\Controller\\ProjectControllerTest::testIndexListsActiveAndInactiveProjects":0.001,"Monitor\\Api\\Tests\\Controller\\ProjectControllerTest::testCreateReturns201AndRequestsImmediateSync":0,"Monitor\\Api\\Tests\\Controller\\ProjectControllerTest::testCreateWithoutRepoReturns422":0,"Monitor\\Api\\Tests\\Controller\\ProjectControllerTest::testUpdateChangesColorWithoutTriggeringSync":0,"Monitor\\Api\\Tests\\Controller\\ProjectControllerTest::testReactivatingAProjectRequestsSync":0,"Monitor\\Api\\Tests\\Controller\\ProjectControllerTest::testUpdateWithUnusableColorReturns422":0,"Monitor\\Api\\Tests\\Controller\\ProjectControllerTest::testDeleteRemovesProject":0,"Monitor\\Api\\Tests\\Controller\\ProjectControllerTest::testDeleteOfUnknownProjectIsNotFound":0,"Monitor\\Api\\Tests\\Realtime\\ChangeFeedTest::testCurrentFingerprintCoversAllResources":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testCreateUsesRepoAsNameAndAssignsDefaultColor":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testCreateRequiresOwnerAndRepo":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testCreateRejectsDuplicateRepo":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testCreateNormalizesColorWithoutLeadingHash":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testCreateRejectsUnusableColor":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testUpdateChangesColorAndVisibility":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testUpdateRejectsEmptyName":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testUpdateOfUnknownProjectIsNotFound":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testAllActiveSkipsDeactivatedProjects":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testDeleteRemovesProjectWithItsTickets":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testDeleteKeepsAgentWorkingOnADeletedTicket":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testSeedFromEnvironmentAdoptsExistingCodebergTickets":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testSeedFromEnvironmentRunsOnlyOnce":0,"Monitor\\Api\\Tests\\Repository\\ProjectRepositoryTest::testFingerprintChangesWithEveryEdit":0,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSyncCoversEveryActiveProject":0,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testIssueNumbersOfDifferentProjectsDoNotCollide":0.001,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testInactiveProjectIsNotSynced":0,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testFailingProjectDoesNotStopTheOthersButIsReported":0,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSeedProjectIsNotRecreatedAfterDeletion":0,"Monitor\\Api\\Tests\\ApplicationTest::testRepoConfigsIndexIsPublic":0.001,"Monitor\\Api\\Tests\\ApplicationTest::testCreateRepoConfigRequiresAdminKey":0,"Monitor\\Api\\Tests\\ApplicationTest::testFullFlowCreateUpdateAndDeleteRepoConfigWithAdminKey":0.001,"Monitor\\Api\\Tests\\Controller\\RepoConfigControllerTest::testIndexReturnsEmptyListInitially":0,"Monitor\\Api\\Tests\\Controller\\RepoConfigControllerTest::testCreateReturns201WithNewRepo":0,"Monitor\\Api\\Tests\\Controller\\RepoConfigControllerTest::testCreateRejectsInvalidColorWith422":0,"Monitor\\Api\\Tests\\Controller\\RepoConfigControllerTest::testUpdateReturnsUpdatedRepo":0,"Monitor\\Api\\Tests\\Controller\\RepoConfigControllerTest::testDeleteReturnsDeletedTrue":0,"Monitor\\Api\\Tests\\Repository\\RepoConfigRepositoryTest::testAllIsEmptyWithoutConfiguredProjects":0,"Monitor\\Api\\Tests\\Repository\\RepoConfigRepositoryTest::testCreateNormalizesColorAndAppendsAtEndOfSortOrder":0,"Monitor\\Api\\Tests\\Repository\\RepoConfigRepositoryTest::testCreateRejectsMissingOwnerOrRepo":0,"Monitor\\Api\\Tests\\Repository\\RepoConfigRepositoryTest::testCreateRejectsInvalidColor":0,"Monitor\\Api\\Tests\\Repository\\RepoConfigRepositoryTest::testCreateRejectsDuplicateOwnerRepo":0,"Monitor\\Api\\Tests\\Repository\\RepoConfigRepositoryTest::testUpdateChangesFields":0,"Monitor\\Api\\Tests\\Repository\\RepoConfigRepositoryTest::testUpdateUnknownIdThrowsNotFound":0,"Monitor\\Api\\Tests\\Repository\\RepoConfigRepositoryTest::testDeleteRemovesEntry":0,"Monitor\\Api\\Tests\\Repository\\RepoConfigRepositoryTest::testDeleteUnknownIdThrowsNotFound":0,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSyncCreatesBacklogAndDoneTicketsColoredByProject":0.004,"Monitor\\Api\\Tests\\Sync\\CodebergSyncServiceTest::testSyncKeepsIssuesFromDifferentProjectsSeparateAndColorsThemPerProject":0.001}} \ No newline at end of file diff --git a/api/src/Db/Database.php b/api/src/Db/Database.php index c353a20..fd47cfc 100644 --- a/api/src/Db/Database.php +++ b/api/src/Db/Database.php @@ -20,6 +20,11 @@ final class Database $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $this->pdo->exec('PRAGMA foreign_keys = ON'); + // Seit der API-Server mit mehreren Workern laeuft (PHP_CLI_SERVER_WORKERS, siehe + // docker-compose.yml) koennen sich zwei Anfragen beim Schreiben begegnen. Ohne + // Wartezeit meldet SQLite dann sofort "database is locked"; fuenf Sekunden sind fuer + // die kurzen Schreibvorgaenge dieser Anwendung reichlich. + $this->pdo->exec('PRAGMA busy_timeout = 5000'); } public static function forFile(string $path): self diff --git a/api/tests/Unit/ApplicationTest.php b/api/tests/Unit/ApplicationTest.php index 7f9d0c5..c85d97c 100644 --- a/api/tests/Unit/ApplicationTest.php +++ b/api/tests/Unit/ApplicationTest.php @@ -143,10 +143,13 @@ final class ApplicationTest extends TestCase public function testFullFlowCreateUpdateAndDeleteRepoConfigWithAdminKey(): void { + // Bewusst ein Repo, das die Migration nicht schon geseedet hat (monitor/Steuerkram/ + // nc-gantt, siehe 003_repo_configs.sql) - sonst scheitert schon das Anlegen an der + // Dublettenpruefung, und der eigentliche Ablauf wird gar nicht geprueft. $createResponse = $this->app->handle(Request::create( 'POST', '/api/v1/repo-configs', - body: ['owner' => 'fds2610', 'repo' => 'monitor', 'color' => '#fbca04'], + body: ['owner' => 'fds2610', 'repo' => 'weiteres-projekt', 'color' => '#fbca04'], headers: ['X-Admin-Key' => 'admin-secret'] )); self::assertSame(201, $createResponse->status); @@ -155,7 +158,7 @@ final class ApplicationTest extends TestCase $updateResponse = $this->app->handle(Request::create( 'PATCH', '/api/v1/repo-configs/' . $id, - body: ['owner' => 'fds2610', 'repo' => 'monitor', 'color' => '#000000'], + body: ['owner' => 'fds2610', 'repo' => 'weiteres-projekt', 'color' => '#000000'], headers: ['X-Admin-Key' => 'admin-secret'] )); self::assertSame(200, $updateResponse->status); diff --git a/docker-compose.yml b/docker-compose.yml index 94807cd..e2c5af4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,12 @@ services: ports: - "8091:8080" environment: + # Der eingebaute PHP-Server arbeitet Anfragen sonst streng nacheinander ab: eine einzige + # offene SSE-Verbindung (GET /api/v1/stream, bis zu 60s) blockiert damit jede weitere + # Anfrage - gemessen 16s fuer ein GET /api/v1/tickets. Ab dem zweiten offenen Monitor + # laeuft die API praktisch dauerhaft dicht, das Frontend faellt auf Demo-Daten zurueck + # und der PO reagiert dann auf nichts mehr (Fehlerbericht zu Issue #27). + PHP_CLI_SERVER_WORKERS: 4 MONITOR_DB_PATH: /app/data/monitor.sqlite MONITOR_ADMIN_API_KEY: ${MONITOR_ADMIN_API_KEY:-dev-admin-key} MONITOR_STATUS_FILE: /status.txt diff --git a/html/css/office.css b/html/css/office.css index 9023ad9..03c33d8 100644 --- a/html/css/office.css +++ b/html/css/office.css @@ -344,6 +344,41 @@ header.office-header h1 { pointer-events: all; } +/* Trefferflaeche des PO-Platzes (Issue #27): macht den ganzen Platz klickbar, nicht nur die + gefuellten Moebelstuecke - ein Klick daneben landete sonst auf dem Boden. */ +.po-desk-hit { + fill: transparent; + pointer-events: all; +} + +/* Zahnrad am PO-Platz (Issue #27): zeigt ueberhaupt erst, dass sich hier die + Projektkonfiguration oeffnen laesst. Beim Hover ueber den Platz tritt es hervor. */ +.po-config-hint-bg { + fill: #ffffff; + stroke: #94a3b8; + stroke-width: 1.5; + opacity: 0.85; +} + +.po-config-hint-tooth, +.po-config-hint-hub { + stroke: #475569; + stroke-width: 2; + stroke-linecap: round; + fill: none; +} + +.po-desk:hover .po-config-hint-bg { + fill: #e0f2fe; + stroke: #0284c7; + opacity: 1; +} + +.po-desk:hover .po-config-hint-tooth, +.po-desk:hover .po-config-hint-hub { + stroke: #0284c7; +} + .just-updated .board-frame, .just-updated .monitor-frame { animation: office-flash 0.9s ease-out; diff --git a/html/js/office-render.js b/html/js/office-render.js index e01f790..0507e91 100644 --- a/html/js/office-render.js +++ b/html/js/office-render.js @@ -533,24 +533,67 @@ function renderDesk({ deskIndex, agent }) { * @param {Array} desks - reguläre 8er-Raster-Plaetze (echte Agenten, F1). * @param {object|null} poAgent - {name: "PO", role: "product_owner", pose} bei erreichbarer * API, sonst null (dann zeigt der feste PO-Platz "frei", Issue #19). - * @param {(() => void)|null} onPoClick - oeffnet das Projekte-Modal (Issue #27); nur gesetzt - * bei erreichbarer API, da das Modal echte Daten braucht und im Demo-Fallback ohne - * funktionierende API nichts zu konfigurieren waere. + * @param {() => void} onPoClick - oeffnet das Projekte-Modal (Issue #27). */ -function renderDesks(desks, poAgent, onPoClick = null) { +function renderDesks(desks, poAgent, onPoClick) { const g = el("g", { class: "desks" }); desks.forEach((desk) => g.appendChild(renderDesk(desk))); const poDeskGroup = renderDeskAt(PO_DESK, poAgent, PO_DESK.w); - poDeskGroup.classList.add("po-desk"); - if (onPoClick) { - poDeskGroup.classList.add("clickable"); - poDeskGroup.setAttribute("aria-label", "Projekte des Codeberg-Sync verwalten"); - poDeskGroup.addEventListener("click", onPoClick); - } + poDeskGroup.classList.add("po-desk", "clickable"); + poDeskGroup.setAttribute("aria-label", "Projekte des Codeberg-Sync verwalten"); + poDeskGroup.appendChild(renderPoConfigHint()); + // Eine selbst faengt keine Klicks - nur ihre gefuellten Kinder. Zwischen Tischplatte, + // Stuhl und Namensschild liegen Luecken, in denen ein Klick "auf den PO" beim Boden landete + // (Fehlerbericht zu Issue #27). Diese unsichtbare Flaeche deckt den ganzen Platz ab, analog + // zu .board-column-hit bei den Board-Spalten. + poDeskGroup.appendChild( + el("rect", { + x: -10, + y: 0, + width: PO_DESK.w + 20, + height: 40 + DESK.h + 90, + class: "po-desk-hit", + }) + ); + poDeskGroup.addEventListener("click", onPoClick); g.appendChild(poDeskGroup); return g; } +// Dass hinter dem PO die Projektkonfiguration steckt, war bisher nirgends zu sehen - der +// Fehlerbericht zu Issue #27 ("damit ich den Konfig Dialog irgendwie oeffnen kann") war +// deshalb auch eine Frage der Auffindbarkeit. Das Zahnrad sitzt auf der Tischplatte des +// PO-Platzes; klickbar ist weiterhin der ganze Platz (und der PO selbst). ist der +// SVG-eigene Tooltip. +function renderPoConfigHint() { + const cx = PO_DESK.w - 34; + const cy = 40 + DESK.h / 2; + const g = el("g", { class: "po-config-hint", transform: `translate(${cx}, ${cy})` }); + + const title = document.createElementNS(SVG_NS, "title"); + title.textContent = "Projekte konfigurieren"; + g.appendChild(title); + + g.appendChild(el("circle", { cx: 0, cy: 0, r: 15, class: "po-config-hint-bg" })); + // Zahnrad aus acht Zaehnen plus Nabe - in dieser Groesse von einem exakten Zahnradpfad + // nicht zu unterscheiden und ohne fremde Icon-Bibliothek zu haben. + for (let i = 0; i < 8; i++) { + const angle = (i * Math.PI) / 4; + g.appendChild( + el("line", { + x1: Math.cos(angle) * 5.5, + y1: Math.sin(angle) * 5.5, + x2: Math.cos(angle) * 9.5, + y2: Math.sin(angle) * 9.5, + class: "po-config-hint-tooth", + }) + ); + } + g.appendChild(el("circle", { cx: 0, cy: 0, r: 5, class: "po-config-hint-hub" })); + + return g; +} + // --------------------------------------------------------------------------------------- // Phase 5 - Agenten-Avatare (bewegliche Schicht, siehe Architektur-Hinweis oben) // --------------------------------------------------------------------------------------- @@ -634,6 +677,9 @@ function updateAgentsLayer(layer, placements) { let node = agentAvatarElements.get(id); if (!node) { node = createAgentAvatarElement(); + if (id === PO_AGENT_ID) { + makePoAvatarClickable(node); + } agentAvatarElements.set(id, node); layer.appendChild(node); } @@ -641,6 +687,11 @@ function updateAgentsLayer(layer, placements) { const feet = feetForLocation(agent.location, homeFeet); const showName = agent.location !== AGENT_LOCATIONS.DESK; updateAgentAvatarElement(node, agent, feet, showName); + if (id === PO_AGENT_ID) { + // updateAgentAvatarElement() setzt class komplett neu (Pose) - ohne diese Zeile + // verloere der PO die Klickbarkeit beim ersten Zustandswechsel. + node.classList.add("clickable"); + } }); for (const [id, node] of agentAvatarElements) { @@ -652,6 +703,17 @@ function updateAgentsLayer(layer, placements) { } } +// Das Ticket sagt woertlich "bei Klick auf den PO" - gemeint ist die Figur, nicht ihr +// Schreibtisch. Der Avatar liegt in einer eigenen Schicht ueber der Szene und hatte bisher +// keinen Klick-Handler; ein Klick auf den PO selbst lief damit ins Leere. Beides ist jetzt +// klickbar, auch wenn der PO gerade am Board steht statt an seinem Platz. +function makePoAvatarClickable(node) { + const title = document.createElementNS(SVG_NS, "title"); + title.textContent = "Projekte konfigurieren"; + node.appendChild(title); + node.addEventListener("click", openRepoConfigModal); +} + function clearIdleCosmeticTimer(agentId) { const handle = idleCosmeticTimers.get(agentId); if (handle) { @@ -1347,7 +1409,10 @@ export function renderOfficeScene(container, data = {}) { const deskLayout = buildDeskLayout(normalizedDeskAgents(agents)); const poDeskAgent = isLive ? { name: PO_DISPLAY_NAME, role: "product_owner", pose: "idle" } : null; - const desksGroup = renderDesks(deskLayout, poDeskAgent, isLive ? openRepoConfigModal : null); + // Auch ohne erreichbare API klickbar: das Modal sagt dann selbst, dass es die Projekte + // nicht laden konnte. Ein toter PO waere in genau der Lage, in der man nachsehen will, + // die schlechtere Antwort (Fehlerbericht zu Issue #27). + const desksGroup = renderDesks(deskLayout, poDeskAgent, openRepoConfigModal); const existingDesks = svg.querySelector(".desks"); if (existingDesks) { existingDesks.replaceWith(desksGroup);