diff --git a/app/Http/Controllers/ServiceController.php b/app/Http/Controllers/ServiceController.php index 95d2d77..7ab41b1 100644 --- a/app/Http/Controllers/ServiceController.php +++ b/app/Http/Controllers/ServiceController.php @@ -112,6 +112,7 @@ public function index(): Response 'has_sermon_slides' => $hasSermonSlides, 'info_slides_count' => (int) $service->info_slides_count, 'agenda_slides_count' => (int) $service->agenda_slides_count, + 'has_agenda' => (bool) $service->has_agenda, ]; }) ->values(); @@ -234,6 +235,7 @@ public function edit(Service $service): Response 'beamer_tech_name' => $service->beamer_tech_name, 'finalized_at' => $service->finalized_at?->toJSON(), 'last_synced_at' => $service->last_synced_at?->toJSON(), + 'has_agenda' => $service->has_agenda, ], 'serviceSongs' => $service->serviceSongs->map(fn ($ss) => [ 'id' => $ss->id, diff --git a/app/Models/Service.php b/app/Models/Service.php index a6eb2a4..b47ed7e 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -21,6 +21,7 @@ class Service extends Model 'finalized_at', 'last_synced_at', 'cts_data', + 'has_agenda', ]; protected function casts(): array @@ -30,6 +31,7 @@ protected function casts(): array 'finalized_at' => 'datetime', 'last_synced_at' => 'datetime', 'cts_data' => 'array', + 'has_agenda' => 'boolean', ]; } diff --git a/app/Services/ChurchToolsService.php b/app/Services/ChurchToolsService.php index 5f9a28b..f4d5a7a 100644 --- a/app/Services/ChurchToolsService.php +++ b/app/Services/ChurchToolsService.php @@ -5,6 +5,9 @@ use App\Models\ApiRequestLog; use Closure; use CTApi\CTConfig; +use CTApi\Exceptions\CTConfigException; +use CTApi\Exceptions\CTConnectException; +use CTApi\Exceptions\CTPermissionException; use CTApi\Models\Events\Event\EventAgendaRequest; use CTApi\Models\Events\Event\EventRequest; use CTApi\Models\Events\Song\SongRequest; @@ -47,6 +50,16 @@ public function sync(): array return $summary; } catch (Throwable $exception) { + $errorCategory = $this->classifyError($exception); + + Log::error('CTS Sync fehlgeschlagen', [ + 'kategorie' => $errorCategory, + 'nachricht' => $exception->getMessage(), + 'exception_klasse' => $exception::class, + 'dauer_sekunden' => $startedAt->diffInSeconds(Carbon::now()), + 'trace' => Str::limit($exception->getTraceAsString(), 2000), + ]); + $summary = [ 'services_count' => 0, 'songs_count' => 0, @@ -56,7 +69,7 @@ public function sync(): array 'catalog_songs_count' => 0, ]; - $this->writeSyncLog('error', $summary, $exception->getMessage(), $startedAt, Carbon::now()); + $this->writeSyncLog('error', $summary, '['.$errorCategory.'] '.$exception->getMessage(), $startedAt, Carbon::now()); throw $exception; } @@ -234,13 +247,24 @@ private function logApiCall(string $method, string $endpoint, Closure $operation return $result; } catch (\Throwable $e) { $duration = (int) ((microtime(true) - $start) * 1000); + $errorCategory = $this->classifyError($e); + + Log::error('CTS API Fehler bei '.$method, [ + 'endpoint' => $endpoint, + 'kategorie' => $errorCategory, + 'nachricht' => $e->getMessage(), + 'exception_klasse' => $e::class, + 'dauer_ms' => $duration, + 'kontext' => $context, + 'trace' => Str::limit($e->getTraceAsString(), 2000), + ]); ApiRequestLog::create([ 'method' => $method, 'endpoint' => $endpoint, 'status' => 'error', 'request_context' => $context, - 'error_message' => $e->getMessage(), + 'error_message' => '['.$errorCategory.'] '.$e->getMessage(), 'duration_ms' => $duration, ]); @@ -293,10 +317,24 @@ private function configureApi(): void $apiUrl = (string) Config::get('services.churchtools.url', ''); $apiToken = (string) Config::get('services.churchtools.api_token', ''); - if ($apiUrl !== '') { - CTConfig::setApiUrl(rtrim($apiUrl, '/')); + Log::debug('CTS API Konfiguration', [ + 'url_gesetzt' => $apiUrl !== '', + 'url' => $apiUrl !== '' ? $apiUrl : '(leer)', + 'token_gesetzt' => $apiToken !== '', + 'token_laenge' => mb_strlen($apiToken), + ]); + + if ($apiUrl === '') { + Log::error('CTS API URL fehlt. Bitte CTS_API_URL in der .env Datei setzen.'); + throw new \RuntimeException('CTS API URL ist nicht konfiguriert. Bitte CTS_API_URL in der .env Datei setzen.'); } + if ($apiToken === '') { + Log::error('CTS API Token fehlt. Bitte CTS_API_TOKEN in der .env Datei setzen.'); + throw new \RuntimeException('CTS API Token ist nicht konfiguriert. Bitte CTS_API_TOKEN in der .env Datei setzen.'); + } + + CTConfig::setApiUrl(rtrim($apiUrl, '/')); CTConfig::setApiKey($apiToken); $this->apiConfigured = true; @@ -333,7 +371,31 @@ private function syncServiceAgendaSongs(int $serviceId, int $eventId): array private function syncServiceAgendaItems(int $serviceId, int $eventId): array { - $agenda = $this->syncAgenda($eventId); + try { + $agenda = $this->syncAgenda($eventId); + } catch (CTRequestException $e) { + if (str_contains(Str::lower($e->getMessage()), 'not found')) { + DB::table('services') + ->where('id', $serviceId) + ->update(['has_agenda' => false]); + + Log::info('Event hat keine Agenda', ['event_id' => $eventId]); + + return [ + 'songs_count' => 0, + 'matched_songs_count' => 0, + 'unmatched_songs_count' => 0, + 'agenda_items_count' => 0, + ]; + } + + throw $e; + } + + DB::table('services') + ->where('id', $serviceId) + ->update(['has_agenda' => true]); + $agendaItems = method_exists($agenda, 'getItems') ? $agenda->getItems() : []; $summary = [ @@ -531,6 +593,47 @@ private function normalizeCcli(?string $ccli): ?string return $value === '' ? null : $value; } + private function classifyError(Throwable $e): string + { + if ($e instanceof CTPermissionException) { + return 'authentifizierung'; + } + + if ($e instanceof CTConnectException) { + return 'verbindung'; + } + + if ($e instanceof CTConfigException) { + return 'konfiguration'; + } + + $message = Str::lower($e->getMessage()); + + if (str_contains($message, 'token') || str_contains($message, '401') || str_contains($message, 'unauthorized')) { + return 'authentifizierung'; + } + + if (str_contains($message, 'could not resolve') || str_contains($message, 'connection refused') || str_contains($message, 'timed out')) { + return 'verbindung'; + } + + if (str_contains($message, '403') || str_contains($message, 'forbidden')) { + return 'berechtigung'; + } + + if (str_contains($message, '404') || str_contains($message, 'not found')) { + return 'nicht_gefunden'; + } + + if (str_contains($message, '5')) { + if (preg_match('/\b5\d{2}\b/', $message)) { + return 'server_fehler'; + } + } + + return 'unbekannt'; + } + private function writeSyncLog(string $status, array $summary, ?string $message, mixed $startedAt, mixed $finishedAt): void { DB::table('cts_sync_log')->insert([ diff --git a/database/factories/ServiceFactory.php b/database/factories/ServiceFactory.php index c4d7999..e6d43be 100644 --- a/database/factories/ServiceFactory.php +++ b/database/factories/ServiceFactory.php @@ -25,6 +25,7 @@ public function definition(): array 'id' => $this->faker->uuid(), 'name' => $this->faker->sentence(3), ], + 'has_agenda' => true, ]; } } diff --git a/database/migrations/2026_03_29_131045_add_missing_cts_song_id_to_service_songs_table.php b/database/migrations/2026_03_29_131045_add_missing_cts_song_id_to_service_songs_table.php new file mode 100644 index 0000000..b6d679e --- /dev/null +++ b/database/migrations/2026_03_29_131045_add_missing_cts_song_id_to_service_songs_table.php @@ -0,0 +1,27 @@ +string('cts_song_id')->nullable()->after('cts_ccli_id'); + }); + } + } + + public function down(): void + { + Schema::table('service_songs', function (Blueprint $table) { + $table->dropColumn('cts_song_id'); + }); + } +}; diff --git a/database/migrations/2026_03_29_131359_add_has_agenda_to_services_table.php b/database/migrations/2026_03_29_131359_add_has_agenda_to_services_table.php new file mode 100644 index 0000000..1df3f05 --- /dev/null +++ b/database/migrations/2026_03_29_131359_add_has_agenda_to_services_table.php @@ -0,0 +1,22 @@ +boolean('has_agenda')->default(false)->after('cts_data'); + }); + } + + public function down(): void + { + Schema::table('services', function (Blueprint $table) { + $table->dropColumn('has_agenda'); + }); + } +}; diff --git a/resources/js/Pages/Services/Index.vue b/resources/js/Pages/Services/Index.vue index 7922c77..6ad6b65 100644 --- a/resources/js/Pages/Services/Index.vue +++ b/resources/js/Pages/Services/Index.vue @@ -320,6 +320,11 @@ function stateIconClass(isDone) {