From 3ec8e471bcb87b24c97afe3e0322decbf9e4346e Mon Sep 17 00:00:00 2001 From: soeren Date: Tue, 2 Dec 2025 21:51:06 +0100 Subject: [PATCH] =?UTF-8?q?Verf=C3=BCgbarkeitstest=20f=C3=BCr=20API=20Prov?= =?UTF-8?q?ider=20erg=C3=A4nzt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .phpunit.result.cache | 2 +- app/Api/Plugins/ApiPluginInterface.php | 1 + app/Api/Plugins/ComfyUi.php | 54 +++++++ app/Api/Plugins/RunwareAi.php | 78 ++++++++++ .../Controllers/Api/AiStatusController.php | 79 ++++++++++ app/Models/AiModel.php | 1 + app/Models/ApiProvider.php | 20 +++ database/factories/AiModelFactory.php | 29 ++++ database/factories/ApiProviderFactory.php | 24 +++ database/factories/StyleFactory.php | 30 ++++ resources/js/Components/ImageContextMenu.vue | 93 ++++++++++-- resources/js/Components/StyleSelector.vue | 39 +++-- routes/api.php | 4 + tests/Feature/AiStatusTest.php | 137 ++++++++++++++++++ 14 files changed, 565 insertions(+), 26 deletions(-) create mode 100644 app/Http/Controllers/Api/AiStatusController.php create mode 100644 database/factories/AiModelFactory.php create mode 100644 database/factories/ApiProviderFactory.php create mode 100644 database/factories/StyleFactory.php create mode 100644 tests/Feature/AiStatusTest.php diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 9db2e54..6ad8f07 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":2,"defects":{"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":8,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":8,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":8,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":8,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":8,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":7,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":8,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":8,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":8,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":8,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":8,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_index_pages_render":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_create_pages_render":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_edit_pages_render":7},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\Auth\\AuthenticationTest::test_login_screen_can_be_rendered":2.652,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":0.844,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":0.003,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":0.003,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":0.004,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":0.003,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":0.004,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":0.003,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":0.004,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":0.011,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_screen_can_be_rendered":0.685,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":0.079,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":0.004,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":0.003,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":0.003,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":0.003,"Tests\\Feature\\Auth\\RegistrationTest::test_registration_screen_can_be_rendered":0.653,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":2.449,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":3.546,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":0.003,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":0.008,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":0.005,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":0.004,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":0.004,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_index_pages_render":6.542,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_create_pages_render":3.055,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_edit_pages_render":3.132}} \ No newline at end of file +{"version":2,"defects":{"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":8,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":8,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":8,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":8,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":8,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":7,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":8,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":8,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":8,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":8,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":8,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_index_pages_render":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_create_pages_render":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_edit_pages_render":7,"Tests\\Feature\\AiStatusTest::test_ai_status_endpoint_returns_correct_structure":8,"Tests\\Feature\\AiStatusTest::test_ai_status_update_disables_unavailable_providers":7,"Tests\\Feature\\AiStatusTest::test_comfyui_check_availability_method":8,"Tests\\Feature\\AiStatusTest::test_runwareai_check_availability_method":8},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\Auth\\AuthenticationTest::test_login_screen_can_be_rendered":2.652,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":0.844,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":0.003,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":0.003,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":0.004,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":0.003,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":0.004,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":0.003,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":0.004,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":0.011,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_screen_can_be_rendered":0.685,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":0.079,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":0.004,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":0.003,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":0.003,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":0.003,"Tests\\Feature\\Auth\\RegistrationTest::test_registration_screen_can_be_rendered":0.653,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":2.449,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":3.546,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":0.003,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":0.008,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":0.005,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":0.004,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":0.004,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_index_pages_render":6.542,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_create_pages_render":3.055,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_edit_pages_render":3.132,"Tests\\Feature\\AiStatusTest::test_ai_status_endpoint_returns_correct_structure":0.637,"Tests\\Feature\\AiStatusTest::test_ai_status_update_disables_unavailable_providers":1.569,"Tests\\Feature\\AiStatusTest::test_comfyui_check_availability_method":0.228,"Tests\\Feature\\AiStatusTest::test_runwareai_check_availability_method":0.233}} \ No newline at end of file diff --git a/app/Api/Plugins/ApiPluginInterface.php b/app/Api/Plugins/ApiPluginInterface.php index 8bc8c42..bc734b1 100644 --- a/app/Api/Plugins/ApiPluginInterface.php +++ b/app/Api/Plugins/ApiPluginInterface.php @@ -15,4 +15,5 @@ interface ApiPluginInterface public function getStyledImage(string $promptId): string; public function testConnection(array $data): bool; public function searchModels(string $searchTerm): array; + public function checkAvailability(): array; } \ No newline at end of file diff --git a/app/Api/Plugins/ComfyUi.php b/app/Api/Plugins/ComfyUi.php index aebdc78..30b6b9a 100644 --- a/app/Api/Plugins/ComfyUi.php +++ b/app/Api/Plugins/ComfyUi.php @@ -255,6 +255,60 @@ class ComfyUi implements ApiPluginInterface } } + public function checkAvailability(): array + { + $this->logInfo('Checking ComfyUI availability.'); + + if (!$this->apiProvider->enabled) { + $this->logDebug('ComfyUI provider is disabled.'); + return [ + 'available' => false, + 'reason' => 'Provider is disabled', + 'provider_id' => $this->apiProvider->id, + 'provider_name' => $this->apiProvider->name + ]; + } + + if (empty($this->apiProvider->api_url)) { + $this->logDebug('ComfyUI API URL is not configured.'); + return [ + 'available' => false, + 'reason' => 'API URL not configured', + 'provider_id' => $this->apiProvider->id, + 'provider_name' => $this->apiProvider->name + ]; + } + + try { + $response = Http::timeout(5)->get(rtrim($this->apiProvider->api_url, '/') . '/queue'); + if ($response->successful()) { + $this->logInfo('ComfyUI is available.'); + return [ + 'available' => true, + 'reason' => 'Connection successful', + 'provider_id' => $this->apiProvider->id, + 'provider_name' => $this->apiProvider->name + ]; + } else { + $this->logError('ComfyUI connection failed.', ['status' => $response->status()]); + return [ + 'available' => false, + 'reason' => 'Connection failed: ' . $response->status(), + 'provider_id' => $this->apiProvider->id, + 'provider_name' => $this->apiProvider->name + ]; + } + } catch (\Exception $e) { + $this->logError('ComfyUI availability check failed.', ['error' => $e->getMessage()]); + return [ + 'available' => false, + 'reason' => 'Connection error: ' . $e->getMessage(), + 'provider_id' => $this->apiProvider->id, + 'provider_name' => $this->apiProvider->name + ]; + } + } + public function searchModels(string $searchTerm): array { $this->logInfo('ComfyUI does not support model search. Returning empty list.', ['searchTerm' => $searchTerm]); diff --git a/app/Api/Plugins/RunwareAi.php b/app/Api/Plugins/RunwareAi.php index e15f831..cc32c97 100644 --- a/app/Api/Plugins/RunwareAi.php +++ b/app/Api/Plugins/RunwareAi.php @@ -139,6 +139,84 @@ class RunwareAi implements ApiPluginInterface } } + public function checkAvailability(): array + { + $this->logInfo('Checking RunwareAI availability.'); + + if (!$this->apiProvider->enabled) { + $this->logDebug('RunwareAI provider is disabled.'); + return [ + 'available' => false, + 'reason' => 'Provider is disabled', + 'provider_id' => $this->apiProvider->id, + 'provider_name' => $this->apiProvider->name + ]; + } + + if (empty($this->apiProvider->api_url)) { + $this->logDebug('RunwareAI API URL is not configured.'); + return [ + 'available' => false, + 'reason' => 'API URL not configured', + 'provider_id' => $this->apiProvider->id, + 'provider_name' => $this->apiProvider->name + ]; + } + + if (empty($this->apiProvider->token)) { + $this->logDebug('RunwareAI API token is not configured.'); + return [ + 'available' => false, + 'reason' => 'API token not configured', + 'provider_id' => $this->apiProvider->id, + 'provider_name' => $this->apiProvider->name + ]; + } + + try { + $response = Http::withHeaders([ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ])->timeout(5)->post(rtrim($this->apiProvider->api_url, '/'), [ + 'taskType' => 'authentication', + 'apiKey' => $this->apiProvider->token, + 'taskUUID' => (string) Str::uuid(), + ]); + + $responseData = $response->json(); + + if ($response->successful() && isset($responseData['data']) && !isset($responseData['error'])) { + $this->logInfo('RunwareAI is available.'); + return [ + 'available' => true, + 'reason' => 'Connection successful', + 'provider_id' => $this->apiProvider->id, + 'provider_name' => $this->apiProvider->name + ]; + } else { + $errorMessage = $responseData['error'] ?? 'Unknown error'; + $this->logError('RunwareAI connection failed.', [ + 'status' => $response->status(), + 'error_message' => $errorMessage + ]); + return [ + 'available' => false, + 'reason' => 'Connection failed: ' . $errorMessage, + 'provider_id' => $this->apiProvider->id, + 'provider_name' => $this->apiProvider->name + ]; + } + } catch (\Exception $e) { + $this->logError('RunwareAI availability check failed.', ['error' => $e->getMessage()]); + return [ + 'available' => false, + 'reason' => 'Connection error: ' . $e->getMessage(), + 'provider_id' => $this->apiProvider->id, + 'provider_name' => $this->apiProvider->name + ]; + } + } + public function searchModels(string $searchTerm): array { diff --git a/app/Http/Controllers/Api/AiStatusController.php b/app/Http/Controllers/Api/AiStatusController.php new file mode 100644 index 0000000..11dad1a --- /dev/null +++ b/app/Http/Controllers/Api/AiStatusController.php @@ -0,0 +1,79 @@ +get(); + $results = []; + + foreach ($providers as $provider) { + try { + $plugin = PluginLoader::getPlugin($provider->plugin, $provider); + $status = $plugin->checkAvailability(); + $results[] = $status; + } catch (\Exception $e) { + $results[] = [ + 'available' => false, + 'reason' => 'Plugin error: ' . $e->getMessage(), + 'provider_id' => $provider->id, + 'provider_name' => $provider->name + ]; + } + } + + return response()->json($results); + } + + public function checkAndUpdateStatus(Request $request) + { + $providers = ApiProvider::all(); + $anyAvailable = false; + + foreach ($providers as $provider) { + try { + $plugin = PluginLoader::getPlugin($provider->plugin, $provider); + $status = $plugin->checkAvailability(); + + if (!$status['available']) { + // Deaktiviere den Provider, wenn nicht verfügbar + $provider->enabled = false; + $provider->save(); + + // Deaktiviere alle zugehörigen Modelle + foreach ($provider->aiModels as $model) { + $model->enabled = false; + $model->save(); + + // Deaktiviere alle zugehörigen Styles + foreach ($model->styles as $style) { + $style->enabled = false; + $style->save(); + } + } + } else { + $anyAvailable = true; + // Stelle sicher, dass der Provider aktiviert ist, wenn er verfügbar ist + $provider->enabled = true; + $provider->save(); + } + } catch (\Exception $e) { + $provider->enabled = false; + $provider->save(); + } + } + + return response()->json([ + 'success' => true, + 'any_available' => $anyAvailable, + 'message' => 'AI status check and update completed' + ]); + } +} \ No newline at end of file diff --git a/app/Models/AiModel.php b/app/Models/AiModel.php index 20c5dc4..6ed85f3 100644 --- a/app/Models/AiModel.php +++ b/app/Models/AiModel.php @@ -19,6 +19,7 @@ class AiModel extends Model protected $casts = [ 'parameters' => 'array', + 'enabled' => 'boolean', ]; public function primaryApiProvider() diff --git a/app/Models/ApiProvider.php b/app/Models/ApiProvider.php index 66b3d01..acf7afa 100644 --- a/app/Models/ApiProvider.php +++ b/app/Models/ApiProvider.php @@ -23,6 +23,26 @@ class ApiProvider extends Model 'enabled' => 'boolean', ]; + public function disableWithDependencies() + { + $this->enabled = false; + $this->save(); + + // Deaktiviere alle zugehörigen Modelle + foreach ($this->aiModels as $model) { + $model->enabled = false; + $model->save(); + + // Deaktiviere alle zugehörigen Styles + foreach ($model->styles as $style) { + $style->enabled = false; + $style->save(); + } + } + + return true; + } + public function styles() { return $this->hasMany(Style::class); diff --git a/database/factories/AiModelFactory.php b/database/factories/AiModelFactory.php new file mode 100644 index 0000000..2354314 --- /dev/null +++ b/database/factories/AiModelFactory.php @@ -0,0 +1,29 @@ + $this->faker->word() . ' Model', + 'model_id' => $this->faker->uuid(), + 'model_type' => $this->faker->randomElement(['text-to-image', 'image-to-image', 'inpainting']), + 'parameters' => json_encode([ + 'steps' => 30, + 'cfg_scale' => 7.5, + 'sampler' => 'Euler a', + 'width' => 512, + 'height' => 512 + ]), + 'api_provider_id' => ApiProvider::factory(), + ]; + } +} \ No newline at end of file diff --git a/database/factories/ApiProviderFactory.php b/database/factories/ApiProviderFactory.php new file mode 100644 index 0000000..0cfa259 --- /dev/null +++ b/database/factories/ApiProviderFactory.php @@ -0,0 +1,24 @@ + $this->faker->company(), + 'api_url' => $this->faker->url(), + 'username' => $this->faker->userName(), + 'password' => $this->faker->password(), + 'token' => $this->faker->sha256(), + 'plugin' => $this->faker->randomElement(['comfyui', 'runwareai']), + 'enabled' => $this->faker->boolean(), + ]; + } +} \ No newline at end of file diff --git a/database/factories/StyleFactory.php b/database/factories/StyleFactory.php new file mode 100644 index 0000000..b9fa51d --- /dev/null +++ b/database/factories/StyleFactory.php @@ -0,0 +1,30 @@ + $this->faker->words(3, true), + 'prompt' => $this->faker->sentence(), + 'description' => $this->faker->paragraph(), + 'preview_image' => 'styles/preview.jpg', + 'parameters' => json_encode([ + 'positive' => $this->faker->sentence(), + 'negative' => $this->faker->sentence(), + 'steps' => 30, + 'cfg_scale' => 7.5 + ]), + 'ai_model_id' => AiModel::factory(), + 'enabled' => $this->faker->boolean(80), + ]; + } +} \ No newline at end of file diff --git a/resources/js/Components/ImageContextMenu.vue b/resources/js/Components/ImageContextMenu.vue index c53f187..f98cf0d 100644 --- a/resources/js/Components/ImageContextMenu.vue +++ b/resources/js/Components/ImageContextMenu.vue @@ -33,25 +33,66 @@ {{ image?.path }}

- +
+
+
+ + + AI {{ aiAvailable ? 'verfügbar' : 'nicht verfügbar' }} + + +
+
+
+
+ API-Provider Status: + + {{ aiAvailable ? 'Online' : 'Offline' }} + +
+
+ Einige Funktionen sind derzeit nicht verfügbar +
+ +
+
+
+ +