From 41e3ce6f253013d7b040908aa2e7266289062a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Burda?= Date: Fri, 9 Jan 2026 21:26:40 +0100 Subject: [PATCH] Initial commit --- .editorconfig | 18 + .env.example | 65 + .gitattributes | 11 + .gitignore | 48 +- Dockerfile | 95 + app/Actions/Fortify/CreateNewUser.php | 40 + .../Fortify/PasswordValidationRules.php | 18 + app/Actions/Fortify/ResetUserPassword.php | 29 + app/Actions/Fortify/UpdateUserPassword.php | 32 + .../Fortify/UpdateUserProfileInformation.php | 58 + app/Enums/QsoErrorCode.php | 35 + app/Http/Controllers/BandController.php | 132 + app/Http/Controllers/CategoryController.php | 138 + app/Http/Controllers/ContestController.php | 287 + .../ContestParameterController.php | 119 + app/Http/Controllers/Controller.php | 8 + app/Http/Controllers/CountryWwlController.php | 101 + app/Http/Controllers/CtyController.php | 110 + app/Http/Controllers/EdiBandController.php | 93 + .../Controllers/EdiCategoryController.php | 128 + .../EvaluationRuleSetController.php | 202 + .../Controllers/EvaluationRunController.php | 338 + app/Http/Controllers/FileController.php | 348 + app/Http/Controllers/LogController.php | 344 + .../Controllers/LogOverrideController.php | 416 + app/Http/Controllers/LogQsoController.php | 171 + app/Http/Controllers/LogResultController.php | 239 + app/Http/Controllers/LoginController.php | 39 + app/Http/Controllers/NewsPostController.php | 309 + .../Controllers/PowerCategoryController.php | 101 + .../Controllers/QsoOverrideController.php | 126 + app/Http/Controllers/QsoResultController.php | 272 + app/Http/Controllers/RoundController.php | 392 + app/Http/Controllers/UserController.php | 114 + app/Http/Middleware/SetLocaleFromCookie.php | 20 + .../Requests/StartEvaluationRunRequest.php | 77 + app/Jobs/AggregateLogResultsJob.php | 429 + app/Jobs/ApplyLogOverridesJob.php | 159 + app/Jobs/BuildWorkingSetLogJob.php | 291 + app/Jobs/DispatchAggregateResultsJobsJob.php | 95 + app/Jobs/DispatchBuildWorkingSetJobsJob.php | 129 + app/Jobs/DispatchMatchJobsJob.php | 71 + app/Jobs/DispatchParseLogsJobsJob.php | 97 + app/Jobs/DispatchScoreJobsJob.php | 56 + app/Jobs/DispatchUnpairedJobsJob.php | 105 + app/Jobs/DuplicateResolutionJob.php | 163 + app/Jobs/FinalizeRunJob.php | 141 + app/Jobs/MatchQsoBucketJob.php | 432 + app/Jobs/MatchQsoGroupJob.php | 1349 +++ app/Jobs/ParseLogJob.php | 120 + app/Jobs/PauseEvaluationRunJob.php | 56 + app/Jobs/PrepareRunJob.php | 379 + app/Jobs/RebuildClaimedLogResultsJob.php | 77 + app/Jobs/RecalculateClaimedRanksJob.php | 212 + app/Jobs/RecalculateOfficialRanksJob.php | 224 + app/Jobs/ScoreGroupJob.php | 808 ++ app/Jobs/StartEvaluationRunJob.php | 80 + app/Jobs/UnpairedClassificationBucketJob.php | 147 + app/Jobs/UnpairedClassificationJob.php | 141 + app/Jobs/UpsertClaimedLogResultJob.php | 329 + app/Models/Band.php | 32 + app/Models/Category.php | 29 + app/Models/Contest.php | 86 + app/Models/ContestParameter.php | 50 + app/Models/CountryWwl.php | 31 + app/Models/Cty.php | 38 + app/Models/EdiBand.php | 23 + app/Models/EdiCategory.php | 24 + app/Models/EvaluationLock.php | 148 + app/Models/EvaluationRuleSet.php | 479 + app/Models/EvaluationRun.php | 95 + app/Models/EvaluationRunEvent.php | 73 + app/Models/File.php | 20 + app/Models/Log.php | 116 + app/Models/LogOverride.php | 70 + app/Models/LogQso.php | 65 + app/Models/LogResult.php | 125 + app/Models/NewsPost.php | 52 + app/Models/PowerCategory.php | 19 + app/Models/QsoOverride.php | 56 + app/Models/QsoResult.php | 92 + app/Models/Round.php | 97 + app/Models/User.php | 53 + app/Models/WorkingQso.php | 56 + app/Policies/BandPolicy.php | 25 + app/Policies/CategoryPolicy.php | 25 + app/Policies/ContestParameterPolicy.php | 25 + app/Policies/ContestPolicy.php | 25 + app/Policies/CountryWwlPolicy.php | 25 + app/Policies/CtyPolicy.php | 26 + app/Policies/EdiBandPolicy.php | 26 + app/Policies/EdiCategoryPolicy.php | 25 + app/Policies/EvaluationRuleSetPolicy.php | 25 + app/Policies/EvaluationRunPolicy.php | 26 + app/Policies/FilePolicy.php | 20 + app/Policies/LogOverridePolicy.php | 24 + app/Policies/LogPolicy.php | 25 + app/Policies/LogQsoPolicy.php | 25 + app/Policies/LogResultPolicy.php | 26 + app/Policies/NewsPostPolicy.php | 25 + app/Policies/PowerCategoryPolicy.php | 25 + app/Policies/QsoOverridePolicy.php | 24 + app/Policies/QsoResultPolicy.php | 25 + app/Policies/RoundPolicy.php | 25 + app/Policies/UserPolicy.php | 33 + app/Providers/AppServiceProvider.php | 26 + app/Providers/FortifyServiceProvider.php | 48 + .../Evaluation/ClaimedRunResolver.php | 36 + app/Services/Evaluation/EdiParserService.php | 380 + .../Evaluation/EvaluationCoordinator.php | 372 + app/Services/Evaluation/MatchingService.php | 153 + .../Evaluation/OperatingWindowService.php | 397 + app/Services/Evaluation/ScoringService.php | 189 + artisan | 18 + bootstrap/app.php | 28 + bootstrap/cache/.gitignore | 2 + bootstrap/providers.php | 7 + composer.json | 93 + composer.lock | 9825 +++++++++++++++++ config/app.php | 126 + config/auth.php | 115 + config/cache.php | 117 + config/cors.php | 34 + config/database.php | 183 + config/filesystems.php | 80 + config/fortify.php | 159 + config/logging.php | 132 + config/mail.php | 118 + config/queue.php | 129 + config/sanctum.php | 85 + config/services.php | 38 + config/session.php | 217 + database/.gitignore | 1 + database/factories/BandFactory.php | 28 + database/factories/CategoryFactory.php | 22 + database/factories/ContestFactory.php | 33 + .../factories/ContestParameterFactory.php | 34 + database/factories/CountryWwlFactory.php | 22 + database/factories/CtyFactory.php | 34 + database/factories/EdiBandFactory.php | 21 + database/factories/EdiCategoryFactory.php | 21 + .../factories/EvaluationRuleSetFactory.php | 23 + database/factories/EvaluationRunFactory.php | 28 + database/factories/FileFactory.php | 29 + database/factories/LogFactory.php | 27 + database/factories/LogOverrideFactory.php | 26 + database/factories/LogQsoFactory.php | 30 + database/factories/LogResultFactory.php | 29 + database/factories/NewsPostFactory.php | 46 + database/factories/PowerCategoryFactory.php | 23 + database/factories/QsoOverrideFactory.php | 26 + database/factories/QsoResultFactory.php | 27 + database/factories/RoundFactory.php | 41 + database/factories/UserFactory.php | 58 + .../0001_01_01_000000_create_users_table.php | 50 + .../0001_01_01_000001_create_cache_table.php | 35 + .../0001_01_01_000002_create_jobs_table.php | 57 + ...03_create_personal_access_tokens_table.php | 33 + ...25_11_12_195547_create_edi_bands_table.php | 28 + .../2025_11_12_195548_create_bands_table.php | 32 + ...5_11_12_195819_create_categories_table.php | 29 + ...2_195836_create_power_categories_table.php | 30 + ...12_210154_create_bands_edi_bands_table.php | 31 + ..._12_213306_create_edi_categories_table.php | 28 + ...create_categories_edi_categories_table.php | 31 + .../2025_11_13_063129_create_cty_table.php | 42 + ...1_13_064116_create_countries_wwl_table.php | 29 + ...025_11_13_141542_create_contests_table.php | 40 + ...create_contests_power_categories_table.php | 31 + ...41626_create_contests_categories_table.php | 31 + ..._13_141638_create_contests_bands_table.php | 31 + .../2025_11_13_173633_create_rounds_table.php | 45 + ...73634_create_contests_parameters_table.php | 44 + ..._173634_create_rounds_parameters_table.php | 45 + ...11_13_181403_create_rounds_bands_table.php | 31 + ..._181429_create_rounds_categories_table.php | 31 + ...1_create_rounds_power_categories_table.php | 31 + .../2025_11_15_072705_create_files_table.php | 33 + .../2025_11_16_151949_create_logs_table.php | 72 + ...25_11_16_151953_create_logs_qsos_table.php | 59 + ...3325_create_evaluation_rule_sets_table.php | 72 + ...16_153423_create_evaluation_runs_table.php | 35 + ..._11_16_153432_create_log_results_table.php | 56 + ..._11_16_153441_create_qso_results_table.php | 54 + ..._add_two_factor_columns_to_users_table.php | 42 + ...nd_logtype_unique_to_rounds_parameters.php | 28 + ...25_11_21_000001_add_edi_fields_to_logs.php | 74 + ...000002_rename_log_columns_to_edi_names.php | 32 + .../2025_11_21_000003_add_rcall_to_logs.php | 26 + ...5_11_22_000001_update_log_qsos_for_edi.php | 54 + ...5_11_30_073728_create_news_posts_table.php | 48 + ...untime_fields_to_evaluation_runs_table.php | 53 + ...4_145353_create_evaluation_locks_table.php | 36 + ...604_create_evaluation_run_events_table.php | 35 + ...2_27_221000_create_qso_overrides_table.php | 60 + ...2_27_221100_create_log_overrides_table.php | 62 + ...00_add_power_category_id_to_logs_table.php | 26 + ..._drop_is_duplicate_from_log_qsos_table.php | 26 + ...d_result_type_to_evaluation_runs_table.php | 28 + ..._sixhr_category_to_log_overrides_table.php | 24 + ...dd_sixhr_category_to_log_results_table.php | 24 + ...0001_update_psect_length_in_logs_table.php | 22 + ..._regex_pattern_to_edi_categories_table.php | 22 + ...0000_add_ok_ranks_to_log_results_table.php | 23 + ...nique_run_log_qso_to_qso_results_table.php | 22 + ...f_fields_to_evaluation_rule_sets_table.php | 46 + ...01_15_160000_create_working_qsos_table.php | 52 + ...date_qso_results_distance_km_to_double.php | 23 + ...add_rule_set_id_to_contests_and_rounds.php | 36 + ...tensions_to_evaluation_rule_sets_table.php | 53 + ...multiplier_fields_to_log_results_table.php | 28 + ...026_01_16_000000_add_dxcc_to_cty_table.php | 24 + ...untry_and_section_to_qso_results_table.php | 23 + ..._cty_prefix_length_and_add_prefix_norm.php | 35 + ...16_120100_drop_rounds_parameters_table.php | 37 + ...dd_penalty_points_to_qso_results_table.php | 22 + ..._round_from_evaluation_rule_sets_table.php | 66 + ...s_report_to_evaluation_rule_sets_table.php | 26 + ...et_flags_to_evaluation_rule_sets_table.php | 76 + ...dq_rules_to_evaluation_rule_sets_table.php | 36 + ...add_time_diff_sec_to_qso_results_table.php | 26 + ...hreshold_to_evaluation_rule_sets_table.php | 26 + ...valuation_run_pointers_to_rounds_table.php | 53 + ...g_fields_to_evaluation_rule_sets_table.php | 49 + ..._add_match_fields_to_qso_results_table.php | 23 + ..._columns_to_evaluation_rule_sets_table.php | 38 + ...d_matching_fields_to_qso_results_table.php | 38 + ...policies_to_evaluation_rule_sets_table.php | 62 + ..._discarded_fields_to_log_results_table.php | 34 + ...01_090000_add_is_active_to_users_table.php | 23 + ...w_fields_to_evaluation_rule_sets_table.php | 27 + ...ing_window_fields_to_log_results_table.php | 30 + ...g_window_excluded_to_qso_results_table.php | 24 + ...ing_mode_to_evaluation_rule_sets_table.php | 24 + ...hr_ranking_bucket_to_log_results_table.php | 22 + ...ow_second_segment_to_log_results_table.php | 30 + database/seeders/BandEdiBandSeeder.php | 57 + database/seeders/BandSeeder.php | 132 + .../seeders/CategoriesEdiCategoriesSeeder.php | 45 + database/seeders/CategoriesSeeder.php | 35 + database/seeders/ContestSeeder.php | 97 + database/seeders/CountriesWwlSeeder.php | 33 + database/seeders/CtySeeder.php | 169 + database/seeders/DatabaseSeeder.php | 42 + database/seeders/EdiBandSeeder.php | 52 + database/seeders/EdiCategoriesSeeder.php | 44 + .../EvaluationPipelineRegressionSeeder.php | 504 + database/seeders/EvaluationRuleSetSeeder.php | 107 + database/seeders/NewsPostSeeder.php | 136 + database/seeders/PowerCategoriesSeeder.php | 36 + database/seeders/RoundSeeder.php | 88 + database/seeders/countries_wwl.csv | 934 ++ docker-compose.yml | 74 + docker/app/entrypoint.sh | 21 + docker/nginx/default.conf | 28 + docker/php/opcache.ini | 8 + docker/php/php.ini | 6 + hero.ts | 3 + package-lock.json | 8425 ++++++++++++++ package.json | 43 + phpunit.xml | 36 + public/.htaccess | 25 + public/favicon.ico | 0 public/index.php | 20 + public/robots.txt | 2 + resources/css/app.css | 8 + resources/docs/EvaluationRuleSet.md | 171 + resources/docs/prezentace_pipeline.md | 149 + resources/icons/Icons.tsx | 100 + resources/icons/types.ts | 5 + resources/js/Header.tsx | 82 + resources/js/Tail.tsx | 27 + resources/js/VkvApp.tsx | 26 + resources/js/app.tsx | 25 + resources/js/bootstrap.js | 4 + resources/js/components/AppBreadcrumbs.tsx | 84 + resources/js/components/AppErrorBoundary.tsx | 35 + resources/js/components/ContestCreateForm.tsx | 502 + resources/js/components/ContestDetail.tsx | 122 + resources/js/components/ContestsListBox.tsx | 130 + resources/js/components/ContestsOverview.tsx | 180 + resources/js/components/ContestsSelectBox.tsx | 158 + resources/js/components/ContestsTable.tsx | 261 + resources/js/components/EvaluationActions.tsx | 50 + .../js/components/EvaluationEventsList.tsx | 32 + .../js/components/EvaluationHistoryPanel.tsx | 46 + .../js/components/EvaluationStatusSummary.tsx | 63 + .../js/components/EvaluationStepsList.tsx | 40 + resources/js/components/FileDropZone.tsx | 62 + resources/js/components/LanguageSwitcher.tsx | 38 + resources/js/components/LogDetail.tsx | 621 ++ resources/js/components/LogQsoTable.tsx | 291 + resources/js/components/LoginDialog.tsx | 206 + resources/js/components/LogsTable.tsx | 246 + resources/js/components/NewsList.tsx | 148 + resources/js/components/ResultsTables.tsx | 640 ++ resources/js/components/RoundCreateForm.tsx | 624 ++ resources/js/components/RoundDetail.tsx | 190 + .../RoundEvaluationLogOverrides.tsx | 341 + .../RoundEvaluationLogOverrides.types.ts | 64 + .../RoundEvaluationLogOverridesModal.tsx | 218 + .../RoundEvaluationLogOverridesSearch.tsx | 21 + .../RoundEvaluationLogOverridesTable.tsx | 300 + .../RoundEvaluationOverrideDetailModal.tsx | 28 + .../components/RoundEvaluationOverrideRow.tsx | 249 + .../components/RoundEvaluationOverrides.tsx | 354 + .../RoundEvaluationOverrides.types.ts | 40 + .../RoundEvaluationOverridesPagination.tsx | 36 + .../js/components/RoundEvaluationPanel.tsx | 153 + .../RoundEvaluationQsoOverrides.tsx | 1261 +++ resources/js/components/RoundFileUpload.tsx | 1015 ++ .../RoundFileUpload/HeaderFormFields.tsx | 325 + .../RoundFileUpload/UploadMessages.tsx | 32 + resources/js/components/RoundsOverview.tsx | 194 + resources/js/components/RoundsTable.tsx | 449 + resources/js/components/ThemeSwitch.tsx | 81 + .../components/admin/news/AdminNewsForm.tsx | 192 + .../components/admin/news/AdminNewsTable.tsx | 112 + .../components/admin/news/adminNewsTypes.ts | 21 + .../admin/rulesets/AdminRulesetForm.tsx | 1325 +++ .../admin/rulesets/AdminRulesetsTable.tsx | 74 + .../admin/rulesets/adminRulesetTypes.ts | 259 + .../components/admin/users/AdminUserForm.tsx | 137 + .../admin/users/AdminUsersTable.tsx | 67 + .../components/layout/ContestsLeftPanel.tsx | 100 + resources/js/fonts.ts | 11 + resources/js/global.css | 0 resources/js/hooks/useRoundEvaluationRun.ts | 326 + resources/js/hooks/useRoundMeta.ts | 151 + resources/js/i18n.ts | 31 + resources/js/layouts/TwoPaneLayout.tsx | 139 + resources/js/locales/cs/common.json | 448 + resources/js/locales/cs/ruleset.json | 78 + resources/js/locales/en/common.json | 444 + resources/js/locales/en/ruleset.json | 78 + resources/js/pages/AboutPage.tsx | 3 + resources/js/pages/AdminContestsPage.tsx | 181 + resources/js/pages/AdminEvaluationPage.tsx | 514 + resources/js/pages/AdminNewsPage.tsx | 143 + resources/js/pages/AdminPage.tsx | 13 + resources/js/pages/AdminUsersPage.tsx | 184 + resources/js/pages/ContestDetailPage.tsx | 42 + resources/js/pages/ContestsIndexPage.tsx | 26 + resources/js/pages/LogDetailPage.tsx | 36 + resources/js/pages/LoginPage.tsx | 12 + resources/js/pages/RoundDetailPage.tsx | 223 + resources/js/routes.tsx | 71 + resources/js/stores/contestRefreshStore.tsx | 12 + resources/js/stores/contestStore.tsx | 87 + resources/js/stores/languageStore.tsx | 69 + resources/js/stores/userStore.tsx | 37 + resources/js/types/edi.ts | 25 + resources/js/utils/ediFileValidation.ts | 297 + resources/js/utils/ediValidation.ts | 355 + resources/views/app.blade.php | 38 + routes/api.php | 93 + routes/console.php | 8 + routes/web.php | 13 + storage/app/.gitignore | 4 + storage/app/private/.gitignore | 2 + storage/app/public/.gitignore | 2 + storage/data/cty_wt.dat.gz | Bin 0 -> 40875 bytes storage/data/cty_wt_mod.dat.gz | Bin 0 -> 114149 bytes storage/framework/.gitignore | 9 + storage/framework/cache/.gitignore | 3 + storage/framework/cache/data/.gitignore | 2 + storage/framework/sessions/.gitignore | 2 + storage/framework/testing/.gitignore | 2 + storage/framework/views/.gitignore | 2 + storage/logs/.gitignore | 2 + tests/Feature/Admin/UserControllerTest.php | 96 + tests/Feature/Auth/LoginControllerTest.php | 43 + tests/Feature/Catalog/BandControllerTest.php | 74 + .../Catalog/CategoryControllerTest.php | 65 + .../ContestParameterControllerTest.php | 100 + .../Catalog/CountryWwlControllerTest.php | 46 + tests/Feature/Catalog/CtyControllerTest.php | 94 + .../Feature/Catalog/EdiBandControllerTest.php | 62 + .../Catalog/EdiCategoryControllerTest.php | 62 + .../EvaluationRuleSetControllerTest.php | 76 + .../Catalog/PowerCategoryControllerTest.php | 68 + .../Contests/ContestControllerTest.php | 82 + .../Evaluation/AggregateLogResultsJobTest.php | 160 + .../EvaluationRunControllerTest.php | 72 + .../RecalculateOfficialRanksSixhrModeTest.php | 102 + tests/Feature/ExampleTest.php | 7 + tests/Feature/Files/FileControllerTest.php | 86 + tests/Feature/Logs/LogControllerTest.php | 82 + tests/Feature/Logs/LogQsoControllerTest.php | 84 + tests/Feature/Logs/LogQsoTableTest.php | 119 + tests/Feature/News/NewsPostControllerTest.php | 73 + .../Results/LogOverrideControllerTest.php | 90 + .../Results/LogResultControllerTest.php | 141 + .../Results/QsoOverrideControllerTest.php | 90 + .../Results/QsoResultControllerTest.php | 85 + tests/Feature/Rounds/RoundControllerTest.php | 104 + tests/Pest.php | 47 + tests/Support/ActsAsUser.php | 38 + tests/Support/CreatesDomainData.php | 144 + tests/TestCase.php | 13 + tests/Unit/ExampleTest.php | 5 + tests/Unit/OperatingWindowServiceTest.php | 160 + tsconfig.json | 23 + vite.config.js | 18 + 404 files changed, 61250 insertions(+), 28 deletions(-) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 Dockerfile create mode 100644 app/Actions/Fortify/CreateNewUser.php create mode 100644 app/Actions/Fortify/PasswordValidationRules.php create mode 100644 app/Actions/Fortify/ResetUserPassword.php create mode 100644 app/Actions/Fortify/UpdateUserPassword.php create mode 100644 app/Actions/Fortify/UpdateUserProfileInformation.php create mode 100644 app/Enums/QsoErrorCode.php create mode 100644 app/Http/Controllers/BandController.php create mode 100644 app/Http/Controllers/CategoryController.php create mode 100644 app/Http/Controllers/ContestController.php create mode 100644 app/Http/Controllers/ContestParameterController.php create mode 100644 app/Http/Controllers/Controller.php create mode 100644 app/Http/Controllers/CountryWwlController.php create mode 100644 app/Http/Controllers/CtyController.php create mode 100644 app/Http/Controllers/EdiBandController.php create mode 100644 app/Http/Controllers/EdiCategoryController.php create mode 100644 app/Http/Controllers/EvaluationRuleSetController.php create mode 100644 app/Http/Controllers/EvaluationRunController.php create mode 100644 app/Http/Controllers/FileController.php create mode 100644 app/Http/Controllers/LogController.php create mode 100644 app/Http/Controllers/LogOverrideController.php create mode 100644 app/Http/Controllers/LogQsoController.php create mode 100644 app/Http/Controllers/LogResultController.php create mode 100644 app/Http/Controllers/LoginController.php create mode 100644 app/Http/Controllers/NewsPostController.php create mode 100644 app/Http/Controllers/PowerCategoryController.php create mode 100644 app/Http/Controllers/QsoOverrideController.php create mode 100644 app/Http/Controllers/QsoResultController.php create mode 100644 app/Http/Controllers/RoundController.php create mode 100644 app/Http/Controllers/UserController.php create mode 100644 app/Http/Middleware/SetLocaleFromCookie.php create mode 100644 app/Http/Requests/StartEvaluationRunRequest.php create mode 100644 app/Jobs/AggregateLogResultsJob.php create mode 100644 app/Jobs/ApplyLogOverridesJob.php create mode 100644 app/Jobs/BuildWorkingSetLogJob.php create mode 100644 app/Jobs/DispatchAggregateResultsJobsJob.php create mode 100644 app/Jobs/DispatchBuildWorkingSetJobsJob.php create mode 100644 app/Jobs/DispatchMatchJobsJob.php create mode 100644 app/Jobs/DispatchParseLogsJobsJob.php create mode 100644 app/Jobs/DispatchScoreJobsJob.php create mode 100644 app/Jobs/DispatchUnpairedJobsJob.php create mode 100644 app/Jobs/DuplicateResolutionJob.php create mode 100644 app/Jobs/FinalizeRunJob.php create mode 100644 app/Jobs/MatchQsoBucketJob.php create mode 100644 app/Jobs/MatchQsoGroupJob.php create mode 100644 app/Jobs/ParseLogJob.php create mode 100644 app/Jobs/PauseEvaluationRunJob.php create mode 100644 app/Jobs/PrepareRunJob.php create mode 100644 app/Jobs/RebuildClaimedLogResultsJob.php create mode 100644 app/Jobs/RecalculateClaimedRanksJob.php create mode 100644 app/Jobs/RecalculateOfficialRanksJob.php create mode 100644 app/Jobs/ScoreGroupJob.php create mode 100644 app/Jobs/StartEvaluationRunJob.php create mode 100644 app/Jobs/UnpairedClassificationBucketJob.php create mode 100644 app/Jobs/UnpairedClassificationJob.php create mode 100644 app/Jobs/UpsertClaimedLogResultJob.php create mode 100644 app/Models/Band.php create mode 100644 app/Models/Category.php create mode 100644 app/Models/Contest.php create mode 100644 app/Models/ContestParameter.php create mode 100644 app/Models/CountryWwl.php create mode 100644 app/Models/Cty.php create mode 100644 app/Models/EdiBand.php create mode 100644 app/Models/EdiCategory.php create mode 100644 app/Models/EvaluationLock.php create mode 100644 app/Models/EvaluationRuleSet.php create mode 100644 app/Models/EvaluationRun.php create mode 100644 app/Models/EvaluationRunEvent.php create mode 100644 app/Models/File.php create mode 100644 app/Models/Log.php create mode 100644 app/Models/LogOverride.php create mode 100644 app/Models/LogQso.php create mode 100644 app/Models/LogResult.php create mode 100644 app/Models/NewsPost.php create mode 100644 app/Models/PowerCategory.php create mode 100644 app/Models/QsoOverride.php create mode 100644 app/Models/QsoResult.php create mode 100644 app/Models/Round.php create mode 100644 app/Models/User.php create mode 100644 app/Models/WorkingQso.php create mode 100644 app/Policies/BandPolicy.php create mode 100644 app/Policies/CategoryPolicy.php create mode 100644 app/Policies/ContestParameterPolicy.php create mode 100644 app/Policies/ContestPolicy.php create mode 100644 app/Policies/CountryWwlPolicy.php create mode 100644 app/Policies/CtyPolicy.php create mode 100644 app/Policies/EdiBandPolicy.php create mode 100644 app/Policies/EdiCategoryPolicy.php create mode 100644 app/Policies/EvaluationRuleSetPolicy.php create mode 100644 app/Policies/EvaluationRunPolicy.php create mode 100644 app/Policies/FilePolicy.php create mode 100644 app/Policies/LogOverridePolicy.php create mode 100644 app/Policies/LogPolicy.php create mode 100644 app/Policies/LogQsoPolicy.php create mode 100644 app/Policies/LogResultPolicy.php create mode 100644 app/Policies/NewsPostPolicy.php create mode 100644 app/Policies/PowerCategoryPolicy.php create mode 100644 app/Policies/QsoOverridePolicy.php create mode 100644 app/Policies/QsoResultPolicy.php create mode 100644 app/Policies/RoundPolicy.php create mode 100644 app/Policies/UserPolicy.php create mode 100644 app/Providers/AppServiceProvider.php create mode 100644 app/Providers/FortifyServiceProvider.php create mode 100644 app/Services/Evaluation/ClaimedRunResolver.php create mode 100644 app/Services/Evaluation/EdiParserService.php create mode 100644 app/Services/Evaluation/EvaluationCoordinator.php create mode 100644 app/Services/Evaluation/MatchingService.php create mode 100644 app/Services/Evaluation/OperatingWindowService.php create mode 100644 app/Services/Evaluation/ScoringService.php create mode 100755 artisan create mode 100644 bootstrap/app.php create mode 100644 bootstrap/cache/.gitignore create mode 100644 bootstrap/providers.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config/app.php create mode 100644 config/auth.php create mode 100644 config/cache.php create mode 100644 config/cors.php create mode 100644 config/database.php create mode 100644 config/filesystems.php create mode 100644 config/fortify.php create mode 100644 config/logging.php create mode 100644 config/mail.php create mode 100644 config/queue.php create mode 100644 config/sanctum.php create mode 100644 config/services.php create mode 100644 config/session.php create mode 100644 database/.gitignore create mode 100644 database/factories/BandFactory.php create mode 100644 database/factories/CategoryFactory.php create mode 100644 database/factories/ContestFactory.php create mode 100644 database/factories/ContestParameterFactory.php create mode 100644 database/factories/CountryWwlFactory.php create mode 100644 database/factories/CtyFactory.php create mode 100644 database/factories/EdiBandFactory.php create mode 100644 database/factories/EdiCategoryFactory.php create mode 100644 database/factories/EvaluationRuleSetFactory.php create mode 100644 database/factories/EvaluationRunFactory.php create mode 100644 database/factories/FileFactory.php create mode 100644 database/factories/LogFactory.php create mode 100644 database/factories/LogOverrideFactory.php create mode 100644 database/factories/LogQsoFactory.php create mode 100644 database/factories/LogResultFactory.php create mode 100644 database/factories/NewsPostFactory.php create mode 100644 database/factories/PowerCategoryFactory.php create mode 100644 database/factories/QsoOverrideFactory.php create mode 100644 database/factories/QsoResultFactory.php create mode 100644 database/factories/RoundFactory.php create mode 100644 database/factories/UserFactory.php create mode 100644 database/migrations/0001_01_01_000000_create_users_table.php create mode 100644 database/migrations/0001_01_01_000001_create_cache_table.php create mode 100644 database/migrations/0001_01_01_000002_create_jobs_table.php create mode 100644 database/migrations/0001_01_01_000003_create_personal_access_tokens_table.php create mode 100644 database/migrations/2025_11_12_195547_create_edi_bands_table.php create mode 100644 database/migrations/2025_11_12_195548_create_bands_table.php create mode 100644 database/migrations/2025_11_12_195819_create_categories_table.php create mode 100644 database/migrations/2025_11_12_195836_create_power_categories_table.php create mode 100644 database/migrations/2025_11_12_210154_create_bands_edi_bands_table.php create mode 100644 database/migrations/2025_11_12_213306_create_edi_categories_table.php create mode 100644 database/migrations/2025_11_12_213319_create_categories_edi_categories_table.php create mode 100644 database/migrations/2025_11_13_063129_create_cty_table.php create mode 100644 database/migrations/2025_11_13_064116_create_countries_wwl_table.php create mode 100644 database/migrations/2025_11_13_141542_create_contests_table.php create mode 100644 database/migrations/2025_11_13_141608_create_contests_power_categories_table.php create mode 100644 database/migrations/2025_11_13_141626_create_contests_categories_table.php create mode 100644 database/migrations/2025_11_13_141638_create_contests_bands_table.php create mode 100644 database/migrations/2025_11_13_173633_create_rounds_table.php create mode 100644 database/migrations/2025_11_13_173634_create_contests_parameters_table.php create mode 100644 database/migrations/2025_11_13_173634_create_rounds_parameters_table.php create mode 100644 database/migrations/2025_11_13_181403_create_rounds_bands_table.php create mode 100644 database/migrations/2025_11_13_181429_create_rounds_categories_table.php create mode 100644 database/migrations/2025_11_13_181441_create_rounds_power_categories_table.php create mode 100644 database/migrations/2025_11_15_072705_create_files_table.php create mode 100644 database/migrations/2025_11_16_151949_create_logs_table.php create mode 100644 database/migrations/2025_11_16_151953_create_logs_qsos_table.php create mode 100644 database/migrations/2025_11_16_153325_create_evaluation_rule_sets_table.php create mode 100644 database/migrations/2025_11_16_153423_create_evaluation_runs_table.php create mode 100644 database/migrations/2025_11_16_153432_create_log_results_table.php create mode 100644 database/migrations/2025_11_16_153441_create_qso_results_table.php create mode 100644 database/migrations/2025_11_16_202516_add_two_factor_columns_to_users_table.php create mode 100644 database/migrations/2025_11_20_000001_add_round_logtype_unique_to_rounds_parameters.php create mode 100644 database/migrations/2025_11_21_000001_add_edi_fields_to_logs.php create mode 100644 database/migrations/2025_11_21_000002_rename_log_columns_to_edi_names.php create mode 100644 database/migrations/2025_11_21_000003_add_rcall_to_logs.php create mode 100644 database/migrations/2025_11_22_000001_update_log_qsos_for_edi.php create mode 100644 database/migrations/2025_11_30_073728_create_news_posts_table.php create mode 100644 database/migrations/2025_12_24_145237_add_runtime_fields_to_evaluation_runs_table.php create mode 100644 database/migrations/2025_12_24_145353_create_evaluation_locks_table.php create mode 100644 database/migrations/2025_12_24_145604_create_evaluation_run_events_table.php create mode 100644 database/migrations/2025_12_27_221000_create_qso_overrides_table.php create mode 100644 database/migrations/2025_12_27_221100_create_log_overrides_table.php create mode 100644 database/migrations/2025_12_29_162500_add_power_category_id_to_logs_table.php create mode 100644 database/migrations/2025_12_30_210500_drop_is_duplicate_from_log_qsos_table.php create mode 100644 database/migrations/2025_12_30_220000_add_result_type_to_evaluation_runs_table.php create mode 100644 database/migrations/2025_12_30_220200_add_forced_sixhr_category_to_log_overrides_table.php create mode 100644 database/migrations/2025_12_30_220210_add_sixhr_category_to_log_results_table.php create mode 100644 database/migrations/2026_01_15_000001_update_psect_length_in_logs_table.php create mode 100644 database/migrations/2026_01_15_100000_add_regex_pattern_to_edi_categories_table.php create mode 100644 database/migrations/2026_01_15_130000_add_ok_ranks_to_log_results_table.php create mode 100644 database/migrations/2026_01_15_140000_add_unique_run_log_qso_to_qso_results_table.php create mode 100644 database/migrations/2026_01_15_150000_add_vhf_fields_to_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_01_15_160000_create_working_qsos_table.php create mode 100644 database/migrations/2026_01_15_160100_update_qso_results_distance_km_to_double.php create mode 100644 database/migrations/2026_01_15_170000_add_rule_set_id_to_contests_and_rounds.php create mode 100644 database/migrations/2026_01_15_170000_add_vhf_ruleset_extensions_to_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_01_15_171000_add_multiplier_fields_to_log_results_table.php create mode 100644 database/migrations/2026_01_16_000000_add_dxcc_to_cty_table.php create mode 100644 database/migrations/2026_01_16_000100_add_country_and_section_to_qso_results_table.php create mode 100644 database/migrations/2026_01_16_000200_update_cty_prefix_length_and_add_prefix_norm.php create mode 100644 database/migrations/2026_01_16_120100_drop_rounds_parameters_table.php create mode 100644 database/migrations/2026_01_16_130000_add_penalty_points_to_qso_results_table.php create mode 100644 database/migrations/2026_01_16_140000_drop_contest_and_round_from_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_01_16_150000_add_exchange_requires_report_to_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_01_16_160000_add_missing_ruleset_flags_to_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_01_16_170000_add_time_diff_dq_rules_to_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_01_16_170100_add_time_diff_sec_to_qso_results_table.php create mode 100644 database/migrations/2026_01_16_171000_add_bad_qso_dq_threshold_to_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_01_16_171001_add_evaluation_run_pointers_to_rounds_table.php create mode 100644 database/migrations/2026_01_16_190000_add_matching_fields_to_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_01_16_190100_add_match_fields_to_qso_results_table.php create mode 100644 database/migrations/2026_01_16_210000_add_matching_columns_to_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_01_16_211000_add_matching_fields_to_qso_results_table.php create mode 100644 database/migrations/2026_01_17_000000_add_unpaired_and_busted_policies_to_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_01_17_120000_add_discarded_fields_to_log_results_table.php create mode 100644 database/migrations/2026_02_01_090000_add_is_active_to_users_table.php create mode 100644 database/migrations/2026_02_10_120000_add_operating_window_fields_to_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_02_10_120100_add_operating_window_fields_to_log_results_table.php create mode 100644 database/migrations/2026_02_10_120200_add_operating_window_excluded_to_qso_results_table.php create mode 100644 database/migrations/2026_02_10_120400_add_sixhr_ranking_mode_to_evaluation_rule_sets_table.php create mode 100644 database/migrations/2026_02_10_120500_add_sixhr_ranking_bucket_to_log_results_table.php create mode 100644 database/migrations/2026_02_10_120600_add_operating_window_second_segment_to_log_results_table.php create mode 100644 database/seeders/BandEdiBandSeeder.php create mode 100644 database/seeders/BandSeeder.php create mode 100644 database/seeders/CategoriesEdiCategoriesSeeder.php create mode 100644 database/seeders/CategoriesSeeder.php create mode 100644 database/seeders/ContestSeeder.php create mode 100644 database/seeders/CountriesWwlSeeder.php create mode 100644 database/seeders/CtySeeder.php create mode 100644 database/seeders/DatabaseSeeder.php create mode 100644 database/seeders/EdiBandSeeder.php create mode 100644 database/seeders/EdiCategoriesSeeder.php create mode 100644 database/seeders/EvaluationPipelineRegressionSeeder.php create mode 100644 database/seeders/EvaluationRuleSetSeeder.php create mode 100644 database/seeders/NewsPostSeeder.php create mode 100644 database/seeders/PowerCategoriesSeeder.php create mode 100644 database/seeders/RoundSeeder.php create mode 100644 database/seeders/countries_wwl.csv create mode 100644 docker-compose.yml create mode 100644 docker/app/entrypoint.sh create mode 100644 docker/nginx/default.conf create mode 100644 docker/php/opcache.ini create mode 100644 docker/php/php.ini create mode 100644 hero.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 phpunit.xml create mode 100644 public/.htaccess create mode 100644 public/favicon.ico create mode 100644 public/index.php create mode 100644 public/robots.txt create mode 100644 resources/css/app.css create mode 100644 resources/docs/EvaluationRuleSet.md create mode 100644 resources/docs/prezentace_pipeline.md create mode 100644 resources/icons/Icons.tsx create mode 100644 resources/icons/types.ts create mode 100644 resources/js/Header.tsx create mode 100644 resources/js/Tail.tsx create mode 100644 resources/js/VkvApp.tsx create mode 100644 resources/js/app.tsx create mode 100644 resources/js/bootstrap.js create mode 100644 resources/js/components/AppBreadcrumbs.tsx create mode 100644 resources/js/components/AppErrorBoundary.tsx create mode 100644 resources/js/components/ContestCreateForm.tsx create mode 100644 resources/js/components/ContestDetail.tsx create mode 100644 resources/js/components/ContestsListBox.tsx create mode 100644 resources/js/components/ContestsOverview.tsx create mode 100644 resources/js/components/ContestsSelectBox.tsx create mode 100644 resources/js/components/ContestsTable.tsx create mode 100644 resources/js/components/EvaluationActions.tsx create mode 100644 resources/js/components/EvaluationEventsList.tsx create mode 100644 resources/js/components/EvaluationHistoryPanel.tsx create mode 100644 resources/js/components/EvaluationStatusSummary.tsx create mode 100644 resources/js/components/EvaluationStepsList.tsx create mode 100644 resources/js/components/FileDropZone.tsx create mode 100644 resources/js/components/LanguageSwitcher.tsx create mode 100644 resources/js/components/LogDetail.tsx create mode 100644 resources/js/components/LogQsoTable.tsx create mode 100644 resources/js/components/LoginDialog.tsx create mode 100644 resources/js/components/LogsTable.tsx create mode 100644 resources/js/components/NewsList.tsx create mode 100644 resources/js/components/ResultsTables.tsx create mode 100644 resources/js/components/RoundCreateForm.tsx create mode 100644 resources/js/components/RoundDetail.tsx create mode 100644 resources/js/components/RoundEvaluationLogOverrides.tsx create mode 100644 resources/js/components/RoundEvaluationLogOverrides.types.ts create mode 100644 resources/js/components/RoundEvaluationLogOverridesModal.tsx create mode 100644 resources/js/components/RoundEvaluationLogOverridesSearch.tsx create mode 100644 resources/js/components/RoundEvaluationLogOverridesTable.tsx create mode 100644 resources/js/components/RoundEvaluationOverrideDetailModal.tsx create mode 100644 resources/js/components/RoundEvaluationOverrideRow.tsx create mode 100644 resources/js/components/RoundEvaluationOverrides.tsx create mode 100644 resources/js/components/RoundEvaluationOverrides.types.ts create mode 100644 resources/js/components/RoundEvaluationOverridesPagination.tsx create mode 100644 resources/js/components/RoundEvaluationPanel.tsx create mode 100644 resources/js/components/RoundEvaluationQsoOverrides.tsx create mode 100644 resources/js/components/RoundFileUpload.tsx create mode 100644 resources/js/components/RoundFileUpload/HeaderFormFields.tsx create mode 100644 resources/js/components/RoundFileUpload/UploadMessages.tsx create mode 100644 resources/js/components/RoundsOverview.tsx create mode 100644 resources/js/components/RoundsTable.tsx create mode 100644 resources/js/components/ThemeSwitch.tsx create mode 100644 resources/js/components/admin/news/AdminNewsForm.tsx create mode 100644 resources/js/components/admin/news/AdminNewsTable.tsx create mode 100644 resources/js/components/admin/news/adminNewsTypes.ts create mode 100644 resources/js/components/admin/rulesets/AdminRulesetForm.tsx create mode 100644 resources/js/components/admin/rulesets/AdminRulesetsTable.tsx create mode 100644 resources/js/components/admin/rulesets/adminRulesetTypes.ts create mode 100644 resources/js/components/admin/users/AdminUserForm.tsx create mode 100644 resources/js/components/admin/users/AdminUsersTable.tsx create mode 100644 resources/js/components/layout/ContestsLeftPanel.tsx create mode 100644 resources/js/fonts.ts create mode 100644 resources/js/global.css create mode 100644 resources/js/hooks/useRoundEvaluationRun.ts create mode 100644 resources/js/hooks/useRoundMeta.ts create mode 100644 resources/js/i18n.ts create mode 100644 resources/js/layouts/TwoPaneLayout.tsx create mode 100644 resources/js/locales/cs/common.json create mode 100644 resources/js/locales/cs/ruleset.json create mode 100644 resources/js/locales/en/common.json create mode 100644 resources/js/locales/en/ruleset.json create mode 100644 resources/js/pages/AboutPage.tsx create mode 100644 resources/js/pages/AdminContestsPage.tsx create mode 100644 resources/js/pages/AdminEvaluationPage.tsx create mode 100644 resources/js/pages/AdminNewsPage.tsx create mode 100644 resources/js/pages/AdminPage.tsx create mode 100644 resources/js/pages/AdminUsersPage.tsx create mode 100644 resources/js/pages/ContestDetailPage.tsx create mode 100644 resources/js/pages/ContestsIndexPage.tsx create mode 100644 resources/js/pages/LogDetailPage.tsx create mode 100644 resources/js/pages/LoginPage.tsx create mode 100644 resources/js/pages/RoundDetailPage.tsx create mode 100644 resources/js/routes.tsx create mode 100644 resources/js/stores/contestRefreshStore.tsx create mode 100644 resources/js/stores/contestStore.tsx create mode 100644 resources/js/stores/languageStore.tsx create mode 100644 resources/js/stores/userStore.tsx create mode 100644 resources/js/types/edi.ts create mode 100644 resources/js/utils/ediFileValidation.ts create mode 100644 resources/js/utils/ediValidation.ts create mode 100644 resources/views/app.blade.php create mode 100644 routes/api.php create mode 100644 routes/console.php create mode 100644 routes/web.php create mode 100644 storage/app/.gitignore create mode 100644 storage/app/private/.gitignore create mode 100644 storage/app/public/.gitignore create mode 100644 storage/data/cty_wt.dat.gz create mode 100644 storage/data/cty_wt_mod.dat.gz create mode 100644 storage/framework/.gitignore create mode 100644 storage/framework/cache/.gitignore create mode 100644 storage/framework/cache/data/.gitignore create mode 100644 storage/framework/sessions/.gitignore create mode 100644 storage/framework/testing/.gitignore create mode 100644 storage/framework/views/.gitignore create mode 100644 storage/logs/.gitignore create mode 100644 tests/Feature/Admin/UserControllerTest.php create mode 100644 tests/Feature/Auth/LoginControllerTest.php create mode 100644 tests/Feature/Catalog/BandControllerTest.php create mode 100644 tests/Feature/Catalog/CategoryControllerTest.php create mode 100644 tests/Feature/Catalog/ContestParameterControllerTest.php create mode 100644 tests/Feature/Catalog/CountryWwlControllerTest.php create mode 100644 tests/Feature/Catalog/CtyControllerTest.php create mode 100644 tests/Feature/Catalog/EdiBandControllerTest.php create mode 100644 tests/Feature/Catalog/EdiCategoryControllerTest.php create mode 100644 tests/Feature/Catalog/EvaluationRuleSetControllerTest.php create mode 100644 tests/Feature/Catalog/PowerCategoryControllerTest.php create mode 100644 tests/Feature/Contests/ContestControllerTest.php create mode 100644 tests/Feature/Evaluation/AggregateLogResultsJobTest.php create mode 100644 tests/Feature/Evaluation/EvaluationRunControllerTest.php create mode 100644 tests/Feature/Evaluation/RecalculateOfficialRanksSixhrModeTest.php create mode 100644 tests/Feature/ExampleTest.php create mode 100644 tests/Feature/Files/FileControllerTest.php create mode 100644 tests/Feature/Logs/LogControllerTest.php create mode 100644 tests/Feature/Logs/LogQsoControllerTest.php create mode 100644 tests/Feature/Logs/LogQsoTableTest.php create mode 100644 tests/Feature/News/NewsPostControllerTest.php create mode 100644 tests/Feature/Results/LogOverrideControllerTest.php create mode 100644 tests/Feature/Results/LogResultControllerTest.php create mode 100644 tests/Feature/Results/QsoOverrideControllerTest.php create mode 100644 tests/Feature/Results/QsoResultControllerTest.php create mode 100644 tests/Feature/Rounds/RoundControllerTest.php create mode 100644 tests/Pest.php create mode 100644 tests/Support/ActsAsUser.php create mode 100644 tests/Support/CreatesDomainData.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/ExampleTest.php create mode 100644 tests/Unit/OperatingWindowServiceTest.php create mode 100644 tsconfig.json create mode 100644 vite.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a186cd2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[compose.yaml] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2ed0376 --- /dev/null +++ b/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mariadb +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=vkv +DB_USERNAME=vkv +DB_PASSWORD=vkv + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/.gitignore b/.gitignore index a703d74..b71b1ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,24 @@ -# ---> Laravel -/vendor/ -node_modules/ -npm-debug.log -yarn-error.log - -# Laravel 4 specific -bootstrap/compiled.php -app/storage/ - -# Laravel 5 & Lumen specific -public/storage -public/hot - -# Laravel 5 & Lumen specific with changed public path -public_html/storage -public_html/hot - -storage/*.key +*.log +.DS_Store .env -Homestead.yaml -Homestead.json -/.vagrant -.phpunit.result.cache - -/public/build -/storage/pail .env.backup .env.production .phpactor.json -auth.json - +.phpunit.result.cache +/.fleet +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6d9ffad --- /dev/null +++ b/Dockerfile @@ -0,0 +1,95 @@ +# Dockerfile +# Multi-stage build for Laravel 12 + React SPA (Vite) +# Targets: +# - app: php-fpm runtime with vendor + built assets +# - nginx: separate container (uses official nginx image, see docker-compose + nginx.conf) + +############################ +# 1) Node build (Vite) +############################ +FROM node:20-alpine AS node-build +WORKDIR /app + +# Install deps +COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./ +RUN \ + if [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable && pnpm i --frozen-lockfile; \ + elif [ -f yarn.lock ]; then yarn install --frozen-lockfile; \ + else npm i; fi + +# Build +COPY resources ./resources +COPY public ./public +COPY vite.config.* hero.ts tailwind.config.* postcss.config.* tsconfig.* ./ +RUN \ + if [ -f package-lock.json ]; then npm run build; \ + elif [ -f pnpm-lock.yaml ]; then pnpm build; \ + elif [ -f yarn.lock ]; then yarn build; \ + else npm run build; fi + +############################ +# 2) Composer deps +############################ +FROM composer:2 AS composer-build +WORKDIR /app +COPY composer.json composer.lock ./ +RUN composer install --no-dev --prefer-dist --no-interaction --no-progress --optimize-autoloader --no-scripts + +############################ +# 3) PHP runtime (php-fpm) +############################ +FROM php:8.3-fpm-alpine AS app + +WORKDIR /var/www/html + +# System deps + PHP extensions +RUN apk add --no-cache \ + bash icu-dev libzip-dev oniguruma-dev zlib-dev \ + freetype-dev libjpeg-turbo-dev libpng-dev \ + git curl \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j$(nproc) \ + pdo_mysql mbstring intl zip bcmath exif gd opcache \ + && rm -rf /var/cache/apk/* + +# PHP configuration (opcache tuned for prod) +COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini +COPY docker/php/php.ini /usr/local/etc/php/conf.d/php.ini + +# Copy application code +COPY . . + +# Copy vendor from composer stage +COPY --from=composer-build /app/vendor ./vendor + +# Copy built assets from node stage (Vite -> public/build by default) +COPY --from=node-build /app/public/build ./public/build + +# Ensure writable dirs +RUN mkdir -p storage bootstrap/cache \ + && chown -R www-data:www-data storage bootstrap/cache + +# Entrypoint: caches/migrations optional; starts php-fpm +COPY docker/app/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +USER www-data + +EXPOSE 9000 +ENTRYPOINT ["/entrypoint.sh"] +CMD ["php-fpm", "-F"] + +############################ +# 4) Nginx runtime (static + proxy to php-fpm) +############################ +FROM nginx:1.27-alpine AS nginx + +WORKDIR /var/www/html + +# Nginx config +COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf + +# Public files (incl. built Vite assets) +COPY public ./public +COPY --from=node-build /app/public/build ./public/build diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php new file mode 100644 index 0000000..7bf18d0 --- /dev/null +++ b/app/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,40 @@ + $input + */ + public function create(array $input): User + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique(User::class), + ], + 'password' => $this->passwordRules(), + ])->validate(); + + return User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => Hash::make($input['password']), + ]); + } +} diff --git a/app/Actions/Fortify/PasswordValidationRules.php b/app/Actions/Fortify/PasswordValidationRules.php new file mode 100644 index 0000000..76b19d3 --- /dev/null +++ b/app/Actions/Fortify/PasswordValidationRules.php @@ -0,0 +1,18 @@ +|string> + */ + protected function passwordRules(): array + { + return ['required', 'string', Password::default(), 'confirmed']; + } +} diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php new file mode 100644 index 0000000..7a57c50 --- /dev/null +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -0,0 +1,29 @@ + $input + */ + public function reset(User $user, array $input): void + { + Validator::make($input, [ + 'password' => $this->passwordRules(), + ])->validate(); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/app/Actions/Fortify/UpdateUserPassword.php b/app/Actions/Fortify/UpdateUserPassword.php new file mode 100644 index 0000000..7005639 --- /dev/null +++ b/app/Actions/Fortify/UpdateUserPassword.php @@ -0,0 +1,32 @@ + $input + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'current_password' => ['required', 'string', 'current_password:web'], + 'password' => $this->passwordRules(), + ], [ + 'current_password.current_password' => __('The provided password does not match your current password.'), + ])->validateWithBag('updatePassword'); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/app/Actions/Fortify/UpdateUserProfileInformation.php b/app/Actions/Fortify/UpdateUserProfileInformation.php new file mode 100644 index 0000000..0930ddf --- /dev/null +++ b/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -0,0 +1,58 @@ + $input + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users')->ignore($user->id), + ], + ])->validateWithBag('updateProfileInformation'); + + if ($input['email'] !== $user->email && + $user instanceof MustVerifyEmail) { + $this->updateVerifiedUser($user, $input); + } else { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + ])->save(); + } + } + + /** + * Update the given verified user's profile information. + * + * @param array $input + */ + protected function updateVerifiedUser(User $user, array $input): void + { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + 'email_verified_at' => null, + ])->save(); + + $user->sendEmailVerificationNotification(); + } +} diff --git a/app/Enums/QsoErrorCode.php b/app/Enums/QsoErrorCode.php new file mode 100644 index 0000000..17f3827 --- /dev/null +++ b/app/Enums/QsoErrorCode.php @@ -0,0 +1,35 @@ +middleware('auth:sanctum')->except(['index', 'show']); + } + + /** + * Seznam pásem (s stránkováním). + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $bands = Band::query() + ->with(['ediBands', 'contests']) + ->orderBy('order') + ->paginate($perPage); + + return response()->json($bands); + } + + /** + * Vytvoří nové pásmo. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', Band::class); + + $data = $this->validateData($request); + + $relations = $request->validate([ + 'edi_band_ids' => ['sometimes', 'array'], + 'edi_band_ids.*' => ['integer', 'exists:edi_bands,id'], + 'contest_ids' => ['sometimes', 'array'], + 'contest_ids.*' => ['integer', 'exists:contests,id'], + ]); + + $band = Band::create($data); + + if (array_key_exists('edi_band_ids', $relations)) { + $band->ediBands()->sync($relations['edi_band_ids']); + } + + if (array_key_exists('contest_ids', $relations)) { + $band->contests()->sync($relations['contest_ids']); + } + + return response()->json($band, 201); + } + + /** + * Detail jednoho pásma. + */ + public function show(Band $band): JsonResponse + { + $band->load(['ediBands', 'contests']); + + return response()->json($band); + } + + /** + * Aktualizace existujícího pásma. + */ + public function update(Request $request, Band $band): JsonResponse + { + $this->authorize('update', $band); + + $data = $this->validateData($request, partial: true); + + $relations = $request->validate([ + 'edi_band_ids' => ['sometimes', 'array'], + 'edi_band_ids.*' => ['integer', 'exists:edi_bands,id'], + 'contest_ids' => ['sometimes', 'array'], + 'contest_ids.*' => ['integer', 'exists:contests,id'], + ]); + + $band->fill($data); + $band->save(); + + if (array_key_exists('edi_band_ids', $relations)) { + $band->ediBands()->sync($relations['edi_band_ids']); + } + + if (array_key_exists('contest_ids', $relations)) { + $band->contests()->sync($relations['contest_ids']); + } + + return response()->json($band); + } + + /** + * Smazání pásma. + */ + public function destroy(Band $band): JsonResponse + { + $this->authorize('delete', $band); + + $band->delete(); + + return response()->json(null, 204); + } + + /** + * Společná validace vstupu pro store/update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'name' => [$required, 'string', 'max:255'], + 'order' => [$required, 'integer'], + 'edi_band_begin' => [$required, 'integer'], + 'edi_band_end' => [$required, 'integer', 'gte:edi_band_begin'], + 'has_power_category' => [$required, 'boolean'], + ]); + } +} diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php new file mode 100644 index 0000000..9a3ac6d --- /dev/null +++ b/app/Http/Controllers/CategoryController.php @@ -0,0 +1,138 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam kategorií (API, JSON). + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $categories = Category::query() + ->with(['ediCategories', 'contests']) + ->orderBy('order') + ->paginate($perPage); + + return response()->json($categories); + } + + /** + * Vytvoření nové kategorie. + * Autorizace přes CategoryPolicy@create. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', Category::class); + + $data = $this->validateData($request); + $relations = $this->validateRelations($request); + + $category = Category::create($data); + + if (array_key_exists('edi_category_ids', $relations)) { + $category->ediCategories()->sync($relations['edi_category_ids']); + } + + if (array_key_exists('contest_ids', $relations)) { + $category->contests()->sync($relations['contest_ids']); + } + + $category->load(['ediCategories', 'contests']); + + return response()->json($category, 201); + } + + /** + * Detail jedné kategorie. + */ + public function show(Category $category): JsonResponse + { + $category->load(['ediCategories', 'contests']); + + return response()->json($category); + } + + /** + * Aktualizace existující kategorie (partial update). + * Autorizace přes CategoryPolicy@update. + */ + public function update(Request $request, Category $category): JsonResponse + { + $this->authorize('update', $category); + + $data = $this->validateData($request, partial: true); + $relations = $this->validateRelations($request); + + $category->fill($data); + $category->save(); + + if (array_key_exists('edi_category_ids', $relations)) { + $category->ediCategories()->sync($relations['edi_category_ids']); + } + + if (array_key_exists('contest_ids', $relations)) { + $category->contests()->sync($relations['contest_ids']); + } + + $category->load(['ediCategories', 'contests']); + + return response()->json($category); + } + + /** + * Smazání kategorie. + * Autorizace přes CategoryPolicy@delete. + */ + public function destroy(Category $category): JsonResponse + { + $this->authorize('delete', $category); + + $category->delete(); + + return response()->json(null, 204); + } + + /** + * Společná validace dat pro store/update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'name' => [$required, 'string', 'max:255'], + 'order' => [$required, 'integer'], + ]); + } + + /** + * Validace ID relací (EDI kategorie a soutěže). + */ + protected function validateRelations(Request $request): array + { + return $request->validate([ + 'edi_category_ids' => ['sometimes', 'array'], + 'edi_category_ids.*' => ['integer', 'exists:edi_categories,id'], + 'contest_ids' => ['sometimes', 'array'], + 'contest_ids.*' => ['integer', 'exists:contests,id'], + ]); + } +} diff --git a/app/Http/Controllers/ContestController.php b/app/Http/Controllers/ContestController.php new file mode 100644 index 0000000..b396695 --- /dev/null +++ b/app/Http/Controllers/ContestController.php @@ -0,0 +1,287 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam soutěží (stránkovaný výstup). + * Podporuje ?lang=cs / ?lang=en – name/description se vrací v daném jazyce. + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + $onlyActive = (bool) $request->query('only_active', false); + $includeTests = filter_var($request->query('include_tests', true), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + if ($includeTests === null) { + $includeTests = true; + } + + $lang = $request->query('lang'); + if (! is_string($lang) || $lang === '') { + $lang = app()->getLocale(); + } + + $items = Contest::query() + ->with([ + 'rounds', + 'parameters', + 'bands', + 'categories', + 'powerCategories', + 'ruleSet', + ]) + ->when($onlyActive, fn ($q) => $q->where('is_active', true)) + ->when(! $includeTests, fn ($q) => $q->where('is_test', false)) + ->orderByDesc('created_at') + ->paginate($perPage); + + // přemapování na konkrétní jazyk (stejný princip jako NewsPostController@index) + $items->getCollection()->transform(function (Contest $contest) use ($lang) { + $data = $contest->toArray(); + + $data['name'] = $contest->getTranslation('name', $lang, true); + $data['description'] = $contest->getTranslation('description', $lang, true); + + return $data; + }); + + return response()->json($items); + } + + + /** + * Vytvoření nové soutěže. + * Autorizace přes ContestPolicy@create. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', Contest::class); + + $data = $this->validateData($request); + $relations = $this->validateRelations($request); + + if (! array_key_exists('rule_set_id', $data) || $data['rule_set_id'] === null) { + $data['rule_set_id'] = $this->resolveDefaultRuleSetId(); + } + + $contest = Contest::create($data); + + $this->syncRelations($contest, $relations); + + $contest->load([ + 'rounds', + 'parameters', + 'bands', + 'categories', + 'powerCategories', + 'ruleSet', + ]); + + return response()->json($contest, 201); + } + + /** + * Detail soutěže. + * Můžeš volat i s ?lang=cs pro konkrétní jazyk. + */ + public function show(Request $request, Contest $contest): JsonResponse + { + $lang = $request->query('lang'); + if (! is_string($lang) || $lang === '') { + $lang = app()->getLocale(); + } + + $contest->load([ + 'rounds', + 'parameters', + 'bands', + 'categories', + 'powerCategories', + 'ruleSet', + ]); + + $data = $contest->toArray(); + + $data['name'] = $contest->getTranslation('name', $lang, true); + $data['description'] = $contest->getTranslation('description', $lang, true); + + return response()->json($data); + } + + /** + * Aktualizace soutěže (partial update). + * Autorizace přes ContestPolicy@update. + */ + public function update(Request $request, Contest $contest): JsonResponse + { + $this->authorize('update', $contest); + + $data = $this->validateData($request, partial: true); + $relations = $this->validateRelations($request); + + $contest->fill($data); + $contest->save(); + + $this->syncRelations($contest, $relations); + + $contest->load([ + 'rounds', + 'parameters', + 'bands', + 'categories', + 'powerCategories', + 'ruleSet', + ]); + + return response()->json($contest); + } + + /** + * Smazání soutěže. + * Autorizace přes ContestPolicy@delete. + */ + public function destroy(Contest $contest): JsonResponse + { + $this->authorize('delete', $contest); + + $contest->delete(); + + return response()->json(null, 204); + } + + /** + * Validace dat pro store / update. + * Stejný princip jako u NewsPost – string nebo array { locale: value }. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'name' => [ + $required, + function (string $attribute, $value, \Closure $fail) { + if (is_string($value)) { + if (mb_strlen($value) > 255) { + $fail('The '.$attribute.' may not be greater than 255 characters.'); + } + return; + } + + if (is_array($value)) { + foreach ($value as $locale => $text) { + if (! is_string($text)) { + $fail("The {$attribute}.{$locale} must be a string."); + return; + } + if (mb_strlen($text) > 255) { + $fail("The {$attribute}.{$locale} may not be greater than 255 characters."); + return; + } + } + return; + } + + $fail('The '.$attribute.' must be a string or an array of translated strings.'); + }, + ], + + 'description' => [ + 'sometimes', + function (string $attribute, $value, \Closure $fail) { + if ($value === null) { + return; + } + + if (is_string($value)) { + // max length pokud chceš, nebo bez omezení + return; + } + + if (is_array($value)) { + foreach ($value as $locale => $text) { + if (! is_string($text)) { + $fail("The {$attribute}.{$locale} must be a string."); + return; + } + } + return; + } + + $fail('The '.$attribute.' must be null, a string, or an array of translated strings.'); + }, + ], + + 'url' => ['sometimes', 'nullable', 'email', 'max:255'], + 'evaluator' => ['sometimes', 'nullable', 'string', 'max:255'], + 'email' => ['sometimes', 'nullable', 'email', 'max:255'], + 'email2' => ['sometimes', 'nullable', 'email', 'max:255'], + + 'is_mcr' => ['sometimes', 'boolean'], + 'is_sixhr' => ['sometimes', 'boolean'], + 'is_active' => ['sometimes', 'boolean'], + + 'start_time' => ['sometimes', 'date_format:H:i:s'], + 'duration' => ['sometimes', 'integer', 'min:1'], + 'logs_deadline_days' => ['sometimes', 'integer', 'min:0'], + 'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'], + ]); + } + + protected function resolveDefaultRuleSetId(): ?int + { + return EvaluationRuleSet::where('code', 'default_vhf_compat')->value('id'); + } + + /** + * Validace ID navázaných entit (bands, categories, powerCategories). + */ + protected function validateRelations(Request $request): array + { + return $request->validate([ + 'band_ids' => ['sometimes', 'array'], + 'band_ids.*' => ['integer', 'exists:bands,id'], + + 'category_ids' => ['sometimes', 'array'], + 'category_ids.*' => ['integer', 'exists:categories,id'], + + 'power_category_ids' => ['sometimes', 'array'], + 'power_category_ids.*' => ['integer', 'exists:power_categories,id'], + ]); + } + + /** + * Sync vazeb pro belongsToMany vztahy. + */ + protected function syncRelations(Contest $contest, array $relations): void + { + if (array_key_exists('band_ids', $relations)) { + $contest->bands()->sync($relations['band_ids']); + } + + if (array_key_exists('category_ids', $relations)) { + $contest->categories()->sync($relations['category_ids']); + } + + if (array_key_exists('power_category_ids', $relations)) { + $contest->powerCategories()->sync($relations['power_category_ids']); + } + } +} diff --git a/app/Http/Controllers/ContestParameterController.php b/app/Http/Controllers/ContestParameterController.php new file mode 100644 index 0000000..651b192 --- /dev/null +++ b/app/Http/Controllers/ContestParameterController.php @@ -0,0 +1,119 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam contest parametrů (stránkovaně). + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $items = ContestParameter::query() + ->with('contest') + ->orderBy('contest_id') + ->orderBy('log_type') + ->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření nových parametrů pro contest. + * Autorizace přes ContestParameterPolicy@create. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', ContestParameter::class); + + $data = $this->validateData($request); + + $item = ContestParameter::create($data); + + $item->load('contest'); + + return response()->json($item, 201); + } + + /** + * Detail parametrů. + */ + public function show(ContestParameter $contest_parameter): JsonResponse + { + $contest_parameter->load('contest'); + + return response()->json($contest_parameter); + } + + /** + * Aktualizace parametrů (partial update). + * Autorizace přes ContestParameterPolicy@update. + */ + public function update(Request $request, ContestParameter $contest_parameter): JsonResponse + { + $this->authorize('update', $contest_parameter); + + $data = $this->validateData($request, partial: true); + + $contest_parameter->fill($data); + $contest_parameter->save(); + + $contest_parameter->load('contest'); + + return response()->json($contest_parameter); + } + + /** + * Smazání parametrů. + * Autorizace přes ContestParameterPolicy@delete. + */ + public function destroy(ContestParameter $contest_parameter): JsonResponse + { + $this->authorize('delete', $contest_parameter); + + $contest_parameter->delete(); + + return response()->json(null, 204); + } + + /** + * Validace vstupu pro store / update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'contest_id' => [$required, 'integer', 'exists:contests,id'], + 'log_type' => [$required, 'in:STANDARD,CHECK'], + 'ignore_slash_part' => [$required, 'boolean'], + 'ignore_third_part' => [$required, 'boolean'], + 'letters_in_rst' => [$required, 'boolean'], + 'discard_qso_rec_diff_call' => [$required, 'boolean'], + 'discard_qso_sent_diff_call' => [$required, 'boolean'], + 'discard_qso_rec_diff_rst' => [$required, 'boolean'], + 'discard_qso_sent_diff_rst' => [$required, 'boolean'], + 'discard_qso_rec_diff_code' => [$required, 'boolean'], + 'discard_qso_sent_diff_code' => [$required, 'boolean'], + 'unique_qso' => [$required, 'boolean'], + 'time_tolerance' => [$required, 'integer', 'min:0'], + ]); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam záznamů country-WWL (stránkovaně). + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $items = CountryWwl::query() + ->orderBy('country_name') + ->orderBy('wwl') + ->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření nového country-WWL záznamu. + * Autorizace přes CountryWwlPolicy@create. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', CountryWwl::class); + + $data = $this->validateData($request); + + $item = CountryWwl::create($data); + + return response()->json($item, 201); + } + + /** + * Detail jednoho country-WWL záznamu. + */ + public function show(CountryWwl $country_wwl): JsonResponse + { + return response()->json($country_wwl); + } + + /** + * Aktualizace existujícího country-WWL záznamu (partial update). + * Autorizace přes CountryWwlPolicy@update. + */ + public function update(Request $request, CountryWwl $country_wwl): JsonResponse + { + $this->authorize('update', $country_wwl); + + $data = $this->validateData($request, partial: true); + + $country_wwl->fill($data); + $country_wwl->save(); + + return response()->json($country_wwl); + } + + /** + * Smazání country-WWL záznamu. + * Autorizace přes CountryWwlPolicy@delete. + */ + public function destroy(CountryWwl $country_wwl): JsonResponse + { + $this->authorize('delete', $country_wwl); + + $country_wwl->delete(); + + return response()->json(null, 204); + } + + /** + * Validace dat pro store / update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'country_name' => [$required, 'string', 'max:150'], + 'wwl' => [$required, 'string', 'size:4'], + ]); + } +} diff --git a/app/Http/Controllers/CtyController.php b/app/Http/Controllers/CtyController.php new file mode 100644 index 0000000..8a1a1a3 --- /dev/null +++ b/app/Http/Controllers/CtyController.php @@ -0,0 +1,110 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam CTY záznamů (stránkovaný výstup). + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $items = Cty::query() + ->orderBy('country_name') + ->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření nového CTY záznamu. + * Autorizace přes CtyPolicy@create (pokud ji používáš). + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', Cty::class); + + $data = $this->validateData($request); + + $item = Cty::create($data); + + return response()->json($item, 201); + } + + /** + * Detail jednoho CTY záznamu. + */ + public function show(Cty $cty): JsonResponse + { + return response()->json($cty); + } + + /** + * Aktualizace CTY záznamu (partial update). + * Autorizace přes CtyPolicy@update. + */ + public function update(Request $request, Cty $cty): JsonResponse + { + $this->authorize('update', $cty); + + $data = $this->validateData($request, partial: true); + + $cty->fill($data); + $cty->save(); + + return response()->json($cty); + } + + /** + * Smazání CTY záznamu. + * Autorizace přes CtyPolicy@delete. + */ + public function destroy(Cty $cty): JsonResponse + { + $this->authorize('delete', $cty); + + $cty->delete(); + + return response()->json(null, 204); + } + + /** + * Validace pro store / update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'country_name' => [$required, 'string', 'max:150'], + 'dxcc' => [$required, 'integer'], + 'cq_zone' => [$required, 'integer'], + 'itu_zone' => [$required, 'integer'], + 'continent' => [$required, 'string', 'size:2'], + 'latitude' => [$required, 'numeric'], + 'longitude' => [$required, 'numeric'], + 'time_offset' => [$required, 'numeric'], + 'prefix' => [$required, 'string', 'max:64'], + 'prefix_norm' => ['sometimes', 'nullable', 'string', 'max:64'], + 'precise' => [$required, 'boolean'], + 'source' => [$required, 'string', 'max:25'], + ]); + } +} diff --git a/app/Http/Controllers/EdiBandController.php b/app/Http/Controllers/EdiBandController.php new file mode 100644 index 0000000..f044686 --- /dev/null +++ b/app/Http/Controllers/EdiBandController.php @@ -0,0 +1,93 @@ +middleware('auth:sanctum')->except(['index', 'show']); + } + + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $items = EdiBand::query() + ->with('bands') // eager load pokud chceš mít vazby + ->orderBy('value') + ->paginate($perPage); + + return response()->json($items); + } + + public function store(Request $request): JsonResponse + { + $this->authorize('create', EdiBand::class); + + $data = $request->validate([ + 'value' => ['required', 'string', 'max:255'], + ]); + + $relations = $request->validate([ + 'band_ids' => ['sometimes', 'array'], + 'band_ids.*' => ['integer', 'exists:bands,id'], + ]); + + $item = EdiBand::create($data); + + if (array_key_exists('band_ids', $relations)) { + $item->bands()->sync($relations['band_ids']); + } + + return response()->json($item, 201); + } + + public function show(EdiBand $edi_band): JsonResponse + { + $edi_band->load('bands'); + + return response()->json($edi_band); + } + + public function update(Request $request, EdiBand $edi_band): JsonResponse + { + $this->authorize('update', $edi_band); + + $data = $request->validate([ + 'value' => ['sometimes', 'string', 'max:255'], + ]); + + $relations = $request->validate([ + 'band_ids' => ['sometimes', 'array'], + 'band_ids.*' => ['integer', 'exists:bands,id'], + ]); + + $edi_band->fill($data); + $edi_band->save(); + + if (array_key_exists('band_ids', $relations)) { + $edi_band->bands()->sync($relations['band_ids']); + } + + return response()->json($edi_band); + } + + public function destroy(EdiBand $edi_band): JsonResponse + { + $this->authorize('delete', $edi_band); + + $edi_band->delete(); + + return response()->json(null, 204); + } +} diff --git a/app/Http/Controllers/EdiCategoryController.php b/app/Http/Controllers/EdiCategoryController.php new file mode 100644 index 0000000..a996054 --- /dev/null +++ b/app/Http/Controllers/EdiCategoryController.php @@ -0,0 +1,128 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam EDI kategorií (API, JSON). + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $items = EdiCategory::query() + ->with('categories') // n:m vazba na Category, pokud ji chceš mít v odpovědi + ->orderBy('value') + ->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření nové EDI kategorie. + * Autorizace přes EdiCategoryPolicy@create. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', EdiCategory::class); + + $data = $this->validateData($request); + $relations = $this->validateRelations($request); + + $item = EdiCategory::create($data); + + if (array_key_exists('category_ids', $relations)) { + $item->categories()->sync($relations['category_ids']); + } + + $item->load('categories'); + + return response()->json($item, 201); + } + + /** + * Detail jedné EDI kategorie. + */ + public function show(EdiCategory $edi_category): JsonResponse + { + $edi_category->load('categories'); + + return response()->json($edi_category); + } + + /** + * Aktualizace existující EDI kategorie (partial update). + * Autorizace přes EdiCategoryPolicy@update. + */ + public function update(Request $request, EdiCategory $edi_category): JsonResponse + { + $this->authorize('update', $edi_category); + + $data = $this->validateData($request, partial: true); + $relations = $this->validateRelations($request); + + $edi_category->fill($data); + $edi_category->save(); + + if (array_key_exists('category_ids', $relations)) { + $edi_category->categories()->sync($relations['category_ids']); + } + + $edi_category->load('categories'); + + return response()->json($edi_category); + } + + /** + * Smazání EDI kategorie. + * Autorizace přes EdiCategoryPolicy@delete. + */ + public function destroy(EdiCategory $edi_category): JsonResponse + { + $this->authorize('delete', $edi_category); + + $edi_category->delete(); + + return response()->json(null, 204); + } + + /** + * Validace dat pro store / update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'value' => [$required, 'string', 'max:255'], + ]); + } + + /** + * Validace ID navázaných kategorií (Category). + */ + protected function validateRelations(Request $request): array + { + return $request->validate([ + 'category_ids' => ['sometimes', 'array'], + 'category_ids.*' => ['integer', 'exists:categories,id'], + ]); + } +} diff --git a/app/Http/Controllers/EvaluationRuleSetController.php b/app/Http/Controllers/EvaluationRuleSetController.php new file mode 100644 index 0000000..4d3809f --- /dev/null +++ b/app/Http/Controllers/EvaluationRuleSetController.php @@ -0,0 +1,202 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam rulesetů (stránkovaně). + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $items = EvaluationRuleSet::query() + ->orderBy('name') + ->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření nového rulesetu. + * Autorizace přes EvaluationRuleSetPolicy@create. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', EvaluationRuleSet::class); + + $data = $this->validateData($request); + + $item = EvaluationRuleSet::create($data); + + return response()->json($item, 201); + } + + /** + * Detail rulesetu. + */ + public function show(EvaluationRuleSet $evaluationRuleSet): JsonResponse + { + $evaluationRuleSet->load(['evaluationRuns']); + + return response()->json($evaluationRuleSet); + } + + /** + * Update (partial). + * Autorizace přes EvaluationRuleSetPolicy@update. + */ + public function update(Request $request, EvaluationRuleSet $evaluationRuleSet): JsonResponse + { + $this->authorize('update', $evaluationRuleSet); + + $data = $this->validateData($request, partial: true); + + $evaluationRuleSet->fill($data); + $evaluationRuleSet->save(); + + return response()->json($evaluationRuleSet); + } + + /** + * Smazání rulesetu. + * Autorizace přes EvaluationRuleSetPolicy@delete. + */ + public function destroy(EvaluationRuleSet $evaluationRuleSet): JsonResponse + { + $this->authorize('delete', $evaluationRuleSet); + + $evaluationRuleSet->delete(); + + return response()->json(null, 204); + } + + /** + * Validace vstupů pro store/update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + $data = $request->validate([ + 'name' => [$required, 'string', 'max:100'], + 'code' => [$required, 'string', 'max:50'], + 'description' => ['sometimes', 'nullable', 'string'], + + 'scoring_mode' => [$required, 'in:DISTANCE,FIXED_POINTS'], + 'points_per_qso' => ['sometimes', 'integer', 'min:0'], + 'points_per_km' => ['sometimes', 'numeric', 'min:0'], + + 'use_multipliers' => ['sometimes', 'boolean'], + 'multiplier_type' => [$required, 'in:NONE,WWL,DXCC,SECTION,COUNTRY'], + + 'dup_qso_policy' => [$required, 'in:ZERO_POINTS,PENALTY'], + 'nil_qso_policy' => [$required, 'in:ZERO_POINTS,PENALTY'], + 'no_counterpart_log_policy' => ['sometimes', 'in:INVALID,ZERO_POINTS,FLAG_ONLY,PENALTY'], + 'not_in_counterpart_log_policy' => ['sometimes', 'in:INVALID,ZERO_POINTS,FLAG_ONLY,PENALTY'], + 'unique_qso_policy' => ['sometimes', 'in:INVALID,ZERO_POINTS,FLAG_ONLY'], + 'busted_call_policy' => [$required, 'in:ZERO_POINTS,PENALTY'], + 'busted_exchange_policy'=> [$required, 'in:ZERO_POINTS,PENALTY'], + 'busted_serial_policy' => ['sometimes', 'in:ZERO_POINTS,PENALTY'], + 'busted_locator_policy' => ['sometimes', 'in:ZERO_POINTS,PENALTY'], + + 'penalty_dup_points' => ['sometimes', 'integer', 'min:0'], + 'penalty_nil_points' => ['sometimes', 'integer', 'min:0'], + 'penalty_busted_call_points' => ['sometimes', 'integer', 'min:0'], + 'penalty_busted_exchange_points' => ['sometimes', 'integer', 'min:0'], + 'penalty_busted_serial_points' => ['sometimes', 'integer', 'min:0'], + 'penalty_busted_locator_points' => ['sometimes', 'integer', 'min:0'], + + 'dupe_scope' => ['sometimes', 'in:BAND,BAND_MODE'], + 'callsign_normalization' => ['sometimes', 'in:STRICT,IGNORE_SUFFIX'], + 'distance_rounding' => ['sometimes', 'in:FLOOR,ROUND,CEIL'], + 'min_distance_km' => ['sometimes', 'nullable', 'integer', 'min:0'], + 'require_locators' => ['sometimes', 'boolean'], + 'out_of_window_policy' => ['sometimes', 'in:IGNORE,ZERO_POINTS,PENALTY,INVALID'], + 'penalty_out_of_window_points' => ['sometimes', 'integer', 'min:0'], + + 'exchange_type' => ['sometimes', 'in:SERIAL,WWL,SERIAL_WWL,CUSTOM'], + 'exchange_requires_wwl' => ['sometimes', 'boolean'], + 'exchange_requires_serial' => ['sometimes', 'boolean'], + 'exchange_requires_report' => ['sometimes', 'boolean'], + 'exchange_pattern' => ['sometimes', 'nullable', 'string', 'max:200'], + + 'ignore_slash_part' => ['sometimes', 'boolean'], + 'ignore_third_part' => ['sometimes', 'boolean'], + 'letters_in_rst' => ['sometimes', 'boolean'], + 'rst_ignore_third_char' => ['sometimes', 'boolean'], + 'discard_qso_rec_diff_call' => ['sometimes', 'boolean'], + 'discard_qso_sent_diff_call' => ['sometimes', 'boolean'], + 'discard_qso_rec_diff_rst' => ['sometimes', 'boolean'], + 'discard_qso_sent_diff_rst' => ['sometimes', 'boolean'], + 'discard_qso_rec_diff_code' => ['sometimes', 'boolean'], + 'discard_qso_sent_diff_code' => ['sometimes', 'boolean'], + 'discard_qso_rec_diff_serial' => ['sometimes', 'boolean'], + 'discard_qso_sent_diff_serial' => ['sometimes', 'boolean'], + 'discard_qso_rec_diff_wwl' => ['sometimes', 'boolean'], + 'discard_qso_sent_diff_wwl' => ['sometimes', 'boolean'], + 'busted_rst_policy' => ['sometimes', 'in:ZERO_POINTS,PENALTY'], + 'penalty_busted_rst_points' => ['sometimes', 'integer', 'min:0'], + + 'match_tiebreak_order' => ['sometimes', 'nullable', 'array'], + 'match_require_locator_match' => ['sometimes', 'boolean'], + 'match_require_exchange_match' => ['sometimes', 'boolean'], + + 'multiplier_scope' => ['sometimes', 'in:PER_BAND,OVERALL'], + 'multiplier_source' => ['sometimes', 'in:VALID_ONLY,ALL_MATCHED'], + 'wwl_multiplier_level' => ['sometimes', 'in:LOCATOR_2,LOCATOR_4,LOCATOR_6'], + + 'checklog_matching' => ['sometimes', 'boolean'], + 'out_of_window_dq_threshold' => ['sometimes', 'nullable', 'integer', 'min:1'], + 'time_diff_dq_threshold_percent' => ['sometimes', 'nullable', 'integer', 'min:1', 'max:100'], + 'time_diff_dq_threshold_sec' => ['sometimes', 'nullable', 'integer', 'min:1'], + 'bad_qso_dq_threshold_percent' => ['sometimes', 'nullable', 'integer', 'min:1', 'max:100'], + + 'time_tolerance_sec' => ['sometimes', 'nullable', 'integer', 'min:0'], + 'require_unique_qso' => ['sometimes', 'boolean'], + 'allow_time_shift_one_hour' => ['sometimes', 'boolean'], + 'time_shift_seconds' => ['sometimes', 'nullable', 'integer', 'min:0'], + 'time_mismatch_policy' => ['sometimes', 'nullable', 'in:INVALID,ZERO_POINTS,FLAG_ONLY,PENALTY'], + 'allow_time_mismatch_pairing' => ['sometimes', 'boolean'], + 'time_mismatch_max_sec' => ['sometimes', 'nullable', 'integer', 'min:0'], + 'callsign_suffix_max_len' => ['sometimes', 'nullable', 'integer', 'min:1'], + 'callsign_levenshtein_max' => ['sometimes', 'nullable', 'integer', 'min:0', 'max:2'], + 'dup_resolution_strategy' => ['sometimes', 'nullable', 'array'], + 'operating_window_mode' => ['sometimes', 'in:NONE,BEST_CONTIGUOUS'], + 'operating_window_hours' => ['sometimes', 'nullable', 'integer', 'min:1', 'max:24'], + 'sixhr_ranking_mode' => ['sometimes', 'in:IARU,CRK'], + + 'options' => ['sometimes', 'nullable', 'array'], + ]); + + if (array_key_exists('operating_window_hours', $data) && ! array_key_exists('operating_window_mode', $data)) { + $data['operating_window_mode'] = 'BEST_CONTIGUOUS'; + } + + if (array_key_exists('operating_window_mode', $data)) { + if ($data['operating_window_mode'] === 'BEST_CONTIGUOUS') { + $data['operating_window_hours'] = 6; + } else { + $data['operating_window_hours'] = null; + } + } + + return $data; + } +} diff --git a/app/Http/Controllers/EvaluationRunController.php b/app/Http/Controllers/EvaluationRunController.php new file mode 100644 index 0000000..9212a2f --- /dev/null +++ b/app/Http/Controllers/EvaluationRunController.php @@ -0,0 +1,338 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam evaluation runů – filtrování podle round_id, is_official. + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + // Seznam běhů slouží hlavně pro monitoring na RoundDetailPage. + $query = EvaluationRun::query() + ->with(['round']); + + if ($request->filled('round_id')) { + $query->where('round_id', (int) $request->get('round_id')); + } + + if ($request->filled('is_official')) { + $query->where( + 'is_official', + filter_var($request->get('is_official'), FILTER_VALIDATE_BOOL) + ); + } + if ($request->filled('result_type')) { + $query->where('result_type', $request->get('result_type')); + } + + $items = $query + ->orderByDesc('created_at') + ->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření nového evaluation runu. + * Typicky před samotným spuštěním vyhodnocovače. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', EvaluationRun::class); + + $data = $this->validateData($request); + + // Samotné spuštění pipeline zajišťuje StartEvaluationRunJob. + $run = EvaluationRun::create($data); + + $run->load(['round']); + + return response()->json($run, 201); + } + + /** + * Detail jednoho evaluation runu včetně vazeb a výsledků. + */ + public function show(EvaluationRun $evaluationRun): JsonResponse + { + // Detail běhu včetně výsledků je náročný – používej s rozumem (paging/limit). + $evaluationRun->load([ + 'round', + 'logResults', + 'qsoResults', + ]); + + return response()->json($evaluationRun); + } + + /** + * Aktualizace evaluation runu (např. změna názvu, poznámky, + * příznaku is_official). + */ + public function update(Request $request, EvaluationRun $evaluationRun): JsonResponse + { + $this->authorize('update', $evaluationRun); + + $data = $this->validateData($request, partial: true); + + $evaluationRun->fill($data); + $evaluationRun->save(); + + $evaluationRun->load(['round']); + + return response()->json($evaluationRun); + } + + /** + * Označí běh jako TEST/PRELIMINARY/FINAL a aktualizuje ukazatele v kole. + */ + public function setResultType(Request $request, EvaluationRun $evaluationRun): JsonResponse + { + $this->authorize('update', $evaluationRun); + + $data = $request->validate([ + 'result_type' => ['required', 'string', 'in:TEST,PRELIMINARY,FINAL'], + ]); + + $resultType = $data['result_type']; + $evaluationRun->update([ + 'result_type' => $resultType, + 'is_official' => $resultType === 'FINAL', + ]); + + $round = $evaluationRun->round; + if ($round) { + if ($resultType === 'FINAL') { + $round->official_evaluation_run_id = $evaluationRun->id; + $round->preliminary_evaluation_run_id = null; + $round->test_evaluation_run_id = null; + } elseif ($resultType === 'PRELIMINARY') { + $round->preliminary_evaluation_run_id = $evaluationRun->id; + $round->official_evaluation_run_id = null; + $round->test_evaluation_run_id = null; + } elseif ($resultType === 'TEST') { + $round->test_evaluation_run_id = $evaluationRun->id; + $round->official_evaluation_run_id = null; + $round->preliminary_evaluation_run_id = null; + } + $round->save(); + } + + $evaluationRun->load(['round']); + + return response()->json($evaluationRun); + } + + /** + * Smazání evaluation runu (včetně log_results / qso_results přes FK). + */ + public function destroy(EvaluationRun $evaluationRun): JsonResponse + { + $this->authorize('delete', $evaluationRun); + + $evaluationRun->delete(); + + return response()->json(null, 204); + } + + /** + * Zruší běh vyhodnocení (pokud je stále aktivní). + */ + public function cancel(EvaluationRun $evaluationRun): JsonResponse + { + $this->authorize('update', $evaluationRun); + + // Cancel je povolený jen pro běhy, které ještě neskončily. + $activeStatuses = ['PENDING', 'RUNNING', 'WAITING_REVIEW_INPUT', 'WAITING_REVIEW_MATCH', 'WAITING_REVIEW_SCORE']; + if (! in_array($evaluationRun->status, $activeStatuses, true)) { + return response()->json([ + 'message' => 'Běh nelze zrušit v aktuálním stavu.', + ], 409); + } + + $evaluationRun->update([ + 'status' => 'CANCELED', + 'finished_at' => now(), + ]); + + if ($evaluationRun->batch_id) { + $batch = Bus::findBatch($evaluationRun->batch_id); + if ($batch) { + $batch->cancel(); + } + } + + EvaluationRunEvent::create([ + 'evaluation_run_id' => $evaluationRun->id, + 'level' => 'warning', + 'message' => 'Vyhodnocení bylo zrušeno uživatelem.', + 'context' => [ + 'step' => 'cancel', + 'round_id' => $evaluationRun->round_id, + 'user_id' => auth()->id(), + ], + ]); + + // Uvolní lock, aby mohl běh navázat nebo se spustit nový. + EvaluationLock::where('evaluation_run_id', $evaluationRun->id)->delete(); + + return response()->json([ + 'status' => 'canceled', + ], 200); + } + + /** + * Vrátí poslední události běhu vyhodnocení. + */ + public function events(EvaluationRun $evaluationRun, Request $request): JsonResponse + { + $this->authorize('update', $evaluationRun); + + $limit = (int) $request->get('limit', 10); + if ($limit < 1) { + $limit = 1; + } elseif ($limit > 100) { + $limit = 100; + } + + $minLevel = $request->get('min_level'); + $levels = ['debug', 'info', 'warning', 'error']; + if (! in_array($minLevel, $levels, true)) { + $minLevel = null; + } + + $events = $evaluationRun->events() + ->when($minLevel, function ($query) use ($minLevel, $levels) { + $query->whereIn('level', array_slice($levels, array_search($minLevel, $levels, true))); + }) + ->orderByDesc('id') + ->limit($limit) + ->get(); + + return response()->json($events); + } + + /** + * Pokračuje v běhu vyhodnocení po manuální kontrole. + */ + public function resume(Request $request, EvaluationRun $evaluationRun): JsonResponse + { + $this->authorize('update', $evaluationRun); + + if ($evaluationRun->isCanceled()) { + return response()->json([ + 'message' => 'Běh byl zrušen.', + ], 409); + } + + $ok = app(EvaluationCoordinator::class)->resume($evaluationRun, [ + 'rebuild_working_set' => $request->boolean('rebuild_working_set'), + ]); + + if ($ok) { + return response()->json([ + 'status' => 'queued', + ], 202); + } + + return response()->json([ + 'message' => 'Běh není ve stavu čekání na kontrolu.', + ], 409); + } + + /** + * Validace vstupů pro store / update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'round_id' => [$required, 'integer', 'exists:rounds,id'], + 'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'], + + 'name' => ['sometimes', 'nullable', 'string', 'max:100'], + 'rules_version' => ['sometimes', 'nullable', 'string', 'in:OFFICIAL,TEST,CLAIMED'], + 'result_type' => ['sometimes', 'nullable', 'string', 'in:PRELIMINARY,FINAL,TEST'], + 'is_official' => ['sometimes', 'boolean'], + 'notes' => ['sometimes', 'nullable', 'string'], + 'scope' => ['sometimes', 'array'], + 'scope.band_ids' => ['sometimes', 'array'], + 'scope.band_ids.*' => ['integer', 'exists:bands,id'], + 'scope.category_ids' => ['sometimes', 'array'], + 'scope.category_ids.*' => ['integer', 'exists:categories,id'], + 'scope.power_category_ids' => ['sometimes', 'array'], + 'scope.power_category_ids.*' => ['integer', 'exists:power_categories,id'], + ]); + } +} diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php new file mode 100644 index 0000000..ddb9b10 --- /dev/null +++ b/app/Http/Controllers/FileController.php @@ -0,0 +1,348 @@ +middleware('auth:sanctum')->only(['delete']); + } + + /** + * Vrátí seznam nahraných souborů (metadata) pro zobrazení v UI. + * Výstupem je JSON kolekce záznamů bez interního pole "path". + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $files = File::query() + ->select([ + 'id', + 'filename', + 'mimetype', + 'filesize', + 'hash', + 'uploaded_by', + 'created_at', + ]) + ->orderByDesc('created_at') + ->paginate($perPage); + + return response()->json($files); + } + + /** + * Vrátí metadata konkrétního souboru jako JSON. + * Path k fyzickému souboru se z bezpečnostních důvodů nevrací. + * + * @param \App\Models\File $file + * @return \Illuminate\Http\JsonResponse + */ + public function show(File $file): JsonResponse + { + // schválně nevracím path, je to interní implementační detail + return response()->json([ + 'id' => $file->id, + 'filename' => $file->filename, + 'mimetype' => $file->mimetype, + 'filesize' => $file->filesize, + 'hash' => $file->hash, + 'uploaded_by' => $file->uploaded_by, + 'created_at' => $file->created_at, + 'updated_at' => $file->updated_at, + ]); + } + + /** + * Vrátí soubor ke stažení (HTTP download) s Content-Disposition: attachment. + * Pokud soubor fyzicky neexistuje, vrátí HTTP 404. + * + * @param \App\Models\File $file + * @return \Symfony\Component\HttpFoundation\StreamedResponse + */ + public function download(File $file): StreamedResponse + { + if (! Storage::exists($file->path)) { + abort(404); + } + + return Storage::download( + $file->path, + $this->buildDownloadName($file), + ['Content-Type' => $file->mimetype] + ); + } + + /** + * Vrátí obsah souboru v HTTP odpovědi (např. pro náhled nebo další zpracování). + * Content-Type je převzat z uloženého mimetype v DB. + * Pokud soubor neexistuje, vrátí HTTP 404. + * + * @param \App\Models\File $file + * @return \Illuminate\Http\Response + */ + public function content(File $file): Response + { + $content = $this->getFileContent($file); + + return response($content, 200) + ->header('Content-Type', $file->mimetype); + } + + /** + * Interní helper pro načtení obsahu souboru pro interní použití v PHP kódu. + * Při neexistenci souboru by měl konzistentně signalizovat chybu + * stejně jako download()/content() – buď abort(404), nebo doménovou výjimkou. + * + * @param \App\Models\File $file + * @return string binární obsah souboru + * + * @throws \RuntimeException pokud soubor neexistuje (aktuální stav) + */ + protected function getFileContent(File $file): string + { + if (! Storage::exists($file->path)) { + throw new \RuntimeException('File not found.'); + } + + return Storage::get($file->path); + } + + /** + * Přijme nahraný soubor z HTTP requestu, uloží ho na disk pod UUID názvem + * do dvouúrovňové adresářové struktury (první znak / první dva znaky UUID), + * spočítá hash obsahu a zapíše metadata do tabulky files. + * + * Vrací JSON s metadaty nově vytvořeného záznamu. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'file' => ['required', 'file', 'max:10240'], + 'round_id' => ['required', 'integer', 'exists:rounds,id'], + ]); + + /** @var \Illuminate\Http\UploadedFile $uploaded */ + $uploaded = $validated['file']; + $roundId = (int) $validated['round_id']; + $round = Round::find($roundId); + if (! $round) { + return response()->json([ + 'message' => 'Kolo nebylo nalezeno.', + ], 404); + } + + if (! auth()->check()) { + $deadline = $round->logs_deadline; + if (! $deadline || now()->greaterThan($deadline)) { + return response()->json([ + 'message' => 'Termín pro nahrání logu již vypršel.', + ], 403); + } + } + + $hash = hash_file('sha256', $uploaded->getRealPath()); + + // pokus o načtení PCall z EDI pro případnou náhradu existujícího logu + $pcall = $this->extractPcallFromUploaded($uploaded); + + // ověř existenci v DB + $existing = File::where('hash', $hash)->first(); + if ($existing) { + return response()->json([ + 'message' => 'Duplicitní soubor již existuje.', + ], 409); + } + + // pokud existuje log se stejnou PCall v daném kole, ale jiným hashem, nahraď ho novým + if ($pcall) { + $existingLog = Log::with('file') + ->where('round_id', $roundId) + ->whereRaw('UPPER(pcall) = ?', [mb_strtoupper($pcall)]) + ->first(); + + if ($existingLog && $existingLog->file && $existingLog->file->hash === $hash) { + return response()->json([ + 'message' => 'Duplicitní soubor již existuje.', + ], 409); + } + + if ($existingLog) { + $this->deleteLogWithFile($existingLog); + } + } + + $uuid = (string) Str::uuid(); + $extension = $uploaded->getClientOriginalExtension(); + $storedFilename = $uuid . ($extension ? '.' . $extension : ''); + + $level1 = substr($uuid, 0, 1); + $level2 = substr($uuid, 0, 2); + $directory = "uploads/{$level1}/{$level2}"; + + if (! Storage::exists($directory)) { + Storage::makeDirectory($directory); + } + + $storedPath = $uploaded->storeAs($directory, $storedFilename); + + try { + $file = File::create([ + 'path' => $storedPath, + 'filename' => $uploaded->getClientOriginalName(), + 'mimetype' => $uploaded->getMimeType() ?? 'application/octet-stream', + 'filesize' => $uploaded->getSize(), + 'hash' => $hash, + 'uploaded_by' => auth()->check() ? (string) auth()->id() : null, + ]); + } catch (QueryException $e) { + // hash už mezitím někdo vložil + return response()->json([ + 'message' => 'Duplicitní soubor již existuje.', + ], 409); + } + + $log = Log::create([ + 'round_id' => $roundId, + 'file_id' => $file->id, + 'ip_address' => $request->ip(), + ]); + + // Předej parsování do asynchronní pipeline (ParseLogJob), + // aby logiku bylo možné volat jednotně z evaluace. + $claimedRun = ClaimedRunResolver::forRound($roundId); + ParseLogJob::dispatch($claimedRun->id, $log->id)->onQueue('evaluation'); + RecalculateClaimedRanksJob::dispatch($claimedRun->id) + ->delay(now()->addSeconds(10)) + ->onQueue('evaluation'); + + return response()->json($file, 201); + } + + + /** + * Smaže fyzický soubor z disku a odpovídající metadata z DB. + * Pokud soubor neexistuje, vrací 404 (pouze pokud nechceš tichý success). + * + * @param \App\Models\File $file + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(File $file): JsonResponse + { + $this->authorize('delete', $file); + + if (Storage::exists($file->path)) { + Storage::delete($file->path); + } + + $file->delete(); + + return response()->json(null, 204); + } + + /** + * Zkusí vytáhnout PCall z nahraného EDI souboru (bez plného parsování). + */ + protected function extractPcallFromUploaded(\Illuminate\Http\UploadedFile $uploaded): ?string + { + $contents = @file($uploaded->getRealPath(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (! $contents) { + return null; + } + + foreach ($contents as $line) { + $trimmed = trim((string) $line); + if (stripos($trimmed, 'PCALL=') === 0) { + return trim(substr($trimmed, 6)); + } + } + + return null; + } + + /** + * Smaže log, jeho QSO a výsledky, navázaný soubor a fyzický obsah. + */ + protected function deleteLogWithFile(Log $log): void + { + $file = $log->file; + $filePath = $file?->path; + + DB::transaction(function () use ($log, $file) { + $log->logResults()->delete(); + $log->qsos()->delete(); + $log->delete(); + if ($file) { + $file->delete(); + } + }); + + if ($filePath && Storage::exists($filePath)) { + Storage::delete($filePath); + } + } + + /** + * Vytvoří název souboru pro download ve formátu XXCALLSIGN_HASH.edi + */ + protected function buildDownloadName(File $file): string + { + $log = Log::where('file_id', $file->id)->first(); + if (! $log) { + return $file->filename; + } + + $pcall = strtoupper(trim($log->pcall ?? '')); + if ($pcall === '') { + return $file->filename; + } + $psect = strtoupper(trim($log->psect ?? '')); + $tokens = preg_split('/[\s;,_-]+/', $psect) ?: []; + $hasCheck = in_array('CHECK', $tokens, true) || $psect === 'CHECK'; + $sixHour = ($log->sixhr_category ?? false) || in_array('6H', $tokens, true); + $isSO = in_array('SO', $tokens, true) || in_array('SOLO', $tokens, true); + $isMO = in_array('MO', $tokens, true) || in_array('MULTI', $tokens, true); + + $prefix = ''; + if (! $hasCheck) { + if ($sixHour && $isSO) { + $prefix = '61'; + } elseif ($sixHour && $isMO) { + $prefix = '62'; + } elseif ($isSO) { + $prefix = '01'; + } elseif ($isMO) { + $prefix = '02'; + } + } + + $hashPart = strtoupper(substr(hash('crc32', $file->hash ?? (string) $file->id), 0, 8)); + + return ($prefix ? $prefix : '') . $pcall . '_' . $hashPart . '.edi'; + } +} diff --git a/app/Http/Controllers/LogController.php b/app/Http/Controllers/LogController.php new file mode 100644 index 0000000..cd32cb9 --- /dev/null +++ b/app/Http/Controllers/LogController.php @@ -0,0 +1,344 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam logů – s možností filtrování podle round_id, pcall, processed/accepted. + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $query = Log::query() + ->with(['round', 'file']) + ->withExists(['logResults as parsed']) + ->withExists(['logResults as parsed_claimed' => function ($q) { + $q->whereHas('evaluationRun', function ($runQuery) { + $runQuery->where('rules_version', 'CLAIMED'); + }); + }]); + + if ($request->filled('round_id')) { + $query->where('round_id', (int) $request->get('round_id')); + } + + if ($request->filled('pcall')) { + $query->where('pcall', $request->get('pcall')); + } + + if ($request->filled('processed')) { + $query->where('processed', filter_var($request->get('processed'), FILTER_VALIDATE_BOOL)); + } + + if ($request->filled('accepted')) { + $query->where('accepted', filter_var($request->get('accepted'), FILTER_VALIDATE_BOOL)); + } + + $logs = $query + ->orderByRaw('parsed_claimed asc, pcall asc') + ->paginate($perPage); + + return response()->json($logs); + } + + /** + * Vytvoření logu. + * Typicky voláno po úspěšném uploadu / parsování EDI ve službě. + * Autorizace přes LogPolicy@create. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', Log::class); + + $data = $this->validateData($request); + + $log = Log::create($data); + + $log->load(['round', 'file']); + + return response()->json($log, 201); + } + + /** + * Detail jednoho logu včetně vazeb a počtu QSO. + */ + public function show(Request $request, Log $log): JsonResponse + { + $includeQsos = $request->boolean('include_qsos', false); + $relations = ['round', 'file']; + if ($includeQsos) { + $relations[] = 'qsos'; + } + $log->load($relations); + + return response()->json($log); + } + + /** + * QSO tabulka pro log: raw QSO + výsledky vyhodnocení + případné overrides. + */ + public function qsoTable(Request $request, Log $log): JsonResponse + { + $evalRunId = $request->filled('evaluation_run_id') + ? (int) $request->get('evaluation_run_id') + : null; + + if ($evalRunId) { + $run = EvaluationRun::find($evalRunId); + if (! $run) { + $evalRunId = null; + } + } + + if (! $evalRunId) { + $run = EvaluationRun::query() + ->where('round_id', $log->round_id) + ->where('status', 'SUCCEEDED') + ->where(function ($q) { + $q->whereNull('rules_version') + ->orWhere('rules_version', '!=', 'CLAIMED'); + }) + ->orderByDesc('id') + ->first(); + $evalRunId = $run?->id; + } + + $qsos = LogQso::query() + ->where('log_id', $log->id) + ->orderBy('qso_index') + ->orderBy('id') + ->get([ + 'id', + 'qso_index', + 'time_on', + 'dx_call', + 'my_rst', + 'my_serial', + 'dx_rst', + 'dx_serial', + 'rx_wwl', + 'rx_exchange', + 'mode_code', + 'new_exchange', + 'new_wwl', + 'new_dxcc', + 'duplicate_qso', + 'points', + ]); + + $qsoIds = $qsos->pluck('id')->all(); + $resultMap = collect(); + $overrideMap = collect(); + + if ($evalRunId && $qsoIds) { + $resultMap = QsoResult::query() + ->where('evaluation_run_id', $evalRunId) + ->whereIn('log_qso_id', $qsoIds) + ->get([ + 'log_qso_id', + 'points', + 'penalty_points', + 'error_code', + 'error_side', + 'match_confidence', + 'match_type', + 'error_flags', + 'is_valid', + 'is_duplicate', + 'is_nil', + 'is_busted_call', + 'is_busted_rst', + 'is_busted_exchange', + 'is_time_out_of_window', + ]) + ->keyBy('log_qso_id'); + + $overrideMap = QsoOverride::query() + ->where('evaluation_run_id', $evalRunId) + ->whereIn('log_qso_id', $qsoIds) + ->get([ + 'id', + 'log_qso_id', + 'forced_status', + 'forced_matched_log_qso_id', + 'forced_points', + 'forced_penalty', + 'reason', + ]) + ->keyBy('log_qso_id'); + } + + $data = $qsos->map(function (LogQso $qso) use ($resultMap, $overrideMap) { + $result = $resultMap->get($qso->id); + $override = $overrideMap->get($qso->id); + + return [ + 'id' => $qso->id, + 'qso_index' => $qso->qso_index, + 'time_on' => $qso->time_on, + 'dx_call' => $qso->dx_call, + 'my_rst' => $qso->my_rst, + 'my_serial' => $qso->my_serial, + 'dx_rst' => $qso->dx_rst, + 'dx_serial' => $qso->dx_serial, + 'rx_wwl' => $qso->rx_wwl, + 'rx_exchange' => $qso->rx_exchange, + 'mode_code' => $qso->mode_code, + 'new_exchange' => $qso->new_exchange, + 'new_wwl' => $qso->new_wwl, + 'new_dxcc' => $qso->new_dxcc, + 'duplicate_qso' => $qso->duplicate_qso, + 'points' => $qso->points, + 'remarks' => null, + 'result' => $result ? [ + 'log_qso_id' => $result->log_qso_id, + 'points' => $result->points, + 'penalty_points' => $result->penalty_points, + 'error_code' => $result->error_code, + 'error_side' => $result->error_side, + 'match_confidence' => $result->match_confidence, + 'match_type' => $result->match_type, + 'error_flags' => $result->error_flags, + 'is_valid' => $result->is_valid, + 'is_duplicate' => $result->is_duplicate, + 'is_nil' => $result->is_nil, + 'is_busted_call' => $result->is_busted_call, + 'is_busted_rst' => $result->is_busted_rst, + 'is_busted_exchange' => $result->is_busted_exchange, + 'is_time_out_of_window' => $result->is_time_out_of_window, + ] : null, + 'override' => $override ? [ + 'id' => $override->id, + 'log_qso_id' => $override->log_qso_id, + 'forced_status' => $override->forced_status, + 'forced_matched_log_qso_id' => $override->forced_matched_log_qso_id, + 'forced_points' => $override->forced_points, + 'forced_penalty' => $override->forced_penalty, + 'reason' => $override->reason, + ] : null, + ]; + }); + + return response()->json([ + 'evaluation_run_id' => $evalRunId, + 'data' => $data, + ]); + } + + /** + * Aktualizace logu (partial update). + * Typicky pro ruční úpravu flagů accepted/processed, případně oprav hlavičky. + * Autorizace přes LogPolicy@update. + */ + public function update(Request $request, Log $log): JsonResponse + { + $this->authorize('update', $log); + + $data = $this->validateData($request, partial: true); + + $log->fill($data); + $log->save(); + + $log->load(['round', 'file']); + + return response()->json($log); + } + + /** + * Smazání logu (včetně QSO přes FK ON DELETE CASCADE). + * Autorizace přes LogPolicy@delete. + */ + public function destroy(Log $log): JsonResponse + { + $this->authorize('delete', $log); + + // pokud je navázaný soubor, smaž i jeho fyzický obsah a záznam + if ($log->file) { + if ($log->file->path && Storage::exists($log->file->path)) { + Storage::delete($log->file->path); + } + $log->file->delete(); + } + + $log->delete(); + + return response()->json(null, 204); + } + + /** + * Jednoduchý parser nahraného souboru – aktuálně podporuje EDI. + * Pokud jde o EDI, naplní základní pole Logu a uloží raw_header (bez sekce QSORecords). + */ + public static function parseUploadedFile(Log $log, string $path): void + { + app(\App\Services\Evaluation\EdiParserService::class)->parseLogFile($log, $path); + } + + /** + * Validace vstupu pro store / update. + * EDI parser bude typicky volat store/update s již připravenými daty. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'round_id' => [$required, 'integer', 'exists:rounds,id'], + 'file_id' => ['sometimes', 'nullable', 'integer', 'exists:files,id'], + + 'accepted' => ['sometimes', 'boolean'], + 'processed' => ['sometimes', 'boolean'], + 'ip_address' => ['sometimes', 'nullable', 'string', 'max:45'], + + 'tname' => ['sometimes', 'nullable', 'string', 'max:100'], + 'tdate' => ['sometimes', 'nullable', 'string', 'max:50'], + 'pcall' => ['sometimes', 'nullable', 'string', 'max:20'], + 'pwwlo' => ['sometimes', 'nullable', 'string', 'max:6'], + 'pexch' => ['sometimes', 'nullable', 'string', 'max:10'], + 'psect' => ['sometimes', 'nullable', 'string', 'max:10'], + 'pband' => ['sometimes', 'nullable', 'string', 'max:10'], + 'pclub' => ['sometimes', 'nullable', 'string', 'max:50'], + + 'country_name' => ['sometimes', 'nullable', 'string', 'max:150'], + 'operator_name' => ['sometimes', 'nullable', 'string', 'max:100'], + 'locator' => ['sometimes', 'nullable', 'string', 'max:6'], + + 'power_watt' => ['sometimes', 'nullable', 'numeric', 'min:0'], + 'power_category' => ['sometimes', 'nullable', 'string', 'max:3'], + 'power_category_id' => ['sometimes', 'nullable', 'integer', 'exists:power_categories,id'], + 'sixhr_category' => ['sometimes', 'nullable', 'boolean'], + + 'claimed_qso_count' => ['sometimes', 'nullable', 'integer', 'min:0'], + 'claimed_score' => ['sometimes', 'nullable', 'integer', 'min:0'], + 'claimed_wwl' => ['sometimes', 'nullable', 'string', 'max:50'], + 'claimed_dxcc' => ['sometimes', 'nullable', 'string', 'max:50'], + + 'remarks' => ['sometimes', 'nullable', 'string', 'max:500'], + 'remarks_eval' => ['sometimes', 'nullable', 'string', 'max:500'], + + 'raw_header' => ['sometimes', 'nullable', 'string'], + ]); + } +} diff --git a/app/Http/Controllers/LogOverrideController.php b/app/Http/Controllers/LogOverrideController.php new file mode 100644 index 0000000..dd21e18 --- /dev/null +++ b/app/Http/Controllers/LogOverrideController.php @@ -0,0 +1,416 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam override záznamů – lze filtrovat podle evaluation_run_id/log_id. + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $query = LogOverride::query() + ->with([ + 'evaluationRun', + 'log', + 'forcedBand', + 'forcedCategory', + 'forcedPowerCategory', + 'createdByUser', + ]); + + if ($request->filled('evaluation_run_id')) { + $query->where('evaluation_run_id', (int) $request->get('evaluation_run_id')); + } + + if ($request->filled('log_id')) { + $query->where('log_id', (int) $request->get('log_id')); + } + + $items = $query->orderByDesc('id')->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření override záznamu. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', LogOverride::class); + + $data = $this->validateData($request); + $data['context'] = $this->mergeOriginalContext($data['context'] ?? null, $data['evaluation_run_id'], $data['log_id']); + if (! isset($data['created_by_user_id']) && $request->user()) { + $data['created_by_user_id'] = $request->user()->id; + } + + $item = LogOverride::create($data); + $this->applyOverrideToLogResult($item); + $statusChanged = array_key_exists('forced_log_status', $data); + if ($this->shouldRecalculateRanks($item->evaluation_run_id, $statusChanged)) { + RecalculateOfficialRanksJob::dispatch($item->evaluation_run_id)->onQueue('evaluation'); + } + $item->load([ + 'evaluationRun', + 'log', + 'forcedBand', + 'forcedCategory', + 'forcedPowerCategory', + 'createdByUser', + ]); + + return response()->json($item, 201); + } + + /** + * Detail override záznamu. + */ + public function show(LogOverride $logOverride): JsonResponse + { + $logOverride->load([ + 'evaluationRun', + 'log', + 'forcedBand', + 'forcedCategory', + 'forcedPowerCategory', + 'createdByUser', + ]); + + return response()->json($logOverride); + } + + /** + * Aktualizace override záznamu. + */ + public function update(Request $request, LogOverride $logOverride): JsonResponse + { + $this->authorize('update', $logOverride); + + $data = $this->validateData($request, partial: true); + if (! array_key_exists('context', $data)) { + $data['context'] = $this->mergeOriginalContext($logOverride->context, $logOverride->evaluation_run_id, $logOverride->log_id); + } else { + $data['context'] = $this->mergeOriginalContext($data['context'], $logOverride->evaluation_run_id, $logOverride->log_id); + } + $statusChanged = array_key_exists('forced_log_status', $data); + + $logOverride->fill($data); + $logOverride->save(); + $this->applyOverrideToLogResult($logOverride); + if ($this->shouldRecalculateRanks($logOverride->evaluation_run_id, $statusChanged)) { + RecalculateOfficialRanksJob::dispatch($logOverride->evaluation_run_id)->onQueue('evaluation'); + } + + $logOverride->load([ + 'evaluationRun', + 'log', + 'forcedBand', + 'forcedCategory', + 'forcedPowerCategory', + 'createdByUser', + ]); + + return response()->json($logOverride); + } + + /** + * Smazání override záznamu. + */ + public function destroy(LogOverride $logOverride): JsonResponse + { + $this->authorize('delete', $logOverride); + + $log = $logOverride->log; + $round = $log ? Round::with(['bands', 'categories', 'powerCategories'])->find($log->round_id) : null; + $bandId = $log && $round ? $this->resolveBandId($log, $round) : null; + $categoryId = $log && $round ? $this->resolveCategoryId($log, $round) : null; + $powerCategoryId = $log?->power_category_id; + + $logOverride->delete(); + LogResult::where('evaluation_run_id', $logOverride->evaluation_run_id) + ->where('log_id', $logOverride->log_id) + ->update([ + 'status' => 'OK', + 'band_id' => $bandId, + 'category_id' => $categoryId, + 'power_category_id' => $powerCategoryId, + 'sixhr_category' => $log?->sixhr_category, + ]); + $statusChanged = $logOverride->forced_log_status && $logOverride->forced_log_status !== 'AUTO'; + if ($this->shouldRecalculateRanks($logOverride->evaluation_run_id, $statusChanged)) { + RecalculateOfficialRanksJob::dispatch($logOverride->evaluation_run_id)->onQueue('evaluation'); + } + + return response()->json(null, 204); + } + + protected function resolveCategoryId(\App\Models\Log $log, Round $round): ?int + { + $value = $log->psect; + if (! $value) { + return null; + } + + $ediCat = EdiCategory::whereRaw('LOWER(value) = ?', [mb_strtolower(trim($value))])->first(); + if (! $ediCat) { + $ediCat = $this->matchEdiCategoryByRegex($value); + } + if (! $ediCat) { + return null; + } + + $mappedCategoryId = $ediCat->categories()->value('categories.id'); + if (! $mappedCategoryId) { + return null; + } + + if ($round->categories()->count() === 0) { + return $mappedCategoryId; + } + + return $round->categories()->where('categories.id', $mappedCategoryId)->exists() + ? $mappedCategoryId + : null; + } + + protected function matchEdiCategoryByRegex(string $value): ?EdiCategory + { + $candidates = EdiCategory::whereNotNull('regex_pattern')->get(); + foreach ($candidates as $candidate) { + $pattern = $candidate->regex_pattern; + if (! $pattern) { + continue; + } + + $delimited = preg_match('/^([#\\/]).+\\1[imsxuADSUXJ]*$/', $pattern) ? $pattern : '/' . $pattern . '/i'; + set_error_handler(function () { + }); + $matched = @preg_match($delimited, $value) === 1; + restore_error_handler(); + + if ($matched) { + return $candidate; + } + } + + return null; + } + + protected function resolveBandId(\App\Models\Log $log, Round $round): ?int + { + if (! $log->pband) { + return null; + } + + $pbandVal = mb_strtolower(trim($log->pband)); + $ediBand = EdiBand::whereRaw('LOWER(value) = ?', [$pbandVal])->first(); + if ($ediBand) { + $mappedBandId = $ediBand->bands()->value('bands.id'); + if (! $mappedBandId) { + return null; + } + if ($round->bands()->count() === 0) { + return $mappedBandId; + } + return $round->bands()->where('bands.id', $mappedBandId)->exists() + ? $mappedBandId + : null; + } + + $num = is_numeric($pbandVal) ? (float) $pbandVal : null; + if ($num === null && $log->pband) { + if (preg_match('/([0-9]+(?:[\\.,][0-9]+)?)/', $log->pband, $m)) { + $num = (float) str_replace(',', '.', $m[1]); + } + } + if ($num === null) { + return null; + } + + $bandMatch = Band::where('edi_band_begin', '<=', $num) + ->where('edi_band_end', '>=', $num) + ->first(); + if (! $bandMatch) { + return null; + } + + if ($round->bands()->count() === 0) { + return $bandMatch->id; + } + + return $round->bands()->where('bands.id', $bandMatch->id)->exists() + ? $bandMatch->id + : null; + } + + protected function shouldRecalculateRanks(int $evaluationRunId, bool $statusChanged): bool + { + $run = EvaluationRun::find($evaluationRunId); + if (! $run) { + return false; + } + + if ($run->status === 'SUCCEEDED') { + return true; + } + + // Ve WAITING_REVIEW_SCORE řešíme jen změny statutu (DQ/IGNORED/OK/CHECK), + // aby se pořadí hned přepočítalo bez ručního pokračování pipeline. + return $run->status === 'WAITING_REVIEW_SCORE' && $statusChanged; + } + + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'evaluation_run_id' => [$required, 'integer', 'exists:evaluation_runs,id'], + 'log_id' => [$required, 'integer', 'exists:logs,id'], + + 'forced_log_status' => ['sometimes', 'string', 'in:AUTO,OK,CHECK,DQ,IGNORED'], + + 'forced_band_id' => ['sometimes', 'nullable', 'integer', 'exists:bands,id'], + 'forced_category_id' => ['sometimes', 'nullable', 'integer', 'exists:categories,id'], + 'forced_power_category_id' => ['sometimes', 'nullable', 'integer', 'exists:power_categories,id'], + 'forced_sixhr_category' => ['sometimes', 'nullable', 'boolean'], + + 'forced_power_w' => ['sometimes', 'nullable', 'integer', 'min:0'], + + 'reason' => ['sometimes', 'nullable', 'string', 'max:500'], + 'context' => ['sometimes', 'nullable', 'array'], + + 'created_by_user_id' => ['sometimes', 'nullable', 'integer', 'exists:users,id'], + ]); + } + + protected function applyOverrideToLogResult(LogOverride $override): void + { + $data = []; + + if ($override->forced_log_status && $override->forced_log_status !== 'AUTO') { + $data['status'] = $override->forced_log_status; + if (in_array($override->forced_log_status, ['DQ', 'IGNORED', 'CHECK'], true)) { + $data['rank_overall'] = null; + $data['rank_in_category'] = null; + $data['rank_overall_ok'] = null; + $data['rank_in_category_ok'] = null; + $data['status_reason'] = null; + $data['official_score'] = 0; + $data['penalty_score'] = 0; + $data['base_score'] = 0; + $data['multiplier_count'] = 0; + $data['multiplier_score'] = 0; + $data['valid_qso_count'] = 0; + $data['dupe_qso_count'] = 0; + $data['busted_qso_count'] = 0; + $data['other_error_qso_count'] = 0; + } + } + + if ($override->forced_band_id !== null) { + $data['band_id'] = $override->forced_band_id; + } + + if ($override->forced_category_id !== null) { + $data['category_id'] = $override->forced_category_id; + } + + if ($override->forced_power_category_id !== null) { + $data['power_category_id'] = $override->forced_power_category_id; + } + + if ($override->forced_sixhr_category !== null) { + $data['sixhr_category'] = $override->forced_sixhr_category; + } + + if (! $data) { + $this->resetLogResultToSource($override); + return; + } + + LogResult::where('evaluation_run_id', $override->evaluation_run_id) + ->where('log_id', $override->log_id) + ->update($data); + } + + protected function resetLogResultToSource(LogOverride $override): void + { + $log = $override->log; + if (! $log) { + return; + } + $round = Round::with(['bands', 'categories', 'powerCategories'])->find($log->round_id); + if (! $round) { + return; + } + + $bandId = $this->resolveBandId($log, $round); + $categoryId = $this->resolveCategoryId($log, $round); + $powerCategoryId = $log->power_category_id; + + LogResult::where('evaluation_run_id', $override->evaluation_run_id) + ->where('log_id', $override->log_id) + ->update([ + 'status' => 'OK', + 'status_reason' => null, + 'band_id' => $bandId, + 'category_id' => $categoryId, + 'power_category_id' => $powerCategoryId, + 'sixhr_category' => $log->sixhr_category, + ]); + } + + protected function mergeOriginalContext(?array $context, int $evaluationRunId, int $logId): array + { + $context = $context ?? []; + if (isset($context['original']) && is_array($context['original'])) { + return $context; + } + + $context['original'] = $this->snapshotLogResult($evaluationRunId, $logId); + return $context; + } + + protected function snapshotLogResult(int $evaluationRunId, int $logId): array + { + $result = LogResult::where('evaluation_run_id', $evaluationRunId) + ->where('log_id', $logId) + ->first(); + if (! $result) { + return []; + } + + return [ + 'status' => $result->status, + 'band_id' => $result->band_id, + 'category_id' => $result->category_id, + 'power_category_id' => $result->power_category_id, + 'sixhr_category' => $result->sixhr_category, + ]; + } +} diff --git a/app/Http/Controllers/LogQsoController.php b/app/Http/Controllers/LogQsoController.php new file mode 100644 index 0000000..4031b04 --- /dev/null +++ b/app/Http/Controllers/LogQsoController.php @@ -0,0 +1,171 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam QSO – s filtrováním podle log_id, round_id, band, call_like, dx_call. + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $query = LogQso::query() + ->with('log'); + + if ($request->filled('log_id')) { + $query->where('log_id', (int) $request->get('log_id')); + } + + if ($request->filled('round_id')) { + $roundId = (int) $request->get('round_id'); + $query->whereHas('log', function ($q) use ($roundId) { + $q->where('round_id', $roundId); + }); + } + + if ($request->filled('band')) { + $query->where('band', $request->get('band')); + } + + if ($request->filled('call_like')) { + $raw = strtoupper((string) $request->get('call_like')); + $pattern = str_replace(['*', '?'], ['%', '_'], $raw); + if (strpos($pattern, '%') === false && strpos($pattern, '_') === false) { + $pattern = '%' . $pattern . '%'; + } + $query->where(function ($q) use ($pattern) { + $q->whereRaw('UPPER(my_call) LIKE ?', [$pattern]) + ->orWhereRaw('UPPER(dx_call) LIKE ?', [$pattern]); + }); + } + + if ($request->filled('dx_call')) { + $query->where('dx_call', $request->get('dx_call')); + } + + if ($request->filled('exclude_log_id')) { + $query->where('log_id', '!=', (int) $request->get('exclude_log_id')); + } + + if ($request->filled('exclude_log_qso_id')) { + $query->where('id', '!=', (int) $request->get('exclude_log_qso_id')); + } + + $items = $query + ->orderBy('log_id') + ->orderBy('qso_index') + ->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření QSO řádku. + * Typicky voláno parserem EDI, ne přímo z UI. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', LogQso::class); + + $data = $this->validateData($request); + + $item = LogQso::create($data); + + $item->load('log'); + + return response()->json($item, 201); + } + + /** + * Detail jednoho QSO řádku. + */ + public function show(LogQso $logQso): JsonResponse + { + $logQso->load('log'); + + return response()->json($logQso); + } + + /** + * Aktualizace QSO (partial update). + * Praktické pro ruční korekce / debug. + */ + public function update(Request $request, LogQso $logQso): JsonResponse + { + $this->authorize('update', $logQso); + + $data = $this->validateData($request, partial: true); + + $logQso->fill($data); + $logQso->save(); + + $logQso->load('log'); + + return response()->json($logQso); + } + + /** + * Smazání QSO. + */ + public function destroy(LogQso $logQso): JsonResponse + { + $this->authorize('delete', $logQso); + + $logQso->delete(); + + return response()->json(null, 204); + } + + /** + * Validace pro store / update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'log_id' => [$required, 'integer', 'exists:logs,id'], + 'qso_index' => ['sometimes', 'nullable', 'integer', 'min:0'], + + 'time_on' => ['sometimes', 'nullable', 'date'], + 'band' => ['sometimes', 'nullable', 'string', 'max:10'], + 'freq_khz' => ['sometimes', 'nullable', 'integer', 'min:0'], + 'mode' => ['sometimes', 'nullable', 'string', 'max:5'], + + 'my_call' => ['sometimes', 'nullable', 'string', 'max:20'], + 'my_rst' => ['sometimes', 'nullable', 'string', 'max:10'], + 'my_serial' => ['sometimes', 'nullable', 'string', 'max:10'], + 'my_locator' => ['sometimes', 'nullable', 'string', 'max:6'], + + 'dx_call' => ['sometimes', 'nullable', 'string', 'max:20'], + 'dx_rst' => ['sometimes', 'nullable', 'string', 'max:10'], + 'dx_serial' => ['sometimes', 'nullable', 'string', 'max:10'], + 'dx_locator' => ['sometimes', 'nullable', 'string', 'max:6'], + + 'points' => ['sometimes', 'nullable', 'integer'], + 'wwl' => ['sometimes', 'nullable', 'string', 'max:6'], + 'dxcc' => ['sometimes', 'nullable', 'string', 'max:10'], + 'is_duplicate'=> ['sometimes', 'boolean'], + 'is_valid' => ['sometimes', 'boolean'], + + 'raw_line' => ['sometimes', 'nullable', 'string', 'max:500'], + ]); + } +} diff --git a/app/Http/Controllers/LogResultController.php b/app/Http/Controllers/LogResultController.php new file mode 100644 index 0000000..36c12c4 --- /dev/null +++ b/app/Http/Controllers/LogResultController.php @@ -0,0 +1,239 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam výsledků logů – filtrování podle evaluation_run_id, + * log_id, band_id, category_id, status. + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + $statusParam = $request->get('status'); + $isClaimedRequest = $statusParam === 'CLAIMED'; + + $query = LogResult::query() + ->with([ + 'evaluationRun.ruleSet:id,sixhr_ranking_mode', + 'log', + 'band:id,name,order', + 'category:id,name,order', + 'powerCategory:id,name,order', + ]); + + if ($request->filled('evaluation_run_id')) { + $query->where('evaluation_run_id', (int) $request->get('evaluation_run_id')); + } + + if ($request->filled('round_id')) { + $roundId = (int) $request->get('round_id'); + $query->whereHas('log', function ($q) use ($roundId) { + $q->where('round_id', $roundId); + }); + if (! $request->filled('evaluation_run_id') && $request->filled('result_type')) { + $round = Round::find($roundId); + $resultType = strtoupper((string) $request->get('result_type')); + $selectedRunId = null; + if ($round) { + if ($resultType === 'FINAL') { + $selectedRunId = $round->official_evaluation_run_id; + } elseif ($resultType === 'PRELIMINARY') { + $selectedRunId = $round->preliminary_evaluation_run_id; + } elseif ($resultType === 'TEST') { + $selectedRunId = $round->test_evaluation_run_id; + } elseif ($resultType === 'AUTO') { + $selectedRunId = $round->official_evaluation_run_id + ?? $round->preliminary_evaluation_run_id; + } + } + if ($selectedRunId) { + $query->where('evaluation_run_id', $selectedRunId); + } else { + $query->whereRaw('1=0'); + } + } + if (! $request->filled('evaluation_run_id') && $isClaimedRequest) { + $latestClaimedRunId = EvaluationRun::where('round_id', $roundId) + ->where('rules_version', 'CLAIMED') + ->orderByDesc('id') + ->value('id'); + if ($latestClaimedRunId) { + $query->where('evaluation_run_id', $latestClaimedRunId); + } + } + } + + if ($request->filled('log_id')) { + $query->where('log_id', (int) $request->get('log_id')); + } + + if ($request->filled('band_id')) { + $query->where('band_id', (int) $request->get('band_id')); + } + + if ($request->filled('category_id')) { + $query->where('category_id', (int) $request->get('category_id')); + } + + if ($request->filled('status') && ! $isClaimedRequest) { + $query->where('status', $statusParam); + } + + if ($request->boolean('only_ok', false)) { + $pcallExpr = "UPPER(REPLACE(TRIM(pcall), ' ', ''))"; + $query->whereHas('log', function ($q) use ($pcallExpr) { + $q->where(function ($sub) use ($pcallExpr) { + $sub->whereRaw("{$pcallExpr} LIKE ?", ['OK%']) + ->orWhereRaw("{$pcallExpr} LIKE ?", ['OL%']) + ->orWhereRaw("{$pcallExpr} LIKE ?", ['%/OK%']) + ->orWhereRaw("{$pcallExpr} LIKE ?", ['%/OL%']); + }); + }); + } + + // implicitně řadit podle oficiálního skóre + $items = $query + ->orderByDesc('official_score') + ->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření záznamu výsledku logu. + * Typicky voláno vyhodnocovačem, ne přímo z UI. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', LogResult::class); + + $data = $this->validateData($request); + + $result = LogResult::create($data); + + $result->load([ + 'evaluationRun.ruleSet:id,sixhr_ranking_mode', + 'log', + 'band:id,name,order', + 'category:id,name,order', + 'powerCategory:id,name,order', + ]); + + return response()->json($result, 201); + } + + /** + * Detail jednoho výsledku. + */ + public function show(LogResult $logResult): JsonResponse + { + $logResult->load([ + 'evaluationRun.ruleSet:id,sixhr_ranking_mode', + 'log', + 'band:id,name,order', + 'category:id,name,order', + 'powerCategory:id,name,order', + ]); + + return response()->json($logResult); + } + + /** + * Aktualizace výsledku (partial). + * Typicky pro ruční korekci statutu / poznámky. + */ + public function update(Request $request, LogResult $logResult): JsonResponse + { + $this->authorize('update', $logResult); + + $data = $this->validateData($request, partial: true); + + $logResult->fill($data); + $logResult->save(); + + $logResult->load([ + 'evaluationRun.ruleSet:id,sixhr_ranking_mode', + 'log', + 'band:id,name,order', + 'category:id,name,order', + 'powerCategory:id,name,order', + ]); + + return response()->json($logResult); + } + + /** + * Smazání výsledku. + */ + public function destroy(LogResult $logResult): JsonResponse + { + $this->authorize('delete', $logResult); + + $logResult->delete(); + + return response()->json(null, 204); + } + + /** + * Validace pro store / update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'evaluation_run_id' => [$required, 'integer', 'exists:evaluation_runs,id'], + 'log_id' => [$required, 'integer', 'exists:logs,id'], + + 'band_id' => ['sometimes', 'nullable', 'integer', 'exists:bands,id'], + 'category_id' => ['sometimes', 'nullable', 'integer', 'exists:categories,id'], + 'power_category_id' => ['sometimes', 'nullable', 'integer', 'exists:power_categories,id'], + + 'claimed_qso_count' => ['sometimes', 'nullable', 'integer', 'min:0'], + 'claimed_score' => ['sometimes', 'nullable', 'integer', 'min:0'], + + 'valid_qso_count' => ['sometimes', 'integer', 'min:0'], + 'dupe_qso_count' => ['sometimes', 'integer', 'min:0'], + 'busted_qso_count' => ['sometimes', 'integer', 'min:0'], + 'other_error_qso_count' => ['sometimes', 'integer', 'min:0'], + 'total_qso_count' => ['sometimes', 'integer', 'min:0'], + 'discarded_qso_count' => ['sometimes', 'integer', 'min:0'], + 'discarded_points' => ['sometimes', 'integer'], + 'discarded_qso_percent' => ['sometimes', 'numeric', 'min:0'], + 'unique_qso_count' => ['sometimes', 'integer', 'min:0'], + + 'official_score' => ['sometimes', 'integer'], + 'penalty_score' => ['sometimes', 'integer'], + 'base_score' => ['sometimes', 'integer'], + 'multiplier_count' => ['sometimes', 'integer', 'min:0'], + 'multiplier_score' => ['sometimes', 'integer'], + 'score_per_qso' => ['sometimes', 'numeric', 'min:0'], + + 'rank_overall' => ['sometimes', 'nullable', 'integer', 'min:1'], + 'rank_in_category' => ['sometimes', 'nullable', 'integer', 'min:1'], + + 'status' => ['sometimes', 'string', 'max:20'], + 'status_reason' => ['sometimes', 'nullable', 'string'], + ]); + } +} diff --git a/app/Http/Controllers/LoginController.php b/app/Http/Controllers/LoginController.php new file mode 100644 index 0000000..08f0eab --- /dev/null +++ b/app/Http/Controllers/LoginController.php @@ -0,0 +1,39 @@ +validate([ + 'email' => ['required', 'email'], + 'password' => ['required'], + 'remember' => ['sometimes', 'boolean'], + ]); + + $remember = $request->boolean('remember', false); + + if (Auth::attempt([ + 'email' => $credentials['email'], + 'password' => $credentials['password'], + 'is_active' => true, + ], $remember)) { + if ($request->hasSession()) { + $request->session()->regenerate(); + } + $user = Auth::user(); + + return response()->json($user); + } + + return response()->json([ + 'errors' => [ + 'email' => 'The provided credentials do not match our records.', + ] + ], 422); + } +} diff --git a/app/Http/Controllers/NewsPostController.php b/app/Http/Controllers/NewsPostController.php new file mode 100644 index 0000000..839018a --- /dev/null +++ b/app/Http/Controllers/NewsPostController.php @@ -0,0 +1,309 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Display a listing of the resource. + * + * Podporuje volitelný dotazový parametr ?lang=cs / ?lang=en + * Pokud je lang zadán, title/content/excerpt budou vráceny jen v daném jazyce. + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 10); + $limit = (int) $request->get('limit', 0); + $includeUnpublished = $request->boolean('include_unpublished', false); + + // volitelný jazyk – pokud není, použije se app locale + $lang = $request->query('lang'); + if (! is_string($lang) || $lang === '') { + $lang = app()->getLocale(); + } + + $query = NewsPost::query() + ->orderByDesc('published_at'); + + if (! $includeUnpublished) { + $query->where('is_published', true) + ->whereNotNull('published_at') + ->where('published_at', '<=', now()); + } + + if ($limit > 0) { + $items = $query->limit($limit)->get(); + } else { + $items = $query->paginate($perPage); + } + + $mapTranslation = function (NewsPost $post) use ($lang) { + $data = $post->toArray(); + + // getTranslation(attr, lang, useFallback=true) + $data['title'] = $post->getTranslation('title', $lang, true); + $data['content'] = $post->getTranslation('content', $lang, true); + $data['excerpt'] = $post->getTranslation('excerpt', $lang, true); + + return $data; + }; + + if ($limit > 0) { + $items = $items->map($mapTranslation); + return response()->json($items); + } + + $items->getCollection()->transform($mapTranslation); + + return response()->json($items); + } + + /** + * Detail novinky (přes slug). + * Public – ale jen pokud je publikovaná, jinak 404. + */ + public function show(NewsPost $news): JsonResponse + { + if ( + ! $news->is_published || + ! $news->published_at || + $news->published_at->isFuture() + ) { + abort(404); + } + + return response()->json($news); + } + + /** + * Vytvoření novinky (admin). + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', NewsPost::class); + + $data = $this->validateData($request); + + if (empty($data['slug'])) { + $data['slug'] = $this->makeSlugFromTitle($data['title'] ?? null); + } + + if (! empty($data['is_published']) && empty($data['published_at'])) { + $data['published_at'] = now(); + } + + $data['author_id'] = $request->user()?->id; + + $news = NewsPost::create($data); + + return response()->json($news, 201); + } + + /** + * Aktualizace novinky (admin). + */ + public function update(Request $request, NewsPost $news): JsonResponse + { + $this->authorize('update', $news); + + $data = $this->validateData($request, partial: true); + + // pokud přišla změna title a není explicitně zadaný slug, dopočítej ho + if ( + array_key_exists('title', $data) && + (! array_key_exists('slug', $data) || empty($data['slug'])) + ) { + $generated = $this->makeSlugFromTitle($data['title']); + if ($generated !== null) { + $data['slug'] = $generated; + } + } + + if ( + array_key_exists('is_published', $data) && + $data['is_published'] && + empty($data['published_at']) + ) { + $data['published_at'] = $news->published_at ?? now(); + } + + $news->fill($data); + $news->save(); + + return response()->json($news); + } + + /** + * Smazání novinky (admin). + */ + public function destroy(NewsPost $news): JsonResponse + { + $this->authorize('delete', $news); + + $news->delete(); + + return response()->json(null, 204); + } + + /** + * Validace dat. + * + * Podporuje: + * - string hodnoty (jednotlivý překlad pro aktuální locale) + * - pole překladů: { "cs": "...", "en": "..." } + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + /** @var NewsPost|null $routeNews */ + $routeNews = $request->route('news'); // může být null, nebo model díky route model bindingu + + $rules = [ + 'title' => [ + $required, + function (string $attribute, $value, \Closure $fail) { + if (is_string($value)) { + if (mb_strlen($value) > 255) { + $fail('The '.$attribute.' may not be greater than 255 characters.'); + } + return; + } + + if (is_array($value)) { + foreach ($value as $locale => $text) { + if (! is_string($text)) { + $fail("The {$attribute}.{$locale} must be a string."); + return; + } + if (mb_strlen($text) > 255) { + $fail("The {$attribute}.{$locale} may not be greater than 255 characters."); + return; + } + } + return; + } + + $fail('The '.$attribute.' must be a string or an array of translated strings.'); + }, + ], + + 'slug' => [ + 'sometimes', + 'nullable', + 'string', + 'max:255', + Rule::unique('news_posts', 'slug')->ignore($routeNews?->getKey()), + ], + + 'content' => [ + $required, + function (string $attribute, $value, \Closure $fail) { + if (is_string($value)) { + return; + } + + if (is_array($value)) { + foreach ($value as $locale => $text) { + if (! is_string($text)) { + $fail("The {$attribute}.{$locale} must be a string."); + return; + } + } + return; + } + + $fail('The '.$attribute.' must be a string or an array of translated strings.'); + }, + ], + + 'excerpt' => [ + 'sometimes', + function (string $attribute, $value, \Closure $fail) { + if ($value === null) { + return; + } + + if (is_string($value)) { + if (mb_strlen($value) > 500) { + $fail('The '.$attribute.' may not be greater than 500 characters.'); + } + return; + } + + if (is_array($value)) { + foreach ($value as $locale => $text) { + if (! is_string($text)) { + $fail("The {$attribute}.{$locale} must be a string."); + return; + } + if (mb_strlen($text) > 500) { + $fail("The {$attribute}.{$locale} may not be greater than 500 characters."); + return; + } + } + return; + } + + $fail('The '.$attribute.' must be null, a string, or an array of translated strings.'); + }, + ], + + 'is_published' => ['sometimes', 'boolean'], + 'published_at' => ['sometimes', 'nullable', 'date'], + ]; + + return $request->validate($rules); + } + + /** + * Vytvoří slug z titulku – umí pracovat jak se stringem, tak s polem překladů. + * + * - pokud je $title string → slug z něj + * - pokud je $title array → použije se: + * title[aktuální_locale] || title['en'] || první dostupná hodnota + */ + protected function makeSlugFromTitle(string|array|null $title): ?string + { + if ($title === null) { + return null; + } + + if (is_array($title)) { + $locale = app()->getLocale(); + + $base = $title[$locale] + ?? $title['en'] + ?? reset($title); + + if (! is_string($base) || $base === '') { + return null; + } + + return Str::slug($base); + } + + if ($title === '') { + return null; + } + + return Str::slug($title); + } +} diff --git a/app/Http/Controllers/PowerCategoryController.php b/app/Http/Controllers/PowerCategoryController.php new file mode 100644 index 0000000..34dc8ce --- /dev/null +++ b/app/Http/Controllers/PowerCategoryController.php @@ -0,0 +1,101 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam power kategorií. + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 50); + + $items = PowerCategory::query() + ->orderBy('order') + ->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření nové power kategorie. + * Autorizace přes PowerCategoryPolicy@create. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', PowerCategory::class); + + $data = $this->validateData($request); + + $item = PowerCategory::create($data); + + return response()->json($item, 201); + } + + /** + * Detail power kategorie. + */ + public function show(PowerCategory $power_category): JsonResponse + { + return response()->json($power_category); + } + + /** + * Aktualizace power kategorie (partial update). + * Autorizace přes PowerCategoryPolicy@update. + */ + public function update(Request $request, PowerCategory $power_category): JsonResponse + { + $this->authorize('update', $power_category); + + $data = $this->validateData($request, partial: true); + + $power_category->fill($data); + $power_category->save(); + + return response()->json($power_category); + } + + /** + * Smazání power kategorie. + * Autorizace přes PowerCategoryPolicy@delete. + */ + public function destroy(PowerCategory $power_category): JsonResponse + { + $this->authorize('delete', $power_category); + + $power_category->delete(); + + return response()->json(null, 204); + } + + /** + * Validace dat pro store / update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'name' => [$required, 'string', 'max:255'], + 'order' => [$required, 'integer'], + 'power_level' => [$required, 'integer'], + ]); + } +} diff --git a/app/Http/Controllers/QsoOverrideController.php b/app/Http/Controllers/QsoOverrideController.php new file mode 100644 index 0000000..819af0d --- /dev/null +++ b/app/Http/Controllers/QsoOverrideController.php @@ -0,0 +1,126 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam override záznamů – lze filtrovat podle evaluation_run_id/log_qso_id. + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + + $query = QsoOverride::query() + ->with(['evaluationRun', 'logQso', 'forcedMatchedLogQso', 'createdByUser']); + + if ($request->filled('evaluation_run_id')) { + $query->where('evaluation_run_id', (int) $request->get('evaluation_run_id')); + } + + if ($request->filled('log_qso_id')) { + $query->where('log_qso_id', (int) $request->get('log_qso_id')); + } + + $items = $query->orderByDesc('id')->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření override záznamu. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', QsoOverride::class); + + $data = $this->validateData($request); + if (! isset($data['created_by_user_id']) && $request->user()) { + $data['created_by_user_id'] = $request->user()->id; + } + + $item = QsoOverride::create($data); + $item->load(['evaluationRun', 'logQso', 'forcedMatchedLogQso', 'createdByUser']); + + return response()->json($item, 201); + } + + /** + * Detail override záznamu. + */ + public function show(QsoOverride $qsoOverride): JsonResponse + { + $qsoOverride->load(['evaluationRun', 'logQso', 'forcedMatchedLogQso', 'createdByUser']); + + return response()->json($qsoOverride); + } + + /** + * Aktualizace override záznamu. + */ + public function update(Request $request, QsoOverride $qsoOverride): JsonResponse + { + $this->authorize('update', $qsoOverride); + + $data = $this->validateData($request, partial: true); + + $qsoOverride->fill($data); + $qsoOverride->save(); + + $qsoOverride->load(['evaluationRun', 'logQso', 'forcedMatchedLogQso', 'createdByUser']); + + return response()->json($qsoOverride); + } + + /** + * Smazání override záznamu. + */ + public function destroy(QsoOverride $qsoOverride): JsonResponse + { + $this->authorize('delete', $qsoOverride); + + $qsoOverride->delete(); + + return response()->json(null, 204); + } + + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'evaluation_run_id' => [$required, 'integer', 'exists:evaluation_runs,id'], + 'log_qso_id' => [$required, 'integer', 'exists:log_qsos,id'], + + 'forced_matched_log_qso_id' => ['sometimes', 'nullable', 'integer', 'exists:log_qsos,id'], + 'forced_status' => [ + 'sometimes', + 'string', + 'in:AUTO,VALID,INVALID,NIL,DUPLICATE,BUSTED_CALL,BUSTED_EXCHANGE,OUT_OF_WINDOW', + ], + + 'forced_points' => ['sometimes', 'nullable', 'numeric'], + 'forced_penalty' => ['sometimes', 'nullable', 'numeric'], + + 'reason' => ['sometimes', 'nullable', 'string', 'max:500'], + 'context' => ['sometimes', 'nullable', 'array'], + + 'created_by_user_id' => ['sometimes', 'nullable', 'integer', 'exists:users,id'], + ]); + } +} diff --git a/app/Http/Controllers/QsoResultController.php b/app/Http/Controllers/QsoResultController.php new file mode 100644 index 0000000..ba38dbd --- /dev/null +++ b/app/Http/Controllers/QsoResultController.php @@ -0,0 +1,272 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam QSO výsledků. + * Filtrování podle evaluation_run_id, log_qso_id, log_id, call_like, matched_qso_id, + * error_code, is_valid, is_duplicate, is_nil, only_ok. + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 200); + + $evalRunId = $request->filled('evaluation_run_id') + ? (int) $request->get('evaluation_run_id') + : null; + + $query = QsoResult::query() + ->with(['evaluationRun', 'logQso', 'matchedQso']) + ->when($evalRunId, function ($q) use ($evalRunId) { + $q->with(['workingQso' => function ($wq) use ($evalRunId) { + $wq->where('evaluation_run_id', $evalRunId); + }]); + }); + + if ($evalRunId !== null) { + $query->where('evaluation_run_id', $evalRunId); + } + + if ($request->filled('log_qso_id')) { + $query->where('log_qso_id', (int) $request->get('log_qso_id')); + } + + if ($request->filled('log_id')) { + $logId = (int) $request->get('log_id'); + $query->whereHas('logQso', function ($q) use ($logId) { + $q->where('log_id', $logId); + }); + } + + if ($request->filled('call_like')) { + $raw = strtoupper((string) $request->get('call_like')); + $pattern = str_replace(['*', '?'], ['%', '_'], $raw); + if (strpos($pattern, '%') === false && strpos($pattern, '_') === false) { + $pattern = '%' . $pattern . '%'; + } + $query->where(function ($q) use ($pattern) { + $q->whereHas('logQso', function ($qq) use ($pattern) { + $qq->whereRaw('UPPER(my_call) LIKE ?', [$pattern]) + ->orWhereRaw('UPPER(dx_call) LIKE ?', [$pattern]); + })->orWhereHas('matchedQso', function ($qq) use ($pattern) { + $qq->whereRaw('UPPER(my_call) LIKE ?', [$pattern]) + ->orWhereRaw('UPPER(dx_call) LIKE ?', [$pattern]); + }); + }); + } + + if ($request->filled('matched_qso_id')) { + $query->where('matched_qso_id', (int) $request->get('matched_qso_id')); + } + + if ($request->filled('error_code')) { + $query->where('error_code', $request->get('error_code')); + } + + if ($request->filled('is_valid')) { + $query->where( + 'is_valid', + filter_var($request->get('is_valid'), FILTER_VALIDATE_BOOL) + ); + } + + if ($request->filled('is_duplicate')) { + $query->where( + 'is_duplicate', + filter_var($request->get('is_duplicate'), FILTER_VALIDATE_BOOL) + ); + } + + if ($request->filled('is_nil')) { + $query->where( + 'is_nil', + filter_var($request->get('is_nil'), FILTER_VALIDATE_BOOL) + ); + } + + if ($request->filled('is_time_out_of_window')) { + $query->where( + 'is_time_out_of_window', + filter_var($request->get('is_time_out_of_window'), FILTER_VALIDATE_BOOL) + ); + } + + if (filter_var($request->get('only_problems'), FILTER_VALIDATE_BOOL)) { + $query->where(function ($q) { + $q->where(function ($qq) { + $qq->whereNotNull('error_code') + ->where('error_code', '!=', 'OK'); + }) + ->orWhere('is_nil', true) + ->orWhere('is_duplicate', true) + ->orWhere('is_busted_call', true) + ->orWhere('is_busted_exchange', true) + ->orWhere('is_time_out_of_window', true); + }); + } + + if (filter_var($request->get('only_ok'), FILTER_VALIDATE_BOOL)) { + $query->where(function ($q) { + $q->whereNull('error_code') + ->orWhere('error_code', 'OK'); + }) + ->where('is_nil', false) + ->where('is_duplicate', false) + ->where('is_busted_call', false) + ->where('is_busted_exchange', false) + ->where('is_time_out_of_window', false); + } + + if (filter_var($request->get('missing_locator'), FILTER_VALIDATE_BOOL)) { + $query->whereHas('workingQso', function ($q) use ($evalRunId) { + if ($evalRunId !== null) { + $q->where('evaluation_run_id', $evalRunId); + } + $q->whereNull('loc_norm') + ->orWhereNull('rloc_norm') + ->orWhereJsonContains('errors', 'INVALID_LOCATOR') + ->orWhereJsonContains('errors', 'INVALID_RLOCATOR'); + }); + } + + $items = $query + ->orderBy('evaluation_run_id') + ->orderBy('log_qso_id') + ->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření QSO výsledku. + * Typicky voláno vyhodnocovačem, ne přímo z UI. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', QsoResult::class); + + $data = $this->validateData($request); + + $result = QsoResult::create($data); + + $result->load([ + 'evaluationRun', + 'logQso', + 'matchedQso', + 'workingQso' => function ($q) use ($result) { + $q->where('evaluation_run_id', $result->evaluation_run_id); + }, + ]); + + return response()->json($result, 201); + } + + /** + * Detail jednoho QSO výsledku. + */ + public function show(QsoResult $qsoResult): JsonResponse + { + $qsoResult->load([ + 'evaluationRun', + 'logQso', + 'matchedQso', + 'workingQso' => function ($q) use ($qsoResult) { + $q->where('evaluation_run_id', $qsoResult->evaluation_run_id); + }, + ]); + + return response()->json($qsoResult); + } + + /** + * Aktualizace QSO výsledku (partial update). + * Praktické pro ruční korekce / override. + */ + public function update(Request $request, QsoResult $qsoResult): JsonResponse + { + $this->authorize('update', $qsoResult); + + $data = $this->validateData($request, partial: true); + + $qsoResult->fill($data); + $qsoResult->save(); + + $qsoResult->load([ + 'evaluationRun', + 'logQso', + 'matchedQso', + 'workingQso' => function ($q) use ($qsoResult) { + $q->where('evaluation_run_id', $qsoResult->evaluation_run_id); + }, + ]); + + return response()->json($qsoResult); + } + + /** + * Smazání QSO výsledku. + */ + public function destroy(QsoResult $qsoResult): JsonResponse + { + $this->authorize('delete', $qsoResult); + + $qsoResult->delete(); + + return response()->json(null, 204); + } + + /** + * Validace vstupu pro store / update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'evaluation_run_id' => [$required, 'integer', 'exists:evaluation_runs,id'], + 'log_qso_id' => [$required, 'integer', 'exists:log_qsos,id'], + + 'is_valid' => ['sometimes', 'boolean'], + 'is_duplicate' => ['sometimes', 'boolean'], + 'is_nil' => ['sometimes', 'boolean'], + 'is_busted_call' => ['sometimes', 'boolean'], + 'is_busted_rst' => ['sometimes', 'boolean'], + 'is_busted_exchange' => ['sometimes', 'boolean'], + 'is_time_out_of_window' => ['sometimes', 'boolean'], + + 'points' => ['sometimes', 'integer'], + 'penalty_points' => ['sometimes', 'integer'], + 'distance_km' => ['sometimes', 'nullable', 'integer', 'min:0'], + + 'wwl' => ['sometimes', 'nullable', 'string', 'max:6'], + 'dxcc' => ['sometimes', 'nullable', 'string', 'max:10'], + 'country' => ['sometimes', 'nullable', 'string', 'max:100'], + 'section' => ['sometimes', 'nullable', 'string', 'max:50'], + + 'matched_qso_id' => ['sometimes', 'nullable', 'integer', 'exists:log_qsos,id'], + 'matched_log_qso_id' => ['sometimes', 'nullable', 'integer', 'exists:log_qsos,id'], + 'match_confidence' => ['sometimes', 'nullable', 'string', 'max:20'], + + 'error_code' => ['sometimes', 'nullable', 'string', 'max:50'], + 'error_side' => ['sometimes', 'nullable', 'string', 'max:10'], + 'error_detail' => ['sometimes', 'nullable', 'string'], + ]); + } +} diff --git a/app/Http/Controllers/RoundController.php b/app/Http/Controllers/RoundController.php new file mode 100644 index 0000000..8181d7e --- /dev/null +++ b/app/Http/Controllers/RoundController.php @@ -0,0 +1,392 @@ +middleware('auth:sanctum')->only(['store', 'update', 'destroy']); + } + + /** + * Seznam kol (rounds) – stránkovaně. + */ + public function index(Request $request): JsonResponse + { + $perPage = (int) $request->get('per_page', 100); + $contestId = $request->query('contest_id'); + $onlyActive = (bool) $request->query('only_active', false); + $includeTests = filter_var($request->query('include_tests', true), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + if ($includeTests === null) { + $includeTests = true; + } + + $items = Round::query() + ->with([ + 'contest', + 'bands', + 'categories', + 'powerCategories', + 'ruleSet', + ]) + ->when($contestId, fn ($q) => $q->where('contest_id', $contestId)) + ->when($onlyActive, fn ($q) => $q->where('is_active', true)) + ->when(! $includeTests, fn ($q) => $q->where('is_test', false)) + ->orderByDesc('start_time') + ->orderByDesc('end_time') + ->paginate($perPage); + + return response()->json($items); + } + + /** + * Vytvoření nového kola. + * Autorizace přes RoundPolicy@create. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', Round::class); + + $data = $this->validateData($request); + $relations = $this->validateRelations($request); + + if (! array_key_exists('rule_set_id', $data) || $data['rule_set_id'] === null) { + $contestRuleSetId = Contest::where('id', $data['contest_id'])->value('rule_set_id'); + $data['rule_set_id'] = $contestRuleSetId; + } + + $round = Round::create($data); + + $this->syncRelations($round, $relations); + + $round->load([ + 'contest', + 'bands', + 'categories', + 'powerCategories', + 'ruleSet', + ]); + + return response()->json($round, 201); + } + + /** + * Detail kola. + */ + public function show(Round $round): JsonResponse + { + $round->load([ + 'contest', + 'bands', + 'categories', + 'powerCategories', + 'ruleSet', + ]); + + return response()->json($round); + } + + /** + * Aktualizace kola (partial update). + * Autorizace přes RoundPolicy@update. + */ + public function update(Request $request, Round $round): JsonResponse + { + $this->authorize('update', $round); + + $data = $this->validateData($request, partial: true); + $relations = $this->validateRelations($request); + + $round->fill($data); + $round->save(); + + $this->syncRelations($round, $relations); + + $round->load([ + 'contest', + 'bands', + 'categories', + 'powerCategories', + 'ruleSet', + ]); + + return response()->json($round); + } + + /** + * Smazání kola. + * Autorizace přes RoundPolicy@delete. + */ + public function destroy(Round $round): JsonResponse + { + $this->authorize('delete', $round); + + $round->delete(); + + return response()->json(null, 204); + } + + /** + * Ručně spustí rebuild deklarovaných výsledků pro kolo. + */ + public function recalculateClaimed(Request $request, Round $round): JsonResponse + { + $this->authorize('update', $round); + + $run = ClaimedRunResolver::createNewForRound($round->id, auth()->id()); + RebuildClaimedLogResultsJob::dispatch($run->id)->onQueue('evaluation'); + + return response()->json([ + 'status' => 'queued', + 'message' => 'Přepočet deklarovaných výsledků byl spuštěn.', + ], 202); + } + + /** + * Spustí kompletní vyhodnocovací pipeline pro nové EvaluationRun. + */ + public function startEvaluation(Request $request, Round $round): JsonResponse + { + $this->authorize('update', $round); + + $data = $request->validate([ + 'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'], + 'rules_version' => ['sometimes', 'nullable', 'string', 'in:OFFICIAL,TEST,CLAIMED'], + 'result_type' => ['sometimes', 'nullable', 'string', 'in:PRELIMINARY,FINAL,TEST'], + 'name' => ['sometimes', 'nullable', 'string', 'max:100'], + 'is_official' => ['sometimes', 'boolean'], + 'scope' => ['sometimes', 'array'], + 'scope.band_ids' => ['sometimes', 'array'], + 'scope.band_ids.*' => ['integer', 'exists:bands,id'], + 'scope.category_ids' => ['sometimes', 'array'], + 'scope.category_ids.*' => ['integer', 'exists:categories,id'], + 'scope.power_category_ids' => ['sometimes', 'array'], + 'scope.power_category_ids.*' => ['integer', 'exists:power_categories,id'], + ]); + + $rulesVersion = $data['rules_version'] ?? 'OFFICIAL'; + $resultType = $data['result_type'] + ?? ($rulesVersion === 'TEST' ? 'TEST' : 'PRELIMINARY'); + + $run = EvaluationRun::create([ + 'round_id' => $round->id, + 'rule_set_id' => $data['rule_set_id'] ?? $round->rule_set_id, + 'rules_version' => $rulesVersion, + 'result_type' => $rulesVersion === 'CLAIMED' ? null : $resultType, + 'name' => $data['name'] ?? 'Vyhodnocení', + 'is_official' => $data['is_official'] ?? ($resultType === 'FINAL'), + 'scope' => $data['scope'] ?? null, + 'status' => 'PENDING', + 'created_by_user_id' => auth()->id(), + ]); + + StartEvaluationRunJob::dispatch($run->id)->onQueue('evaluation'); + + $run->load(['round']); + + return response()->json($run, 201); + } + + /** + * Spustí nový EvaluationRun jako re-run s převzetím override z posledního běhu. + */ + public function startEvaluationIncremental(Request $request, Round $round): JsonResponse + { + $this->authorize('update', $round); + + $data = $request->validate([ + 'source_run_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_runs,id'], + 'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'], + 'rules_version' => ['sometimes', 'nullable', 'string', 'in:OFFICIAL,TEST'], + 'result_type' => ['sometimes', 'nullable', 'string', 'in:PRELIMINARY,FINAL,TEST'], + 'name' => ['sometimes', 'nullable', 'string', 'max:100'], + 'scope' => ['sometimes', 'array'], + 'scope.band_ids' => ['sometimes', 'array'], + 'scope.band_ids.*' => ['integer', 'exists:bands,id'], + 'scope.category_ids' => ['sometimes', 'array'], + 'scope.category_ids.*' => ['integer', 'exists:categories,id'], + 'scope.power_category_ids' => ['sometimes', 'array'], + 'scope.power_category_ids.*' => ['integer', 'exists:power_categories,id'], + ]); + + $sourceRun = null; + if (! empty($data['source_run_id'])) { + $sourceRun = EvaluationRun::where('round_id', $round->id) + ->where('id', (int) $data['source_run_id']) + ->first(); + } + if (! $sourceRun) { + $sourceRun = EvaluationRun::where('round_id', $round->id) + ->where('rules_version', '!=', 'CLAIMED') + ->orderByDesc('id') + ->first(); + } + + $rulesVersion = $data['rules_version'] + ?? ($sourceRun?->rules_version ?? 'OFFICIAL'); + $resultType = $data['result_type'] + ?? ($rulesVersion === 'TEST' ? 'TEST' : 'PRELIMINARY'); + + $run = EvaluationRun::create([ + 'round_id' => $round->id, + 'rule_set_id' => $data['rule_set_id'] ?? ($sourceRun?->rule_set_id ?? $round->rule_set_id), + 'rules_version' => $rulesVersion, + 'result_type' => $resultType, + 'name' => $data['name'] ?? 'Vyhodnocení (re-run)', + 'is_official' => $resultType === 'FINAL', + 'scope' => $data['scope'] ?? ($sourceRun?->scope ?? null), + 'status' => 'PENDING', + 'created_by_user_id' => auth()->id(), + ]); + + if ($sourceRun) { + $this->cloneOverrides($sourceRun->id, $run->id, auth()->id()); + } + + StartEvaluationRunJob::dispatch($run->id)->onQueue('evaluation'); + + $run->load(['round']); + + return response()->json($run, 201); + } + + protected function cloneOverrides(int $sourceRunId, int $targetRunId, ?int $userId = null): void + { + $logOverrides = LogOverride::where('evaluation_run_id', $sourceRunId)->get(); + if ($logOverrides->isNotEmpty()) { + $rows = $logOverrides->map(function ($override) use ($targetRunId, $userId) { + return [ + 'evaluation_run_id' => $targetRunId, + 'log_id' => $override->log_id, + 'forced_log_status' => $override->forced_log_status, + 'forced_band_id' => $override->forced_band_id, + 'forced_category_id' => $override->forced_category_id, + 'forced_power_category_id' => $override->forced_power_category_id, + 'forced_sixhr_category' => $override->forced_sixhr_category, + 'forced_power_w' => $override->forced_power_w, + 'reason' => $override->reason, + 'context' => $this->encodeContext($override->context), + 'created_by_user_id' => $override->created_by_user_id ?? $userId, + 'created_at' => now(), + 'updated_at' => now(), + ]; + })->all(); + LogOverride::insert($rows); + } + + $qsoOverrides = QsoOverride::where('evaluation_run_id', $sourceRunId)->get(); + if ($qsoOverrides->isNotEmpty()) { + $rows = $qsoOverrides->map(function ($override) use ($targetRunId, $userId) { + return [ + 'evaluation_run_id' => $targetRunId, + 'log_qso_id' => $override->log_qso_id, + 'forced_matched_log_qso_id' => $override->forced_matched_log_qso_id, + 'forced_status' => $override->forced_status, + 'forced_points' => $override->forced_points, + 'forced_penalty' => $override->forced_penalty, + 'reason' => $override->reason, + 'context' => $this->encodeContext($override->context), + 'created_by_user_id' => $override->created_by_user_id ?? $userId, + 'created_at' => now(), + 'updated_at' => now(), + ]; + })->all(); + QsoOverride::insert($rows); + } + } + + protected function encodeContext(mixed $context): ?string + { + if ($context === null) { + return null; + } + $encoded = json_encode($context); + return $encoded === false ? null : $encoded; + } + + /** + * Validace vstupu pro store / update. + */ + protected function validateData(Request $request, bool $partial = false): array + { + $required = $partial ? 'sometimes' : 'required'; + + return $request->validate([ + 'contest_id' => [$required, 'integer', 'exists:contests,id'], + + // name/description – pokud používáš překlady jako u Contest: + 'name' => [$required, 'array'], + 'name.*' => ['string', 'max:255'], + 'description' => ['sometimes', 'nullable', 'array'], + 'description.*' => ['string'], + + 'start_time' => [$required, 'date'], + 'end_time' => [$required, 'date', 'after:start_time'], + 'logs_deadline' => [$required, 'date'], + + 'is_active' => ['sometimes', 'boolean'], + 'is_test' => ['sometimes', 'boolean'], + 'is_sixhr' => ['sometimes', 'boolean'], + + 'first_check' => ['sometimes', 'nullable', 'date'], + 'second_check' => ['sometimes', 'nullable', 'date'], + 'unique_qso_check' => ['sometimes', 'nullable', 'date'], + 'third_check' => ['sometimes', 'nullable', 'date'], + 'fourth_check' => ['sometimes', 'nullable', 'date'], + 'prelimitary_results'=> ['sometimes', 'nullable', 'date'], + 'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'], + ]); + } + + /** + * Validace ID navázaných entit pro belongsToMany vztahy. + */ + protected function validateRelations(Request $request): array + { + return $request->validate([ + 'band_ids' => ['sometimes', 'array'], + 'band_ids.*' => ['integer', 'exists:bands,id'], + + 'category_ids' => ['sometimes', 'array'], + 'category_ids.*' => ['integer', 'exists:categories,id'], + + 'power_category_ids' => ['sometimes', 'array'], + 'power_category_ids.*' => ['integer', 'exists:power_categories,id'], + ]); + } + + /** + * Sync vazeb pro belongsToMany vztahy. + */ + protected function syncRelations(Round $round, array $relations): void + { + if (array_key_exists('band_ids', $relations)) { + $round->bands()->sync($relations['band_ids']); + } + + if (array_key_exists('category_ids', $relations)) { + $round->categories()->sync($relations['category_ids']); + } + + if (array_key_exists('power_category_ids', $relations)) { + $round->powerCategories()->sync($relations['power_category_ids']); + } + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..b8b3cce --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,114 @@ +middleware('auth:sanctum'); + } + + public function index(Request $request): JsonResponse + { + $this->authorize('viewAny', User::class); + + $perPage = (int) $request->get('per_page', 20); + $query = trim((string) $request->get('query', '')); + + $users = User::query() + ->when($query !== '', function ($q) use ($query) { + $q->where('name', 'like', '%' . $query . '%') + ->orWhere('email', 'like', '%' . $query . '%'); + }) + ->orderBy('name') + ->paginate($perPage); + + return response()->json($users); + } + + public function show(User $user): JsonResponse + { + $this->authorize('view', $user); + + return response()->json($user); + } + + public function store(Request $request): JsonResponse + { + $this->authorize('create', User::class); + + $data = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', 'unique:users,email'], + 'password' => ['required', 'string', 'min:8'], + 'is_admin' => ['sometimes', 'boolean'], + 'is_active' => ['sometimes', 'boolean'], + ]); + + $user = User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => $data['password'], + 'is_admin' => (bool) ($data['is_admin'] ?? false), + 'is_active' => (bool) ($data['is_active'] ?? true), + ]); + + return response()->json($user, 201); + } + + public function update(Request $request, User $user): JsonResponse + { + $this->authorize('update', $user); + + $data = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'email', + 'max:255', + Rule::unique('users', 'email')->ignore($user->id), + ], + 'password' => ['nullable', 'string', 'min:8'], + 'is_admin' => ['sometimes', 'boolean'], + 'is_active' => ['sometimes', 'boolean'], + ]); + + $payload = [ + 'name' => $data['name'], + 'email' => $data['email'], + 'is_admin' => (bool) ($data['is_admin'] ?? $user->is_admin), + 'is_active' => (bool) ($data['is_active'] ?? $user->is_active), + ]; + if (! empty($data['password'])) { + $payload['password'] = $data['password']; + } + + $user->update($payload); + + return response()->json($user); + } + + public function destroy(Request $request, User $user): JsonResponse + { + $this->authorize('delete', $user); + + if ($request->user()?->id === $user->id) { + return response()->json(['message' => 'Nelze deaktivovat vlastního uživatele.'], 422); + } + + $user->update(['is_active' => false]); + + return response()->json($user); + } +} diff --git a/app/Http/Middleware/SetLocaleFromCookie.php b/app/Http/Middleware/SetLocaleFromCookie.php new file mode 100644 index 0000000..944f9f7 --- /dev/null +++ b/app/Http/Middleware/SetLocaleFromCookie.php @@ -0,0 +1,20 @@ +cookie('locale', config('app.locale')); + + if (in_array($locale, ['cs', 'en'], true)) { + app()->setLocale($locale); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/StartEvaluationRunRequest.php b/app/Http/Requests/StartEvaluationRunRequest.php new file mode 100644 index 0000000..3be7a02 --- /dev/null +++ b/app/Http/Requests/StartEvaluationRunRequest.php @@ -0,0 +1,77 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + try { + $ruleSet = EvaluationRuleSet::find($run->rule_set_id); + if (! $ruleSet) { + $coordinator->eventError($run, 'Agregace nelze spustit: chybí ruleset.', [ + 'step' => 'aggregate', + ]); + return; + } + + $timeDiffThresholdSec = $ruleSet->time_diff_dq_threshold_sec; + $timeDiffThresholdPercent = $ruleSet->time_diff_dq_threshold_percent; + $badQsoThresholdPercent = $ruleSet->bad_qso_dq_threshold_percent; + + $logResult = LogResult::firstOrNew([ + 'evaluation_run_id' => $run->id, + 'log_id' => $this->logId, + ]); + + // 6H je omezení agregace, nikoli matchingu; dříve to byl jen flag bez operating-window logiky. + $useOperatingWindow = $logResult->sixhr_category + && $ruleSet->operating_window_mode === 'BEST_CONTIGUOUS' + && (int) $ruleSet->operating_window_hours === 6; + if ($useOperatingWindow) { + $service = new OperatingWindowService(); + $window = $service->pickBestOperatingWindow($run->id, $this->logId, 6, $ruleSet); + if ($window) { + $this->applyOperatingWindow($run->id, $logResult, $window); + } else { + $this->resetOperatingWindow($run->id, $logResult); + } + } else { + $this->resetOperatingWindow($run->id, $logResult); + } + + $stats = [ + 'base_score' => 0, + 'penalty_score' => 0, + 'valid_qso_count' => 0, + 'dupe_qso_count' => 0, + 'busted_qso_count' => 0, + 'other_error_qso_count' => 0, + 'out_of_window_qso_count' => 0, + 'total_qso_count' => 0, + 'discarded_qso_count' => 0, + 'discarded_points' => 0, + 'unique_qso_count' => 0, + 'bad_qso_count' => 0, + 'matched_qso_count' => 0, + 'time_diff_over_threshold_count' => 0, + 'multipliers' => [], + ]; + + $query = QsoResult::query() + ->where('qso_results.evaluation_run_id', $run->id) + ->join('log_qsos', 'log_qsos.id', '=', 'qso_results.log_qso_id') + ->leftJoin('working_qsos', function ($join) use ($run) { + $join->on('working_qsos.log_qso_id', '=', 'qso_results.log_qso_id') + ->where('working_qsos.evaluation_run_id', '=', $run->id); + }) + ->where('log_qsos.log_id', $this->logId) + ->select([ + 'qso_results.id', + 'qso_results.log_qso_id', + 'qso_results.points', + 'qso_results.penalty_points', + 'qso_results.time_diff_sec', + 'qso_results.error_code', + 'qso_results.error_side', + 'qso_results.is_nil', + 'qso_results.is_duplicate', + 'qso_results.is_busted_call', + 'qso_results.is_busted_rst', + 'qso_results.is_busted_exchange', + 'qso_results.is_time_out_of_window', + 'qso_results.is_valid', + 'qso_results.matched_qso_id', + 'qso_results.wwl', + 'qso_results.dxcc', + 'qso_results.country', + 'qso_results.section', + 'working_qsos.band_id as band_id', + ]); + + if ($useOperatingWindow) { + $query->where('qso_results.is_operating_window_excluded', false); + } + + $query->chunkById(1000, function ($rows) use (&$stats, $ruleSet, $timeDiffThresholdSec, $run) { + foreach ($rows as $row) { + if (EvaluationRun::isCanceledRun($run->id)) { + return false; + } + $stats['total_qso_count']++; + + if ($row->is_valid) { + $stats['base_score'] += (int) $row->points; + } + + $errorCode = $row->error_code; + $errorSide = $row->error_side ?? 'NONE'; + + $isNil = (bool) $row->is_nil + || in_array($errorCode, [QsoErrorCode::NOT_IN_COUNTERPART_LOG, QsoErrorCode::NO_COUNTERPART_LOG], true); + $isUnique = $errorCode === QsoErrorCode::UNIQUE; + $isDuplicate = (bool) $row->is_duplicate || $errorCode === QsoErrorCode::DUP; + $isBusted = (bool) $row->is_busted_call + || (bool) $row->is_busted_rst + || (bool) $row->is_busted_exchange + || (in_array($errorCode, [ + QsoErrorCode::BUSTED_CALL, + QsoErrorCode::BUSTED_RST, + QsoErrorCode::BUSTED_SERIAL, + QsoErrorCode::BUSTED_LOCATOR, + ], true) + && $errorSide !== 'TX'); + $isTimeMismatch = $errorCode === QsoErrorCode::TIME_MISMATCH; + $isOutOfWindow = (bool) $row->is_time_out_of_window; + + $isValid = (bool) $row->is_valid; + if ($isValid) { + $stats['valid_qso_count']++; + } else { + $stats['discarded_qso_count']++; + $stats['discarded_points'] += (int) ($row->points ?? 0); + } + + if ($isDuplicate) { + $stats['dupe_qso_count']++; + } + if ($isBusted) { + $stats['busted_qso_count']++; + } + if ($isOutOfWindow) { + $stats['out_of_window_qso_count']++; + } + if ($isNil || $isOutOfWindow || $isUnique) { + $stats['other_error_qso_count']++; + } + if ($isDuplicate || $isBusted || $isOutOfWindow || $isTimeMismatch || $isUnique) { + $stats['bad_qso_count']++; + } + if ($isUnique) { + $stats['unique_qso_count']++; + } + if ($row->matched_qso_id !== null) { + $stats['matched_qso_count']++; + if ( + $timeDiffThresholdSec !== null + && $row->time_diff_sec !== null + && (int) $row->time_diff_sec > (int) $timeDiffThresholdSec + ) { + $stats['time_diff_over_threshold_count']++; + } + } + + $penalty = (int) ($row->penalty_points ?? 0); + if ($row->is_valid && $penalty !== 0) { + $stats['penalty_score'] -= $penalty; + } + + if ($ruleSet->usesMultipliers()) { + $bandKey = $ruleSet->multiplier_scope === 'PER_BAND' + ? (int) ($row->band_id ?? 0) + : 0; + + if (! isset($stats['multipliers'][$bandKey])) { + $stats['multipliers'][$bandKey] = []; + } + + $eligible = false; + if ($ruleSet->multiplier_source === 'VALID_ONLY') { + $eligible = ! $isNil && ! $isDuplicate && ! $isBusted && ! $isOutOfWindow && (bool) $row->is_valid; + } elseif ($ruleSet->multiplier_source === 'ALL_MATCHED') { + $eligible = $row->matched_qso_id !== null + && ! $isNil + && ! $isDuplicate + && ! $isBusted + && ! $isOutOfWindow; + } + + if ($eligible) { + $multiplier = null; + if ($ruleSet->multiplier_type === 'WWL') { + $multiplier = $row->wwl; + } elseif ($ruleSet->multiplier_type === 'DXCC') { + $multiplier = $row->dxcc; + } elseif ($ruleSet->multiplier_type === 'COUNTRY') { + $multiplier = $row->country; + } elseif ($ruleSet->multiplier_type === 'SECTION') { + $multiplier = $row->section; + } + + if ($multiplier) { + $stats['multipliers'][$bandKey][$multiplier] = true; + } + } + } + } + }, 'qso_results.id', 'id'); + + if ($stats['total_qso_count'] === 0) { + $logResult->update([ + 'base_score' => 0, + 'penalty_score' => 0, + 'multiplier_count' => 0, + 'multiplier_score' => 0, + 'official_score' => 0, + 'valid_qso_count' => 0, + 'dupe_qso_count' => 0, + 'busted_qso_count' => 0, + 'other_error_qso_count' => 0, + 'total_qso_count' => 0, + 'discarded_qso_count' => 0, + 'discarded_points' => 0, + 'discarded_qso_percent' => 0, + 'unique_qso_count' => 0, + 'score_per_qso' => null, + ]); + EvaluationRun::where('id', $run->id)->increment('progress_done'); + return; + } + + $multiplierCount = 1; + if ($ruleSet->usesMultipliers()) { + $multiplierCount = 0; + foreach ($stats['multipliers'] as $values) { + $multiplierCount += count($values); + } + } + + $baseScore = (int) $stats['base_score']; + $penaltyScore = (int) $stats['penalty_score']; + $scoreBeforeMultiplier = $baseScore + $penaltyScore; + if (! $ruleSet->usesMultipliers()) { + $multiplierCount = 1; + } + $multiplierScore = $ruleSet->usesMultipliers() ? $scoreBeforeMultiplier * $multiplierCount : $scoreBeforeMultiplier; + $officialScore = max(0, $multiplierScore); + $totalQsoCount = (int) ($stats['total_qso_count'] ?? 0); + $discardedQsoCount = (int) ($stats['discarded_qso_count'] ?? 0); + $discardedPercent = $totalQsoCount > 0 + ? round(($discardedQsoCount / $totalQsoCount) * 100, 2) + : 0; + $validQsoCount = (int) ($stats['valid_qso_count'] ?? 0); + $scorePerQso = $validQsoCount > 0 ? round($officialScore / $validQsoCount, 2) : null; + + $update = [ + 'base_score' => $baseScore, + 'penalty_score' => $penaltyScore, + 'multiplier_count' => $multiplierCount, + 'multiplier_score' => $multiplierScore, + 'official_score' => $officialScore, + 'valid_qso_count' => $stats['valid_qso_count'], + 'dupe_qso_count' => $stats['dupe_qso_count'], + 'busted_qso_count' => $stats['busted_qso_count'], + 'other_error_qso_count' => $stats['other_error_qso_count'], + 'total_qso_count' => $totalQsoCount, + 'discarded_qso_count' => $discardedQsoCount, + 'discarded_points' => (int) ($stats['discarded_points'] ?? 0), + 'discarded_qso_percent' => $discardedPercent, + 'unique_qso_count' => (int) ($stats['unique_qso_count'] ?? 0), + 'score_per_qso' => $scorePerQso, + ]; + + $outOfWindowThreshold = $ruleSet->out_of_window_dq_threshold; + if ($outOfWindowThreshold && in_array($logResult->status, ['OK', 'CHECK'], true)) { + $outOfWindowCount = (int) ($stats['out_of_window_qso_count'] ?? 0); + if ($outOfWindowCount >= (int) $outOfWindowThreshold) { + $reason = 'OUT_OF_WINDOW >= ' . (int) $outOfWindowThreshold; + $update['status'] = 'DQ'; + $update['status_reason'] = $logResult->status_reason + ? $logResult->status_reason . '; ' . $reason + : $reason; + } + } + if ( + $timeDiffThresholdSec !== null + && $timeDiffThresholdPercent !== null + && in_array($logResult->status, ['OK', 'CHECK'], true) + ) { + $matchedCount = (int) ($stats['matched_qso_count'] ?? 0); + if ($matchedCount > 0) { + $overCount = (int) ($stats['time_diff_over_threshold_count'] ?? 0); + $percent = ($overCount / $matchedCount) * 100; + if ($percent > (float) $timeDiffThresholdPercent) { + $percentLabel = rtrim(rtrim(number_format($percent, 1, '.', ''), '0'), '.'); + $reason = sprintf( + 'TIME_DIFF > %ss (%s/%s = %s%%)', + (int) $timeDiffThresholdSec, + $overCount, + $matchedCount, + $percentLabel + ); + $update['status'] = 'DQ'; + $update['status_reason'] = $logResult->status_reason + ? $logResult->status_reason . '; ' . $reason + : $reason; + } + } + } + if ($badQsoThresholdPercent !== null && in_array($logResult->status, ['OK', 'CHECK'], true)) { + $totalCount = (int) ($stats['total_qso_count'] ?? 0); + if ($totalCount > 0) { + $badCount = (int) ($stats['bad_qso_count'] ?? 0); + $percent = ($badCount / $totalCount) * 100; + if ($percent >= (float) $badQsoThresholdPercent) { + $percentLabel = rtrim(rtrim(number_format($percent, 1, '.', ''), '0'), '.'); + $reason = sprintf( + 'BAD_QSO >= %s%% (%s/%s = %s%%)', + (int) $badQsoThresholdPercent, + $badCount, + $totalCount, + $percentLabel + ); + $update['status'] = 'DQ'; + $update['status_reason'] = $logResult->status_reason + ? $logResult->status_reason . '; ' . $reason + : $reason; + } + } + } + + $logResult->update($update); + EvaluationRun::where('id', $run->id)->increment('progress_done'); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Aggregate log: krok selhal.', [ + 'step' => 'aggregate', + 'round_id' => $run->round_id, + 'log_id' => $this->logId, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } + + protected function applyOperatingWindow(int $runId, LogResult $logResult, array $window): void + { + $logQsoIds = LogQso::where('log_id', $logResult->log_id)->pluck('id')->all(); + if (! $logQsoIds) { + $this->resetOperatingWindow($runId, $logResult); + return; + } + + $logResult->update([ + 'operating_window_start_utc' => $window['startUtc'], + 'operating_window_end_utc' => $window['endUtc'], + 'operating_window_2_start_utc' => $window['secondStartUtc'] ?? null, + 'operating_window_2_end_utc' => $window['secondEndUtc'] ?? null, + 'operating_window_hours' => 6, + 'operating_window_qso_count' => $window['qsoCount'], + ]); + + QsoResult::where('evaluation_run_id', $runId) + ->whereIn('log_qso_id', $logQsoIds) + ->update(['is_operating_window_excluded' => true]); + + if (! empty($window['includedLogQsoIds'])) { + QsoResult::where('evaluation_run_id', $runId) + ->whereIn('log_qso_id', $window['includedLogQsoIds']) + ->update(['is_operating_window_excluded' => false]); + } + } + + protected function resetOperatingWindow(int $runId, LogResult $logResult): void + { + $logResult->update([ + 'operating_window_start_utc' => null, + 'operating_window_end_utc' => null, + 'operating_window_2_start_utc' => null, + 'operating_window_2_end_utc' => null, + 'operating_window_hours' => null, + 'operating_window_qso_count' => null, + ]); + + $logQsoIds = LogQso::where('log_id', $logResult->log_id)->pluck('id')->all(); + if (! $logQsoIds) { + return; + } + + QsoResult::where('evaluation_run_id', $runId) + ->whereIn('log_qso_id', $logQsoIds) + ->update(['is_operating_window_excluded' => false]); + } +} diff --git a/app/Jobs/ApplyLogOverridesJob.php b/app/Jobs/ApplyLogOverridesJob.php new file mode 100644 index 0000000..c606acb --- /dev/null +++ b/app/Jobs/ApplyLogOverridesJob.php @@ -0,0 +1,159 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + try { + $coordinator->eventInfo($run, 'Apply log overrides: krok spuštěn.', [ + 'step' => 'apply_log_overrides', + 'round_id' => $run->round_id, + ]); + $overrides = LogOverride::where('evaluation_run_id', $this->evaluationRunId)->get(); + if ($overrides->isEmpty()) { + $coordinator->eventInfo($run, 'Apply log overrides: nic ke zpracování.', [ + 'step' => 'apply_log_overrides', + 'round_id' => $run->round_id, + ]); + $coordinator->eventInfo($run, 'Apply log overrides: krok dokončen.', [ + 'step' => 'apply_log_overrides', + 'round_id' => $run->round_id, + ]); + return; + } + + foreach ($overrides as $override) { + if (EvaluationRun::isCanceledRun($run->id)) { + return; + } + $data = []; + + if ($override->forced_log_status && $override->forced_log_status !== 'AUTO') { + $data['status'] = $override->forced_log_status; + } + + if ($override->forced_band_id !== null) { + $data['band_id'] = $override->forced_band_id; + } + + if ($override->forced_category_id !== null) { + $data['category_id'] = $override->forced_category_id; + } + + if ($override->forced_power_category_id !== null) { + $data['power_category_id'] = $override->forced_power_category_id; + } + + if ($override->forced_sixhr_category !== null) { + $data['sixhr_category'] = $override->forced_sixhr_category; + } + + if (! $data) { + continue; + } + + $logResult = LogResult::where('evaluation_run_id', $this->evaluationRunId) + ->where('log_id', $override->log_id) + ->first(); + if (! $logResult) { + continue; + } + + $bandId = $data['band_id'] ?? $logResult->band_id; + $sixhrCategory = $data['sixhr_category'] ?? $logResult->sixhr_category; + if ($sixhrCategory && ! $this->isSixHourBand($bandId)) { + $this->addSixHourRemark($override->log_id); + } + + $logResult->update($data); + } + + $coordinator->eventInfo($run, 'Apply log overrides: krok dokončen.', [ + 'step' => 'apply_log_overrides', + 'round_id' => $run->round_id, + ]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Apply log overrides: krok selhal.', [ + 'step' => 'apply_log_overrides', + 'round_id' => $run->round_id, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } + + protected function isSixHourBand(?int $bandId): bool + { + if (! $bandId) { + return false; + } + return in_array($bandId, [1, 2], true); + } + + protected function addSixHourRemark(int $logId): void + { + $log = Log::find($logId); + if (! $log) { + return; + } + $remarksEval = $this->decodeRemarksEval($log->remarks_eval); + $message = '6H kategorie je povolena jen pro pásma 145 MHz a 435 MHz.'; + if (! in_array($message, $remarksEval, true)) { + $remarksEval[] = $message; + $log->remarks_eval = $this->encodeRemarksEval($remarksEval); + $log->save(); + } + } + + protected function decodeRemarksEval(?string $value): array + { + if (! $value) { + return []; + } + $decoded = json_decode($value, true); + return is_array($decoded) ? $decoded : []; + } + + protected function encodeRemarksEval(array $value): ?string + { + $filtered = array_values(array_filter($value, fn ($item) => is_string($item) && trim($item) !== '')); + $filtered = array_values(array_unique($filtered)); + if (count($filtered) === 0) { + return null; + } + return json_encode($filtered, JSON_UNESCAPED_UNICODE); + } +} diff --git a/app/Jobs/BuildWorkingSetLogJob.php b/app/Jobs/BuildWorkingSetLogJob.php new file mode 100644 index 0000000..fee9893 --- /dev/null +++ b/app/Jobs/BuildWorkingSetLogJob.php @@ -0,0 +1,291 @@ +logId = $logId; + } + + public function handle(): void + { + $run = EvaluationRun::find($this->evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + try { + $ruleSet = EvaluationRuleSet::find($run->rule_set_id); + if (! $ruleSet) { + $coordinator->eventError($run, 'Working set nelze připravit: chybí ruleset.', [ + 'step' => 'build_working_set', + 'round_id' => $run->round_id, + ]); + return; + } + + $round = Round::with(['bands'])->find($run->round_id); + if (! $round) { + return; + } + + $log = Log::find($this->logId); + if (! $log || (int) $log->round_id !== (int) $run->round_id) { + return; + } + + $override = LogOverride::where('evaluation_run_id', $run->id) + ->where('log_id', $this->logId) + ->first(); + if ($override && $override->forced_log_status === 'IGNORED') { + return; + } + + $logLocator = $log->pwwlo; + $logCallsign = $log->pcall; + $logBand = $log->pband; + $total = LogQso::where('log_id', $this->logId)->count(); + if ($total === 0) { + return; + } + + $matcher = new MatchingService(); + $processed = 0; + $lastReported = 0; + + LogQso::where('log_id', $this->logId) + ->chunkById(200, function ($qsos) use ($run, $round, $ruleSet, $matcher, $total, &$processed, &$lastReported, $override, $logLocator, $logCallsign, $logBand, $coordinator) { + foreach ($qsos as $qso) { + if (EvaluationRun::isCanceledRun($run->id)) { + return false; + } + + $processed++; + $errors = []; + + $rawMyCall = $qso->my_call ?: ($logCallsign ?? ''); + $callNorm = $matcher->normalizeCallsign($rawMyCall, $ruleSet); + $rcallNorm = $matcher->normalizeCallsign($qso->dx_call ?? '', $ruleSet); + + // Lokátor může být v QSO nebo jen v hlavičce logu (PWWLo) – ber jako fallback. + $rawLocator = $qso->my_locator ?: ($logLocator ?? null); + $locNorm = $this->normalizeLocator($rawLocator); + if ($rawLocator && $locNorm === null) { + $errors[] = 'INVALID_LOCATOR'; + } + + $rlocNorm = $this->normalizeLocator($qso->rx_wwl); + if ($qso->rx_wwl && $rlocNorm === null) { + $errors[] = 'INVALID_RLOCATOR'; + } + + $bandId = $override && $override->forced_band_id + ? (int) $override->forced_band_id + : $this->resolveBandId($qso, $round); + if (! $bandId) { + $bandId = $this->resolveBandIdFromPband($logBand ?? null, $round); + } + $mode = $qso->mode_code ?: $qso->mode; + $modeNorm = $mode ? mb_strtoupper(trim($mode)) : null; + + $matchKey = $bandId && $callNorm && $rcallNorm + ? $bandId . '|' . $callNorm . '|' . $rcallNorm + : null; + + // Klíč pro detekci duplicit – závisí na dupe_scope v rulesetu. + $dupeKey = null; + if ($bandId && $rcallNorm) { + $dupeKey = $bandId . '|' . $rcallNorm; + if ($ruleSet->dupe_scope === 'BAND_MODE') { + $dupeKey .= '|' . ($modeNorm ?? ''); + } + } + + $tsUtc = $qso->time_on ? Carbon::parse($qso->time_on)->utc() : null; + // Out-of-window se řeší per QSO, ale v agregaci může vést až k DQ celého logu. + $outOfWindow = $matcher->isOutOfWindow($tsUtc, $round->start_time, $round->end_time); + + WorkingQso::updateOrCreate( + [ + 'evaluation_run_id' => $run->id, + 'log_qso_id' => $qso->id, + ], + [ + 'log_id' => $qso->log_id, + 'ts_utc' => $tsUtc, + 'call_norm' => $callNorm ?: null, + 'rcall_norm' => $rcallNorm ?: null, + 'loc_norm' => $locNorm, + 'rloc_norm' => $rlocNorm, + 'band_id' => $bandId, + 'mode' => $modeNorm, + 'match_key' => $matchKey, + 'dupe_key' => $dupeKey, + 'out_of_window' => $outOfWindow, + 'errors' => $errors ?: null, + ] + ); + + if ($processed - $lastReported >= 100 || $processed === $total) { + $delta = $processed - $lastReported; + if ($delta > 0) { + EvaluationRun::where('id', $run->id)->increment('progress_done', $delta); + $lastReported = $processed; + } + } + if ($processed % 500 === 0 || $processed === $total) { + $coordinator->eventInfo($run, "Working set: {$processed}/{$total}", [ + 'step' => 'build_working_set', + 'round_id' => $run->round_id, + 'step_progress_done' => $processed, + 'step_progress_total' => $total, + ]); + } + } + }); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Working set log: krok selhal.', [ + 'step' => 'build_working_set', + 'round_id' => $run->round_id, + 'log_id' => $this->logId, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } + + protected function normalizeLocator(?string $value): ?string + { + if (! $value) { + return null; + } + + $normalized = strtoupper(trim($value)); + $normalized = preg_replace('/\\s+/', '', $normalized) ?? ''; + $normalized = substr($normalized, 0, 6); + + if (! preg_match('/^[A-R]{2}[0-9]{2}([A-X]{2})?$/', $normalized)) { + return null; + } + + return $normalized; + } + + protected function resolveBandId(LogQso $qso, Round $round): ?int + { + $bandValue = $qso->band; + if ($bandValue) { + $pbandVal = mb_strtolower(trim($bandValue)); + $ediBand = EdiBand::whereRaw('LOWER(value) = ?', [$pbandVal])->first(); + if ($ediBand) { + $mappedBandId = $ediBand->bands()->value('bands.id'); + if (! $mappedBandId) { + return null; + } + if ($round->bands()->count() === 0) { + return $mappedBandId; + } + return $round->bands()->where('bands.id', $mappedBandId)->exists() + ? $mappedBandId + : null; + } + } + + $freqKHz = $qso->freq_khz; + if (! $freqKHz) { + return null; + } + + $mhz = $freqKHz / 1000; + $bandMatch = Band::where('edi_band_begin', '<=', $mhz) + ->where('edi_band_end', '>=', $mhz) + ->first(); + if (! $bandMatch) { + return null; + } + + if ($round->bands()->count() === 0) { + return $bandMatch->id; + } + + return $round->bands()->where('bands.id', $bandMatch->id)->exists() + ? $bandMatch->id + : null; + } + + protected function resolveBandIdFromPband(?string $pband, Round $round): ?int + { + if (! $pband) { + return null; + } + + $pbandVal = mb_strtolower(trim($pband)); + $ediBand = EdiBand::whereRaw('LOWER(value) = ?', [$pbandVal])->first(); + if ($ediBand) { + $mappedBandId = $ediBand->bands()->value('bands.id'); + if (! $mappedBandId) { + return null; + } + if ($round->bands()->count() === 0) { + return $mappedBandId; + } + return $round->bands()->where('bands.id', $mappedBandId)->exists() + ? $mappedBandId + : null; + } + + if (preg_match('/([0-9]+(?:[\\.,][0-9]+)?)/', $pbandVal, $m)) { + $mhz = (float) str_replace(',', '.', $m[1]); + $bandMatch = Band::where('edi_band_begin', '<=', $mhz) + ->where('edi_band_end', '>=', $mhz) + ->first(); + if (! $bandMatch) { + return null; + } + if ($round->bands()->count() === 0) { + return $bandMatch->id; + } + return $round->bands()->where('bands.id', $bandMatch->id)->exists() + ? $bandMatch->id + : null; + } + + return null; + } +} diff --git a/app/Jobs/DispatchAggregateResultsJobsJob.php b/app/Jobs/DispatchAggregateResultsJobsJob.php new file mode 100644 index 0000000..aaedd5a --- /dev/null +++ b/app/Jobs/DispatchAggregateResultsJobsJob.php @@ -0,0 +1,95 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + try { + $coordinator->eventInfo($run, 'Aggregate: krok spuštěn.', [ + 'step' => 'aggregate', + 'round_id' => $run->round_id, + ]); + + $logIds = LogResult::where('evaluation_run_id', $run->id) + ->pluck('log_id') + ->all(); + + $run->update([ + 'status' => 'RUNNING', + 'current_step' => 'aggregate', + 'progress_total' => count($logIds), + 'progress_done' => 0, + ]); + + $jobs = []; + foreach ($logIds as $logId) { + $jobs[] = new AggregateLogResultsJob($run->id, (int) $logId); + } + + $next = function () use ($run) { + Bus::chain([ + new ApplyLogOverridesJob($run->id), + new RecalculateOfficialRanksJob($run->id), + new PauseEvaluationRunJob( + $run->id, + 'WAITING_REVIEW_SCORE', + 'waiting_review_score', + 'Čeká na kontrolu skóre.' + ), + ])->onQueue('evaluation')->dispatch(); + }; + + if (! $jobs) { + $next(); + return; + } + + $batch = Bus::batch($jobs) + ->then($next) + ->onQueue('evaluation') + ->dispatch(); + $run->update(['batch_id' => $batch->id]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Aggregate: krok selhal.', [ + 'step' => 'aggregate', + 'round_id' => $run->round_id, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } +} diff --git a/app/Jobs/DispatchBuildWorkingSetJobsJob.php b/app/Jobs/DispatchBuildWorkingSetJobsJob.php new file mode 100644 index 0000000..5b7697b --- /dev/null +++ b/app/Jobs/DispatchBuildWorkingSetJobsJob.php @@ -0,0 +1,129 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + + $coordinator = new EvaluationCoordinator(); + + try { + $ruleSet = EvaluationRuleSet::find($run->rule_set_id); + if (! $ruleSet) { + $coordinator->eventError($run, 'Working set nelze připravit: chybí ruleset.', [ + 'step' => 'build_working_set', + 'round_id' => $run->round_id, + ]); + return; + } + + $coordinator->eventInfo($run, 'Working set: krok spuštěn.', [ + 'step' => 'build_working_set', + 'round_id' => $run->round_id, + ]); + + $run->update([ + 'status' => 'RUNNING', + 'current_step' => 'build_working_set', + 'progress_total' => 0, + 'progress_done' => 0, + ]); + + WorkingQso::where('evaluation_run_id', $run->id)->delete(); + + $logIds = Log::where('round_id', $run->round_id)->pluck('id'); + $logOverrides = LogOverride::where('evaluation_run_id', $run->id)->get()->keyBy('log_id'); + $ignoredLogIds = $logOverrides + ->filter(fn ($override) => $override->forced_log_status === 'IGNORED') + ->keys() + ->all(); + if ($ignoredLogIds) { + $logIds = $logIds->reject(fn ($id) => in_array($id, $ignoredLogIds, true))->values(); + } + + $total = $logIds->isEmpty() + ? 0 + : LogQso::whereIn('log_id', $logIds)->count(); + + $run->update([ + 'progress_total' => $total, + 'progress_done' => 0, + ]); + $coordinator->eventInfo($run, 'Příprava working setu.', [ + 'step' => 'build_working_set', + 'round_id' => $run->round_id, + 'step_progress_done' => 0, + 'step_progress_total' => $total, + ]); + + $jobs = []; + foreach ($logIds as $logId) { + $jobs[] = new BuildWorkingSetLogJob($run->id, (int) $logId); + } + + $next = function () use ($run) { + Bus::chain([ + new PauseEvaluationRunJob( + $run->id, + 'WAITING_REVIEW_INPUT', + 'waiting_review_input', + 'Čeká na kontrolu vstupů.' + ), + ])->onQueue('evaluation')->dispatch(); + }; + + if (! $jobs) { + $next(); + return; + } + + $batch = Bus::batch($jobs) + ->then($next) + ->onQueue('evaluation') + ->dispatch(); + $run->update(['batch_id' => $batch->id]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Working set: krok selhal.', [ + 'step' => 'build_working_set', + 'round_id' => $run->round_id, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } +} diff --git a/app/Jobs/DispatchMatchJobsJob.php b/app/Jobs/DispatchMatchJobsJob.php new file mode 100644 index 0000000..2815d09 --- /dev/null +++ b/app/Jobs/DispatchMatchJobsJob.php @@ -0,0 +1,71 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + + $coordinator = app(EvaluationCoordinator::class); + $coordinator->eventInfo($run, 'Dispatch match: krok spuštěn.', [ + 'step' => 'dispatch_match', + 'round_id' => $run->round_id, + ]); + + try { + $coordinator->dispatchStep($run, 'match'); + $coordinator->eventInfo($run, 'Dispatch match: krok dokončen.', [ + 'step' => 'dispatch_match', + 'round_id' => $run->round_id, + ]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Dispatch match: krok selhal.', [ + 'step' => 'dispatch_match', + 'round_id' => $run->round_id, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } +} diff --git a/app/Jobs/DispatchParseLogsJobsJob.php b/app/Jobs/DispatchParseLogsJobsJob.php new file mode 100644 index 0000000..ea7f2b7 --- /dev/null +++ b/app/Jobs/DispatchParseLogsJobsJob.php @@ -0,0 +1,97 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + + $coordinator = new EvaluationCoordinator(); + + try { + $coordinator->eventInfo($run, 'Parsování logů: krok spuštěn.', [ + 'step' => 'parse_logs', + 'round_id' => $run->round_id, + ]); + + $logIds = Log::where('round_id', $run->round_id)->pluck('id'); + $total = $logIds->count(); + + $run->update([ + 'status' => 'RUNNING', + 'current_step' => 'parse_logs', + 'progress_total' => $total, + 'progress_done' => 0, + 'started_at' => $run->started_at ?? Carbon::now(), + ]); + + $jobs = []; + foreach ($logIds as $logId) { + $jobs[] = new ParseLogJob($run->id, (int) $logId); + } + + $next = function () use ($run) { + if ($run->rules_version === 'CLAIMED') { + RecalculateClaimedRanksJob::dispatch($run->id) + ->delay(now()->addSeconds(10)) + ->onQueue('evaluation'); + } + Bus::chain([ + new DispatchBuildWorkingSetJobsJob($run->id), + ])->onQueue('evaluation')->dispatch(); + }; + + if (! $jobs) { + $next(); + return; + } + + $batch = Bus::batch($jobs) + ->then($next) + ->onQueue('evaluation') + ->dispatch(); + $run->update(['batch_id' => $batch->id]); + } catch (Throwable $e) { + $message = "DispatchParseLogsJobsJob selhal (run {$run->id}): {$e->getMessage()}"; + \Log::error($message); + $coordinator->eventError($run, $message, [ + 'step' => 'parse_logs', + 'round_id' => $run->round_id, + ]); + throw $e; + } + } +} diff --git a/app/Jobs/DispatchScoreJobsJob.php b/app/Jobs/DispatchScoreJobsJob.php new file mode 100644 index 0000000..0cee2f0 --- /dev/null +++ b/app/Jobs/DispatchScoreJobsJob.php @@ -0,0 +1,56 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + + $coordinator = app(EvaluationCoordinator::class); + $coordinator->eventInfo($run, 'Dispatch score: krok spuštěn.', [ + 'step' => 'dispatch_score', + 'round_id' => $run->round_id, + ]); + + try { + $coordinator->dispatchStep($run, 'score'); + $coordinator->eventInfo($run, 'Dispatch score: krok dokončen.', [ + 'step' => 'dispatch_score', + 'round_id' => $run->round_id, + ]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Dispatch score: krok selhal.', [ + 'step' => 'dispatch_score', + 'round_id' => $run->round_id, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } +} diff --git a/app/Jobs/DispatchUnpairedJobsJob.php b/app/Jobs/DispatchUnpairedJobsJob.php new file mode 100644 index 0000000..220dc4b --- /dev/null +++ b/app/Jobs/DispatchUnpairedJobsJob.php @@ -0,0 +1,105 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + + $coordinator = new EvaluationCoordinator(); + $coordinator->eventInfo($run, 'Dispatch unpaired: krok spuštěn.', [ + 'step' => 'dispatch_unpaired', + 'round_id' => $run->round_id, + ]); + + try { + $buckets = WorkingQso::where('evaluation_run_id', $run->id) + ->whereNotNull('rcall_norm') + ->distinct() + ->get(['band_id', 'rcall_norm']); + + $jobs = []; + foreach ($buckets as $bucket) { + $jobs[] = new UnpairedClassificationBucketJob( + $run->id, + $bucket->band_id !== null ? (int) $bucket->band_id : null, + $bucket->rcall_norm + ); + } + + $run->refresh(); + $progressDone = (int) $run->progress_done; + $progressTotal = $progressDone + count($jobs) + 1; + $run->update([ + 'progress_total' => $progressTotal, + 'progress_done' => $progressDone, + ]); + + $next = function () use ($run) { + Bus::chain([ + new DuplicateResolutionJob($run->id), + new PauseEvaluationRunJob( + $run->id, + 'WAITING_REVIEW_MATCH', + 'waiting_review_match', + 'Čeká na kontrolu matchingu.' + ), + ])->onQueue('evaluation')->dispatch(); + }; + + if (! $jobs) { + $next(); + return; + } + + $batch = Bus::batch($jobs) + ->then($next) + ->onQueue('evaluation') + ->dispatch(); + $run->update(['batch_id' => $batch->id]); + + $coordinator->eventInfo($run, 'Dispatch unpaired: krok dokončen.', [ + 'step' => 'dispatch_unpaired', + 'round_id' => $run->round_id, + ]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Dispatch unpaired: krok selhal.', [ + 'step' => 'dispatch_unpaired', + 'round_id' => $run->round_id, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } +} diff --git a/app/Jobs/DuplicateResolutionJob.php b/app/Jobs/DuplicateResolutionJob.php new file mode 100644 index 0000000..402a6f4 --- /dev/null +++ b/app/Jobs/DuplicateResolutionJob.php @@ -0,0 +1,163 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + try { + $ruleSet = EvaluationRuleSet::find($run->rule_set_id); + if (! $ruleSet) { + $coordinator->eventError($run, 'Duplicitní QSO nelze vyhodnotit: chybí ruleset.', [ + 'step' => 'duplicate_resolution', + ]); + return; + } + + $coordinator->eventInfo($run, 'Duplicate: krok spuštěn.', [ + 'step' => 'duplicate_resolution', + 'round_id' => $run->round_id, + ]); + + $coordinator->eventInfo($run, 'Detekce duplicitních QSO.', [ + 'step' => 'match', + 'round_id' => $run->round_id, + 'step_progress_done' => null, + 'step_progress_total' => $run->progress_total, + ]); + + $strategy = $ruleSet->dupResolutionStrategy(); + + $working = WorkingQso::where('evaluation_run_id', $run->id)->get(); + $byLog = $working->groupBy('log_id'); + + foreach ($byLog as $logId => $items) { + if (EvaluationRun::isCanceledRun($run->id)) { + return; + } + $byDupeKey = $items->groupBy('dupe_key'); + foreach ($byDupeKey as $dupeKey => $dupes) { + if (! $dupeKey || $dupes->count() < 2) { + continue; + } + + $sorted = $dupes->sort(function ($a, $b) use ($strategy, $run) { + $resultA = QsoResult::where('evaluation_run_id', $run->id) + ->where('log_qso_id', $a->log_qso_id) + ->first(); + $resultB = QsoResult::where('evaluation_run_id', $run->id) + ->where('log_qso_id', $b->log_qso_id) + ->first(); + + foreach ($strategy as $rule) { + if ($rule === 'paired_first') { + $aPaired = $resultA && $resultA->matched_log_qso_id !== null; + $bPaired = $resultB && $resultB->matched_log_qso_id !== null; + if ($aPaired !== $bPaired) { + return $aPaired ? -1 : 1; + } + } + if ($rule === 'ok_first') { + $aOk = $resultA && $resultA->error_code === QsoErrorCode::OK; + $bOk = $resultB && $resultB->error_code === QsoErrorCode::OK; + if ($aOk !== $bOk) { + return $aOk ? -1 : 1; + } + } + if ($rule === 'earlier_time') { + $tsA = $a->ts_utc?->getTimestamp() ?? PHP_INT_MAX; + $tsB = $b->ts_utc?->getTimestamp() ?? PHP_INT_MAX; + if ($tsA !== $tsB) { + return $tsA <=> $tsB; + } + } + if ($rule === 'lower_id') { + if ($a->log_qso_id !== $b->log_qso_id) { + return $a->log_qso_id <=> $b->log_qso_id; + } + } + } + + return $a->log_qso_id <=> $b->log_qso_id; + })->values(); + + $survivor = $sorted->shift(); + foreach ($sorted as $dupe) { + QsoResult::where('evaluation_run_id', $run->id) + ->where('log_qso_id', $dupe->log_qso_id) + ->update([ + 'is_duplicate' => true, + 'is_valid' => false, + 'error_code' => QsoErrorCode::DUP, + 'error_side' => 'NONE', + ]); + } + } + } + + EvaluationRun::where('id', $run->id)->increment('progress_done'); + $coordinator->eventInfo($run, 'Duplicate: krok dokončen.', [ + 'step' => 'duplicate_resolution', + 'round_id' => $run->round_id, + ]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Duplicate: krok selhal.', [ + 'step' => 'duplicate_resolution', + 'round_id' => $run->round_id, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } +} diff --git a/app/Jobs/FinalizeRunJob.php b/app/Jobs/FinalizeRunJob.php new file mode 100644 index 0000000..4d633e6 --- /dev/null +++ b/app/Jobs/FinalizeRunJob.php @@ -0,0 +1,141 @@ + SUCCEEDED (nebo FAILED při chybě) + * - nastaví finished_at + * - Uvolní locky držené pro scope vyhodnocení. + * - Zapíše závěrečné auditní události (EvaluationRunEvent). + * + * Co job NEDĚLÁ: + * - nepočítá body ani skóre + * - nemění výsledky jednotlivých QSO + * - neřeší export ani prezentaci v UI + * + * Zásady návrhu: + * - Tento krok musí být atomický z pohledu stavu vyhodnocení. + * - Při selhání musí být EvaluationRun jednoznačně označen jako FAILED + * a nesmí zůstat v nekonzistentním stavu. + * - Veškerá logika patří do service layer (např. EvaluationFinalizerService). + * + * Queue: + * - Spouští se ve frontě "evaluation". + * - Nemá běžet paralelně nad stejným EvaluationRun. + */ +class FinalizeRunJob implements ShouldQueue +{ + use Queueable; + + public int $tries = 2; + public array $backoff = [60]; + + /** + * Create a new job instance. + */ + public function __construct( + protected int $evaluationRunId, + protected ?string $lockKey = null + ) + { + // + } + + /** + * Finalizuje vyhodnocovací běh. + * + * Metoda handle(): + * - provede závěrečné kontroly konzistence dat + * - označí výsledky jako finální / oficiální + * - přepne EvaluationRun do koncového stavu (SUCCEEDED / FAILED) + * - uvolní zdroje a locky držené během vyhodnocení + * + * Poznámky: + * - Tento job je posledním krokem pipeline. + * - Po jeho úspěšném dokončení musí být možné výsledky bezpečně + * zobrazit nebo exportovat. + */ + public function handle(): void + { + $run = EvaluationRun::find($this->evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + try { + $coordinator->eventInfo($run, 'Finalize: krok spuštěn.', [ + 'step' => 'finalize', + 'round_id' => $run->round_id, + ]); + // Po ručních zásazích může být potřeba znovu aplikovat override a přepočítat pořadí. + (new ApplyLogOverridesJob($run->id))->handle(); + (new RecalculateOfficialRanksJob($run->id))->handle(); + $coordinator->eventInfo($run, 'Před finalizací byl znovu aplikován override a přepočítáno pořadí.', [ + 'step' => 'finalize', + 'round_id' => $run->round_id, + ]); + + $run->update([ + 'status' => 'SUCCEEDED', + 'current_step' => 'finalize', + 'progress_total' => 1, + 'progress_done' => 1, + 'finished_at' => now(), + ]); + $coordinator->eventInfo($run, 'Vyhodnocení dokončeno.', [ + 'step' => 'finalize', + 'round_id' => $run->round_id, + 'step_progress_done' => 1, + 'step_progress_total' => 1, + ]); + $coordinator->eventInfo($run, 'Finalize: krok dokončen.', [ + 'step' => 'finalize', + 'round_id' => $run->round_id, + ]); + } catch (Throwable $e) { + $run->update([ + 'status' => 'FAILED', + 'error' => $e->getMessage(), + 'finished_at' => now(), + ]); + $coordinator->eventError($run, "FinalizeRunJob selhal: {$e->getMessage()}", [ + 'step' => 'finalize', + 'round_id' => $run->round_id, + ]); + throw $e; + } finally { + // Lock musí být uvolněn vždy, i při chybě – jinak zablokuje další běhy. + $lockKey = $this->lockKey ?? "evaluation:round:{$run->round_id}"; + EvaluationLock::release($lockKey, $run); + } + } +} diff --git a/app/Jobs/MatchQsoBucketJob.php b/app/Jobs/MatchQsoBucketJob.php new file mode 100644 index 0000000..2deb544 --- /dev/null +++ b/app/Jobs/MatchQsoBucketJob.php @@ -0,0 +1,432 @@ +bandId = $bandId; + $this->callNorm = $callNorm; + } + + public function handle(): void + { + $run = EvaluationRun::find($this->evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + try { + $ruleSet = EvaluationRuleSet::find($run->rule_set_id); + if (! $ruleSet) { + $coordinator->eventError($run, 'Matching nelze spustit: chybí ruleset.', [ + 'step' => 'match', + 'round_id' => $run->round_id, + ]); + return; + } + + $round = Round::with(['bands', 'categories', 'powerCategories'])->find($run->round_id); + if (! $round) { + return; + } + + $coordinator->eventInfo($run, 'Matching bucket.', [ + 'step' => 'match', + 'round_id' => $run->round_id, + 'band_id' => $this->bandId, + 'call_norm' => $this->callNorm, + 'pass' => $this->pass, + ]); + + $logOverrides = LogOverride::where('evaluation_run_id', $run->id)->get()->keyBy('log_id'); + $qsoOverrides = QsoOverride::where('evaluation_run_id', $run->id)->get()->keyBy('log_qso_id'); + $groupLogIds = $this->groupLogsByKey($round, $ruleSet, $logOverrides); + $groupKey = 'b' . ($this->bandId ?? 0); + $logIds = $groupLogIds[$groupKey] ?? []; + if (! $logIds) { + $coordinator->progressTick($run, 1); + return; + } + + $sourceQsos = WorkingQso::where('evaluation_run_id', $run->id) + ->whereIn('log_id', $logIds) + ->when($this->bandId !== null, fn ($q) => $q->where('band_id', $this->bandId), fn ($q) => $q->whereNull('band_id')) + ->when($this->callNorm !== null, fn ($q) => $q->where('call_norm', $this->callNorm), fn ($q) => $q->whereNull('call_norm')) + ->get(); + + if ($sourceQsos->isEmpty()) { + $coordinator->progressTick($run, 1); + return; + } + + $candidateQsos = WorkingQso::where('evaluation_run_id', $run->id) + ->whereIn('log_id', $logIds) + ->when($this->bandId !== null, fn ($q) => $q->where('band_id', $this->bandId), fn ($q) => $q->whereNull('band_id')) + ->when($this->callNorm !== null, fn ($q) => $q->where('rcall_norm', $this->callNorm), fn ($q) => $q->whereNull('rcall_norm')) + ->get(); + + $workingQsos = $sourceQsos->concat($candidateQsos)->unique('log_qso_id'); + $logQsoIds = $workingQsos->pluck('log_qso_id')->all(); + $logQsoMap = LogQso::whereIn('id', $logQsoIds)->get()->keyBy('id'); + $workingMap = $workingQsos->keyBy('log_qso_id'); + + $alreadyMatched = []; + if ($this->pass > 1) { + $alreadyMatched = QsoResult::where('evaluation_run_id', $run->id) + ->whereNotNull('matched_log_qso_id') + ->pluck('log_qso_id') + ->all(); + $alreadyMatched = array_fill_keys($alreadyMatched, true); + } + + $byMatchKey = []; + $byBandRcall = []; + foreach ($candidateQsos as $wqso) { + if ($wqso->match_key) { + $byMatchKey[$wqso->match_key][] = $wqso; + } + if ($wqso->band_id && $wqso->rcall_norm) { + $byBandRcall[$wqso->band_id . '|' . $wqso->rcall_norm][] = $wqso; + } + } + + $timeTolerance = $ruleSet->time_tolerance_sec !== null + ? (int) $ruleSet->time_tolerance_sec + : 300; + $forcedMap = []; + foreach ($qsoOverrides as $override) { + if ($override->forced_matched_log_qso_id) { + $forcedMap[$override->log_qso_id] = (int) $override->forced_matched_log_qso_id; + } + } + $forcedBackMap = []; + $forcedConflicts = []; + foreach ($forcedMap as $a => $b) { + if (isset($forcedMap[$b]) && $forcedMap[$b] !== $a) { + $forcedConflicts[$a] = true; + $forcedConflicts[$b] = true; + } + if (! isset($forcedMap[$b])) { + $forcedBackMap[$b] = $a; + } + } + + $paired = []; + foreach ($sourceQsos as $wqso) { + if ($this->pass > 1 && isset($alreadyMatched[$wqso->log_qso_id])) { + continue; + } + $reverseKey = $wqso->band_id && $wqso->call_norm && $wqso->rcall_norm + ? $wqso->band_id . '|' . $wqso->rcall_norm . '|' . $wqso->call_norm + : null; + + $candidates = []; + if ($reverseKey && isset($byMatchKey[$reverseKey])) { + $candidates = $byMatchKey[$reverseKey]; + } + if ($wqso->band_id && $wqso->call_norm) { + $fuzzyKey = $wqso->band_id . '|' . $wqso->call_norm; + if (isset($byBandRcall[$fuzzyKey])) { + $candidates = array_merge($candidates, $byBandRcall[$fuzzyKey]); + } + } + if ($candidates) { + $unique = []; + foreach ($candidates as $candidate) { + $unique[$candidate->log_qso_id] = $candidate; + } + $candidates = array_values($unique); + } + + $best = null; + $bestDecision = null; + $tiebreakOrder = $this->resolveTiebreakOrder($ruleSet); + $forcedMatchId = $forcedMap[$wqso->log_qso_id] + ?? $forcedBackMap[$wqso->log_qso_id] + ?? null; + $forcedMissing = false; + $forcedDecision = null; + if ($forcedMatchId) { + if (! $workingMap->has($forcedMatchId)) { + $forcedWorking = WorkingQso::where('evaluation_run_id', $run->id) + ->where('log_qso_id', $forcedMatchId) + ->first(); + if ($forcedWorking) { + $workingMap->put($forcedMatchId, $forcedWorking); + if (! $logQsoMap->has($forcedMatchId)) { + $forcedLogQso = LogQso::find($forcedMatchId); + if ($forcedLogQso) { + $logQsoMap->put($forcedMatchId, $forcedLogQso); + } + } + } + } + $best = $workingMap->get($forcedMatchId); + if (! $best || $best->log_id === $wqso->log_id) { + $best = null; + $forcedMissing = true; + } + if ($best) { + $forcedDecision = $this->evaluateMatchDecision( + $wqso, + $best, + $logQsoMap, + $ruleSet, + $timeTolerance + ); + $bestDecision = $forcedDecision; + } + } else { + foreach ($candidates as $candidate) { + if ($candidate->log_id === $wqso->log_id) { + continue; + } + if (isset($paired[$candidate->log_qso_id]) && $paired[$candidate->log_qso_id] !== $wqso->log_qso_id) { + continue; + } + + $decision = $this->evaluateMatchDecision( + $wqso, + $candidate, + $logQsoMap, + $ruleSet, + $timeTolerance + ); + if (! $decision) { + continue; + } + if ($this->pass === 1 && ($decision['match_type'] ?? '') !== 'MATCH_EXACT') { + continue; + } + + if ($bestDecision === null || $decision['rank'] < $bestDecision['rank']) { + $best = $candidate; + $bestDecision = $decision; + continue; + } + + if ($decision['rank'] === $bestDecision['rank']) { + $cmp = $this->compareCandidates( + $decision['tiebreak'], + $bestDecision['tiebreak'], + $tiebreakOrder + ); + if ($cmp < 0) { + $best = $candidate; + $bestDecision = $decision; + } + } + } + } + + $timeDiffSec = null; + if ($best && $wqso->ts_utc && $best->ts_utc) { + $timeDiffSec = abs($best->ts_utc->getTimestamp() - $wqso->ts_utc->getTimestamp()); + } + $isNil = $best === null; + $isDuplicate = false; + $isBustedExchange = false; + $isBustedCall = false; + $isBustedRst = false; + $bustedCallTx = false; + $bustedRstTx = false; + $bustedExchangeReason = null; + $customMismatch = false; + $isBustedExchangeOnly = false; + $bustedSerialRx = false; + $bustedSerialTx = false; + $bustedWwlRx = false; + $bustedWwlTx = false; + $matchType = null; + $errorFlags = []; + $timeMismatch = false; + $isOutOfWindow = (bool) $wqso->out_of_window; + $errorCode = null; + + if (! $isNil && $best && $bestDecision) { + $a = $logQsoMap->get($wqso->log_qso_id); + $b = $logQsoMap->get($best->log_qso_id); + $aWork = $workingMap->get($wqso->log_qso_id); + $bWork = $workingMap->get($best->log_qso_id); + + if ($a && $b) { + $exchange = $this->evaluateExchange($a, $b, $aWork, $bWork, $ruleSet); + $bustedCallTx = $exchange['sent_call_mismatch'] && $ruleSet->discard_qso_sent_diff_call; + $isBustedCall = $exchange['recv_call_mismatch'] && $ruleSet->discard_qso_rec_diff_call; + + $bustedRstTx = ($ruleSet->exchange_requires_report ?? false) + && $exchange['report_sent_mismatch'] + && $ruleSet->discard_qso_sent_diff_rst; + $isBustedRst = ($ruleSet->exchange_requires_report ?? false) + && $exchange['report_recv_mismatch'] + && $ruleSet->discard_qso_rec_diff_rst; + + $discardSerialRx = $ruleSet->discard_qso_rec_diff_serial ?? $ruleSet->discard_qso_rec_diff_code; + $discardSerialTx = $ruleSet->discard_qso_sent_diff_serial ?? $ruleSet->discard_qso_sent_diff_code; + $discardWwlRx = $ruleSet->discard_qso_rec_diff_wwl ?? $ruleSet->discard_qso_rec_diff_code; + $discardWwlTx = $ruleSet->discard_qso_sent_diff_wwl ?? $ruleSet->discard_qso_sent_diff_code; + + $customMismatch = (bool) ($exchange['custom_mismatch'] ?? false); + $bustedSerialRx = ($exchange['serial_recv_mismatch'] || $exchange['missing_serial'] || $customMismatch) && $discardSerialRx; + $bustedSerialTx = ($exchange['serial_sent_mismatch'] || $customMismatch) && $discardSerialTx; + $bustedWwlRx = ($exchange['locator_recv_mismatch'] || $exchange['missing_wwl']) && $discardWwlRx; + $bustedWwlTx = $exchange['locator_sent_mismatch'] && $discardWwlTx; + + $isBustedExchangeOnly = ($customMismatch && $ruleSet->discard_qso_rec_diff_code); + $isBustedExchange = $isBustedExchangeOnly || $bustedSerialRx || $bustedWwlRx; + $bustedExchangeReason = $exchange['busted_exchange_reason'] ?? null; + $matchType = $bestDecision['match_type'] ?? null; + $errorFlags = $bestDecision['error_flags'] ?? []; + $timeMismatch = (bool) ($bestDecision['time_mismatch'] ?? false); + } + } + + $isValid = ! $isNil && ! $isBustedCall && ! $isBustedRst && ! $isBustedExchange; + $errorDetail = null; + $bustedCallRx = $isBustedCall; + $bustedRstRx = $isBustedRst; + + if ($forcedMissing) { + $errorDetail = 'FORCED_MATCH_MISSING'; + } elseif (isset($forcedConflicts[$wqso->log_qso_id])) { + $errorDetail = 'FORCED_MATCH_CONFLICT'; + } elseif ($isBustedExchange && $bustedExchangeReason) { + $errorDetail = $bustedExchangeReason; + } + + $errorSide = $this->resolveErrorSide( + $bustedCallRx, + $bustedCallTx, + $bustedRstRx, + $bustedRstTx, + $bustedSerialRx, + $bustedSerialTx, + $bustedWwlRx, + $bustedWwlTx, + $timeMismatch + ); + + $override = $qsoOverrides->get($wqso->log_qso_id); + if ($override && $override->forced_status && $override->forced_status !== 'AUTO') { + $this->applyForcedStatus( + $override->forced_status, + $isValid, + $isDuplicate, + $isNil, + $isBustedCall, + $isBustedRst, + $isBustedExchange, + $isOutOfWindow, + $errorCode, + $errorSide + ); + } + $matchConfidence = $this->resolveMatchConfidence($bestDecision['match_type'] ?? null, $timeMismatch); + + if (! $errorCode) { + if ($timeMismatch) { + $errorCode = QsoErrorCode::TIME_MISMATCH; + } elseif ($isBustedCall || $bustedCallTx) { + $errorCode = QsoErrorCode::BUSTED_CALL; + } elseif ($isBustedRst || $bustedRstTx) { + $errorCode = QsoErrorCode::BUSTED_RST; + } elseif ($bustedSerialRx || $bustedSerialTx) { + $errorCode = QsoErrorCode::BUSTED_SERIAL; + } elseif ($bustedWwlRx || $bustedWwlTx) { + $errorCode = QsoErrorCode::BUSTED_LOCATOR; + } elseif ($isBustedExchangeOnly) { + $errorCode = QsoErrorCode::BUSTED_SERIAL; + } elseif (! $isNil && ! $isDuplicate && ! $isBustedExchange && ! $isOutOfWindow) { + $errorCode = QsoErrorCode::OK; + } + } + + QsoResult::updateOrCreate( + [ + 'evaluation_run_id' => $run->id, + 'log_qso_id' => $wqso->log_qso_id, + ], + [ + 'is_valid' => $isValid, + 'is_duplicate' => $isDuplicate, + 'is_nil' => $isNil, + 'is_busted_call' => $isBustedCall, + 'is_busted_rst' => $isBustedRst, + 'is_busted_exchange' => $isBustedExchange, + 'is_time_out_of_window' => $isOutOfWindow, + 'points' => 0, + 'distance_km' => null, + 'time_diff_sec' => $timeDiffSec, + 'wwl' => null, + 'dxcc' => null, + 'matched_qso_id' => $best?->log_qso_id, + 'matched_log_qso_id' => $best?->log_qso_id, + 'match_type' => $matchType, + 'match_confidence' => $matchConfidence, + 'error_code' => $errorCode, + 'error_side' => $errorSide, + 'error_detail' => $errorDetail, + 'error_flags' => $errorFlags ?: null, + ] + ); + + if ($best && $best->log_qso_id) { + $paired[$wqso->log_qso_id] = $best->log_qso_id; + $paired[$best->log_qso_id] = $wqso->log_qso_id; + } + } + + $coordinator->progressTick($run, 1); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Matching bucket: krok selhal.', [ + 'step' => 'match', + 'round_id' => $run->round_id, + 'band_id' => $this->bandId, + 'call_norm' => $this->callNorm, + 'pass' => $this->pass, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } +} diff --git a/app/Jobs/MatchQsoGroupJob.php b/app/Jobs/MatchQsoGroupJob.php new file mode 100644 index 0000000..238bf8d --- /dev/null +++ b/app/Jobs/MatchQsoGroupJob.php @@ -0,0 +1,1349 @@ + stejné výstupy. + * - Job musí být idempotentní: opakované spuštění přepíše/obnoví výstupy + * pro daný run+group bez duplicit. + * - Veškerá matching logika patří do service layer (např. MatchingService). + * - Job pouze načte kontext, deleguje práci a zapíše výsledek. + * + * Decision trail (v QsoResult): + * - error_code: OK / NIL / NOT_IN_COUNTERPART_LOG / NO_COUNTERPART_LOG / UNIQUE / + * BUSTED_CALL / BUSTED_RST / BUSTED_SERIAL / BUSTED_LOCATOR / DUP / TIME_MISMATCH / + * OUT_OF_WINDOW (kód popisuje primární důvod). + * - error_flags: doplňkové signály (např. TIME_MISMATCH), mohou existovat i při OK. + * - error_side: RX / TX / NONE (kdo udělal chybu, rozhoduje o penalizaci). + * - match_type: EXACT / LEVENSHTEIN / TIME_SHIFT / TIME_MISMATCH (typ párování). + * - match_confidence: HIGH / MEDIUM / LOW / TIME_MISMATCH (syntetické skóre důvěry). + * + * Queue: + * - Spouští se ve frontě "evaluation". + * - Musí být chráněn proti souběhu nad stejnou group (lock / WithoutOverlapping). + */ +class MatchQsoGroupJob implements ShouldQueue +{ + use Batchable; + use Queueable; + + /** + * Create a new job instance. + */ + public function __construct( + protected int $evaluationRunId, + protected ?string $groupKey = null, + protected ?array $group = null, + protected int $pass = 1 + ) { + // + } + + /** + * Provede matching QSO pro jednu skupinu (group). + * + * Metoda handle(): + * - získá kontext EvaluationRun + group parametry + * - provede matching a označení QSO (NIL/busted/duplicate/...) + * - zapíše mezivýsledky pro scoring + * - aktualizuje progress a auditní události pro UI + * + * Poznámky: + * - Job by měl pracovat dávkově a používat indexy (výkon). + * - Při chybě v jedné skupině má selhat job (retry), + * protože bez kompletního matchingu nelze korektně skórovat. + */ + public function handle(): void + { + $run = EvaluationRun::find($this->evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + try { + $coordinator->eventInfo($run, 'Matching: krok spuštěn.', [ + 'step' => 'match', + 'round_id' => $run->round_id, + 'group_key' => $this->groupKey, + ]); + + $ruleSet = EvaluationRuleSet::find($run->rule_set_id); + if (! $ruleSet) { + $coordinator->eventError($run, 'Matching nelze spustit: chybí ruleset.', [ + 'step' => 'match', + 'round_id' => $run->round_id, + ]); + return; + } + + $round = Round::with(['bands', 'categories', 'powerCategories'])->find($run->round_id); + if (! $round) { + return; + } + + $run->update([ + 'status' => 'RUNNING', + 'current_step' => 'match', + ]); + $groups = []; + $singleGroup = (bool) ($this->groupKey || $this->group); + if ($this->groupKey || $this->group) { + $bandId = $this->group['band_id'] ?? null; + $groups[] = [ + 'key' => 'b' . ($bandId ?? 0), + 'band_id' => $bandId, + 'category_id' => null, + 'power_category_id' => null, + ]; + } elseif (! empty($run->scope['band_ids']) && is_array($run->scope['band_ids'])) { + foreach ($run->scope['band_ids'] as $bandId) { + $groups[] = [ + 'key' => 'b' . ($bandId ?? 0), + 'band_id' => $bandId, + 'category_id' => null, + 'power_category_id' => null, + ]; + } + } else { + $groups[] = [ + 'key' => 'b0', + 'band_id' => null, + 'category_id' => null, + 'power_category_id' => null, + ]; + } + + $total = count($groups); + if (! $singleGroup) { + $run->update([ + 'progress_total' => $total, + 'progress_done' => 0, + ]); + } + + $matcher = new MatchingService(); + $logOverrides = LogOverride::where('evaluation_run_id', $run->id)->get()->keyBy('log_id'); + $qsoOverrides = QsoOverride::where('evaluation_run_id', $run->id)->get()->keyBy('log_qso_id'); + $groupLogIds = $this->groupLogsByKey($round, $ruleSet, $logOverrides); + + $processed = 0; + foreach ($groups as $group) { + if (EvaluationRun::isCanceledRun($run->id)) { + return; + } + $processed++; + $groupKey = $group['key'] ?? 'all'; + $logIds = $groupLogIds[$groupKey] ?? []; + + $coordinator->eventInfo($run, 'Matching QSO.', [ + 'step' => 'match', + 'round_id' => $run->round_id, + 'group_key' => $group['key'] ?? null, + 'group' => [ + 'band_id' => $group['band_id'] ?? null, + 'category_id' => null, + 'power_category_id' => null, + ], + 'group_logs' => count($logIds), + 'step_progress_done' => $processed, + 'step_progress_total' => $total, + ]); + + if (! $logIds) { + if ($singleGroup) { + EvaluationRun::where('id', $run->id)->increment('progress_done'); + } else { + $run->update(['progress_done' => $processed]); + } + continue; + } + + $workingQsos = WorkingQso::where('evaluation_run_id', $run->id) + ->whereIn('log_id', $logIds) + ->when(! empty($group['band_id']), fn ($q) => $q->where('band_id', $group['band_id'])) + ->get(); + + $alreadyMatched = []; + if ($this->pass > 1) { + $alreadyMatched = QsoResult::where('evaluation_run_id', $run->id) + ->whereNotNull('matched_log_qso_id') + ->pluck('log_qso_id') + ->all(); + $alreadyMatched = array_fill_keys($alreadyMatched, true); + } + + $logQsoMap = LogQso::whereIn('id', $workingQsos->pluck('log_qso_id')->all()) + ->get() + ->keyBy('id'); + $workingMap = $workingQsos->keyBy('log_qso_id'); + + // Indexace pracovních QSO pro rychlé vyhledání kandidátů a detekci duplicit. + $byMatchKey = []; + $byLog = []; + $byBandRcall = []; + foreach ($workingQsos as $wqso) { + if ($wqso->match_key) { + $byMatchKey[$wqso->match_key][] = $wqso; + } + $byLog[$wqso->log_id][] = $wqso; + if ($wqso->band_id && $wqso->rcall_norm) { + $byBandRcall[$wqso->band_id . '|' . $wqso->rcall_norm][] = $wqso; + } + } + + // Duplicitní QSO se řeší až po matchingu v samostatném kroku. + + // Tolerance času určuje maximální rozdíl mezi oběma stranami QSO. + $timeTolerance = $ruleSet->time_tolerance_sec !== null + ? (int) $ruleSet->time_tolerance_sec + : 300; + $forcedMap = []; + foreach ($qsoOverrides as $override) { + if ($override->forced_matched_log_qso_id) { + $forcedMap[$override->log_qso_id] = (int) $override->forced_matched_log_qso_id; + } + } + $forcedBackMap = []; + $forcedConflicts = []; + foreach ($forcedMap as $a => $b) { + if (isset($forcedMap[$b]) && $forcedMap[$b] !== $a) { + $forcedConflicts[$a] = true; + $forcedConflicts[$b] = true; + } + if (! isset($forcedMap[$b])) { + $forcedBackMap[$b] = $a; + } + } + + $paired = []; + foreach ($workingQsos as $wqso) { + // PASS 1 (EXACT) WHY: minimalizuje riziko chybných párování; + // ORDER: běží vždy první, aby přesné shody byly „uzamčené“ před fuzzy passy; + // IRREVERSIBLE: jakmile je QSO spárované, další kroky už ho nepřepárují. + if ($this->pass > 1 && isset($alreadyMatched[$wqso->log_qso_id])) { + continue; + } + $reverseKey = $wqso->band_id && $wqso->call_norm && $wqso->rcall_norm + ? $wqso->band_id . '|' . $wqso->rcall_norm . '|' . $wqso->call_norm + : null; + + // Kandidáti jsou protistanice se shodným reverse match key. + $candidates = []; + if ($reverseKey && isset($byMatchKey[$reverseKey])) { + $candidates = $byMatchKey[$reverseKey]; + } + if ($wqso->band_id && $wqso->call_norm) { + $fuzzyKey = $wqso->band_id . '|' . $wqso->call_norm; + if (isset($byBandRcall[$fuzzyKey])) { + $candidates = array_merge($candidates, $byBandRcall[$fuzzyKey]); + } + } + if ($candidates) { + $unique = []; + foreach ($candidates as $candidate) { + $unique[$candidate->log_qso_id] = $candidate; + } + $candidates = array_values($unique); + } + + $best = null; + $bestDecision = null; + // Výběr nejlepšího kandidáta je deterministický podle pravidel tiebreaku. + $tiebreakOrder = $this->resolveTiebreakOrder($ruleSet); + $forcedMatchId = $forcedMap[$wqso->log_qso_id] + ?? $forcedBackMap[$wqso->log_qso_id] + ?? null; + $forcedMissing = false; + $forcedDecision = null; + if ($forcedMatchId) { + $best = $workingMap->get($forcedMatchId); + if (! $best || $best->log_id === $wqso->log_id) { + $best = null; + $forcedMissing = true; + } + if ($best) { + $forcedDecision = $this->evaluateMatchDecision( + $wqso, + $best, + $logQsoMap, + $ruleSet, + $timeTolerance + ); + $bestDecision = $forcedDecision; + } + } else { + foreach ($candidates as $candidate) { + if ($candidate->log_id === $wqso->log_id) { + continue; + } + if (isset($paired[$candidate->log_qso_id]) && $paired[$candidate->log_qso_id] !== $wqso->log_qso_id) { + continue; + } + + $decision = $this->evaluateMatchDecision( + $wqso, + $candidate, + $logQsoMap, + $ruleSet, + $timeTolerance + ); + if (! $decision) { + continue; + } + // PASS 2 (FAULTY) WHY: umožní dohledat „pravděpodobné“ shody s chybami; + // ORDER: běží až po exact passu a pracuje jen s nenapárovanými QSO; + // IRREVERSIBLE: výsledek se považuje za finální vstup pro scoring. + if ($this->pass === 1 && ($decision['match_type'] ?? '') !== 'MATCH_EXACT') { + continue; + } + + if ($bestDecision === null || $decision['rank'] < $bestDecision['rank']) { + $best = $candidate; + $bestDecision = $decision; + continue; + } + + if ($decision['rank'] === $bestDecision['rank']) { + $cmp = $this->compareCandidates( + $decision['tiebreak'], + $bestDecision['tiebreak'], + $tiebreakOrder + ); + if ($cmp < 0) { + $best = $candidate; + $bestDecision = $decision; + } + } + } + } + + $timeDiffSec = null; + if ($best && $wqso->ts_utc && $best->ts_utc) { + $timeDiffSec = abs($best->ts_utc->getTimestamp() - $wqso->ts_utc->getTimestamp()); + } + $isNil = $best === null; + $isDuplicate = false; + $isBustedExchange = false; + $isBustedCall = false; + $isBustedRst = false; + $bustedCallTx = false; + $bustedRstTx = false; + $bustedExchangeReason = null; + $customMismatch = false; + $isBustedExchangeOnly = false; + $bustedSerialRx = false; + $bustedSerialTx = false; + $bustedWwlRx = false; + $bustedWwlTx = false; + $matchType = null; + $errorFlags = []; + $timeMismatch = false; + $isOutOfWindow = (bool) $wqso->out_of_window; + $errorCode = null; + + if (! $isNil && $best && $bestDecision) { + $a = $logQsoMap->get($wqso->log_qso_id); + $b = $logQsoMap->get($best->log_qso_id); + $aWork = $workingMap->get($wqso->log_qso_id); + $bWork = $workingMap->get($best->log_qso_id); + + if ($a && $b) { + $exchange = $this->evaluateExchange($a, $b, $aWork, $bWork, $ruleSet); + $bustedCallTx = $exchange['sent_call_mismatch'] && $ruleSet->discard_qso_sent_diff_call; + $isBustedCall = $exchange['recv_call_mismatch'] && $ruleSet->discard_qso_rec_diff_call; + + $bustedRstTx = ($ruleSet->exchange_requires_report ?? false) + && $exchange['report_sent_mismatch'] + && $ruleSet->discard_qso_sent_diff_rst; + $isBustedRst = ($ruleSet->exchange_requires_report ?? false) + && $exchange['report_recv_mismatch'] + && $ruleSet->discard_qso_rec_diff_rst; + + $discardSerialRx = $ruleSet->discard_qso_rec_diff_serial ?? $ruleSet->discard_qso_rec_diff_code; + $discardSerialTx = $ruleSet->discard_qso_sent_diff_serial ?? $ruleSet->discard_qso_sent_diff_code; + $discardWwlRx = $ruleSet->discard_qso_rec_diff_wwl ?? $ruleSet->discard_qso_rec_diff_code; + $discardWwlTx = $ruleSet->discard_qso_sent_diff_wwl ?? $ruleSet->discard_qso_sent_diff_code; + + $customMismatch = (bool) ($exchange['custom_mismatch'] ?? false); + $bustedSerialRx = ($exchange['serial_recv_mismatch'] || $exchange['missing_serial'] || $customMismatch) && $discardSerialRx; + $bustedSerialTx = ($exchange['serial_sent_mismatch'] || $customMismatch) && $discardSerialTx; + $bustedWwlRx = ($exchange['locator_recv_mismatch'] || $exchange['missing_wwl']) && $discardWwlRx; + $bustedWwlTx = $exchange['locator_sent_mismatch'] && $discardWwlTx; + + $isBustedExchangeOnly = ($customMismatch && $ruleSet->discard_qso_rec_diff_code); + $isBustedExchange = $isBustedExchangeOnly || $bustedSerialRx || $bustedWwlRx; + $bustedExchangeReason = $exchange['busted_exchange_reason'] ?? null; + $matchType = $bestDecision['match_type'] ?? null; + $errorFlags = $bestDecision['error_flags'] ?? []; + $timeMismatch = (bool) ($bestDecision['time_mismatch'] ?? false); + } + } + + // TIME_MISMATCH se neřeší jako invalidita v matchingu, ale až ve scorování dle policy. + $isValid = ! $isNil && ! $isBustedCall && ! $isBustedRst && ! $isBustedExchange; + $errorDetail = null; + $bustedCallRx = $isBustedCall; + $bustedRstRx = $isBustedRst; + + if ($forcedMissing) { + $errorDetail = 'FORCED_MATCH_MISSING'; + } elseif (isset($forcedConflicts[$wqso->log_qso_id])) { + $errorDetail = 'FORCED_MATCH_CONFLICT'; + } elseif ($isBustedExchange && $bustedExchangeReason) { + $errorDetail = $bustedExchangeReason; + } + + $errorSide = $this->resolveErrorSide( + $bustedCallRx, + $bustedCallTx, + $bustedRstRx, + $bustedRstTx, + $bustedSerialRx, + $bustedSerialTx, + $bustedWwlRx, + $bustedWwlTx, + $timeMismatch + ); + + $override = $qsoOverrides->get($wqso->log_qso_id); + if ($override && $override->forced_status && $override->forced_status !== 'AUTO') { + $this->applyForcedStatus( + $override->forced_status, + $isValid, + $isDuplicate, + $isNil, + $isBustedCall, + $isBustedRst, + $isBustedExchange, + $isOutOfWindow, + $errorCode, + $errorSide + ); + } + $matchConfidence = $this->resolveMatchConfidence($bestDecision['match_type'] ?? null, $timeMismatch); + + if (! $errorCode) { + if ($timeMismatch) { + $errorCode = QsoErrorCode::TIME_MISMATCH; + } elseif ($isBustedCall || $bustedCallTx) { + $errorCode = QsoErrorCode::BUSTED_CALL; + } elseif ($isBustedRst || $bustedRstTx) { + $errorCode = QsoErrorCode::BUSTED_RST; + } elseif ($bustedSerialRx || $bustedSerialTx) { + $errorCode = QsoErrorCode::BUSTED_SERIAL; + } elseif ($bustedWwlRx || $bustedWwlTx) { + $errorCode = QsoErrorCode::BUSTED_LOCATOR; + } elseif ($isBustedExchangeOnly) { + $errorCode = QsoErrorCode::BUSTED_SERIAL; + } elseif (! $isNil && ! $isDuplicate && ! $isBustedExchange && ! $isOutOfWindow) { + $errorCode = QsoErrorCode::OK; + } + } + + QsoResult::updateOrCreate( + [ + 'evaluation_run_id' => $run->id, + 'log_qso_id' => $wqso->log_qso_id, + ], + [ + 'is_valid' => $isValid, + 'is_duplicate' => $isDuplicate, + 'is_nil' => $isNil, + 'is_busted_call' => $isBustedCall, + 'is_busted_rst' => $isBustedRst, + 'is_busted_exchange' => $isBustedExchange, + 'is_time_out_of_window' => $isOutOfWindow, + 'points' => 0, + 'distance_km' => null, + 'time_diff_sec' => $timeDiffSec, + 'wwl' => null, + 'dxcc' => null, + 'matched_qso_id' => $best?->log_qso_id, + 'matched_log_qso_id' => $best?->log_qso_id, + 'match_type' => $matchType, + 'match_confidence' => $matchConfidence, + 'error_code' => $errorCode, + 'error_side' => $errorSide, + 'error_detail' => $errorDetail, + 'error_flags' => $errorFlags ?: null, + ] + ); + + if ($best && $best->log_qso_id) { + $paired[$wqso->log_qso_id] = $best->log_qso_id; + $paired[$best->log_qso_id] = $wqso->log_qso_id; + } + } + + if ($singleGroup) { + EvaluationRun::where('id', $run->id)->increment('progress_done'); + } else { + $run->update(['progress_done' => $processed]); + } + } + + $coordinator->eventInfo($run, 'Matching: krok dokončen.', [ + 'step' => 'match', + 'round_id' => $run->round_id, + 'group_key' => $this->groupKey, + ]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Matching: krok selhal.', [ + 'step' => 'match', + 'round_id' => $run->round_id, + 'group_key' => $this->groupKey, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } + + protected function groupLogsByKey( + Round $round, + EvaluationRuleSet $ruleSet, + \Illuminate\Support\Collection $logOverrides + ): array + { + $logs = Log::where('round_id', $round->id)->get(); + $map = []; + + foreach ($logs as $log) { + if (! $ruleSet->checklog_matching && $this->isCheckLog($log)) { + continue; + } + $override = $logOverrides->get($log->id); + if ($override && $override->forced_log_status === 'IGNORED') { + continue; + } + + $bandId = $override && $override->forced_band_id + ? (int) $override->forced_band_id + : $this->resolveBandId($log, $round); + $key = 'b' . ($bandId ?? 0); + + $map[$key][] = $log->id; + } + + return $map; + } + + protected function resolveCategoryId(Log $log, Round $round): ?int + { + $value = $log->psect; + if (! $value) { + return null; + } + + $ediCat = EdiCategory::whereRaw('LOWER(value) = ?', [mb_strtolower(trim($value))])->first(); + if (! $ediCat) { + $ediCat = $this->matchEdiCategoryByRegex($value); + } + if (! $ediCat) { + return null; + } + + $mappedCategoryId = $ediCat->categories()->value('categories.id'); + if (! $mappedCategoryId) { + return null; + } + + if ($round->categories()->count() === 0) { + return $mappedCategoryId; + } + + return $round->categories()->where('categories.id', $mappedCategoryId)->exists() + ? $mappedCategoryId + : null; + } + + protected function matchEdiCategoryByRegex(string $value): ?EdiCategory + { + $candidates = EdiCategory::whereNotNull('regex_pattern')->get(); + foreach ($candidates as $candidate) { + $pattern = $candidate->regex_pattern; + if (! $pattern) { + continue; + } + + $delimited = preg_match('/^([#\\/]).+\\1[imsxuADSUXJ]*$/', $pattern) ? $pattern : '/' . $pattern . '/i'; + set_error_handler(function () { + }); + $matched = @preg_match($delimited, $value) === 1; + restore_error_handler(); + + if ($matched) { + return $candidate; + } + } + + return null; + } + + protected function resolveBandId(Log $log, Round $round): ?int + { + if (! $log->pband) { + return null; + } + + $pbandVal = mb_strtolower(trim($log->pband)); + $ediBand = EdiBand::whereRaw('LOWER(value) = ?', [$pbandVal])->first(); + if ($ediBand) { + $mappedBandId = $ediBand->bands()->value('bands.id'); + if (! $mappedBandId) { + return null; + } + if ($round->bands()->count() === 0) { + return $mappedBandId; + } + return $round->bands()->where('bands.id', $mappedBandId)->exists() + ? $mappedBandId + : null; + } + + $num = is_numeric($pbandVal) ? (float) $pbandVal : null; + if ($num === null && $log->pband) { + if (preg_match('/([0-9]+(?:[\\.,][0-9]+)?)/', $log->pband, $m)) { + $num = (float) str_replace(',', '.', $m[1]); + } + } + if ($num === null) { + return null; + } + + $bandMatch = \App\Models\Band::where('edi_band_begin', '<=', $num) + ->where('edi_band_end', '>=', $num) + ->first(); + if (! $bandMatch) { + return null; + } + + if ($round->bands()->count() === 0) { + return $bandMatch->id; + } + + return $round->bands()->where('bands.id', $bandMatch->id)->exists() + ? $bandMatch->id + : null; + } + + protected function normalizeCallsignStrict(string $call): string + { + $value = mb_strtoupper(trim($call)); + $value = preg_replace('/\s+/', '', $value); + return $value ?? ''; + } + + protected function normalizeCallsignForBusted(string $call, EvaluationRuleSet $ruleSet): string + { + $value = $this->normalizeCallsignStrict($call); + if ($ruleSet->ignoreSlashPart()) { + $value = $this->stripCallsignSuffix($value, $ruleSet->callsignSuffixMaxLen()); + } elseif ($ruleSet->ignoreThirdPart()) { + $parts = explode('/', $value); + if (count($parts) > 2) { + $value = $parts[0] . '/' . $parts[1]; + } + } elseif ($ruleSet->callsign_normalization === 'IGNORE_SUFFIX') { + $value = $this->stripCallsignSuffix($value, $ruleSet->callsignSuffixMaxLen()); + } + return $value; + } + + protected function stripCallsignSuffix(string $value, int $maxLen): string + { + $parts = explode('/', $value); + if (count($parts) < 2) { + return $value; + } + + $suffix = end($parts) ?: ''; + if ($suffix !== '' && mb_strlen($suffix) <= $maxLen) { + array_pop($parts); + return implode('/', $parts); + } + + return $value; + } + + protected function isCheckLog(Log $log): bool + { + $psect = trim((string) $log->psect); + return $psect !== '' && preg_match('/\\bcheck\\b/i', $psect) === 1; + } + + protected function resolveTiebreakOrder(EvaluationRuleSet $ruleSet): array + { + // Umožňuje soutěži nastavit pořadí preferencí při výběru kandidáta. + $order = $ruleSet->match_tiebreak_order; + if (is_array($order) && count($order) > 0) { + return $order; + } + + return ['time_diff', 'exchange_match', 'locator_match', 'report_match', 'log_qso_id']; + } + + protected function evaluateCandidate( + WorkingQso $aWork, + WorkingQso $bWork, + int $diffSeconds, + \Illuminate\Support\Collection $logQsoMap, + EvaluationRuleSet $ruleSet + ): ?array { + // Shromáždí metriky pro tiebreak (čas, exchange/locator/report match). + $a = $logQsoMap->get($aWork->log_qso_id); + $b = $logQsoMap->get($bWork->log_qso_id); + if (! $a || ! $b) { + return null; + } + + $exchange = $this->evaluateExchange($a, $b, $aWork, $bWork, $ruleSet); + $locatorMatch = $exchange['locator_match']; + $exchangeMatch = $exchange['exchange_match']; + $reportMatch = $exchange['report_match']; + + return [ + 'candidate' => $bWork, + 'time_diff' => $diffSeconds, + 'exchange_match' => $exchangeMatch, + 'locator_match' => $locatorMatch, + 'report_match' => $reportMatch, + 'log_qso_id' => $bWork->log_qso_id, + ]; + } + + protected function compareCandidates(?array $a, ?array $b, array $order): int + { + // Deterministické srovnání podle zadaného pořadí kritérií. + if (! $a && ! $b) { + return 0; + } + if (! $a) { + return 1; + } + if (! $b) { + return -1; + } + + foreach ($order as $key) { + if (! array_key_exists($key, $a) || ! array_key_exists($key, $b)) { + continue; + } + + $av = $a[$key]; + $bv = $b[$key]; + + if ($key === 'time_diff' || $key === 'log_qso_id') { + if ($av === $bv) { + continue; + } + return $av <=> $bv; + } + + if (is_bool($av) || is_bool($bv)) { + if ($av === $bv) { + continue; + } + return $av ? -1 : 1; + } + } + + return $a['log_qso_id'] <=> $b['log_qso_id']; + } + + protected function evaluateMatchDecision( + WorkingQso $aWork, + WorkingQso $bWork, + \Illuminate\Support\Collection $logQsoMap, + EvaluationRuleSet $ruleSet, + int $timeTolerance + ): ?array { + $a = $logQsoMap->get($aWork->log_qso_id); + $b = $logQsoMap->get($bWork->log_qso_id); + if (! $a || ! $b) { + return null; + } + + if (! $aWork->call_norm || ! $aWork->rcall_norm || ! $bWork->call_norm || ! $bWork->rcall_norm) { + return null; + } + + $callMatch = $aWork->call_norm === $bWork->rcall_norm + && $aWork->rcall_norm === $bWork->call_norm; + $callDistance = $this->maxCallDistance($aWork, $bWork); + $maxLev = $ruleSet->callsignLevenshteinMax(); + if (! $callMatch && ($callDistance === null || $callDistance > $maxLev)) { + return null; + } + + $timeDiff = null; + if ($aWork->ts_utc && $bWork->ts_utc) { + $timeDiff = abs($aWork->ts_utc->getTimestamp() - $bWork->ts_utc->getTimestamp()); + } elseif ($timeTolerance === 0) { + return null; + } else { + $timeDiff = 0; + } + + $timeInTolerance = $timeDiff <= $timeTolerance; + $maxTimeDiff = $timeTolerance + (int) ($ruleSet->time_shift_seconds ?? 0); + if (! $timeInTolerance && $timeDiff > $maxTimeDiff && ! $ruleSet->allowTimeMismatchPairing()) { + return null; + } + + $timeShiftMatch = false; + if ($ruleSet->allowTimeShiftOneHour() && $aWork->ts_utc && $bWork->ts_utc) { + $shift = (int) ($ruleSet->time_shift_seconds ?? 3600); + $shiftForward = abs(($aWork->ts_utc->getTimestamp() + $shift) - $bWork->ts_utc->getTimestamp()); + $shiftBackward = abs(($aWork->ts_utc->getTimestamp() - $shift) - $bWork->ts_utc->getTimestamp()); + $shiftDiff = min($shiftForward, $shiftBackward); + if ($shiftDiff <= $timeTolerance) { + $timeShiftMatch = true; + } + } + + $exchange = $this->evaluateExchange($a, $b, $aWork, $bWork, $ruleSet); + $requireLocatorMatch = $ruleSet->matchRequireLocatorMatch(); + $requireExchangeMatch = $ruleSet->matchRequireExchangeMatch(); + + $serialMismatch = $exchange['missing_serial'] + || $exchange['serial_sent_mismatch'] + || $exchange['serial_recv_mismatch']; + $wwlMismatch = $exchange['missing_wwl'] + || $exchange['locator_sent_mismatch'] + || $exchange['locator_recv_mismatch']; + $rstMismatch = ($ruleSet->exchange_requires_report ?? false) + && ($exchange['report_sent_mismatch'] || $exchange['report_recv_mismatch']); + $customMismatch = ($ruleSet->exchange_type === 'CUSTOM') && $exchange['custom_mismatch']; + $wwlMismatch = $wwlMismatch || ($requireLocatorMatch && ! $exchange['locator_match']); + $customMismatch = $customMismatch || ($requireExchangeMatch && ! $exchange['exchange_match']); + + $callMismatch = ! $callMatch; + $mismatchCount = 0; + $mismatchCount += $callMismatch ? 1 : 0; + $mismatchCount += $rstMismatch ? 1 : 0; + $mismatchCount += $serialMismatch ? 1 : 0; + $mismatchCount += $wwlMismatch ? 1 : 0; + $mismatchCount += $customMismatch ? 1 : 0; + + $errorFlags = []; + if ($exchange['recv_call_mismatch']) { + $errorFlags[] = 'BUSTED_CALL_RX'; + } + if ($exchange['sent_call_mismatch']) { + $errorFlags[] = 'BUSTED_CALL_TX'; + } + if (($ruleSet->exchange_requires_report ?? false) && $exchange['report_recv_mismatch']) { + $errorFlags[] = 'BUSTED_RST_RX'; + } + if (($ruleSet->exchange_requires_report ?? false) && $exchange['report_sent_mismatch']) { + $errorFlags[] = 'BUSTED_RST_TX'; + } + if (($ruleSet->exchange_requires_serial ?? false) && ($exchange['serial_recv_mismatch'] || $exchange['missing_serial'])) { + $errorFlags[] = 'BUSTED_SERIAL_RX'; + } + if (($ruleSet->exchange_requires_serial ?? false) && $exchange['serial_sent_mismatch']) { + $errorFlags[] = 'BUSTED_SERIAL_TX'; + } + if (($ruleSet->exchange_requires_wwl ?? false || $requireLocatorMatch) && ($exchange['locator_recv_mismatch'] || $exchange['missing_wwl'])) { + $errorFlags[] = 'BUSTED_LOCATOR_RX'; + } + if (($ruleSet->exchange_requires_wwl ?? false || $requireLocatorMatch) && $exchange['locator_sent_mismatch']) { + $errorFlags[] = 'BUSTED_LOCATOR_TX'; + } + if ($customMismatch || ($requireExchangeMatch && ! $exchange['exchange_match'])) { + $errorFlags[] = 'BUSTED_EXCHANGE'; + } + + $decision = null; + if ($timeInTolerance && $callMatch && ! $rstMismatch && ! $serialMismatch && ! $wwlMismatch && ! $customMismatch) { + $decision = ['rank' => 0, 'match_type' => 'MATCH_EXACT']; + } elseif ($timeInTolerance && $callMatch && $mismatchCount === 1) { + if ($rstMismatch) { + $decision = ['rank' => 1, 'match_type' => 'MATCH_ONE_ERROR_RST']; + } elseif ($serialMismatch) { + $decision = ['rank' => 2, 'match_type' => 'MATCH_ONE_ERROR_SERIAL']; + } elseif ($wwlMismatch) { + $decision = ['rank' => 3, 'match_type' => 'MATCH_ONE_ERROR_WWL']; + } elseif ($customMismatch) { + $decision = ['rank' => 4, 'match_type' => 'MATCH_ONE_ERROR_CUSTOM']; + } + } elseif (! $timeInTolerance && $callMatch && $mismatchCount === 0 && $ruleSet->allowTimeMismatchPairing()) { + $maxMismatch = $ruleSet->timeMismatchMaxSec(); + if ($maxMismatch === null || $timeDiff <= $maxMismatch) { + $decision = ['rank' => 5, 'match_type' => 'TIME_MISMATCH']; + $errorFlags[] = 'TIME_MISMATCH'; + } + } elseif ($timeInTolerance && $callMismatch && $mismatchCount === 1 && $callDistance !== null) { + $maxLevenshtein = max(0, $ruleSet->callsignLevenshteinMax()); + if ($maxLevenshtein >= 1 && $callDistance <= 1) { + $decision = ['rank' => 6, 'match_type' => 'MATCH_FUZZY_CALL_1']; + } elseif ($maxLevenshtein >= 2 && $callDistance <= 2) { + $decision = ['rank' => 7, 'match_type' => 'MATCH_FUZZY_CALL_2']; + } + } elseif ($timeInTolerance && $mismatchCount === 2) { + $decision = ['rank' => 8, 'match_type' => 'MATCH_COMBINED_ERRORS']; + } elseif ($timeShiftMatch && $mismatchCount === 1 && $ruleSet->allowTimeShiftOneHour()) { + $decision = ['rank' => 9, 'match_type' => 'MATCH_TIME_SHIFT_1H']; + $errorFlags[] = 'TIME_SHIFT_1H'; + } + + if (! $decision) { + return null; + } + + return [ + 'rank' => $decision['rank'], + 'match_type' => $decision['match_type'], + 'error_flags' => array_values(array_unique($errorFlags)), + 'time_mismatch' => $decision['match_type'] === 'TIME_MISMATCH', + 'tiebreak' => [ + 'time_diff' => $timeDiff, + 'exchange_match' => $exchange['exchange_match'], + 'locator_match' => $exchange['locator_match'], + 'report_match' => $exchange['report_match'], + 'log_qso_id' => $bWork->log_qso_id, + ], + ]; + } + + protected function maxCallDistance(WorkingQso $aWork, WorkingQso $bWork): ?int + { + if (! $aWork->call_norm || ! $aWork->rcall_norm || ! $bWork->call_norm || ! $bWork->rcall_norm) { + return null; + } + + $distanceA = levenshtein($aWork->call_norm, $bWork->rcall_norm); + $distanceB = levenshtein($aWork->rcall_norm, $bWork->call_norm); + + return max($distanceA, $distanceB); + } + + protected function evaluateExchange( + LogQso $a, + LogQso $b, + ?WorkingQso $aWork, + ?WorkingQso $bWork, + EvaluationRuleSet $ruleSet + ): array { + // Výsledek porovnání výměn slouží pro busted_exchange i tiebreak. + $serialMatch = $this->serialsMatch($a, $b); + $locatorMatch = $this->locatorsMatch($aWork, $bWork); + $reportMatch = $this->reportsMatch($a, $b, $ruleSet); + $reportSentMismatch = $this->reportSideMismatch($a->my_rst, $b->dx_rst, $ruleSet); + $reportRecvMismatch = $this->reportSideMismatch($a->dx_rst, $b->my_rst, $ruleSet); + $customMatch = $this->customExchangeMatch($a, $b, $ruleSet); + + $serialSentMatch = $this->serialSentMatch($a, $b); + $serialRecvMatch = $this->serialRecvMatch($a, $b); + $serialSentMismatch = $serialSentMatch === false; + $serialRecvMismatch = $serialRecvMatch === false; + + $locatorSentMatch = $this->locatorSentMatch($aWork, $bWork); + $locatorRecvMatch = $this->locatorRecvMatch($aWork, $bWork); + $locatorSentMismatch = $locatorSentMatch === false; + $locatorRecvMismatch = $locatorRecvMatch === false; + + $sentCallMismatch = $this->sentCallMismatch($a, $b, $ruleSet); + $recvCallMismatch = $this->recvCallMismatch($a, $b, $ruleSet); + + $requiresSerial = $ruleSet->exchange_requires_serial; + $requiresWwl = $ruleSet->exchange_requires_wwl; + + $exchangeMatch = true; + if ($ruleSet->exchange_type === 'SERIAL') { + $exchangeMatch = $serialMatch; + } elseif ($ruleSet->exchange_type === 'WWL') { + $exchangeMatch = $locatorMatch; + } elseif ($ruleSet->exchange_type === 'SERIAL_WWL') { + $exchangeMatch = $serialMatch && $locatorMatch; + } elseif ($ruleSet->exchange_type === 'CUSTOM') { + $exchangeMatch = $customMatch; + } + + $missingSerial = $requiresSerial && ! $this->hasSerialExchange($a, $b); + $missingWwl = $requiresWwl && ! $this->hasWwlExchange($aWork, $bWork); + $missingReport = $ruleSet->exchange_requires_report && ! $reportMatch; + $reportMismatch = $reportSentMismatch || $reportRecvMismatch; + + $exchangeMismatch = (! $exchangeMatch && $ruleSet->exchange_type !== 'CUSTOM') + || ($ruleSet->exchange_type === 'CUSTOM' && ! $customMatch) + || $missingSerial + || $missingWwl; + $shouldDiscardExchange = $ruleSet->discard_qso_rec_diff_code || $ruleSet->discard_qso_sent_diff_code; + $bustedExchange = $shouldDiscardExchange && $exchangeMismatch; + $bustedExchangeReason = null; + if ($bustedExchange) { + if ($missingSerial) { + $bustedExchangeReason = 'EXCHANGE_SERIAL_MISSING'; + } elseif ($missingWwl) { + $bustedExchangeReason = 'EXCHANGE_WWL_MISSING'; + } elseif ($ruleSet->exchange_type === 'CUSTOM' && ! $customMatch) { + $bustedExchangeReason = 'EXCHANGE_CUSTOM_MISMATCH'; + } elseif ($ruleSet->exchange_type === 'SERIAL' && ! $serialMatch) { + $bustedExchangeReason = 'EXCHANGE_SERIAL_MISMATCH'; + } elseif ($ruleSet->exchange_type === 'WWL' && ! $locatorMatch) { + $bustedExchangeReason = 'EXCHANGE_WWL_MISMATCH'; + } elseif ($ruleSet->exchange_type === 'SERIAL_WWL') { + if (! $serialMatch && ! $locatorMatch) { + $bustedExchangeReason = 'EXCHANGE_SERIAL_WWL_MISMATCH'; + } elseif (! $serialMatch) { + $bustedExchangeReason = 'EXCHANGE_SERIAL_MISMATCH'; + } elseif (! $locatorMatch) { + $bustedExchangeReason = 'EXCHANGE_WWL_MISMATCH'; + } + } + $bustedExchangeReason = $bustedExchangeReason ?? 'EXCHANGE_MISMATCH'; + } + + $bustedRstRx = $ruleSet->exchange_requires_report + && $ruleSet->discard_qso_rec_diff_rst + && ($reportRecvMismatch || $missingReport); + $bustedRstTx = $ruleSet->exchange_requires_report + && $ruleSet->discard_qso_sent_diff_rst + && $reportSentMismatch; + + return [ + 'serial_match' => $serialMatch, + 'serial_sent_mismatch' => $serialSentMismatch, + 'serial_recv_mismatch' => $serialRecvMismatch, + 'locator_match' => $locatorMatch, + 'locator_sent_mismatch' => $locatorSentMismatch, + 'locator_recv_mismatch' => $locatorRecvMismatch, + 'report_match' => $reportMatch, + 'report_sent_mismatch' => $reportSentMismatch, + 'report_recv_mismatch' => $reportRecvMismatch, + 'exchange_match' => $exchangeMatch, + 'busted_exchange' => $bustedExchange, + 'busted_exchange_reason' => $bustedExchangeReason, + 'busted_rst_rx' => $bustedRstRx, + 'busted_rst_tx' => $bustedRstTx, + 'custom_match' => $customMatch, + 'custom_mismatch' => ! $customMatch, + 'missing_serial' => $missingSerial, + 'missing_wwl' => $missingWwl, + 'sent_call_mismatch' => $sentCallMismatch, + 'recv_call_mismatch' => $recvCallMismatch, + ]; + } + + protected function serialSentMatch(LogQso $a, LogQso $b): ?bool + { + if ($a->my_serial === null || $b->dx_serial === null) { + return null; + } + return (string) $a->my_serial === (string) $b->dx_serial; + } + + protected function serialRecvMatch(LogQso $a, LogQso $b): ?bool + { + if ($a->dx_serial === null || $b->my_serial === null) { + return null; + } + return (string) $a->dx_serial === (string) $b->my_serial; + } + + protected function locatorSentMatch(?WorkingQso $aWork, ?WorkingQso $bWork): ?bool + { + if (! $aWork || ! $bWork || ! $aWork->loc_norm || ! $bWork->rloc_norm) { + return null; + } + return strtoupper($aWork->loc_norm) === strtoupper($bWork->rloc_norm); + } + + protected function locatorRecvMatch(?WorkingQso $aWork, ?WorkingQso $bWork): ?bool + { + if (! $aWork || ! $bWork || ! $aWork->rloc_norm || ! $bWork->loc_norm) { + return null; + } + return strtoupper($aWork->rloc_norm) === strtoupper($bWork->loc_norm); + } + + protected function resolveErrorSide( + bool $bustedCallRx, + bool $bustedCallTx, + bool $bustedRstRx, + bool $bustedRstTx, + bool $bustedSerialRx, + bool $bustedSerialTx, + bool $bustedWwlRx, + bool $bustedWwlTx, + bool $timeMismatch + ): ?string { + if ($timeMismatch) { + return 'BOTH'; + } + + $rx = $bustedCallRx || $bustedRstRx || $bustedSerialRx || $bustedWwlRx; + $tx = $bustedCallTx || $bustedRstTx || $bustedSerialTx || $bustedWwlTx; + + if ($rx && $tx) { + return 'BOTH'; + } + if ($rx) { + return 'RX'; + } + if ($tx) { + return 'TX'; + } + + return 'NONE'; + } + + protected function resolveMatchConfidence(?string $matchType, bool $timeMismatch): ?string + { + if (! $matchType) { + return null; + } + if ($matchType === 'MATCH_EXACT') { + return 'EXACT'; + } + if (str_contains($matchType, 'MATCH_FUZZY_CALL')) { + return 'FUZZY_CALL'; + } + if ($timeMismatch || $matchType === 'TIME_MISMATCH') { + return 'TIME_MISMATCH'; + } + + return 'PARTIAL'; + } + + protected function sentCallMismatch(LogQso $a, LogQso $b, EvaluationRuleSet $ruleSet): bool + { + $aMy = $this->normalizeCallsignForBusted($a->my_call ?? '', $ruleSet); + $bDx = $this->normalizeCallsignForBusted($b->dx_call ?? '', $ruleSet); + if ($aMy === '' || $bDx === '') { + return false; + } + return $aMy !== $bDx; + } + + protected function recvCallMismatch(LogQso $a, LogQso $b, EvaluationRuleSet $ruleSet): bool + { + $aDx = $this->normalizeCallsignForBusted($a->dx_call ?? '', $ruleSet); + $bMy = $this->normalizeCallsignForBusted($b->my_call ?? '', $ruleSet); + if ($aDx === '' || $bMy === '') { + return false; + } + return $aDx !== $bMy; + } + + protected function serialsMatch(LogQso $a, LogQso $b): bool + { + $aSent = $a->my_serial; + $aRecv = $a->dx_serial; + $bSent = $b->my_serial; + $bRecv = $b->dx_serial; + + if ($aSent === null || $aRecv === null || $bSent === null || $bRecv === null) { + return false; + } + + return (string) $aSent === (string) $bRecv && (string) $aRecv === (string) $bSent; + } + + protected function hasSerialExchange(LogQso $a, LogQso $b): bool + { + return $a->my_serial !== null && $a->dx_serial !== null && $b->my_serial !== null && $b->dx_serial !== null; + } + + protected function locatorsMatch(?WorkingQso $aWork, ?WorkingQso $bWork): bool + { + if (! $aWork || ! $bWork) { + return false; + } + if (! $aWork->loc_norm || ! $aWork->rloc_norm || ! $bWork->loc_norm || ! $bWork->rloc_norm) { + return false; + } + + return strtoupper($aWork->loc_norm) === strtoupper($bWork->rloc_norm) + && strtoupper($aWork->rloc_norm) === strtoupper($bWork->loc_norm); + } + + protected function hasWwlExchange(?WorkingQso $aWork, ?WorkingQso $bWork): bool + { + return $aWork && $bWork + && $aWork->loc_norm && $aWork->rloc_norm + && $bWork->loc_norm && $bWork->rloc_norm; + } + + protected function reportsMatch(LogQso $a, LogQso $b, EvaluationRuleSet $ruleSet): bool + { + $aMy = $this->normalizeReport($a->my_rst, $ruleSet); + $aDx = $this->normalizeReport($a->dx_rst, $ruleSet); + $bMy = $this->normalizeReport($b->my_rst, $ruleSet); + $bDx = $this->normalizeReport($b->dx_rst, $ruleSet); + + if ($aMy === '' || $aDx === '' || $bMy === '' || $bDx === '') { + return false; + } + + return $aMy === $bDx && $aDx === $bMy; + } + + protected function reportSideMismatch(?string $sent, ?string $received, EvaluationRuleSet $ruleSet): bool + { + $sentValue = $this->normalizeReport($sent, $ruleSet); + $receivedValue = $this->normalizeReport($received, $ruleSet); + if ($sentValue === '' || $receivedValue === '') { + return true; + } + + return $sentValue !== $receivedValue; + } + + protected function normalizeReport(?string $value, EvaluationRuleSet $ruleSet): string + { + $value = strtoupper(trim((string) $value)); + $value = preg_replace('/\s+/', '', $value) ?? ''; + if ($value === '') { + return ''; + } + + if (! $ruleSet->lettersInRst()) { + $value = preg_replace('/[A-Z]/', '', $value) ?? ''; + } + + if ($ruleSet->rstIgnoreThirdChar() && strlen($value) >= 3) { + $value = substr($value, 0, 2); + } + + return $value; + } + + protected function customExchangeMatch(LogQso $a, LogQso $b, EvaluationRuleSet $ruleSet): bool + { + // CUSTOM exchange: regex + symetrie hodnot (obě strany musí shodně sedět). + $pattern = $ruleSet->exchange_pattern; + if (! $pattern) { + return true; + } + + $delimited = preg_match('/^([#\\/]).+\\1[imsxuADSUXJ]*$/', $pattern) ? $pattern : '/' . $pattern . '/i'; + set_error_handler(function () { + }); + $aVal = trim((string) $a->rx_exchange); + $bVal = trim((string) $b->rx_exchange); + $aMatch = @preg_match($delimited, $aVal) === 1; + $bMatch = @preg_match($delimited, $bVal) === 1; + restore_error_handler(); + + if (! ($aMatch && $bMatch)) { + return false; + } + + return $aVal !== '' && $aVal === $bVal; + } + + protected function applyForcedStatus( + string $status, + bool &$isValid, + bool &$isDuplicate, + bool &$isNil, + bool &$isBustedCall, + bool &$isBustedRst, + bool &$isBustedExchange, + bool &$isOutOfWindow, + ?string &$errorCode, + ?string &$errorSide + ): void { + $isValid = false; + $isDuplicate = false; + $isNil = false; + $isBustedCall = false; + $isBustedRst = false; + $isBustedExchange = false; + $isOutOfWindow = false; + $errorCode = null; + $errorSide = 'NONE'; + + switch ($status) { + case 'VALID': + $isValid = true; + $errorCode = QsoErrorCode::OK; + break; + case 'INVALID': + $errorCode = QsoErrorCode::NO_COUNTERPART_LOG; + break; + case 'NIL': + $isNil = true; + $errorCode = QsoErrorCode::NO_COUNTERPART_LOG; + break; + case 'DUPLICATE': + $isDuplicate = true; + $errorCode = QsoErrorCode::DUP; + break; + case 'BUSTED_CALL': + $isBustedCall = true; + $errorCode = QsoErrorCode::BUSTED_CALL; + $errorSide = 'RX'; + break; + case 'BUSTED_EXCHANGE': + $isBustedExchange = true; + $errorCode = QsoErrorCode::BUSTED_SERIAL; + $errorSide = 'RX'; + break; + case 'OUT_OF_WINDOW': + $isOutOfWindow = true; + $errorCode = null; + break; + } + } +} diff --git a/app/Jobs/ParseLogJob.php b/app/Jobs/ParseLogJob.php new file mode 100644 index 0000000..d513923 --- /dev/null +++ b/app/Jobs/ParseLogJob.php @@ -0,0 +1,120 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + $lock = null; + $lockKey = "evaluation:parse:round:{$run->round_id}:log:{$this->logId}"; + try { + $lock = EvaluationLock::acquire( + key: $lockKey, + run: $run, + ttl: 1800 + ); + + if (! $lock) { + throw new \RuntimeException("ParseLogJob nelze spustit – lock je držen (log_id={$this->logId})."); + } + + $log = Log::with('file')->find($this->logId); + if (! $log || (int) $log->round_id !== (int) $run->round_id) { + return; + } + + if (! $log->file || ! $log->file->path) { + $coordinator->eventWarn($run, "Log #{$log->id} nemá soubor, parser přeskočen.", [ + 'step' => 'parse_logs', + 'round_id' => $run->round_id, + 'log_id' => $log->id, + ]); + EvaluationRun::where('id', $run->id)->increment('progress_done'); + return; + } + + try { + LogController::parseUploadedFile($log, $log->file->path); + } catch (Throwable $e) { + $coordinator->eventError($run, "Chyba parsování logu #{$log->id}: {$e->getMessage()}", [ + 'step' => 'parse_logs', + 'round_id' => $run->round_id, + 'log_id' => $log->id, + ]); + } + + if ($run->rules_version === 'CLAIMED') { + try { + UpsertClaimedLogResultJob::dispatchSync($log->id); + } catch (Throwable $e) { + $coordinator->eventError($run, "Chyba deklarace výsledků logu #{$log->id}: {$e->getMessage()}", [ + 'step' => 'claimed_upsert', + 'round_id' => $run->round_id, + 'log_id' => $log->id, + ]); + } + } + + EvaluationRun::where('id', $run->id)->increment('progress_done'); + $progressTotal = (int) ($run->progress_total ?? 0); + $done = $progressTotal > 0 + ? (int) EvaluationRun::where('id', $run->id)->value('progress_done') + : 0; + if ($progressTotal > 0 && $done % 10 === 0) { + $coordinator->eventInfo($run, "Parsování logů: {$done}/{$progressTotal}", [ + 'step' => 'parse_logs', + 'round_id' => $run->round_id, + 'step_progress_done' => $done, + 'step_progress_total' => $progressTotal, + ]); + } + } catch (Throwable $e) { + $coordinator->eventError($run, "ParseLogJob selhal: {$e->getMessage()}", [ + 'step' => 'parse_logs', + 'round_id' => $run->round_id, + 'log_id' => $this->logId, + ]); + throw $e; + } finally { + if ($lock) { + EvaluationLock::release($lockKey, $run); + } + } + } +} diff --git a/app/Jobs/PauseEvaluationRunJob.php b/app/Jobs/PauseEvaluationRunJob.php new file mode 100644 index 0000000..7df1a3b --- /dev/null +++ b/app/Jobs/PauseEvaluationRunJob.php @@ -0,0 +1,56 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + + $coordinator = app(EvaluationCoordinator::class); + try { + $coordinator->eventInfo($run, 'Pause: krok spuštěn.', [ + 'step' => $this->step, + 'round_id' => $run->round_id, + ]); + $coordinator->transition($run, '*', $this->status, $this->step); + + // WAITING_* stavy umožňují manuální zásah rozhodčího mezi fázemi pipeline. + $coordinator->eventInfo($run, $this->message, [ + 'step' => $this->step, + 'round_id' => $run->round_id, + ]); + $coordinator->eventInfo($run, 'Pause: krok dokončen.', [ + 'step' => $this->step, + 'round_id' => $run->round_id, + ]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Pause: krok selhal.', [ + 'step' => $this->step, + 'round_id' => $run->round_id, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } +} diff --git a/app/Jobs/PrepareRunJob.php b/app/Jobs/PrepareRunJob.php new file mode 100644 index 0000000..f4d46aa --- /dev/null +++ b/app/Jobs/PrepareRunJob.php @@ -0,0 +1,379 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + $coordinator->eventInfo($run, 'Prepare: krok spuštěn.', [ + 'step' => 'prepare', + 'round_id' => $run->round_id, + ]); + + try { + $lockKey = "evaluation:round:{$run->round_id}"; + $existingLock = EvaluationLock::where('key', $lockKey)->first(); + if ($existingLock && (int) $existingLock->evaluation_run_id !== (int) $run->id) { + $run->update([ + 'status' => 'FAILED', + 'error' => 'Nelze spustit vyhodnocení: probíhá jiný běh pro stejné kolo.', + ]); + $coordinator->eventError($run, 'PrepareRunJob selhal: lock je držen jiným během.', [ + 'step' => 'prepare', + 'round_id' => $run->round_id, + ]); + return; + } + + if (! $existingLock) { + EvaluationLock::acquire( + key: $lockKey, + run: $run, + ttl: 7200 + ); + } + + $run->update([ + 'status' => 'RUNNING', + 'current_step' => 'prepare', + 'started_at' => $run->started_at ?? now(), + ]); + + // Idempotence: vyčisti staging data pro tento run a připrav čistý start. + QsoResult::where('evaluation_run_id', $run->id)->delete(); + LogResult::where('evaluation_run_id', $run->id)->delete(); + WorkingQso::where('evaluation_run_id', $run->id)->delete(); + + $round = Round::with(['bands', 'categories', 'powerCategories'])->find($run->round_id); + if (! $round) { + $run->update([ + 'status' => 'FAILED', + 'error' => 'Kolo nebylo nalezeno.', + ]); + $coordinator->eventError($run, 'PrepareRunJob selhal: kolo nebylo nalezeno.', [ + 'step' => 'prepare', + 'round_id' => $run->round_id, + ]); + return; + } + + // Scope určuje kombinace skupin (band/category/power), které se budou hodnotit. + $scope = $run->scope ?? []; + $bandIds = $scope['band_ids'] ?? $round->bands->pluck('id')->all(); + $categoryIds = $scope['category_ids'] ?? $round->categories->pluck('id')->all(); + $powerCategoryIds = $scope['power_category_ids'] ?? $round->powerCategories->pluck('id')->all(); + + $bandIds = $bandIds ?: [null]; + $categoryIds = $categoryIds ?: [null]; + $powerCategoryIds = $powerCategoryIds ?: [null]; + + $groups = []; + foreach ($bandIds as $bandId) { + foreach ($categoryIds as $categoryId) { + foreach ($powerCategoryIds as $powerCategoryId) { + $groups[] = [ + 'key' => 'b' . ($bandId ?? 0) . '-c' . ($categoryId ?? 0) . '-p' . ($powerCategoryId ?? 0), + 'band_id' => $bandId, + 'category_id' => $categoryId, + 'power_category_id' => $powerCategoryId, + ]; + } + } + } + + $scope['band_ids'] = array_values(array_filter($bandIds)); + $scope['category_ids'] = array_values(array_filter($categoryIds)); + $scope['power_category_ids'] = array_values(array_filter($powerCategoryIds)); + $scope['groups'] = $groups; + + $run->update([ + 'scope' => $scope, + 'progress_total' => count($groups), + 'progress_done' => 0, + ]); + + $logsQuery = Log::where('round_id', $run->round_id); + $logOverrides = LogOverride::where('evaluation_run_id', $run->id)->get()->keyBy('log_id'); + + // Skeleton log_results umožní pozdější agregaci a ranking bez podmíněného "create". + $logsQuery->chunkById(200, function ($logs) use ($run, $round, $logOverrides) { + foreach ($logs as $log) { + $override = $logOverrides->get($log->id); + $bandId = $override && $override->forced_band_id + ? (int) $override->forced_band_id + : $this->resolveBandId($log, $round); + $categoryId = $override && $override->forced_category_id + ? (int) $override->forced_category_id + : $this->resolveCategoryId($log, $round); + $powerCategoryId = $override && $override->forced_power_category_id + ? (int) $override->forced_power_category_id + : $log->power_category_id; + $sixhrCategory = $override && $override->forced_sixhr_category !== null + ? (bool) $override->forced_sixhr_category + : $log->sixhr_category; + if ($sixhrCategory && ! $this->isSixHourBand($bandId)) { + $this->addSixHourRemark($log); + } + + LogResult::updateOrCreate( + [ + 'evaluation_run_id' => $run->id, + 'log_id' => $log->id, + ], + [ + 'status' => 'OK', + 'band_id' => $bandId, + 'category_id' => $categoryId, + 'power_category_id' => $powerCategoryId, + 'sixhr_category' => $sixhrCategory, + 'claimed_qso_count' => $log->claimed_qso_count, + 'claimed_score' => $log->claimed_score, + ] + ); + } + }); + + $coordinator->eventInfo($run, 'Příprava vyhodnocení.', [ + 'step' => 'prepare', + 'round_id' => $run->round_id, + 'groups_total' => count($groups), + 'step_progress_done' => 1, + 'step_progress_total' => 1, + ]); + $coordinator->eventInfo($run, 'Prepare: krok dokončen.', [ + 'step' => 'prepare', + 'round_id' => $run->round_id, + ]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Prepare: krok selhal.', [ + 'step' => 'prepare', + 'round_id' => $run->round_id, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } + + protected function resolveCategoryId(Log $log, Round $round): ?int + { + $value = $log->psect; + if (! $value) { + return null; + } + + $ediCat = EdiCategory::whereRaw('LOWER(value) = ?', [mb_strtolower(trim($value))])->first(); + if (! $ediCat) { + $ediCat = $this->matchEdiCategoryByRegex($value); + } + if (! $ediCat) { + return null; + } + + $mappedCategoryId = $ediCat->categories()->value('categories.id'); + if (! $mappedCategoryId) { + return null; + } + + if ($round->categories()->count() === 0) { + return $mappedCategoryId; + } + + return $round->categories()->where('categories.id', $mappedCategoryId)->exists() + ? $mappedCategoryId + : null; + } + + protected function isSixHourBand(?int $bandId): bool + { + if (! $bandId) { + return false; + } + return in_array($bandId, [1, 2], true); + } + + protected function addSixHourRemark(Log $log): void + { + $remarksEval = $this->decodeRemarksEval($log->remarks_eval); + $message = '6H kategorie je povolena jen pro pásma 145 MHz a 435 MHz.'; + if (! in_array($message, $remarksEval, true)) { + $remarksEval[] = $message; + $log->remarks_eval = $this->encodeRemarksEval($remarksEval); + $log->save(); + } + } + + protected function decodeRemarksEval(?string $value): array + { + if (! $value) { + return []; + } + $decoded = json_decode($value, true); + return is_array($decoded) ? $decoded : []; + } + + protected function encodeRemarksEval(array $value): ?string + { + $filtered = array_values(array_filter($value, fn ($item) => is_string($item) && trim($item) !== '')); + $filtered = array_values(array_unique($filtered)); + if (count($filtered) === 0) { + return null; + } + return json_encode($filtered, JSON_UNESCAPED_UNICODE); + } + + protected function matchEdiCategoryByRegex(string $value): ?EdiCategory + { + $candidates = EdiCategory::whereNotNull('regex_pattern')->get(); + foreach ($candidates as $candidate) { + $pattern = $candidate->regex_pattern; + if (! $pattern) { + continue; + } + + $delimited = preg_match('/^([#\\/]).+\\1[imsxuADSUXJ]*$/', $pattern) ? $pattern : '/' . $pattern . '/i'; + set_error_handler(function () { + }); + $matched = @preg_match($delimited, $value) === 1; + restore_error_handler(); + + if ($matched) { + return $candidate; + } + } + + return null; + } + + protected function resolveBandId(Log $log, Round $round): ?int + { + if (! $log->pband) { + return null; + } + + $pbandVal = mb_strtolower(trim($log->pband)); + $ediBand = EdiBand::whereRaw('LOWER(value) = ?', [$pbandVal])->first(); + if ($ediBand) { + $mappedBandId = $ediBand->bands()->value('bands.id'); + if (! $mappedBandId) { + return null; + } + if ($round->bands()->count() === 0) { + return $mappedBandId; + } + return $round->bands()->where('bands.id', $mappedBandId)->exists() + ? $mappedBandId + : null; + } + + $num = is_numeric($pbandVal) ? (float) $pbandVal : null; + if ($num === null && $log->pband) { + if (preg_match('/([0-9]+(?:[\\.,][0-9]+)?)/', $log->pband, $m)) { + $num = (float) str_replace(',', '.', $m[1]); + } + } + if ($num === null) { + return null; + } + + $bandMatch = Band::where('edi_band_begin', '<=', $num) + ->where('edi_band_end', '>=', $num) + ->first(); + if (! $bandMatch) { + return null; + } + + if ($round->bands()->count() === 0) { + return $bandMatch->id; + } + + return $round->bands()->where('bands.id', $bandMatch->id)->exists() + ? $bandMatch->id + : null; + } +} diff --git a/app/Jobs/RebuildClaimedLogResultsJob.php b/app/Jobs/RebuildClaimedLogResultsJob.php new file mode 100644 index 0000000..8b53547 --- /dev/null +++ b/app/Jobs/RebuildClaimedLogResultsJob.php @@ -0,0 +1,77 @@ +evaluationRunId); + if (! $run) { + return; + } + + $lock = EvaluationLock::acquire( + key: "evaluation:claimed-rebuild:round:{$run->round_id}", + run: $run, + ttl: 1800 + ); + + if (! $lock) { + return; + } + + try { + // Rebuild je deterministická rekonstrukce claimed projekce pro celé kolo. + $total = Log::where('round_id', $run->round_id)->count(); + $run->update([ + 'status' => 'RUNNING', + 'current_step' => 'rebuild_claimed', + 'progress_total' => $total, + 'progress_done' => 0, + 'started_at' => $run->started_at ?? now(), + ]); + + $processed = 0; + Log::where('round_id', $run->round_id) + ->chunkById(50, function ($logs) use (&$processed, $run) { + foreach ($logs as $log) { + $processed++; + // Projekce claimed výsledků je synchronní, aby rebuild skončil konzistentně. + UpsertClaimedLogResultJob::dispatchSync($log->id); + $run->update(['progress_done' => $processed]); + } + }); + + // Po projekci je nutné přepočítat pořadí claimed scoreboardu. + RecalculateClaimedRanksJob::dispatchSync($run->id); + + $run->update([ + 'status' => 'SUCCEEDED', + 'current_step' => 'rebuild_claimed_done', + 'finished_at' => now(), + ]); + } finally { + EvaluationLock::release("evaluation:claimed-rebuild:round:{$run->round_id}", $run); + } + } +} diff --git a/app/Jobs/RecalculateClaimedRanksJob.php b/app/Jobs/RecalculateClaimedRanksJob.php new file mode 100644 index 0000000..3bd7662 --- /dev/null +++ b/app/Jobs/RecalculateClaimedRanksJob.php @@ -0,0 +1,212 @@ +evaluationRunId; + } + + public function handle(): void + { + $run = EvaluationRun::find($this->evaluationRunId); + if (! $run) { + return; + } + + // Zabraňuje souběžnému přepočtu pro stejné kolo (claimed scoreboard). + $lock = EvaluationLock::acquire( + key: "evaluation:claimed-ranks:round:{$run->round_id}", + run: $run, + ttl: 300 + ); + if (! $lock) { + return; + } + + try { + // Vynuluje pořadí, aby staré hodnoty neovlivnily nový přepočet. + LogResult::where('evaluation_run_id', $run->id) + ->update([ + 'rank_overall' => null, + 'rank_in_category' => null, + 'rank_overall_ok' => null, + 'rank_in_category_ok' => null, + ]); + + // Načte všechny deklarované výsledky včetně vazeb pro kategorii a výkon. + $results = LogResult::with(['log', 'category', 'powerCategory']) + ->where('evaluation_run_id', $run->id) + ->get(); + + // Do pořadí vstupují jen logy se statusem OK a kategorií SINGLE/MULTI. + $eligible = $results->filter(function (LogResult $r) { + return $r->status === 'OK' && $this->getCategoryType($r) !== null; + }); + + // Celkové pořadí: podle pásma + SINGLE/MULTI + 6H/standard. + $allOverall = $eligible->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getSixHourBucket($r)); + foreach ($allOverall as $items) { + $this->applyRanking($items, 'rank_overall'); + } + + // Pořadí výkonových kategorií: pásmo + SINGLE/MULTI + výkon (jen LP/QRP/N) + 6H/standard. + $allPower = $eligible->filter(fn (LogResult $r) => $this->getPowerClass($r) !== null) + ->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getPowerClass($r) . '|' . $this->getSixHourBucket($r)); + foreach ($allPower as $items) { + $this->applyRanking($items, 'rank_in_category'); + } + + // Česká podmnožina (OK/OL) pro národní pořadí. + $okOnly = $eligible->filter(fn (LogResult $r) => $this->isOkCall($r)); + $okOverall = $okOnly->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getSixHourBucket($r)); + foreach ($okOverall as $items) { + $this->applyRanking($items, 'rank_overall_ok'); + } + + // České pořadí výkonových kategorií: stejné jako power, ale jen OK/OL a 6H/standard. + $okPower = $okOnly->filter(fn (LogResult $r) => $this->getPowerClass($r) !== null) + ->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getPowerClass($r) . '|' . $this->getSixHourBucket($r)); + foreach ($okPower as $items) { + $this->applyRanking($items, 'rank_in_category_ok'); + } + + } finally { + EvaluationLock::release("evaluation:claimed-ranks:round:{$run->round_id}", $run); + } + } + + protected function applyRanking(Collection $items, string $rankField): void + { + // Řazení podle claimed_score (desc), pak QSO (desc), pak log_id (asc) kvůli stabilitě. + $sorted = $items->sort(function (LogResult $a, LogResult $b) { + $scoreA = $a->claimed_score ?? 0; + $scoreB = $b->claimed_score ?? 0; + if ($scoreA !== $scoreB) { + return $scoreB <=> $scoreA; + } + $qsoA = $a->claimed_qso_count ?? 0; + $qsoB = $b->claimed_qso_count ?? 0; + if ($qsoA !== $qsoB) { + return $qsoB <=> $qsoA; + } + return $a->log_id <=> $b->log_id; + })->values(); + + $lastScore = null; + $lastQso = null; + $lastRank = 0; + + foreach ($sorted as $index => $result) { + $score = $result->claimed_score ?? 0; + $qso = $result->claimed_qso_count ?? 0; + + // Shodný výsledek (stejné skóre + QSO) = stejné pořadí. + if ($score === $lastScore && $qso === $lastQso) { + $rank = $lastRank; + } else { + $rank = $index + 1; + } + + $result->{$rankField} = $rank; + $result->save(); + + $lastScore = $score; + $lastQso = $qso; + $lastRank = $rank; + } + } + + protected function getCategoryType(LogResult $r): ?string + { + $name = $r->category?->name; + if (! $name) { + return null; + } + $lower = mb_strtolower($name); + if (str_contains($lower, 'single')) { + return 'SINGLE'; + } + if (str_contains($lower, 'multi')) { + return 'MULTI'; + } + return null; + } + + protected function getCategoryBucket(LogResult $r): ?string + { + $type = $this->getCategoryType($r); + if ($type === null) { + return null; + } + return $this->getSixHourBucket($r) === '6H' ? 'ALL' : $type; + } + + protected function getPowerClass(LogResult $r): ?string + { + $name = $r->powerCategory?->name; + if (! $name) { + return null; + } + $upper = mb_strtoupper($name); + if (in_array($upper, ['LP', 'QRP', 'N'], true)) { + return $upper; + } + return null; + } + + protected function isOkCall(LogResult $r): bool + { + $call = $this->normalizeCallsign($r->log?->pcall ?? ''); + return Str::startsWith($call, ['OK', 'OL']); + } + + protected function normalizeCallsign(string $call): string + { + $value = mb_strtoupper(trim($call)); + $value = preg_replace('/\s+/', '', $value); + return $value ?? ''; + } + + protected function getSixHourBucket(LogResult $r): string + { + $sixh = $r->sixhr_category; + if ($sixh === null) { + $sixh = $r->log?->sixhr_category; + } + return $sixh ? '6H' : 'STD'; + } + + +} diff --git a/app/Jobs/RecalculateOfficialRanksJob.php b/app/Jobs/RecalculateOfficialRanksJob.php new file mode 100644 index 0000000..20efbd8 --- /dev/null +++ b/app/Jobs/RecalculateOfficialRanksJob.php @@ -0,0 +1,224 @@ +evaluationRunId; + } + + public function handle(): void + { + $run = EvaluationRun::find($this->evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $ruleSet = EvaluationRuleSet::find($run->rule_set_id); + if ($ruleSet && $ruleSet->sixhr_ranking_mode) { + $this->sixhrRankingMode = strtoupper((string) $ruleSet->sixhr_ranking_mode); + } + + // Krátký lock brání souběžnému přepočtu pořadí nad stejným kolem. + $lock = EvaluationLock::acquire( + key: "evaluation:official-ranks:round:{$run->round_id}", + run: $run, + ttl: 300 + ); + if (! $lock) { + return; + } + + try { + LogResult::where('evaluation_run_id', $run->id) + ->update([ + 'rank_overall' => null, + 'rank_in_category' => null, + 'rank_overall_ok' => null, + 'rank_in_category_ok' => null, + ]); + + $results = LogResult::with(['log', 'category', 'powerCategory']) + ->where('evaluation_run_id', $run->id) + ->get(); + + // Do pořadí jdou jen logy ve stavu OK a s rozpoznanou kategorií. + $eligible = $results->filter(function (LogResult $r) { + return $r->status === 'OK' && $this->getCategoryType($r) !== null; + }); + + $allOverall = $eligible->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getSixHourBucket($r)); + foreach ($allOverall as $items) { + if (EvaluationRun::isCanceledRun($run->id)) { + return; + } + $this->applyRanking($items, 'rank_overall'); + } + + $allPower = $eligible->filter(fn (LogResult $r) => $this->getPowerClass($r) !== null) + ->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getPowerClass($r) . '|' . $this->getSixHourBucket($r)); + foreach ($allPower as $items) { + if (EvaluationRun::isCanceledRun($run->id)) { + return; + } + $this->applyRanking($items, 'rank_in_category'); + } + + $okOnly = $eligible->filter(fn (LogResult $r) => $this->isOkCall($r)); + $okOverall = $okOnly->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getSixHourBucket($r)); + foreach ($okOverall as $items) { + if (EvaluationRun::isCanceledRun($run->id)) { + return; + } + $this->applyRanking($items, 'rank_overall_ok'); + } + + $okPower = $okOnly->filter(fn (LogResult $r) => $this->getPowerClass($r) !== null) + ->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getPowerClass($r) . '|' . $this->getSixHourBucket($r)); + foreach ($okPower as $items) { + if (EvaluationRun::isCanceledRun($run->id)) { + return; + } + $this->applyRanking($items, 'rank_in_category_ok'); + } + } finally { + EvaluationLock::release("evaluation:official-ranks:round:{$run->round_id}", $run); + } + } + + protected function applyRanking(Collection $items, string $rankField): void + { + // Deterministický sort: skóre -> valid QSO -> log_id. + $sorted = $items->sort(function (LogResult $a, LogResult $b) { + $scoreA = $a->official_score ?? 0; + $scoreB = $b->official_score ?? 0; + if ($scoreA !== $scoreB) { + return $scoreB <=> $scoreA; + } + $qsoA = $a->valid_qso_count ?? 0; + $qsoB = $b->valid_qso_count ?? 0; + if ($qsoA !== $qsoB) { + return $qsoB <=> $qsoA; + } + return $a->log_id <=> $b->log_id; + })->values(); + + $lastScore = null; + $lastQso = null; + $lastRank = 0; + + foreach ($sorted as $index => $result) { + $score = $result->official_score ?? 0; + $qso = $result->valid_qso_count ?? 0; + + if ($score === $lastScore && $qso === $lastQso) { + $rank = $lastRank; + } else { + $rank = $index + 1; + } + + $result->{$rankField} = $rank; + $result->sixhr_ranking_bucket = $this->getCategoryBucket($result); + $result->save(); + + $lastScore = $score; + $lastQso = $qso; + $lastRank = $rank; + } + } + + protected function getCategoryType(LogResult $r): ?string + { + $name = $r->category?->name; + if (! $name) { + return null; + } + $lower = mb_strtolower($name); + if (str_contains($lower, 'single')) { + return 'SINGLE'; + } + if (str_contains($lower, 'multi')) { + return 'MULTI'; + } + return null; + } + + protected function getCategoryBucket(LogResult $r): ?string + { + $type = $this->getCategoryType($r); + if ($type === null) { + return null; + } + if ($this->getSixHourBucket($r) !== '6H') { + return $type; + } + return $this->sixhrRankingMode === 'IARU' ? 'ALL' : $type; + } + + protected function getPowerClass(LogResult $r): ?string + { + $name = $r->powerCategory?->name; + if (! $name) { + return null; + } + $upper = mb_strtoupper($name); + if (in_array($upper, ['LP', 'QRP', 'N'], true)) { + return $upper; + } + return null; + } + + protected function isOkCall(LogResult $r): bool + { + $call = $this->normalizeCallsign($r->log?->pcall ?? ''); + return Str::startsWith($call, ['OK', 'OL']); + } + + protected function normalizeCallsign(string $call): string + { + $value = mb_strtoupper(trim($call)); + $value = preg_replace('/\s+/', '', $value); + return $value ?? ''; + } + + protected function getSixHourBucket(LogResult $r): string + { + $sixh = $r->sixhr_category; + if ($sixh === null) { + $sixh = $r->log?->sixhr_category; + } + return $sixh ? '6H' : 'STD'; + } +} diff --git a/app/Jobs/ScoreGroupJob.php b/app/Jobs/ScoreGroupJob.php new file mode 100644 index 0000000..83a42de --- /dev/null +++ b/app/Jobs/ScoreGroupJob.php @@ -0,0 +1,808 @@ + stejné body. + * - Job musí být idempotentní: opakované spuštění přepíše/obnoví výstupy + * pro daný run+group bez duplicit. + * - Veškerá výpočetní logika patří do service layer (např. ScoringService). + * - Job pouze načte kontext, deleguje výpočty a uloží výsledky. + * + * Queue: + * - Spouští se ve frontě "evaluation". + * - Musí být chráněn proti souběhu nad stejnou group (lock / WithoutOverlapping). + */ +class ScoreGroupJob implements ShouldQueue +{ + use Batchable; + use Queueable; + + public int $tries = 3; + public array $backoff = [30, 120, 300]; + + protected array $ctyCache = []; + + /** + * Create a new job instance. + */ + public function __construct( + protected int $evaluationRunId, + protected ?string $groupKey = null, + protected ?array $group = null + ) { + // + } + + /** + * Provede výpočet bodů pro jednu skupinu (group). + * + * Metoda handle(): + * - získá kontext EvaluationRun + group parametry + * - načte mezivýsledky matchingu + * - aplikuje pravidla EvaluationRuleSet a spočítá body + * - zapíše mezivýsledky pro agregaci a finalizaci + * - aktualizuje progress a auditní události pro UI + * + * Poznámky: + * - Tento job má být výkonnostně bezpečný (chunking, minimalizace N+1). + * - Pokud scoring jedné skupiny selže, má selhat job (retry), + * protože bez kompletního scoringu nelze korektně agregovat výsledky. + */ + public function handle(): void + { + $run = EvaluationRun::find($this->evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + try { + $ruleSet = EvaluationRuleSet::find($run->rule_set_id); + if (! $ruleSet) { + $coordinator->eventError($run, 'Scoring nelze spustit: chybí ruleset.', [ + 'step' => 'score', + 'round_id' => $run->round_id, + ]); + return; + } + + $round = Round::with(['bands', 'categories', 'powerCategories'])->find($run->round_id); + if (! $round) { + return; + } + + $coordinator->eventInfo($run, 'Scoring: krok spuštěn.', [ + 'step' => 'score', + 'round_id' => $run->round_id, + 'group_key' => $this->groupKey, + ]); + + $run->update([ + 'status' => 'RUNNING', + 'current_step' => 'score', + ]); + $groups = []; + $singleGroup = (bool) ($this->groupKey || $this->group); + if ($this->groupKey || $this->group) { + $groups[] = [ + 'key' => $this->groupKey ?? 'custom', + 'band_id' => $this->group['band_id'] ?? null, + 'category_id' => $this->group['category_id'] ?? null, + 'power_category_id' => $this->group['power_category_id'] ?? null, + ]; + } elseif (! empty($run->scope['groups']) && is_array($run->scope['groups'])) { + $groups = $run->scope['groups']; + } else { + $groups[] = [ + 'key' => 'all', + 'band_id' => null, + 'category_id' => null, + 'power_category_id' => null, + ]; + } + + $total = count($groups); + if (! $singleGroup) { + $run->update([ + 'progress_total' => $total, + 'progress_done' => 0, + ]); + } + + $scoring = new ScoringService(); + $logOverrides = LogOverride::where('evaluation_run_id', $run->id)->get()->keyBy('log_id'); + $qsoOverrides = QsoOverride::where('evaluation_run_id', $run->id)->get()->keyBy('log_qso_id'); + $groupLogIds = $this->groupLogsByKey($round, $ruleSet, $logOverrides); + + $processed = 0; + foreach ($groups as $group) { + if (EvaluationRun::isCanceledRun($run->id)) { + return; + } + $processed++; + $groupKey = $group['key'] ?? 'all'; + $logIds = $groupLogIds[$groupKey] ?? []; + + $coordinator->eventInfo($run, 'Výpočet skóre.', [ + 'step' => 'score', + 'round_id' => $run->round_id, + 'group_key' => $group['key'] ?? null, + 'group' => [ + 'band_id' => $group['band_id'] ?? null, + 'category_id' => $group['category_id'] ?? null, + 'power_category_id' => $group['power_category_id'] ?? null, + ], + 'group_logs' => count($logIds), + 'step_progress_done' => $processed, + 'step_progress_total' => $total, + ]); + + if (! $logIds) { + if ($singleGroup) { + EvaluationRun::where('id', $run->id)->increment('progress_done'); + } else { + $run->update(['progress_done' => $processed]); + } + continue; + } + + LogQso::whereIn('log_id', $logIds) + ->chunkById(200, function ($qsos) use ($run, $ruleSet, $scoring, $qsoOverrides, $coordinator) { + $qsoIds = $qsos->pluck('id')->all(); + $working = WorkingQso::where('evaluation_run_id', $run->id) + ->whereIn('log_qso_id', $qsoIds) + ->get() + ->keyBy('log_qso_id'); + + foreach ($qsos as $qso) { + $result = QsoResult::firstOrNew([ + 'evaluation_run_id' => $run->id, + 'log_qso_id' => $qso->id, + ]); + + // Ruční override může přepsat matching/validaci z předchozího kroku. + $override = $qsoOverrides->get($qso->id); + if ($override) { + $this->applyQsoOverride($result, $override); + } + + $workingQso = $working->get($qso->id); + // Vzdálenost se počítá z lokátorů obou stran (pokud existují). + $distanceKm = $workingQso + ? $scoring->calculateDistanceKm($workingQso->loc_norm, $workingQso->rloc_norm) + : null; + + // V některých soutěžích jsou lokátory povinné pro platné bodování. + $requireLocators = $ruleSet->require_locators; + $hasLocators = $workingQso && $workingQso->loc_norm && $workingQso->rloc_norm; + + $result->distance_km = $distanceKm; + $points = $scoring->computeBasePoints($distanceKm, $ruleSet); + $forcedStatus = $override?->forced_status; + $applyPolicy = ! $forcedStatus || $forcedStatus === 'AUTO'; + + if ($applyPolicy) { + $result->is_valid = true; + } + $result->penalty_points = 0; + + if ($applyPolicy && $requireLocators && ! $hasLocators) { + $result->is_valid = false; + } + + // Out-of-window policy určuje, jak bodovat QSO mimo časové okno. + if ($applyPolicy && $result->is_time_out_of_window) { + $policy = $scoring->outOfWindowDecision($ruleSet); + $decision = $this->applyPolicyDecision($policy, $points, false); + if ($result->is_valid) { + $result->is_valid = $decision['is_valid']; + } + $points = $decision['points']; + } + + $result->error_code = $this->resolveErrorCode($result); + $errorCode = $result->error_code; + $errorSide = $result->error_side ?? 'NONE'; + + if ($applyPolicy) { + if ($errorCode && ! in_array($errorCode, QsoErrorCode::all(), true)) { + $coordinator->eventWarn($run, 'Scoring: neznámý error_code.', [ + 'step' => 'score', + 'round_id' => $run->round_id, + 'log_qso_id' => $qso->id, + 'error_code' => $errorCode, + ]); + $result->is_valid = false; + } else { + $points = $this->applyErrorPolicy($ruleSet, $errorCode, $errorSide, $points, $result); + } + } + + $result->points = $points; + $result->penalty_points = $result->is_valid + ? $this->resolvePenaltyPoints($result, $ruleSet, $scoring) + : 0; + // Multiplikátory se ukládají per-QSO a agregují až v AggregateLogResultsJob. + $this->applyMultipliers($result, $qso, $workingQso, $ruleSet); + if ($override && $override->forced_points !== null) { + // Ruční override má přednost před vypočtenými body. + $result->points = (float) $override->forced_points; + } + $result->save(); + } + }); + + if ($singleGroup) { + EvaluationRun::where('id', $run->id)->increment('progress_done'); + } else { + $run->update(['progress_done' => $processed]); + } + } + $coordinator->eventInfo($run, 'Scoring: krok dokončen.', [ + 'step' => 'score', + 'round_id' => $run->round_id, + 'group_key' => $this->groupKey, + ]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Scoring: krok selhal.', [ + 'step' => 'score', + 'round_id' => $run->round_id, + 'group_key' => $this->groupKey, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } + + protected function groupLogsByKey( + Round $round, + EvaluationRuleSet $ruleSet, + \Illuminate\Support\Collection $logOverrides + ): array + { + $logs = Log::where('round_id', $round->id)->get(); + $map = []; + + foreach ($logs as $log) { + if (! $ruleSet->checklog_matching && $this->isCheckLog($log)) { + continue; + } + $override = $logOverrides->get($log->id); + if ($override && $override->forced_log_status === 'IGNORED') { + continue; + } + + $bandId = $override && $override->forced_band_id + ? (int) $override->forced_band_id + : $this->resolveBandId($log, $round); + $categoryId = $override && $override->forced_category_id + ? (int) $override->forced_category_id + : $this->resolveCategoryId($log, $round); + $powerCategoryId = $override && $override->forced_power_category_id + ? (int) $override->forced_power_category_id + : $log->power_category_id; + $key = 'b' . ($bandId ?? 0) . '-c' . ($categoryId ?? 0) . '-p' . ($powerCategoryId ?? 0); + + $map[$key][] = $log->id; + } + + return $map; + } + + protected function resolveCategoryId(Log $log, Round $round): ?int + { + $value = $log->psect; + if (! $value) { + return null; + } + + $ediCat = EdiCategory::whereRaw('LOWER(value) = ?', [mb_strtolower(trim($value))])->first(); + if (! $ediCat) { + $ediCat = $this->matchEdiCategoryByRegex($value); + } + if (! $ediCat) { + return null; + } + + $mappedCategoryId = $ediCat->categories()->value('categories.id'); + if (! $mappedCategoryId) { + return null; + } + + if ($round->categories()->count() === 0) { + return $mappedCategoryId; + } + + return $round->categories()->where('categories.id', $mappedCategoryId)->exists() + ? $mappedCategoryId + : null; + } + + protected function matchEdiCategoryByRegex(string $value): ?EdiCategory + { + $candidates = EdiCategory::whereNotNull('regex_pattern')->get(); + foreach ($candidates as $candidate) { + $pattern = $candidate->regex_pattern; + if (! $pattern) { + continue; + } + + $delimited = preg_match('/^([#\\/]).+\\1[imsxuADSUXJ]*$/', $pattern) ? $pattern : '/' . $pattern . '/i'; + set_error_handler(function () { + }); + $matched = @preg_match($delimited, $value) === 1; + restore_error_handler(); + + if ($matched) { + return $candidate; + } + } + + return null; + } + + protected function isCheckLog(Log $log): bool + { + $psect = trim((string) $log->psect); + return $psect !== '' && preg_match('/\\bcheck\\b/i', $psect) === 1; + } + + protected function resolveBandId(Log $log, Round $round): ?int + { + if (! $log->pband) { + return null; + } + + $pbandVal = mb_strtolower(trim($log->pband)); + $ediBand = EdiBand::whereRaw('LOWER(value) = ?', [$pbandVal])->first(); + if ($ediBand) { + $mappedBandId = $ediBand->bands()->value('bands.id'); + if (! $mappedBandId) { + return null; + } + if ($round->bands()->count() === 0) { + return $mappedBandId; + } + return $round->bands()->where('bands.id', $mappedBandId)->exists() + ? $mappedBandId + : null; + } + + $num = is_numeric($pbandVal) ? (float) $pbandVal : null; + if ($num === null && $log->pband) { + if (preg_match('/([0-9]+(?:[\\.,][0-9]+)?)/', $log->pband, $m)) { + $num = (float) str_replace(',', '.', $m[1]); + } + } + if ($num === null) { + return null; + } + + $bandMatch = \App\Models\Band::where('edi_band_begin', '<=', $num) + ->where('edi_band_end', '>=', $num) + ->first(); + if (! $bandMatch) { + return null; + } + + if ($round->bands()->count() === 0) { + return $bandMatch->id; + } + + return $round->bands()->where('bands.id', $bandMatch->id)->exists() + ? $bandMatch->id + : null; + } + + protected function applyPolicyDecision(string $policy, int $points, bool $keepPointsOnPenalty): array + { + $policy = strtoupper(trim($policy)); + return match ($policy) { + 'INVALID' => ['is_valid' => false, 'points' => $points], + 'ZERO_POINTS' => ['is_valid' => true, 'points' => 0], + 'FLAG_ONLY' => ['is_valid' => true, 'points' => $points], + 'PENALTY' => ['is_valid' => true, 'points' => $keepPointsOnPenalty ? $points : 0], + default => ['is_valid' => true, 'points' => $points], + }; + } + + protected function resolvePolicyForError(EvaluationRuleSet $ruleSet, ?string $errorCode): ?string + { + return match ($errorCode) { + QsoErrorCode::DUP => $ruleSet->dup_qso_policy ?? 'ZERO_POINTS', + QsoErrorCode::NO_COUNTERPART_LOG => $ruleSet->getString( + 'no_counterpart_log_policy', + $ruleSet->no_counterpart_log_policy ?? null, + $ruleSet->nil_qso_policy ?? 'ZERO_POINTS' + ), + QsoErrorCode::NOT_IN_COUNTERPART_LOG => $ruleSet->getString( + 'not_in_counterpart_log_policy', + $ruleSet->not_in_counterpart_log_policy ?? null, + $ruleSet->nil_qso_policy ?? 'ZERO_POINTS' + ), + QsoErrorCode::UNIQUE => $ruleSet->getString( + 'unique_qso_policy', + $ruleSet->unique_qso_policy ?? null, + 'ZERO_POINTS' + ), + QsoErrorCode::BUSTED_CALL => $ruleSet->busted_call_policy ?? 'ZERO_POINTS', + QsoErrorCode::BUSTED_RST => $ruleSet->busted_rst_policy ?? 'ZERO_POINTS', + QsoErrorCode::BUSTED_SERIAL => $ruleSet->getString( + 'busted_serial_policy', + $ruleSet->busted_serial_policy ?? null, + $ruleSet->busted_exchange_policy ?? 'ZERO_POINTS' + ), + QsoErrorCode::BUSTED_LOCATOR => $ruleSet->getString( + 'busted_locator_policy', + $ruleSet->busted_locator_policy ?? null, + $ruleSet->busted_exchange_policy ?? 'ZERO_POINTS' + ), + QsoErrorCode::TIME_MISMATCH => $ruleSet->time_mismatch_policy ?? 'ZERO_POINTS', + default => null, + }; + } + + /** + * Scoring policy (error_code → policy → efekt). + * + * - INVALID → is_valid=false, points beze změny + * - ZERO_POINTS → is_valid=true, points=0 + * - FLAG_ONLY → is_valid=true, points beze změny + * - PENALTY → is_valid=true, points=0 (u BUSTED_RST body ponechány) + * + * Poznámka: is_valid se určuje až ve scoringu, není přebíráno z matchingu. + */ + protected function applyErrorPolicy( + EvaluationRuleSet $ruleSet, + ?string $errorCode, + string $errorSide, + int $points, + QsoResult $result + ): int { + if (! $errorCode || $errorCode === QsoErrorCode::OK) { + return $points; + } + + if (in_array($errorCode, [ + QsoErrorCode::BUSTED_CALL, + QsoErrorCode::BUSTED_RST, + QsoErrorCode::BUSTED_SERIAL, + QsoErrorCode::BUSTED_LOCATOR, + ], true) && $errorSide === 'TX') { + return $points; + } + + $policy = $this->resolvePolicyForError($ruleSet, $errorCode); + if (! $policy) { + return $points; + } + + $keepPointsOnPenalty = $errorCode === QsoErrorCode::BUSTED_RST; + $decision = $this->applyPolicyDecision($policy, $points, $keepPointsOnPenalty); + if ($result->is_valid) { + $result->is_valid = $decision['is_valid']; + } + return $decision['points']; + } + + protected function applyMultipliers( + QsoResult $result, + LogQso $qso, + ?WorkingQso $workingQso, + EvaluationRuleSet $ruleSet + ): void { + // Multiplikátor se ukládá do QSO a agreguje se až v AggregateLogResultsJob. + $result->wwl = null; + $result->dxcc = null; + $result->country = null; + $result->section = null; + + if (! $ruleSet->usesMultipliers()) { + return; + } + + if ($ruleSet->multiplier_type === 'WWL') { + $result->wwl = $this->formatWwlMultiplier($workingQso?->rloc_norm, $ruleSet); + return; + } + + if ($ruleSet->multiplier_type === 'SECTION') { + $result->section = $this->normalizeSection($qso->rx_exchange); + return; + } + + if (in_array($ruleSet->multiplier_type, ['DXCC', 'COUNTRY'], true)) { + // DXCC/COUNTRY se odvozují z protistanice přes CTY prefix mapu. + $call = $workingQso?->rcall_norm ?: $qso->dx_call; + $cty = $this->resolveCtyForCall($call); + if ($cty) { + if ($ruleSet->multiplier_type === 'DXCC' && $cty->dxcc) { + $result->dxcc = (string) $cty->dxcc; + } + if ($ruleSet->multiplier_type === 'COUNTRY') { + $result->country = $cty->country_name; + } + } + } + } + + protected function normalizeSection(?string $value): ?string + { + $value = trim((string) $value); + if ($value === '') { + return null; + } + + $value = strtoupper(preg_replace('/\s+/', '', $value) ?? ''); + return $value !== '' ? substr($value, 0, 50) : null; + } + + protected function resolveCtyForCall(?string $call): ?Cty + { + $call = strtoupper(trim((string) $call)); + if ($call === '') { + return null; + } + + if (array_key_exists($call, $this->ctyCache)) { + return $this->ctyCache[$call]; + } + + // Nejprve zkus přesný match (precise=true), potom nejdelší prefix. + $precise = Cty::where('prefix_norm', $call) + ->where('precise', true) + ->first(); + if ($precise) { + $this->ctyCache[$call] = $precise; + return $precise; + } + + $prefixes = []; + $len = strlen($call); + for ($i = $len; $i >= 1; $i--) { + $prefixes[] = substr($call, 0, $i); + } + + $match = Cty::whereIn('prefix_norm', $prefixes) + ->where('precise', false) + ->orderByRaw('LENGTH(prefix_norm) DESC') + ->first(); + + $this->ctyCache[$call] = $match; + return $match; + } + + protected function applyQsoOverride(QsoResult $result, QsoOverride $override): void + { + if ($override->forced_matched_log_qso_id !== null) { + $result->matched_qso_id = $override->forced_matched_log_qso_id; + $result->matched_log_qso_id = $override->forced_matched_log_qso_id; + $result->is_nil = false; + } + + if (! $override->forced_status || $override->forced_status === 'AUTO') { + return; + } + + $result->is_valid = false; + $result->is_duplicate = false; + $result->is_nil = false; + $result->is_busted_call = false; + $result->is_busted_rst = false; + $result->is_busted_exchange = false; + $result->is_time_out_of_window = false; + $result->error_code = null; + $result->error_side = 'NONE'; + $result->penalty_points = 0; + + switch ($override->forced_status) { + case 'VALID': + $result->is_valid = true; + $result->error_code = QsoErrorCode::OK; + break; + case 'INVALID': + $result->error_code = QsoErrorCode::NO_COUNTERPART_LOG; + break; + case 'NIL': + $result->is_nil = true; + $result->error_code = QsoErrorCode::NO_COUNTERPART_LOG; + break; + case 'DUPLICATE': + $result->is_duplicate = true; + $result->error_code = QsoErrorCode::DUP; + break; + case 'BUSTED_CALL': + $result->is_busted_call = true; + $result->error_code = QsoErrorCode::BUSTED_CALL; + $result->error_side = 'RX'; + break; + case 'BUSTED_EXCHANGE': + $result->is_busted_exchange = true; + $result->error_code = QsoErrorCode::BUSTED_SERIAL; + $result->error_side = 'RX'; + break; + case 'OUT_OF_WINDOW': + $result->is_time_out_of_window = true; + $result->error_code = null; + break; + } + } + + protected function resolveErrorCode(QsoResult $result): ?string + { + if ($result->error_code) { + return $result->error_code; + } + if ($result->is_duplicate) { + return QsoErrorCode::DUP; + } + if ($result->is_nil) { + return QsoErrorCode::NO_COUNTERPART_LOG; + } + if ($result->is_busted_call) { + return QsoErrorCode::BUSTED_CALL; + } + if ($result->is_busted_rst) { + return QsoErrorCode::BUSTED_RST; + } + if ($result->is_busted_exchange) { + return QsoErrorCode::BUSTED_SERIAL; + } + + return $result->is_valid ? QsoErrorCode::OK : null; + } + + protected function resolvePenaltyPoints(QsoResult $result, EvaluationRuleSet $ruleSet, ScoringService $scoring): int + { + $penalty = 0; + $errorSide = $result->error_side ?? 'NONE'; + + if ($result->error_code === QsoErrorCode::DUP && $ruleSet->dup_qso_policy === 'PENALTY') { + $penalty += $scoring->penaltyPointsFor(QsoErrorCode::DUP, $ruleSet); + } + if ($result->error_code === QsoErrorCode::NO_COUNTERPART_LOG) { + $policy = $ruleSet->getString( + 'no_counterpart_log_policy', + $ruleSet->no_counterpart_log_policy ?? null, + $ruleSet->nil_qso_policy ?? 'ZERO_POINTS' + ); + if ($policy === 'PENALTY') { + $penalty += $scoring->penaltyPointsFor('NIL', $ruleSet); + } + } + if ($result->error_code === QsoErrorCode::NOT_IN_COUNTERPART_LOG) { + $policy = $ruleSet->getString( + 'not_in_counterpart_log_policy', + $ruleSet->not_in_counterpart_log_policy ?? null, + $ruleSet->nil_qso_policy ?? 'ZERO_POINTS' + ); + if ($policy === 'PENALTY') { + $penalty += $scoring->penaltyPointsFor('NIL', $ruleSet); + } + } + if ($result->error_code === QsoErrorCode::BUSTED_CALL + && $errorSide !== 'TX' + && $ruleSet->busted_call_policy === 'PENALTY') { + $penalty += $scoring->penaltyPointsFor(QsoErrorCode::BUSTED_CALL, $ruleSet); + } + if ($result->error_code === QsoErrorCode::BUSTED_RST + && $errorSide !== 'TX' + && $ruleSet->busted_rst_policy === 'PENALTY') { + $penalty += $scoring->penaltyPointsFor(QsoErrorCode::BUSTED_RST, $ruleSet); + } + if ($result->error_code === QsoErrorCode::BUSTED_SERIAL + && $errorSide !== 'TX') { + $policy = $ruleSet->getString( + 'busted_serial_policy', + $ruleSet->busted_serial_policy ?? null, + $ruleSet->busted_exchange_policy ?? 'ZERO_POINTS' + ); + if ($policy === 'PENALTY') { + $penalty += $scoring->penaltyPointsFor(QsoErrorCode::BUSTED_SERIAL, $ruleSet); + } + } + if ($result->error_code === QsoErrorCode::BUSTED_LOCATOR + && $errorSide !== 'TX') { + $policy = $ruleSet->getString( + 'busted_locator_policy', + $ruleSet->busted_locator_policy ?? null, + $ruleSet->busted_exchange_policy ?? 'ZERO_POINTS' + ); + if ($policy === 'PENALTY') { + $penalty += $scoring->penaltyPointsFor(QsoErrorCode::BUSTED_LOCATOR, $ruleSet); + } + } + if ($result->error_code === QsoErrorCode::TIME_MISMATCH + && ($ruleSet->time_mismatch_policy ?? 'ZERO_POINTS') === 'PENALTY') { + $penalty += $scoring->penaltyPointsFor(QsoErrorCode::TIME_MISMATCH, $ruleSet); + } + if ($result->is_time_out_of_window && $ruleSet->out_of_window_policy === 'PENALTY') { + $penalty += $scoring->penaltyPointsFor(QsoErrorCode::OUT_OF_WINDOW, $ruleSet); + } + + return $penalty; + } + + protected function formatWwlMultiplier(?string $locator, EvaluationRuleSet $ruleSet): ?string + { + if (! $locator) { + return null; + } + $value = strtoupper(trim($locator)); + $value = preg_replace('/\s+/', '', $value) ?? ''; + if ($value === '') { + return null; + } + + $length = match ($ruleSet->wwl_multiplier_level) { + 'LOCATOR_2' => 2, + 'LOCATOR_4' => 4, + default => 6, + }; + + if (strlen($value) < $length) { + return null; + } + + return substr($value, 0, $length); + } +} diff --git a/app/Jobs/StartEvaluationRunJob.php b/app/Jobs/StartEvaluationRunJob.php new file mode 100644 index 0000000..3bab07c --- /dev/null +++ b/app/Jobs/StartEvaluationRunJob.php @@ -0,0 +1,80 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + + app(EvaluationCoordinator::class)->start($run); + } +} diff --git a/app/Jobs/UnpairedClassificationBucketJob.php b/app/Jobs/UnpairedClassificationBucketJob.php new file mode 100644 index 0000000..f32205d --- /dev/null +++ b/app/Jobs/UnpairedClassificationBucketJob.php @@ -0,0 +1,147 @@ +bandId = $bandId; + $this->rcallNorm = $rcallNorm; + } + + public function handle(): void + { + $run = EvaluationRun::find($this->evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + try { + $ruleSet = EvaluationRuleSet::find($run->rule_set_id); + if (! $ruleSet) { + $coordinator->eventError($run, 'Unpaired klasifikace nelze spustit: chybí ruleset.', [ + 'step' => 'unpaired_classification', + ]); + return; + } + + $coordinator->eventInfo($run, 'Unpaired bucket: krok spuštěn.', [ + 'step' => 'unpaired_classification', + 'round_id' => $run->round_id, + 'band_id' => $this->bandId, + 'rcall_norm' => $this->rcallNorm, + ]); + + $workingQuery = WorkingQso::where('evaluation_run_id', $run->id) + ->where('rcall_norm', $this->rcallNorm); + if ($this->bandId !== null) { + $workingQuery->where('band_id', $this->bandId); + } else { + $workingQuery->whereNull('band_id'); + } + $logQsoIds = $workingQuery->pluck('log_qso_id')->all(); + if (! $logQsoIds) { + return; + } + + QsoResult::where('evaluation_run_id', $run->id) + ->whereNull('matched_log_qso_id') + ->whereIn('log_qso_id', $logQsoIds) + ->chunkById(500, function ($results) use ($run, $ruleSet) { + foreach ($results as $result) { + $wqso = WorkingQso::where('evaluation_run_id', $run->id) + ->where('log_qso_id', $result->log_qso_id) + ->first(); + if (! $wqso) { + continue; + } + + $hasCounterpartLog = false; + if ($wqso->band_id && $wqso->rcall_norm) { + $hasCounterpartLog = WorkingQso::where('evaluation_run_id', $run->id) + ->where('band_id', $wqso->band_id) + ->where('call_norm', $wqso->rcall_norm) + ->exists(); + } + + if ($hasCounterpartLog) { + $result->error_code = QsoErrorCode::NOT_IN_COUNTERPART_LOG; + $result->is_nil = true; + } else { + $isUnique = false; + if ($ruleSet->uniqueQsoEnabled() && $wqso->rcall_norm) { + $uniqueQuery = WorkingQso::where('evaluation_run_id', $run->id) + ->where('rcall_norm', $wqso->rcall_norm); + if ($wqso->band_id) { + $uniqueQuery->where('band_id', $wqso->band_id); + } + $count = $uniqueQuery->count(); + $isUnique = $count === 1; + } + + if ($isUnique) { + $result->error_code = QsoErrorCode::UNIQUE; + $result->is_nil = false; + } else { + $result->error_code = QsoErrorCode::NO_COUNTERPART_LOG; + $result->is_nil = true; + } + } + + $result->is_valid = false; + $result->error_side = 'NONE'; + $result->save(); + } + }); + + EvaluationRun::where('id', $run->id)->increment('progress_done'); + $coordinator->eventInfo($run, 'Unpaired bucket: krok dokončen.', [ + 'step' => 'unpaired_classification', + 'round_id' => $run->round_id, + 'band_id' => $this->bandId, + 'rcall_norm' => $this->rcallNorm, + ]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Unpaired bucket: krok selhal.', [ + 'step' => 'unpaired_classification', + 'round_id' => $run->round_id, + 'band_id' => $this->bandId, + 'rcall_norm' => $this->rcallNorm, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } +} diff --git a/app/Jobs/UnpairedClassificationJob.php b/app/Jobs/UnpairedClassificationJob.php new file mode 100644 index 0000000..d97e708 --- /dev/null +++ b/app/Jobs/UnpairedClassificationJob.php @@ -0,0 +1,141 @@ +evaluationRunId); + if (! $run || $run->isCanceled()) { + return; + } + $coordinator = new EvaluationCoordinator(); + + try { + $ruleSet = EvaluationRuleSet::find($run->rule_set_id); + if (! $ruleSet) { + $coordinator->eventError($run, 'Unpaired klasifikace nelze spustit: chybí ruleset.', [ + 'step' => 'unpaired_classification', + ]); + return; + } + + $coordinator->eventInfo($run, 'Unpaired: krok spuštěn.', [ + 'step' => 'unpaired_classification', + 'round_id' => $run->round_id, + ]); + + $coordinator->eventInfo($run, 'Klasifikace nenapárovaných QSO.', [ + 'step' => 'match', + 'round_id' => $run->round_id, + 'step_progress_done' => null, + 'step_progress_total' => $run->progress_total, + ]); + + QsoResult::where('evaluation_run_id', $run->id) + ->whereNull('matched_log_qso_id') + ->chunkById(500, function ($results) use ($run, $ruleSet) { + foreach ($results as $result) { + $wqso = WorkingQso::where('evaluation_run_id', $run->id) + ->where('log_qso_id', $result->log_qso_id) + ->first(); + if (! $wqso) { + continue; + } + + $hasCounterpartLog = false; + if ($wqso->band_id && $wqso->rcall_norm) { + $hasCounterpartLog = WorkingQso::where('evaluation_run_id', $run->id) + ->where('band_id', $wqso->band_id) + ->where('call_norm', $wqso->rcall_norm) + ->exists(); + } + + if ($hasCounterpartLog) { + $result->error_code = QsoErrorCode::NOT_IN_COUNTERPART_LOG; + $result->is_nil = true; + } else { + $isUnique = false; + // UNIQUE je globální v rámci runu (min. evaluation_run_id + band_id). + if ($ruleSet->uniqueQsoEnabled() && $wqso->rcall_norm) { + $uniqueQuery = WorkingQso::where('evaluation_run_id', $run->id) + ->where('rcall_norm', $wqso->rcall_norm); + if ($wqso->band_id) { + $uniqueQuery->where('band_id', $wqso->band_id); + } + $count = $uniqueQuery->count(); + $isUnique = $count === 1; + } + + if ($isUnique) { + $result->error_code = QsoErrorCode::UNIQUE; + $result->is_nil = false; + } else { + $result->error_code = QsoErrorCode::NO_COUNTERPART_LOG; + $result->is_nil = true; + } + } + + $result->error_side = 'NONE'; + $result->is_valid = false; + $result->save(); + } + }); + + EvaluationRun::where('id', $run->id)->increment('progress_done'); + $coordinator->eventInfo($run, 'Unpaired: krok dokončen.', [ + 'step' => 'unpaired_classification', + 'round_id' => $run->round_id, + ]); + } catch (Throwable $e) { + $coordinator->eventError($run, 'Unpaired: krok selhal.', [ + 'step' => 'unpaired_classification', + 'round_id' => $run->round_id, + 'error' => $e->getMessage(), + ]); + throw $e; + } + } +} diff --git a/app/Jobs/UpsertClaimedLogResultJob.php b/app/Jobs/UpsertClaimedLogResultJob.php new file mode 100644 index 0000000..66d5213 --- /dev/null +++ b/app/Jobs/UpsertClaimedLogResultJob.php @@ -0,0 +1,329 @@ +logId); + if (! $log) { + return; + } + + $round = Round::with(['bands', 'categories', 'powerCategories'])->find($log->round_id); + $remarksEval = $this->decodeRemarksEval($log->remarks_eval); + + // TDate validace hlídá, že log odpovídá termínu kola. + $tDateInvalid = ! $this->isTDateWithinRound($log->tdate, $round); + if ($tDateInvalid) { + $this->addRemark($remarksEval, 'Datum v TDate neodpovídá termínu závodu.'); + $log->remarks_eval = $this->encodeRemarksEval($remarksEval); + $log->save(); + } + + $categoryId = $this->resolveCategoryId($log, $round, $remarksEval); + $bandId = $this->resolveBandId($log, $round, $remarksEval); + [$powerCategoryId, $powerMismatch] = $this->resolvePowerCategoryId($log, $round, $remarksEval); + + // 6H kategorie je povolená jen pro vybraná pásma. + if ($log->sixhr_category && ! $this->isSixHourBand($bandId)) { + $remarksEval[] = '6H kategorie je povolena jen pro pásma 145 MHz a 435 MHz.'; + } + + $missingClaimedQso = $log->claimed_qso_count === null; + $missingClaimedScore = $log->claimed_score === null; + if ($missingClaimedQso) { + $remarksEval[] = 'Nebyl načten CQSOs.'; + } + if ($missingClaimedScore) { + $remarksEval[] = 'Nebyl načten CToSc.'; + } + + $log->remarks_eval = $this->encodeRemarksEval($remarksEval); + $log->save(); + + $claimedRun = ClaimedRunResolver::forRound($log->round_id); + $claimedQsoCount = $log->claimed_qso_count ?? 0; + $claimedScore = $log->claimed_score ?? 0; + $scorePerQso = $claimedQsoCount > 0 ? round($claimedScore / $claimedQsoCount, 2) : null; + + $status = 'OK'; + $statusReason = null; + + if ($this->isCheckLog($log)) { + $status = 'CHECK'; + } + + // IGNORED = log nelze bezpečně zařadit do claimed scoreboardu. + if ($tDateInvalid || $categoryId === null || $bandId === null || $missingClaimedQso || $missingClaimedScore || $powerMismatch) { + $status = 'IGNORED'; + $reasons = []; + if ($tDateInvalid) { + $reasons[] = 'TDate mimo termín závodu.'; + } + if ($categoryId === null) { + $reasons[] = 'Kategorie nebyla rozpoznána.'; + } + if ($bandId === null) { + $reasons[] = 'Pásmo nebylo rozpoznáno.'; + } + if ($missingClaimedQso) { + $reasons[] = 'Chybí CQSOs.'; + } + if ($missingClaimedScore) { + $reasons[] = 'Chybí CToSc.'; + } + if ($powerMismatch) { + $reasons[] = 'Výkon neodpovídá zvolené kategorii.'; + } + $statusReason = implode(' ', $reasons); + } + + LogResult::updateOrCreate( + ['log_id' => $log->id, 'evaluation_run_id' => $claimedRun->id], + [ + 'evaluation_run_id' => $claimedRun->id, + 'category_id' => $categoryId, + 'band_id' => $bandId, + 'power_category_id' => $powerCategoryId, + 'claimed_qso_count' => $log->claimed_qso_count, + 'claimed_score' => $log->claimed_score, + 'total_qso_count' => $claimedQsoCount, + 'discarded_qso_count' => 0, + 'discarded_points' => 0, + 'discarded_qso_percent' => 0, + 'unique_qso_count' => 0, + 'score_per_qso' => $scorePerQso, + 'official_score' => 0, + 'penalty_score' => 0, + 'status' => $status, + 'status_reason' => $statusReason, + ] + ); + } + + protected function resolveCategoryId(Log $log, ?Round $round, array &$remarksEval): ?int + { + $resolveCategory = function (?string $value) use ($round): ?int { + if (! $value) { + return null; + } + + $ediCat = EdiCategory::whereRaw('LOWER(value) = ?', [mb_strtolower(trim($value))])->first(); + if (! $ediCat) { + $ediCat = $this->matchEdiCategoryByRegex($value); + if (! $ediCat) { + return null; + } + } + + $mappedCategoryId = $ediCat->categories()->value('categories.id'); + if (! $mappedCategoryId) { + return null; + } + + if ($round && $round->categories()->where('categories.id', $mappedCategoryId)->exists()) { + return $mappedCategoryId; + } + + if (! $round || $round->categories()->count() === 0) { + return $mappedCategoryId; + } + + return null; + }; + + // V PSect může být více tokenů – zkoušíme je postupně. + $categoryId = null; + if ($log->psect) { + $categoryId = $resolveCategory($log->psect); + + if ($categoryId === null) { + $parts = preg_split('/\\s+/', trim((string) $log->psect)) ?: []; + if (count($parts) > 1) { + foreach ($parts as $part) { + $categoryId = $resolveCategory($part); + if ($categoryId !== null) { + break; + } + } + } + } + } + + if ($categoryId === null) { + $remarksEval[] = 'Kategorie nebyla rozpoznána.'; + } + + return $categoryId; + } + + protected function matchEdiCategoryByRegex(string $value): ?EdiCategory + { + $candidates = EdiCategory::whereNotNull('regex_pattern')->get(); + foreach ($candidates as $candidate) { + $pattern = $candidate->regex_pattern; + if (! $pattern) { + continue; + } + + $delimited = preg_match('/^([#\\/]).+\\1[imsxuADSUXJ]*$/', $pattern) ? $pattern : '/' . $pattern . '/i'; + set_error_handler(function () { + }); + $matched = @preg_match($delimited, $value) === 1; + restore_error_handler(); + + if ($matched) { + return $candidate; + } + } + + return null; + } + + protected function resolveBandId(Log $log, ?Round $round, array &$remarksEval): ?int + { + $bandId = null; + if ($log->pband) { + // Nejprve přímá mapa přes EDI bandy, fallback je interval v MHz. + $pbandVal = mb_strtolower(trim($log->pband)); + $ediBand = EdiBand::whereRaw('LOWER(value) = ?', [$pbandVal])->first(); + if ($ediBand) { + $mappedBandId = $ediBand->bands()->value('bands.id'); + if ($mappedBandId) { + if ($round && $round->bands()->where('bands.id', $mappedBandId)->exists()) { + $bandId = $mappedBandId; + } elseif (! $round || $round->bands()->count() === 0) { + $bandId = $mappedBandId; + } + } + } else { + $num = is_numeric($pbandVal) ? (float) $pbandVal : null; + if ($num === null && $log->pband) { + if (preg_match('/([0-9]+(?:[\\.,][0-9]+)?)/', $log->pband, $m)) { + $num = (float) str_replace(',', '.', $m[1]); + } + } + if ($num !== null) { + $bandMatch = \App\Models\Band::where('edi_band_begin', '<=', $num) + ->where('edi_band_end', '>=', $num) + ->first(); + if ($bandMatch) { + if ($round && $round->bands()->where('bands.id', $bandMatch->id)->exists()) { + $bandId = $bandMatch->id; + } elseif (! $round || $round->bands()->count() === 0) { + $bandId = $bandMatch->id; + } + } + } + } + } + + if ($bandId === null) { + $remarksEval[] = 'Pásmo nebylo rozpoznáno.'; + } + + return $bandId; + } + + protected function resolvePowerCategoryId(Log $log, ?Round $round, array &$remarksEval): array + { + $powerCategoryId = null; + $powerMismatch = false; + + if ($log->power_category_id) { + $powerCategoryId = $log->power_category_id; + } + + if ($round && $round->powerCategories()->count() > 0) { + $exists = $round->powerCategories()->where('power_categories.id', $powerCategoryId)->exists(); + if (! $exists) { + $powerMismatch = true; + } + } + + return [$powerCategoryId, $powerMismatch]; + } + + protected function isSixHourBand(?int $bandId): bool + { + if (! $bandId) { + return false; + } + return in_array($bandId, [1, 2], true); + } + + protected function isCheckLog(Log $log): bool + { + $psect = trim((string) $log->psect); + return $psect !== '' && preg_match('/\\bcheck\\b/i', $psect) === 1; + } + + protected function decodeRemarksEval(?string $value): array + { + if (! $value) { + return []; + } + $decoded = json_decode($value, true); + return is_array($decoded) ? $decoded : []; + } + + protected function encodeRemarksEval(array $value): ?string + { + $filtered = array_values(array_filter($value, fn ($item) => is_string($item) && trim($item) !== '')); + $filtered = array_values(array_unique($filtered)); + if (count($filtered) === 0) { + return null; + } + return json_encode($filtered, JSON_UNESCAPED_UNICODE); + } + + protected function addRemark(array &$remarksEval, string $message): void + { + if (! in_array($message, $remarksEval, true)) { + $remarksEval[] = $message; + } + } + + protected function isTDateWithinRound(?string $tdate, ?Round $round): bool + { + if (! $tdate || ! $round || ! $round->start_time || ! $round->end_time) { + return true; + } + $parts = explode(';', $tdate); + if (count($parts) !== 2) { + return false; + } + if (!preg_match('/^\\d{8}$/', $parts[0]) || !preg_match('/^\\d{8}$/', $parts[1])) { + return false; + } + $start = \Carbon\Carbon::createFromFormat('Ymd', $parts[0])->startOfDay(); + $end = \Carbon\Carbon::createFromFormat('Ymd', $parts[1])->endOfDay(); + + return $start->lte($round->end_time) && $end->gte($round->start_time); + } + +} diff --git a/app/Models/Band.php b/app/Models/Band.php new file mode 100644 index 0000000..0da1aaa --- /dev/null +++ b/app/Models/Band.php @@ -0,0 +1,32 @@ +belongsToMany(EdiBand::class, 'bands_edi_bands', 'band_id', 'edi_band_id'); + } + + public function contests(): BelongsToMany + { + return $this->belongsToMany(Contest::class, 'contests_bands', 'band_id', 'contest_id'); + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php new file mode 100644 index 0000000..30bbf50 --- /dev/null +++ b/app/Models/Category.php @@ -0,0 +1,29 @@ +belongsToMany(EdiCategory::class, 'categories_edi_categories', 'category_id', 'edi_category_id'); + } + + public function contests(): BelongsToMany + { + return $this->belongsToMany(Contest::class, 'contests_categories', 'category_id', 'category_id'); + } +} diff --git a/app/Models/Contest.php b/app/Models/Contest.php new file mode 100644 index 0000000..8240075 --- /dev/null +++ b/app/Models/Contest.php @@ -0,0 +1,86 @@ + 'array', + 'description' => 'array', + 'is_mcr' => 'boolean', + 'is_test' => 'boolean', + 'is_sixhr' => 'boolean', + 'is_active' => 'boolean', + 'duration' => 'integer', + 'logs_deadline_days' => 'integer', + 'rule_set_id' => 'integer', + // 'start_time' => 'string', // pokud chceš čistý string; pro fancy práci s časem můžeš dát vlastní cast + ]; + + public function ruleSet(): BelongsTo + { + return $this->belongsTo(EvaluationRuleSet::class, 'rule_set_id'); + } + + public function rounds(): HasMany + { + return $this->hasMany(Round::class, 'contest_id') + ->orderByDesc('start_time') + ->orderByDesc('end_time'); + } + + public function parameters(): HasMany + { + return $this->hasMany(ContestParameter::class, 'contest_id'); + } + + public function bands(): BelongsToMany + { + return $this->belongsToMany(Band::class, 'contests_bands', 'contest_id', 'band_id'); + } + + public function categories(): BelongsToMany + { + return $this->belongsToMany(Category::class, 'contests_categories', 'contest_id', 'category_id'); + } + + public function powerCategories(): BelongsToMany + { + return $this->belongsToMany(PowerCategory::class, 'contests_power_categories', 'contest_id', 'power_category_id'); + } +} diff --git a/app/Models/ContestParameter.php b/app/Models/ContestParameter.php new file mode 100644 index 0000000..c454961 --- /dev/null +++ b/app/Models/ContestParameter.php @@ -0,0 +1,50 @@ + 'integer', + 'ignore_slash_part' => 'boolean', + 'ignore_third_part' => 'boolean', + 'letters_in_rst' => 'boolean', + 'discard_qso_rec_diff_call' => 'boolean', + 'discard_qso_sent_diff_call' => 'boolean', + 'discard_qso_rec_diff_rst' => 'boolean', + 'discard_qso_sent_diff_rst' => 'boolean', + 'discard_qso_rec_diff_code' => 'boolean', + 'discard_qso_sent_diff_code' => 'boolean', + 'unique_qso' => 'boolean', + 'time_tolerance' => 'integer', + ]; + + public function contest(): BelongsTo + { + return $this->belongsTo(Contest::class, 'contest_id'); + } +} diff --git a/app/Models/CountryWwl.php b/app/Models/CountryWwl.php new file mode 100644 index 0000000..a366d3a --- /dev/null +++ b/app/Models/CountryWwl.php @@ -0,0 +1,31 @@ + 'integer', + 'cq_zone' => 'integer', + 'itu_zone' => 'integer', + 'latitude' => 'float', + 'longitude' => 'float', + 'time_offset' => 'float', + 'precise' => 'boolean', + ]; +} diff --git a/app/Models/EdiBand.php b/app/Models/EdiBand.php new file mode 100644 index 0000000..a4080f6 --- /dev/null +++ b/app/Models/EdiBand.php @@ -0,0 +1,23 @@ +belongsToMany(Band::class, 'bands_edi_bands', 'edi_band_id', 'band_id'); + } +} diff --git a/app/Models/EdiCategory.php b/app/Models/EdiCategory.php new file mode 100644 index 0000000..9c7bd36 --- /dev/null +++ b/app/Models/EdiCategory.php @@ -0,0 +1,24 @@ +belongsToMany(Category::class, 'categories_edi_categories', 'edi_category_id', 'category_id'); + } +} diff --git a/app/Models/EvaluationLock.php b/app/Models/EvaluationLock.php new file mode 100644 index 0000000..c5f159b --- /dev/null +++ b/app/Models/EvaluationLock.php @@ -0,0 +1,148 @@ + 'integer', + 'locked_at' => 'datetime', + 'expires_at' => 'datetime', + ]; + + public function evaluationRun(): BelongsTo + { + return $this->belongsTo(EvaluationRun::class); + } + + /** + * Pokusí se získat lock pro daný klíč. + * + * - Pokud existuje expirovaný lock, před pokusem ho smaže. + * - Pokud už aktivní lock existuje, vrátí null. + * + * @param string $key Jedinečný identifikátor scope (např. "round:5"). + * @param \App\Models\EvaluationRun|null $run Volitelný běh, ke kterému se lock váže. + * @param \DateInterval|int|\Carbon\Carbon|null $ttl Doba platnosti (sekundy nebo DateInterval nebo konkrétní expirace). + */ + public static function acquire(string $key, ?EvaluationRun $run = null, \DateInterval|int|Carbon|null $ttl = null): ?self + { + $now = Carbon::now(); + + // uklidíme expirovaný lock se stejným klíčem, aby neblokoval unikátní index + self::where('key', $key) + ->whereNotNull('expires_at') + ->where('expires_at', '<', $now) + ->delete(); + + $expiresAt = null; + if ($ttl instanceof Carbon) { + $expiresAt = $ttl; + } elseif ($ttl instanceof \DateInterval) { + $expiresAt = (clone $now)->add($ttl); + } elseif (is_int($ttl)) { + $expiresAt = (clone $now)->addSeconds($ttl); + } + + try { + return self::create([ + 'key' => $key, + 'evaluation_run_id' => $run?->id, + 'locked_at' => $now, + 'expires_at' => $expiresAt, + ]); + } catch (QueryException $e) { + // unikátní klíč porušen -> lock už drží někdo jiný + if (str_contains(strtolower($e->getMessage()), 'duplicate') || str_contains(strtolower($e->getMessage()), 'unique')) { + return null; + } + throw $e; + } + } + + /** + * Uvolní lock podle klíče (a volitelně evaluation_run_id). + */ + public static function release(string $key, ?EvaluationRun $run = null): int + { + $query = self::where('key', $key); + if ($run) { + $query->where('evaluation_run_id', $run->id); + } + return $query->delete(); + } + + /** + * Zjistí, zda lock existuje a neexpiroval. + */ + public static function isLocked(string $key): bool + { + $now = Carbon::now(); + return self::where('key', $key) + ->where(function ($q) use ($now) { + $q->whereNull('expires_at') + ->orWhere('expires_at', '>', $now); + }) + ->exists(); + } +} diff --git a/app/Models/EvaluationRuleSet.php b/app/Models/EvaluationRuleSet.php new file mode 100644 index 0000000..e3e44ed --- /dev/null +++ b/app/Models/EvaluationRuleSet.php @@ -0,0 +1,479 @@ + BUSTED_CALL (error_side=RX) + 'discard_qso_sent_diff_call', // TX neshoda callsign -> BUSTED_CALL (error_side=TX) + 'discard_qso_rec_diff_rst', // RX neshoda RST -> BUSTED_RST (error_side=RX) + 'discard_qso_sent_diff_rst', // TX neshoda RST -> BUSTED_RST (error_side=TX) + 'discard_qso_rec_diff_code', // RX neshoda exchange -> BUSTED_SERIAL/LOCATOR (error_side=RX) + 'discard_qso_sent_diff_code',// TX neshoda exchange -> BUSTED_SERIAL/LOCATOR (error_side=TX) + 'discard_qso_rec_diff_serial', // RX neshoda serial -> BUSTED_SERIAL (error_side=RX) + 'discard_qso_sent_diff_serial',// TX neshoda serial -> BUSTED_SERIAL (error_side=TX) + 'discard_qso_rec_diff_wwl', // RX neshoda WWL -> BUSTED_LOCATOR (error_side=RX) + 'discard_qso_sent_diff_wwl', // TX neshoda WWL -> BUSTED_LOCATOR (error_side=TX) + 'busted_rst_policy', // ZERO_POINTS / PENALTY + 'penalty_busted_rst_points', // Penalizace za BUSTED_RST + + 'match_tiebreak_order', // Pořadí tiebreak kritérií + 'match_require_locator_match', // Matching vyžaduje lokátor + 'match_require_exchange_match',// Matching vyžaduje exchange + + 'multiplier_scope', // PER_BAND / OVERALL + 'multiplier_source', // VALID_ONLY / ALL_MATCHED + 'wwl_multiplier_level', // LOCATOR_2 / LOCATOR_4 / LOCATOR_6 + + 'checklog_matching', // CHECK logy v matchingu + 'out_of_window_dq_threshold', // DQ logu při nadlimitních OOW QSO + 'time_diff_dq_threshold_percent', // DQ logu při nadlimitním % časového rozdílu + 'time_diff_dq_threshold_sec', // Prah časového rozdílu v sekundách + 'bad_qso_dq_threshold_percent', // DQ logu při nadlimitním % špatných QSO + + 'time_tolerance_sec', // Tolerance času v matchingu (sekundy) + 'require_unique_qso', // Zapnout detekci duplicit v logu + 'allow_time_shift_one_hour', // Povolit posun o 1 hodinu při matchingu + 'time_shift_seconds', // Velikost časového posunu v sekundách + 'time_mismatch_policy', // INVALID / ZERO_POINTS / FLAG_ONLY / PENALTY + 'callsign_suffix_max_len', // Max délka suffixu za / + 'callsign_levenshtein_max', // Maximální Levenshtein vzdálenost pro fuzzy match + 'allow_time_mismatch_pairing', // Povolit párování mimo toleranci + 'time_mismatch_max_sec', // Max. odchylka mimo toleranci (NULL = bez limitu) + 'dup_resolution_strategy', // Pořadí pravidel pro volbu survivor v duplicitách + + 'operating_window_mode', // NONE / BEST_CONTIGUOUS + 'operating_window_hours', // Délka okna v hodinách + 'sixhr_ranking_mode', // IARU / CRK + + 'options', // Volitelné JSON rozšíření pravidel + ]; + + protected $casts = [ + + 'points_per_qso' => 'integer', + 'points_per_km' => 'float', + + 'use_multipliers' => 'boolean', + 'time_tolerance_sec' => 'integer', + 'require_unique_qso' => 'boolean', + 'penalty_dup_points' => 'integer', + 'penalty_nil_points' => 'integer', + 'penalty_busted_call_points' => 'integer', + 'penalty_busted_exchange_points' => 'integer', + 'penalty_busted_serial_points' => 'integer', + 'penalty_busted_locator_points' => 'integer', + 'min_distance_km' => 'integer', + 'require_locators' => 'boolean', + 'penalty_out_of_window_points' => 'integer', + + 'exchange_requires_wwl' => 'boolean', + 'exchange_requires_serial' => 'boolean', + 'exchange_requires_report' => 'boolean', + 'ignore_slash_part' => 'boolean', + 'ignore_third_part' => 'boolean', + 'letters_in_rst' => 'boolean', + 'rst_ignore_third_char' => 'boolean', + 'discard_qso_rec_diff_call' => 'boolean', + 'discard_qso_sent_diff_call' => 'boolean', + 'discard_qso_rec_diff_rst' => 'boolean', + 'discard_qso_sent_diff_rst' => 'boolean', + 'discard_qso_rec_diff_code' => 'boolean', + 'discard_qso_sent_diff_code' => 'boolean', + 'discard_qso_rec_diff_serial' => 'boolean', + 'discard_qso_sent_diff_serial' => 'boolean', + 'discard_qso_rec_diff_wwl' => 'boolean', + 'discard_qso_sent_diff_wwl' => 'boolean', + 'penalty_busted_rst_points' => 'integer', + + 'match_tiebreak_order' => 'array', + 'match_require_locator_match' => 'boolean', + 'match_require_exchange_match' => 'boolean', + + 'checklog_matching' => 'boolean', + 'out_of_window_dq_threshold' => 'integer', + 'time_diff_dq_threshold_percent' => 'integer', + 'time_diff_dq_threshold_sec' => 'integer', + 'bad_qso_dq_threshold_percent' => 'integer', + 'allow_time_shift_one_hour' => 'boolean', + 'time_shift_seconds' => 'integer', + 'callsign_suffix_max_len' => 'integer', + 'callsign_levenshtein_max' => 'integer', + 'allow_time_mismatch_pairing' => 'boolean', + 'time_mismatch_max_sec' => 'integer', + 'dup_resolution_strategy' => 'array', + + 'operating_window_mode' => 'string', + 'operating_window_hours' => 'integer', + 'sixhr_ranking_mode' => 'string', + + 'options' => 'array', + ]; + + /** + * Mapování: kde je jednotlivý atribut rulesetu použit. + * Pokud je hodnota "unused", musí být explicitně zdůvodněno. + */ + public const FLAG_USAGE = [ + 'name' => 'metadata (UI, identifikace rulesetu)', + 'code' => 'metadata (UI, identifikace rulesetu)', + 'description' => 'metadata (UI, identifikace rulesetu)', + 'scoring_mode' => 'ScoringService::computeBasePoints', + 'points_per_qso' => 'ScoringService::computeBasePoints', + 'points_per_km' => 'ScoringService::computeBasePoints', + 'use_multipliers' => 'ScoreGroupJob::applyMultipliers + AggregateLogResultsJob', + 'multiplier_type' => 'ScoreGroupJob::applyMultipliers + AggregateLogResultsJob', + 'dup_qso_policy' => 'ScoringService::penaltyPointsFor', + 'nil_qso_policy' => 'ScoringService::penaltyPointsFor', + 'no_counterpart_log_policy' => 'ScoreGroupJob (validita/bodování)', + 'not_in_counterpart_log_policy' => 'ScoreGroupJob (validita/bodování)', + 'unique_qso_policy' => 'ScoreGroupJob (validita/bodování)', + 'busted_call_policy' => 'ScoringService::penaltyPointsFor', + 'busted_exchange_policy' => 'ScoringService::penaltyPointsFor', + 'busted_serial_policy' => 'ScoringService::penaltyPointsFor', + 'busted_locator_policy' => 'ScoringService::penaltyPointsFor', + 'penalty_dup_points' => 'ScoringService::penaltyPointsFor', + 'penalty_nil_points' => 'ScoringService::penaltyPointsFor', + 'penalty_busted_call_points' => 'ScoringService::penaltyPointsFor', + 'penalty_busted_exchange_points' => 'ScoringService::penaltyPointsFor', + 'penalty_busted_serial_points' => 'ScoringService::penaltyPointsFor', + 'penalty_busted_locator_points' => 'ScoringService::penaltyPointsFor', + 'dupe_scope' => 'BuildWorkingSetLogJob::dupe_key', + 'callsign_normalization' => 'MatchingService::normalizeCallsign + MatchQsoGroupJob', + 'distance_rounding' => 'ScoringService::calculateDistanceKm', + 'min_distance_km' => 'ScoringService::calculateDistanceKm', + 'require_locators' => 'ScoreGroupJob (validita/bodování)', + 'out_of_window_policy' => 'ScoringService::outOfWindowDecision + ScoreGroupJob', + 'penalty_out_of_window_points' => 'ScoringService::penaltyPointsFor', + 'exchange_type' => 'MatchQsoGroupJob::resolveExchangeMatch', + 'exchange_requires_wwl' => 'MatchQsoGroupJob::resolveExchangeMatch', + 'exchange_requires_serial' => 'MatchQsoGroupJob::resolveExchangeMatch', + 'exchange_requires_report' => 'MatchQsoGroupJob::resolveExchangeMatch', + 'exchange_pattern' => 'MatchQsoGroupJob::resolveExchangeMatch', + 'ignore_slash_part' => 'MatchQsoGroupJob::normalizeCallsign', + 'ignore_third_part' => 'MatchQsoGroupJob::normalizeCallsign', + 'letters_in_rst' => 'MatchQsoGroupJob::normalizeRst', + 'rst_ignore_third_char' => 'MatchQsoGroupJob::reportsMatch', + 'discard_qso_rec_diff_call' => 'MatchQsoGroupJob (error_side=RX)', + 'discard_qso_sent_diff_call' => 'MatchQsoGroupJob (error_side=TX)', + 'discard_qso_rec_diff_rst' => 'MatchQsoGroupJob (error_side=RX)', + 'discard_qso_sent_diff_rst' => 'MatchQsoGroupJob (error_side=TX)', + 'discard_qso_rec_diff_code' => 'MatchQsoGroupJob (RX exchange mismatch)', + 'discard_qso_sent_diff_code' => 'MatchQsoGroupJob (TX exchange mismatch)', + 'discard_qso_rec_diff_serial' => 'MatchQsoGroupJob (RX serial mismatch)', + 'discard_qso_sent_diff_serial' => 'MatchQsoGroupJob (TX serial mismatch)', + 'discard_qso_rec_diff_wwl' => 'MatchQsoGroupJob (RX locator mismatch)', + 'discard_qso_sent_diff_wwl' => 'MatchQsoGroupJob (TX locator mismatch)', + 'busted_rst_policy' => 'ScoringService::penaltyPointsFor', + 'penalty_busted_rst_points' => 'ScoringService::penaltyPointsFor', + 'match_tiebreak_order' => 'MatchQsoGroupJob::rankDecision', + 'match_require_locator_match' => 'MatchQsoGroupJob::resolveExchangeMatch', + 'match_require_exchange_match' => 'MatchQsoGroupJob::resolveExchangeMatch', + 'multiplier_scope' => 'AggregateLogResultsJob::aggregateMultipliers', + 'multiplier_source' => 'AggregateLogResultsJob::aggregateMultipliers', + 'wwl_multiplier_level' => 'ScoreGroupJob::applyMultipliers', + 'checklog_matching' => 'MatchQsoGroupJob::groupLogsByKey', + 'out_of_window_dq_threshold' => 'AggregateLogResultsJob (DQ logu)', + 'time_diff_dq_threshold_percent' => 'AggregateLogResultsJob (DQ logu)', + 'time_diff_dq_threshold_sec' => 'AggregateLogResultsJob (DQ logu)', + 'bad_qso_dq_threshold_percent' => 'AggregateLogResultsJob (DQ logu)', + 'time_tolerance_sec' => 'MatchQsoGroupJob::findCandidates', + 'require_unique_qso' => 'UnpairedClassificationJob::UNIQUE', + 'allow_time_shift_one_hour' => 'MatchQsoGroupJob::matchWithTimeShift', + 'time_shift_seconds' => 'MatchQsoGroupJob::matchWithTimeShift', + 'time_mismatch_policy' => 'ScoreGroupJob (validita/bodování)', + 'callsign_suffix_max_len' => 'MatchingService::normalizeCallsign', + 'callsign_levenshtein_max' => 'MatchQsoGroupJob::rankDecision', + 'allow_time_mismatch_pairing' => 'MatchQsoGroupJob::findCandidates', + 'time_mismatch_max_sec' => 'MatchQsoGroupJob::findCandidates', + 'dup_resolution_strategy' => 'DuplicateResolutionJob::sort', + 'operating_window_mode' => 'AggregateLogResultsJob (operating window, 6H agregace)', + 'operating_window_hours' => 'AggregateLogResultsJob (operating window, 6H agregace)', + 'sixhr_ranking_mode' => 'RecalculateOfficialRanksJob (6H ranking mode)', + 'options' => 'fallback pro hodnoty bez sloupců (EvaluationRuleSet::getOption)', + ]; + + /** + * Options fallback (používá se jen pokud není vyplněn odpovídající sloupec). + * + * Klíče, typy, defaulty a fáze použití: + * - ignore_slash_part (bool, default=true) – matching (normalizace callsign) + * - callsign_suffix_max_len (int, default=4) – matching (normalizace callsign) + * - rst_ignore_third_char (bool, default=true) – matching (porovnání RST) + * - letters_in_rst (bool, default=false) – matching (porovnání RST) + * - callsign_levenshtein_max (int, default=0) – matching (fuzzy callsign) + * - allow_time_shift_one_hour (bool, default=true) – matching (časový posun) + * - time_shift_seconds (int, default=3600) – matching (časový posun) + * - allow_time_mismatch_pairing (bool, default=false) – matching (TIME_MISMATCH pairing) + * - time_mismatch_max_sec (int|null, default=null) – matching (max odchylka mimo toleranci) + * - match_require_locator_match (bool, default=false) – matching (mismatch flagging) + * - match_require_exchange_match (bool, default=false) – matching (mismatch flagging) + * - unique_qso_enabled (bool, default=true) – unpaired klasifikace (UNIQUE) + * - unique_qso_policy (string, default=FLAG_ONLY) – scoring (validita/bodování) + * - no_counterpart_log_policy (string, default=FLAG_ONLY) – scoring + * - not_in_counterpart_log_policy (string, default=ZERO_POINTS) – scoring + * - duplicate_resolution_strategy (array, default=[paired_first, ok_first, earlier_time, lower_id]) – duplicity + * - distance_rounding (string, default=CEIL) – scoring (vzdálenost) + * - min_distance_km (int, default=1) – scoring (vzdálenost) + */ + + // ----- Vztahy ----- + + public function evaluationRuns(): HasMany + { + return $this->hasMany(EvaluationRun::class, 'rule_set_id'); + } + + // ----- Pomocné metody pro logiku vyhodnocení ----- + + public function isDistanceScoring(): bool + { + return $this->scoring_mode === 'DISTANCE'; + } + + public function isFixedPointsScoring(): bool + { + return $this->scoring_mode === 'FIXED_POINTS'; + } + + public function usesMultipliers(): bool + { + return $this->use_multipliers && $this->multiplier_type !== 'NONE'; + } + + public function multiplierIsWwl(): bool + { + return $this->multiplier_type === 'WWL'; + } + + public function multiplierIsDxcc(): bool + { + return $this->multiplier_type === 'DXCC'; + } + + public function multiplierIsSection(): bool + { + return $this->multiplier_type === 'SECTION'; + } + + public function multiplierIsCountry(): bool + { + return $this->multiplier_type === 'COUNTRY'; + } + + public function dupCountsAsPenalty(): bool + { + return $this->dup_qso_policy === 'PENALTY'; + } + + // ----- Options fallback & typed accessors ----- + + protected function getOption(string $key, mixed $default = null): mixed + { + $options = $this->options ?? []; + return array_key_exists($key, $options) ? $options[$key] : $default; + } + + public function getString(string $key, ?string $columnValue, ?string $default = null): ?string + { + if ($columnValue !== null) { + return $columnValue; + } + $value = $this->getOption($key, $default); + return is_string($value) ? $value : $default; + } + + public function getBool(string $key, ?bool $columnValue, bool $default = false): bool + { + if ($columnValue !== null) { + return (bool) $columnValue; + } + return (bool) $this->getOption($key, $default); + } + + public function getInt(string $key, ?int $columnValue, ?int $default = null): ?int + { + if ($columnValue !== null) { + return (int) $columnValue; + } + $value = $this->getOption($key, $default); + return $value === null ? null : (int) $value; + } + + public function getArray(string $key, ?array $columnValue, array $default = []): array + { + if ($columnValue !== null) { + return $columnValue; + } + $value = $this->getOption($key, $default); + return is_array($value) ? $value : $default; + } + + public function ignoreSlashPart(): bool + { + return $this->getBool('ignore_slash_part', $this->ignore_slash_part); + } + + public function ignoreThirdPart(): bool + { + return $this->getBool('ignore_third_part', $this->ignore_third_part); + } + + public function lettersInRst(): bool + { + return $this->getBool('letters_in_rst', $this->letters_in_rst); + } + + public function callsignLevenshteinMax(): int + { + return (int) ($this->getInt('callsign_levenshtein_max', $this->callsign_levenshtein_max, 0) ?? 0); + } + + public function callsignSuffixMaxLen(): int + { + return (int) ($this->getInt('callsign_suffix_max_len', $this->callsign_suffix_max_len, 4) ?? 4); + } + + public function rstIgnoreThirdChar(): bool + { + return $this->getBool('rst_ignore_third_char', $this->rst_ignore_third_char, true); + } + + public function matchRequireLocatorMatch(): bool + { + return $this->getBool('match_require_locator_match', $this->match_require_locator_match, false); + } + + public function matchRequireExchangeMatch(): bool + { + return $this->getBool('match_require_exchange_match', $this->match_require_exchange_match, false); + } + + public function allowTimeShiftOneHour(): bool + { + return $this->getBool('allow_time_shift_one_hour', $this->allow_time_shift_one_hour, true); + } + + public function allowTimeMismatchPairing(): bool + { + return $this->getBool('allow_time_mismatch_pairing', $this->allow_time_mismatch_pairing, false); + } + + public function timeMismatchMaxSec(): ?int + { + return $this->getInt('time_mismatch_max_sec', $this->time_mismatch_max_sec, null); + } + + public function uniqueQsoEnabled(): bool + { + return $this->getBool('unique_qso_enabled', $this->require_unique_qso, true); + } + + public function dupResolutionStrategy(): array + { + $fallback = $this->getArray('duplicate_resolution_strategy', null, [ + 'paired_first', + 'ok_first', + 'earlier_time', + 'lower_id', + ]); + + $strategy = $this->getArray('duplicate_resolution_strategy', $this->dup_resolution_strategy, $fallback); + if ($strategy !== $fallback) { + return $strategy; + } + + return $this->getArray('dup_resolution_strategy', $this->dup_resolution_strategy, $fallback); + } + + public function distanceRounding(): string + { + return $this->getString('distance_rounding', $this->distance_rounding, 'CEIL') ?? 'CEIL'; + } + + public function minDistanceKm(): ?int + { + return $this->getInt('min_distance_km', $this->min_distance_km, 1); + } + + public function nilCountsAsPenalty(): bool + { + return $this->nil_qso_policy === 'PENALTY'; + } + + public function bustedCallCountsAsPenalty(): bool + { + return $this->busted_call_policy === 'PENALTY'; + } + + public function bustedExchangeCountsAsPenalty(): bool + { + return $this->busted_exchange_policy === 'PENALTY'; + } + +} diff --git a/app/Models/EvaluationRun.php b/app/Models/EvaluationRun.php new file mode 100644 index 0000000..65b1b53 --- /dev/null +++ b/app/Models/EvaluationRun.php @@ -0,0 +1,95 @@ + 'integer', + 'rule_set_id' => 'integer', + 'is_official' => 'boolean', + 'result_type' => 'string', + 'progress_total' => 'integer', + 'progress_done' => 'integer', + 'scope' => 'array', + 'started_at' => 'datetime', + 'finished_at' => 'datetime', + ]; + + public function round(): BelongsTo + { + return $this->belongsTo(Round::class); + } + + public function logResults(): HasMany + { + return $this->hasMany(LogResult::class); + } + + public function qsoResults(): HasMany + { + return $this->hasMany(QsoResult::class); + } + + public function evaluationLocks(): HasMany + { + return $this->hasMany(EvaluationLock::class); + } + + public function events(): HasMany + { + return $this->hasMany(EvaluationRunEvent::class); + } + + public function ruleSet(): BelongsTo + { + return $this->belongsTo(EvaluationRuleSet::class, 'rule_set_id'); + } + + public function createdBy(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by_user_id'); + } + + public function isCanceled(): bool + { + return strtoupper((string) $this->status) === 'CANCELED'; + } + + public static function isCanceledRun(int $runId): bool + { + return static::where('id', $runId)->value('status') === 'CANCELED'; + } +} diff --git a/app/Models/EvaluationRunEvent.php b/app/Models/EvaluationRunEvent.php new file mode 100644 index 0000000..1975a8d --- /dev/null +++ b/app/Models/EvaluationRunEvent.php @@ -0,0 +1,73 @@ + 'integer', + 'context' => 'array', + ]; + + public function evaluationRun(): BelongsTo + { + return $this->belongsTo(EvaluationRun::class); + } +} diff --git a/app/Models/File.php b/app/Models/File.php new file mode 100644 index 0000000..9090a64 --- /dev/null +++ b/app/Models/File.php @@ -0,0 +1,20 @@ + 'integer', + 'file_id' => 'integer', + + 'accepted' => 'boolean', + 'processed' => 'boolean', + + 'power_watt' => 'float', + 'power_category_id' => 'integer', + 'sixhr_category' => 'boolean', + + 'claimed_qso_count' => 'integer', + 'claimed_score' => 'integer', + ]; + + public function round(): BelongsTo + { + return $this->belongsTo(Round::class); + } + + public function file(): BelongsTo + { + return $this->belongsTo(File::class); + } + + public function qsos(): HasMany + { + return $this->hasMany(LogQso::class); + } + + public function logResults(): HasMany + { + return $this->hasMany(LogResult::class); + } + + public function powerCategory(): BelongsTo + { + return $this->belongsTo(PowerCategory::class); + } +} diff --git a/app/Models/LogOverride.php b/app/Models/LogOverride.php new file mode 100644 index 0000000..d7da3d5 --- /dev/null +++ b/app/Models/LogOverride.php @@ -0,0 +1,70 @@ + 'integer', + 'log_id' => 'integer', + 'forced_band_id' => 'integer', + 'forced_category_id' => 'integer', + 'forced_power_category_id' => 'integer', + 'forced_sixhr_category' => 'boolean', + 'forced_power_w' => 'integer', + 'context' => 'array', + 'created_by_user_id' => 'integer', + ]; + + public function evaluationRun(): BelongsTo + { + return $this->belongsTo(EvaluationRun::class); + } + + public function log(): BelongsTo + { + return $this->belongsTo(Log::class); + } + + public function forcedBand(): BelongsTo + { + return $this->belongsTo(Band::class, 'forced_band_id'); + } + + public function forcedCategory(): BelongsTo + { + return $this->belongsTo(Category::class, 'forced_category_id'); + } + + public function forcedPowerCategory(): BelongsTo + { + return $this->belongsTo(PowerCategory::class, 'forced_power_category_id'); + } + + public function createdByUser(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by_user_id'); + } +} diff --git a/app/Models/LogQso.php b/app/Models/LogQso.php new file mode 100644 index 0000000..959a137 --- /dev/null +++ b/app/Models/LogQso.php @@ -0,0 +1,65 @@ + 'integer', + 'qso_index' => 'integer', + + 'time_on' => 'datetime', + 'freq_khz' => 'integer', + 'points' => 'integer', + + 'new_exchange'=> 'boolean', + 'new_wwl' => 'boolean', + 'new_dxcc' => 'boolean', + 'duplicate_qso'=> 'boolean', + ]; + + public function log(): BelongsTo + { + return $this->belongsTo(Log::class); + } +} diff --git a/app/Models/LogResult.php b/app/Models/LogResult.php new file mode 100644 index 0000000..5db832b --- /dev/null +++ b/app/Models/LogResult.php @@ -0,0 +1,125 @@ + 'integer', + 'log_id' => 'integer', + 'band_id' => 'integer', + 'category_id' => 'integer', + 'power_category_id' => 'integer', + 'sixhr_category' => 'boolean', + 'sixhr_ranking_bucket' => 'string', + 'operating_window_start_utc' => 'datetime', + 'operating_window_end_utc' => 'datetime', + 'operating_window_2_start_utc' => 'datetime', + 'operating_window_2_end_utc' => 'datetime', + 'operating_window_hours' => 'integer', + 'operating_window_qso_count' => 'integer', + + 'claimed_qso_count' => 'integer', + 'claimed_score' => 'integer', + + 'valid_qso_count' => 'integer', + 'dupe_qso_count' => 'integer', + 'busted_qso_count' => 'integer', + 'other_error_qso_count' => 'integer', + 'total_qso_count' => 'integer', + 'discarded_qso_count' => 'integer', + 'discarded_points' => 'integer', + 'discarded_qso_percent' => 'float', + 'unique_qso_count' => 'integer', + + 'official_score' => 'integer', + 'penalty_score' => 'integer', + 'base_score' => 'integer', + 'multiplier_count' => 'integer', + 'multiplier_score' => 'integer', + 'score_per_qso' => 'float', + + 'rank_overall' => 'integer', + 'rank_in_category' => 'integer', + 'rank_overall_ok' => 'integer', + 'rank_in_category_ok' => 'integer', + ]; + + public function evaluationRun(): BelongsTo + { + return $this->belongsTo(EvaluationRun::class); + } + + public function log(): BelongsTo + { + return $this->belongsTo(Log::class); + } + + public function band(): BelongsTo + { + return $this->belongsTo(Band::class); + } + + public function category(): BelongsTo + { + return $this->belongsTo(Category::class); + } + + public function powerCategory(): BelongsTo + { + return $this->belongsTo(PowerCategory::class); + } +} diff --git a/app/Models/NewsPost.php b/app/Models/NewsPost.php new file mode 100644 index 0000000..936cca1 --- /dev/null +++ b/app/Models/NewsPost.php @@ -0,0 +1,52 @@ + 'boolean', + 'published_at' => 'datetime', + 'author_id' => 'integer', + ]; + + public function author(): BelongsTo + { + return $this->belongsTo(User::class, 'author_id'); + } + + // route model binding přes slug (pro /api/news/{slug}) + public function getRouteKeyName(): string + { + return 'slug'; + } +} diff --git a/app/Models/PowerCategory.php b/app/Models/PowerCategory.php new file mode 100644 index 0000000..318f9b1 --- /dev/null +++ b/app/Models/PowerCategory.php @@ -0,0 +1,19 @@ + 'integer', + 'log_qso_id' => 'integer', + 'forced_matched_log_qso_id' => 'integer', + 'forced_points' => 'float', + 'forced_penalty' => 'float', + 'context' => 'array', + 'created_by_user_id' => 'integer', + ]; + + public function evaluationRun(): BelongsTo + { + return $this->belongsTo(EvaluationRun::class); + } + + public function logQso(): BelongsTo + { + return $this->belongsTo(LogQso::class); + } + + public function forcedMatchedLogQso(): BelongsTo + { + return $this->belongsTo(LogQso::class, 'forced_matched_log_qso_id'); + } + + public function createdByUser(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by_user_id'); + } +} diff --git a/app/Models/QsoResult.php b/app/Models/QsoResult.php new file mode 100644 index 0000000..c127de3 --- /dev/null +++ b/app/Models/QsoResult.php @@ -0,0 +1,92 @@ + 'integer', + 'log_qso_id' => 'integer', + + 'is_valid' => 'boolean', + 'is_duplicate' => 'boolean', + 'is_nil' => 'boolean', + 'is_busted_call' => 'boolean', + 'is_busted_rst' => 'boolean', + 'is_busted_exchange' => 'boolean', + 'is_time_out_of_window' => 'boolean', + 'is_operating_window_excluded' => 'boolean', + + 'points' => 'integer', + 'penalty_points' => 'integer', + 'distance_km' => 'float', + 'time_diff_sec' => 'integer', + + 'matched_qso_id' => 'integer', + 'matched_log_qso_id' => 'integer', + 'error_flags' => 'array', + ]; + + public function evaluationRun(): BelongsTo + { + return $this->belongsTo(EvaluationRun::class); + } + + public function logQso(): BelongsTo + { + return $this->belongsTo(LogQso::class); + } + + public function matchedQso(): BelongsTo + { + return $this->belongsTo(LogQso::class, 'matched_qso_id'); + } + + public function workingQso(): HasOne + { + return $this->hasOne(WorkingQso::class, 'log_qso_id', 'log_qso_id'); + } +} diff --git a/app/Models/Round.php b/app/Models/Round.php new file mode 100644 index 0000000..cd49aa7 --- /dev/null +++ b/app/Models/Round.php @@ -0,0 +1,97 @@ + 'integer', + 'rule_set_id' => 'integer', + 'preliminary_evaluation_run_id' => 'integer', + 'official_evaluation_run_id' => 'integer', + 'test_evaluation_run_id' => 'integer', + + 'name' => 'array', + 'description' => 'array', + + 'is_active' => 'boolean', + 'is_test' => 'boolean', + 'is_sixhr' => 'boolean', + + 'start_time' => 'datetime', + 'end_time' => 'datetime', + 'logs_deadline' => 'datetime', + + 'first_check' => 'datetime', + 'second_check' => 'datetime', + 'unique_qso_check' => 'datetime', + 'third_check' => 'datetime', + 'fourth_check' => 'datetime', + 'prelimitary_results'=> 'datetime', + ]; + + public function contest(): BelongsTo + { + return $this->belongsTo(Contest::class); + } + + public function ruleSet(): BelongsTo + { + return $this->belongsTo(EvaluationRuleSet::class, 'rule_set_id'); + } + + public function bands(): BelongsToMany + { + return $this->belongsToMany(Band::class, 'rounds_bands', 'round_id', 'band_id'); + } + + public function categories(): BelongsToMany + { + return $this->belongsToMany(Category::class, 'rounds_categories', 'round_id', 'category_id'); + } + + public function powerCategories(): BelongsToMany + { + return $this->belongsToMany(PowerCategory::class, 'rounds_power_categories', 'round_id', 'power_category_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..23163b0 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,53 @@ + */ + use HasFactory, Notifiable, HasApiTokens; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'email', + 'password', + 'is_admin', + 'is_active', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'is_admin' => 'boolean', + 'is_active' => 'boolean', + ]; + } +} diff --git a/app/Models/WorkingQso.php b/app/Models/WorkingQso.php new file mode 100644 index 0000000..929e42d --- /dev/null +++ b/app/Models/WorkingQso.php @@ -0,0 +1,56 @@ + 'integer', + 'log_qso_id' => 'integer', + 'log_id' => 'integer', + 'band_id' => 'integer', + 'out_of_window' => 'boolean', + 'errors' => 'array', + 'ts_utc' => 'datetime', + ]; + + public function evaluationRun(): BelongsTo + { + return $this->belongsTo(EvaluationRun::class); + } + + public function logQso(): BelongsTo + { + return $this->belongsTo(LogQso::class); + } + + public function log(): BelongsTo + { + return $this->belongsTo(Log::class); + } +} diff --git a/app/Policies/BandPolicy.php b/app/Policies/BandPolicy.php new file mode 100644 index 0000000..b6e2511 --- /dev/null +++ b/app/Policies/BandPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, Band $band): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, Band $band): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/CategoryPolicy.php b/app/Policies/CategoryPolicy.php new file mode 100644 index 0000000..0d2de87 --- /dev/null +++ b/app/Policies/CategoryPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, Category $category): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, Category $category): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/ContestParameterPolicy.php b/app/Policies/ContestParameterPolicy.php new file mode 100644 index 0000000..fe61e3c --- /dev/null +++ b/app/Policies/ContestParameterPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, ContestParameter $contestParameter): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, ContestParameter $contestParameter): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/ContestPolicy.php b/app/Policies/ContestPolicy.php new file mode 100644 index 0000000..e59799a --- /dev/null +++ b/app/Policies/ContestPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, Contest $contest): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, Contest $contest): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/CountryWwlPolicy.php b/app/Policies/CountryWwlPolicy.php new file mode 100644 index 0000000..2c7b4b8 --- /dev/null +++ b/app/Policies/CountryWwlPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, CountryWwl $countryWwl): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, CountryWwl $countryWwl): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/CtyPolicy.php b/app/Policies/CtyPolicy.php new file mode 100644 index 0000000..4b52cef --- /dev/null +++ b/app/Policies/CtyPolicy.php @@ -0,0 +1,26 @@ +is_admin; + } + + public function update(User $user, Cty $cty): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, Cty $cty): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/EdiBandPolicy.php b/app/Policies/EdiBandPolicy.php new file mode 100644 index 0000000..314ceb4 --- /dev/null +++ b/app/Policies/EdiBandPolicy.php @@ -0,0 +1,26 @@ +is_admin; + } + + public function update(User $user, EdiBand $edi_band): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, EdiBand $edi_band): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/EdiCategoryPolicy.php b/app/Policies/EdiCategoryPolicy.php new file mode 100644 index 0000000..4b9666b --- /dev/null +++ b/app/Policies/EdiCategoryPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, EdiCategory $edi_category): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, EdiCategory $edi_category): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/EvaluationRuleSetPolicy.php b/app/Policies/EvaluationRuleSetPolicy.php new file mode 100644 index 0000000..2ebe836 --- /dev/null +++ b/app/Policies/EvaluationRuleSetPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, EvaluationRuleSet $evaluationRuleSet): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, EvaluationRuleSet $evaluationRuleSet): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/EvaluationRunPolicy.php b/app/Policies/EvaluationRunPolicy.php new file mode 100644 index 0000000..5159f86 --- /dev/null +++ b/app/Policies/EvaluationRunPolicy.php @@ -0,0 +1,26 @@ +is_admin; + } + + public function delete(User $user, EvaluationRun $evaluationRun): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/FilePolicy.php b/app/Policies/FilePolicy.php new file mode 100644 index 0000000..9575b92 --- /dev/null +++ b/app/Policies/FilePolicy.php @@ -0,0 +1,20 @@ +is_admin; + } + + public function delete(User $user, File $file): bool + { + return (bool) $user->is_admin; + } + +} diff --git a/app/Policies/LogOverridePolicy.php b/app/Policies/LogOverridePolicy.php new file mode 100644 index 0000000..07a9c8f --- /dev/null +++ b/app/Policies/LogOverridePolicy.php @@ -0,0 +1,24 @@ +is_admin; + } + + public function update(User $user, LogOverride $logOverride): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, LogOverride $logOverride): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/LogPolicy.php b/app/Policies/LogPolicy.php new file mode 100644 index 0000000..bfa11eb --- /dev/null +++ b/app/Policies/LogPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, Log $log): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, Log $log): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/LogQsoPolicy.php b/app/Policies/LogQsoPolicy.php new file mode 100644 index 0000000..a2ef30d --- /dev/null +++ b/app/Policies/LogQsoPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, LogQso $logQso): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, LogQso $logQso): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/LogResultPolicy.php b/app/Policies/LogResultPolicy.php new file mode 100644 index 0000000..af1b720 --- /dev/null +++ b/app/Policies/LogResultPolicy.php @@ -0,0 +1,26 @@ +is_admin; + } + + public function update(User $user, LogResult $logResult): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, LogResult $logResult): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/NewsPostPolicy.php b/app/Policies/NewsPostPolicy.php new file mode 100644 index 0000000..342e96b --- /dev/null +++ b/app/Policies/NewsPostPolicy.php @@ -0,0 +1,25 @@ +is_admin; // nebo kontrola role + } + + public function update(User $user, NewsPost $news): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, NewsPost $news): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/PowerCategoryPolicy.php b/app/Policies/PowerCategoryPolicy.php new file mode 100644 index 0000000..7b23834 --- /dev/null +++ b/app/Policies/PowerCategoryPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, PowerCategory $powerCategory): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, PowerCategory $powerCategory): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/QsoOverridePolicy.php b/app/Policies/QsoOverridePolicy.php new file mode 100644 index 0000000..f1c5507 --- /dev/null +++ b/app/Policies/QsoOverridePolicy.php @@ -0,0 +1,24 @@ +is_admin; + } + + public function update(User $user, QsoOverride $qsoOverride): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, QsoOverride $qsoOverride): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/QsoResultPolicy.php b/app/Policies/QsoResultPolicy.php new file mode 100644 index 0000000..a672454 --- /dev/null +++ b/app/Policies/QsoResultPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, QsoResult $qsoResult): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, QsoResult $qsoResult): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/RoundPolicy.php b/app/Policies/RoundPolicy.php new file mode 100644 index 0000000..4dd01fb --- /dev/null +++ b/app/Policies/RoundPolicy.php @@ -0,0 +1,25 @@ +is_admin; + } + + public function update(User $user, Round $round): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, Round $round): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php new file mode 100644 index 0000000..4dfdfce --- /dev/null +++ b/app/Policies/UserPolicy.php @@ -0,0 +1,33 @@ +is_admin; + } + + public function view(User $user, User $model): bool + { + return (bool) $user->is_admin; + } + + public function create(User $user): bool + { + return (bool) $user->is_admin; + } + + public function update(User $user, User $model): bool + { + return (bool) $user->is_admin; + } + + public function delete(User $user, User $model): bool + { + return (bool) $user->is_admin; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..3fbca73 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,26 @@ +input(Fortify::username())).'|'.$request->ip()); + + return Limit::perMinute(5)->by($throttleKey); + }); + + RateLimiter::for('two-factor', function (Request $request) { + return Limit::perMinute(5)->by($request->session()->get('login.id')); + }); + } +} diff --git a/app/Services/Evaluation/ClaimedRunResolver.php b/app/Services/Evaluation/ClaimedRunResolver.php new file mode 100644 index 0000000..0922b45 --- /dev/null +++ b/app/Services/Evaluation/ClaimedRunResolver.php @@ -0,0 +1,36 @@ +where('rules_version', 'CLAIMED') + ->orderByDesc('created_at') + ->first(); + + if ($run) { + return $run; + } + + return self::createNewForRound($roundId); + } + + public static function createNewForRound(int $roundId, ?int $createdByUserId = null): EvaluationRun + { + // CLAIMED run je projekce deklarovaných výsledků, ne finální vyhodnocení. + return EvaluationRun::create([ + 'round_id' => $roundId, + 'rules_version' => 'CLAIMED', + 'name' => 'Deklarované výsledky', + 'is_official' => false, + 'status' => 'PENDING', + 'created_by_user_id' => $createdByUserId, + ]); + } +} diff --git a/app/Services/Evaluation/EdiParserService.php b/app/Services/Evaluation/EdiParserService.php new file mode 100644 index 0000000..6f6ae61 --- /dev/null +++ b/app/Services/Evaluation/EdiParserService.php @@ -0,0 +1,380 @@ + stejný výstup. + * - Musí být idempotentní z pohledu pipeline (opakované parsování + * nesmí vytvářet nekonzistentní data). + * - Implementace má striktně vycházet ze specifikace REG1TEST. + * - Veškeré heuristiky nebo odchylky od specifikace musí být explicitní + * a dobře zdokumentované. + */ +class EdiParserService +{ + /** + * Služba je bezstavová (stateless). + * + * Všechny závislosti (např. helpery pro validaci, mapování pásma, + * případně přístup ke konfiguraci REG1TEST) mají být injektovány + * přes DI container. + */ + public function __construct() + { + // + } + + /** + * Naparsuje EDI soubor a zapíše/aktualizuje data v Log a LogQso. + * Založeno na původní logice z LogController::parseUploadedFile. + */ + public function parseLogFile(Log $log, string $path): void + { + if (! Storage::exists($path)) { + return; + } + + // Parser pracuje se souborem už uloženým ve storage. + $content = $this->sanitizeToUtf8(Storage::get($path)); + $trimmed = ltrim($content); + + // REG1TEST je jediný podporovaný formát v tomto parseru. + if (! str_starts_with($trimmed, '[REG1TEST')) { + return; + } + + $lines = preg_split('/\r\n|\r|\n/', $content); + $headerLines = []; + $data = []; + $remarksLines = []; + $section = 'header'; + + // Rozdělení do sekcí: header / remarks / QSORecords. + foreach ($lines as $line) { + if (str_starts_with($line, '[QSORecords')) { + $section = 'qso'; + continue; + } + if (str_starts_with($line, '[Remarks')) { + $section = 'remarks'; + continue; + } + + if ($section === 'header') { + $headerLines[] = $line; + $trimmedLine = trim($line); + if (preg_match('/^([A-Za-z0-9]+)=(.*)$/', $trimmedLine, $m)) { + $key = mb_strtoupper($m[1]); + $val = trim($m[2]); + $data[$key] = $val; + } + continue; + } + + if ($section === 'remarks') { + $remarksLines[] = $line; + continue; + } + } + + // Mapování header klíčů na sloupce Log. + $update = []; + $map = [ + 'TNAME' => 'tname', + 'TDATE' => 'tdate', + 'PCALL' => 'pcall', + 'PWWLO' => 'pwwlo', + 'PEXCH' => 'pexch', + 'PSECT' => 'psect', + 'PBAND' => 'pband', + 'PCLUB' => 'pclub', + 'PADR1' => 'padr1', + 'PADR2' => 'padr2', + 'RNAME' => 'rname', + 'RCALL' => 'rcall', + 'RCOUN' => 'rcoun', + 'LOCATOR' => 'locator', + 'RADR1' => 'radr1', + 'RADR2' => 'radr2', + 'RPOCO' => 'rpoco', + 'RCITY' => 'rcity', + 'RPHON' => 'rphon', + 'RHBBS' => 'rhbbs', + 'MOPE1' => 'mope1', + 'MOPE2' => 'mope2', + 'STXEQ' => 'stxeq', + 'SRXEQ' => 'srxeq', + 'SANTE' => 'sante', + 'SANTH' => 'santh', + 'CQSOS' => 'cqsos', + 'CQSOP' => 'cqsop', + 'CWWLS' => 'cwwls', + 'CWWLB' => 'cwwlb', + 'CEXCS' => 'cexcs', + 'CEXCB' => 'cexcb', + 'CDXCS' => 'cdxcs', + 'CDXCB' => 'cdxcb', + 'CTOSC' => 'ctosc', + 'CODXC' => 'codxc', + ]; + + foreach ($map as $ediKey => $dbKey) { + if (isset($data[$ediKey]) && $data[$ediKey] !== '') { + $update[$dbKey] = $data[$ediKey]; + } + } + + // Detekce 6H kategorie z PSect (založeno na tokenu v hlavičce). + if (isset($data['PSECT']) + && (stripos($data['PSECT'], '6H') !== false + || stripos($data['PSECT'], '6') !== false) + ) { + $update['sixhr_category'] = true; + } + + $rawHeader = implode("\n", $headerLines); + if ($rawHeader !== '') { + $update['raw_header'] = $rawHeader; + } + + if (! empty($update)) { + $log->fill($update); + $log->save(); + } + + // Claims / totals jsou deklarované hodnoty účastníka v hlavičce. + $claimedQsoRaw = $data['CQSOS'] ?? $log->cqsos ?? null; + if ($claimedQsoRaw !== null && $log->claimed_qso_count === null) { + $parts = explode(';', (string) $claimedQsoRaw); + $log->claimed_qso_count = isset($parts[0]) ? (int) $parts[0] : null; + } + $claimedScoreRaw = $data['CTOSC'] ?? $log->ctosc ?? null; + if ($claimedScoreRaw !== null && $log->claimed_score === null) { + $log->claimed_score = is_numeric($claimedScoreRaw) ? (int) $claimedScoreRaw : null; + } + $claimedWwlRaw = $data['CWWLS'] ?? $log->cwwls ?? null; + if ($claimedWwlRaw !== null && $log->claimed_wwl === null) { + $parts = explode(';', (string) $claimedWwlRaw); + $log->claimed_wwl = isset($parts[0]) ? $parts[0] : null; + } + $claimedDxccRaw = $data['CDXCS'] ?? $log->cdxcs ?? null; + if ($claimedDxccRaw !== null && $log->claimed_dxcc === null) { + $parts = explode(';', (string) $claimedDxccRaw); + $log->claimed_dxcc = isset($parts[0]) ? $parts[0] : null; + } + // SPowe se ukládá jako výkon ve wattech, akceptujeme i desetinný formát 0,5 / 0.5. + if (isset($data['SPOWE'])) { + $parsedPower = $this->parseSpoweValue($data['SPOWE']); + if ($parsedPower !== null) { + $log->power_watt = $parsedPower; + } + } + if (isset($data['PSECT'])) { + // Power kategorie se odvozuje z PSect (fallback z výkonu). + $powerName = $this->extractPowerFromPsect($data['PSECT'], $log->power_watt) ?? 'A'; + $log->power_category = $powerName; + $log->power_category_id = PowerCategory::whereRaw('UPPER(name) = ?', [mb_strtoupper($powerName)]) + ->value('id'); + } + $log->save(); + + if (! empty($remarksLines)) { + $log->remarks = implode("\n", $remarksLines); + $log->save(); + } + + // Smazat staré QSO a znovu naparsovat QSORecords (idempotence). + LogQso::where('log_id', $log->id)->delete(); + + $qsoIndex = 1; + $qsoStarted = false; + + foreach ($lines as $line) { + if (str_starts_with($line, '[QSORecords')) { + $qsoStarted = true; + continue; + } + if (str_starts_with($line, '[END')) { + break; + } + if (! $qsoStarted) { + continue; + } + if (trim($line) === '' || str_starts_with($line, '[')) { + continue; + } + + // QSO záznam je oddělený středníky podle REG1TEST. + $parts = explode(';', $line); + if (count($parts) < 4) { + continue; + } + + $dateRaw = $parts[0] ?? ''; + $timeRaw = $parts[1] ?? ''; + + $timeOn = null; + if (strlen($dateRaw) === 6 && strlen($timeRaw) >= 3) { + $year = substr($dateRaw, 0, 2); + $month = substr($dateRaw, 2, 2); + $day = substr($dateRaw, 4, 2); + $timeRaw = str_pad($timeRaw, 4, '0', STR_PAD_LEFT); + $hour = substr($timeRaw, 0, 2); + $minute = substr($timeRaw, 2, 2); + $yearFull = 2000 + (int) $year; + $validDate = checkdate((int) $month, (int) $day, $yearFull); + $validTime = (int) $hour >= 0 && (int) $hour <= 23 && (int) $minute >= 0 && (int) $minute <= 59; + if ($validDate && $validTime) { + $timeOn = sprintf('20%s-%s-%s %s:%s:00', $year, $month, $day, $hour, $minute); + } + } + + $qsoData = [ + 'log_id' => $log->id, + 'qso_index' => $qsoIndex++, + 'time_on' => $timeOn, + 'band' => $log->pband, + 'freq_khz' => null, + 'my_call' => $log->pcall, + 'my_locator' => $log->pwwlo, + 'mode_code' => $parts[3] ?? null, + 'dx_call' => $parts[2] ?? null, + 'my_rst' => $parts[4] ?? null, + 'my_serial' => $parts[5] ?? null, + 'dx_rst' => $parts[6] ?? null, + 'dx_serial' => $parts[7] ?? null, + 'rx_exchange' => $parts[8] ?? null, + 'rx_wwl' => $parts[9] ?? null, + 'points' => isset($parts[10]) ? (int) $parts[10] : null, + 'new_exchange'=> (isset($parts[11]) && strtoupper($parts[11]) === 'N') ? true : null, + 'new_wwl' => (isset($parts[12]) && strtoupper($parts[12]) === 'N') ? true : null, + 'new_dxcc' => (isset($parts[13]) && strtoupper($parts[13]) === 'N') ? true : null, + 'duplicate_qso'=> (isset($parts[14]) && strtoupper($parts[14]) === 'D') ? true : null, + 'raw_line' => $line, + ]; + + LogQso::create($qsoData); + } + } + + /** + * Snaží se převést obsah do validního UTF-8, ignoruje nevalidní sekvence. + */ + protected function sanitizeToUtf8(string $content): string + { + if (str_starts_with($content, "\xEF\xBB\xBF")) { + $content = substr($content, 3); + } + + $converted = @iconv('UTF-8', 'UTF-8//IGNORE', $content); + if ($converted === false) { + $converted = mb_convert_encoding($content, 'UTF-8', 'auto'); + } + + return $converted ?? ''; + } + + protected function extractPowerFromPsect(string $psect, ?float $powerWatt): ?string + { + $value = mb_strtoupper($psect); + if (preg_match('/\\bQRP\\b/', $value)) { + return 'QRP'; + } + if (preg_match('/\\bLP\\b/', $value)) { + return 'LP'; + } + if (preg_match('/\\bN\\b/', $value)) { + return 'N'; + } + if (preg_match('/\\bA\\b/', $value)) { + return 'A'; + } + if ($powerWatt !== null) { + $category = PowerCategory::whereNotNull('power_level') + ->where('power_level', '>=', $powerWatt) + ->orderBy('power_level') + ->first(); + if ($category && $category->name) { + return mb_strtoupper($category->name); + } + } + return null; + } + + protected function parseSpoweValue(?string $raw): ?float + { + $value = trim((string) $raw); + if ($value === '') { + return null; + } + + $compact = strtolower(preg_replace('/\s+/', '', $value) ?? ''); + $factor = 1.0; + if (str_ends_with($compact, 'kw')) { + $compact = substr($compact, 0, -2); + $factor = 1000.0; + } elseif (str_ends_with($compact, 'mw')) { + $compact = substr($compact, 0, -2); + $factor = 0.001; + } elseif (str_ends_with($compact, 'w')) { + $compact = substr($compact, 0, -1); + } + + $compact = str_replace(',', '.', $compact); + if (! preg_match('/^[0-9]*\\.?[0-9]+$/', $compact)) { + return null; + } + + $numeric = (float) $compact * $factor; + return is_finite($numeric) ? $numeric : null; + } +} diff --git a/app/Services/Evaluation/EvaluationCoordinator.php b/app/Services/Evaluation/EvaluationCoordinator.php new file mode 100644 index 0000000..268ae5c --- /dev/null +++ b/app/Services/Evaluation/EvaluationCoordinator.php @@ -0,0 +1,372 @@ + stejné výstupy). + * - Transakční hranice a bezpečné zápisy (staging vs finální tabulky). + * - Aktualizace stavu vyhodnocovacího běhu: + * - current_step + * - progress_total/progress_done + * - run events (EvaluationRunEvent) pro UI monitoring + * + * Co koordinátor nedělá: + * - Není to HTTP vrstva (žádné request/response). + * - Není to UI ani prezentace. + * - Nemá obsahovat detailní algoritmy parsingu/matchingu/scoringu; + * ty patří do dedikovaných služeb. + * + * Doporučené zásady implementace: + * - Všechny metody mají být navrženy tak, aby byly bezpečné pro opakované + * spuštění (idempotentní). + * - Vstupem je vždy identifikátor nebo instance EvaluationRun + volitelně scope. + * - Vracej strukturované výsledky (DTO/Value Objects) a drž zápisy do DB + * na jasně definovaných místech. + */ +class EvaluationCoordinator +{ + /** + * Koordinátor je typicky bezstavový (stateless) a jeho závislosti jsou + * injektované přes DI container. + * + * V praxi sem budou patřit služby typu EdiParserService, MatchingService, + * ScoringService, ResultsAggregationService a případně repozitáře. + */ + public function __construct() + { + // + } + + public function start(EvaluationRun $run): void + { + if ($run->isCanceled()) { + return; + } + $lockKey = $this->lockKey($run); + $lock = EvaluationLock::acquire( + key: $lockKey, + run: $run, + ttl: 7200 + ); + + if (! $lock) { + $run->update([ + 'status' => 'FAILED', + 'error' => 'Nelze spustit vyhodnocení: probíhá jiný běh pro stejné kolo.', + ]); + $this->event($run, 'error', 'StartEvaluationRunJob selhal: lock je držen jiným během.'); + return; + } + + $run->update(['batch_id' => null]); + $this->transition($run, $run->status, 'RUNNING', 'start', [ + 'started_at' => $run->started_at ?? now(), + ]); + $this->event($run, 'info', 'Spuštění vyhodnocení.', [ + 'step' => 'start', + 'round_id' => $run->round_id, + ]); + + Bus::chain([ + new PrepareRunJob($run->id), + new DispatchParseLogsJobsJob($run->id), + ])->catch(function (Throwable $e) use ($run, $lockKey) { + $this->fail($run, $e, $lockKey); + })->onQueue('evaluation')->dispatch(); + } + + public function resume(EvaluationRun $run, array $options = []): bool + { + if ($run->isCanceled()) { + return false; + } + $lockKey = $this->lockKey($run); + if (! EvaluationLock::isLocked($lockKey)) { + $lock = EvaluationLock::acquire( + key: $lockKey, + run: $run, + ttl: 7200 + ); + if (! $lock) { + return false; + } + } + + if ($run->status === 'WAITING_REVIEW_INPUT') { + $this->transition($run, 'WAITING_REVIEW_INPUT', 'RUNNING', 'resume_input'); + $this->event($run, 'info', 'Pokračování po kontrole vstupů.', [ + 'step' => 'resume_input', + 'round_id' => $run->round_id, + 'user_id' => auth()->id(), + ]); + + $jobs = []; + if (! empty($options['rebuild_working_set'])) { + $jobs[] = new DispatchBuildWorkingSetJobsJob($run->id); + } + $jobs[] = new DispatchMatchJobsJob($run->id); + + Bus::chain($jobs)->onQueue('evaluation')->dispatch(); + return true; + } + + if ($run->status === 'WAITING_REVIEW_MATCH') { + $this->transition($run, 'WAITING_REVIEW_MATCH', 'RUNNING', 'resume_match'); + $this->event($run, 'info', 'Pokračování po kontrole matchingu.', [ + 'step' => 'resume_match', + 'round_id' => $run->round_id, + 'user_id' => auth()->id(), + ]); + + DispatchScoreJobsJob::dispatch($run->id)->onQueue('evaluation'); + return true; + } + + if ($run->status === 'WAITING_REVIEW_SCORE') { + $this->transition($run, 'WAITING_REVIEW_SCORE', 'RUNNING', 'resume_score'); + $this->event($run, 'info', 'Pokračování po kontrole skóre.', [ + 'step' => 'resume_score', + 'round_id' => $run->round_id, + 'user_id' => auth()->id(), + ]); + + Bus::chain([ + new FinalizeRunJob($run->id, $lockKey), + ])->onQueue('evaluation')->dispatch(); + return true; + } + + return false; + } + + public function fail(EvaluationRun $run, Throwable $e, ?string $lockKey = null): void + { + $run->update([ + 'status' => 'FAILED', + 'error' => $e->getMessage(), + 'finished_at' => now(), + ]); + $this->event($run, 'error', "Evaluation run selhal: {$e->getMessage()}", [ + 'step' => 'chain', + 'round_id' => $run->round_id, + ]); + if ($lockKey) { + EvaluationLock::release($lockKey, $run); + } + } + + public function dispatchStep(EvaluationRun $run, string $step): void + { + if ($step === 'match') { + $this->dispatchMatch($run); + return; + } + if ($step === 'score') { + $this->dispatchScore($run); + } + } + + public function transition(EvaluationRun $run, string $from, string $to, ?string $step = null, array $extra = []): bool + { + if ($from !== '*' && $run->status !== $from) { + return false; + } + + $payload = array_merge([ + 'status' => $to, + ], $extra); + + if ($step !== null) { + $payload['current_step'] = $step; + } + + $run->update($payload); + + return true; + } + + public function event(EvaluationRun $run, string $level, string $message, array $context = []): void + { + EvaluationRunEvent::create([ + 'evaluation_run_id' => $run->id, + 'level' => $level, + 'message' => $message, + 'context' => $context, + ]); + } + + public function eventInfo(EvaluationRun $run, string $message, array $context = []): void + { + $this->event($run, 'info', $message, $context); + } + + public function eventWarn(EvaluationRun $run, string $message, array $context = []): void + { + $this->event($run, 'warning', $message, $context); + } + + public function eventError(EvaluationRun $run, string $message, array $context = []): void + { + $this->event($run, 'error', $message, $context); + } + + public function progressInit(EvaluationRun $run, int $total, int $done = 0): void + { + $run->update([ + 'progress_total' => $total, + 'progress_done' => $done, + ]); + } + + public function progressTick(EvaluationRun $run, int $n = 1): void + { + EvaluationRun::where('id', $run->id)->increment('progress_done', $n); + } + + protected function dispatchMatch(EvaluationRun $run): void + { + if ($run->isCanceled()) { + return; + } + $bandIds = $run->scope['band_ids'] ?? []; + if (! $bandIds) { + $bandIds = WorkingQso::where('evaluation_run_id', $run->id) + ->distinct() + ->pluck('band_id') + ->all(); + } + if (! $bandIds) { + $bandIds = [null]; + } + + $this->transition($run, $run->status, 'RUNNING', 'match'); + $jobs = []; + foreach ($bandIds as $bandId) { + $callNorms = WorkingQso::where('evaluation_run_id', $run->id) + ->when($bandId !== null, fn ($q) => $q->where('band_id', $bandId), fn ($q) => $q->whereNull('band_id')) + ->distinct() + ->pluck('call_norm') + ->all(); + + if (! $callNorms) { + continue; + } + + foreach ($callNorms as $callNorm) { + $jobs[] = new MatchQsoBucketJob($run->id, $bandId, $callNorm, 1); + $jobs[] = new MatchQsoBucketJob($run->id, $bandId, $callNorm, 2); + } + } + + $this->progressInit($run, count($jobs) + 2, 0); + $this->event($run, 'info', 'Spuštění matchingu.', [ + 'step' => 'match', + 'round_id' => $run->round_id, + 'step_progress_done' => 0, + 'step_progress_total' => count($jobs), + ]); + + $next = function () use ($run) { + Bus::chain([ + new DispatchUnpairedJobsJob($run->id), + ])->onQueue('evaluation')->dispatch(); + }; + + if (! $jobs) { + $next(); + return; + } + + $batch = Bus::batch($jobs) + ->then($next) + ->onQueue('evaluation') + ->dispatch(); + $run->update(['batch_id' => $batch->id]); + } + + protected function dispatchScore(EvaluationRun $run): void + { + if ($run->isCanceled()) { + return; + } + $groups = $run->scope['groups'] ?? [ + [ + 'key' => 'all', + 'band_id' => null, + 'category_id' => null, + 'power_category_id' => null, + ], + ]; + + $this->transition($run, $run->status, 'RUNNING', 'score'); + $this->progressInit($run, count($groups), 0); + $this->event($run, 'info', 'Spuštění scoringu.', [ + 'step' => 'score', + 'round_id' => $run->round_id, + 'step_progress_done' => 0, + 'step_progress_total' => count($groups), + ]); + + $jobs = []; + foreach ($groups as $group) { + $jobs[] = new \App\Jobs\ScoreGroupJob( + $run->id, + $group['key'] ?? 'all', + $group + ); + } + + $batch = Bus::batch($jobs) + ->then(function () use ($run) { + Bus::chain([ + new DispatchAggregateResultsJobsJob($run->id), + ])->onQueue('evaluation')->dispatch(); + }) + ->onQueue('evaluation') + ->dispatch(); + $run->update(['batch_id' => $batch->id]); + } + + protected function lockKey(EvaluationRun $run): string + { + return "evaluation:round:{$run->round_id}"; + } +} diff --git a/app/Services/Evaluation/MatchingService.php b/app/Services/Evaluation/MatchingService.php new file mode 100644 index 0000000..3cef33d --- /dev/null +++ b/app/Services/Evaluation/MatchingService.php @@ -0,0 +1,153 @@ + stejné párování a stejné příznaky. + * - Idempotence: opakované spuštění pro stejný run+group musí přepsat + * předchozí mezivýsledky bez duplicit. + * - Výkon: matching musí být navržen pro dávkové zpracování (chunking), + * s využitím indexů a minimalizací N+1 dotazů. + * - Pravidla: veškeré odchylky (např. ignorovat část značky za lomítkem, + * tolerovat třetí znak reportu) musí být řízené konfigurací (RuleSet/options) + * a explicitně testované. + */ +class MatchingService +{ + /** + * Služba je bezstavová (stateless). + * + * V praxi sem patří injektované závislosti, např.: + * - repository / query helpery pro čtení pracovních dat + * - helpery pro normalizaci značek/lokátorů + * - výpočet vzdálenosti (pokud je potřeba pro rozhodování) + * + * Matching algoritmus a jeho varianty mají být testované a řízené + * konfigurací soutěže (EvaluationRuleSet). + */ + public function __construct() + { + // + } + + /** + * Normalizuje volaci znak podle pravidel rulesetu. + * + * - STRICT: pouze trim + uppercase. + * - IGNORE_SUFFIX: odstrani suffix za lomitkem (/P, /M, /9...). + */ + public function normalizeCallsign(string $call, EvaluationRuleSet $ruleSet): string + { + $value = mb_strtoupper(trim($call)); + $value = preg_replace('/\s+/', '', $value) ?? ''; + + if ($ruleSet->ignoreSlashPart()) { + // Ignoruje portable suffixy (/P, /M, /9...), ale ponechá základ prefixu. + $value = $this->stripCallsignSuffix($value, $ruleSet->callsignSuffixMaxLen()); + } elseif ($ruleSet->ignoreThirdPart()) { + // Zachová první dvě části (např. OK1ABC/1), zbytek odřízne. + $parts = explode('/', $value); + if (count($parts) > 2) { + $value = $parts[0] . '/' . $parts[1]; + } + } elseif ($ruleSet->callsign_normalization === 'IGNORE_SUFFIX') { + $value = $this->stripCallsignSuffix($value, $ruleSet->callsignSuffixMaxLen()); + } + + return $value; + } + + protected function stripCallsignSuffix(string $value, int $maxLen): string + { + $parts = explode('/', $value); + if (count($parts) < 2) { + return $value; + } + + $suffix = end($parts) ?: ''; + if ($suffix !== '' && mb_strlen($suffix) <= $maxLen) { + array_pop($parts); + return implode('/', $parts); + } + + return $value; + } + + /** + * Rozhodne, zda jsou dve QSO ve stejnem dupe scope. + * MVP: BAND nebo BAND_MODE. + */ + public function isSameDupeScope(array $qsoA, array $qsoB, EvaluationRuleSet $ruleSet): bool + { + // Dupe scope určuje, zda se duplicity řeší jen v pásmu nebo i v rámci módu. + $bandMatch = ($qsoA['band_id'] ?? null) === ($qsoB['band_id'] ?? null); + if (! $bandMatch) { + return false; + } + + if ($ruleSet->dupe_scope === 'BAND_MODE') { + return ($qsoA['mode'] ?? null) === ($qsoB['mode'] ?? null); + } + + return true; + } + + /** + * Vyhodnoti, zda QSO spada mimo casove okno kola. + * MVP: pouze vraci boolean pro dalsi zpracovani. + */ + public function isOutOfWindow(?\DateTimeInterface $qsoTime, ?\DateTimeInterface $start, ?\DateTimeInterface $end): bool + { + if (! $qsoTime || ! $start || ! $end) { + return false; + } + + // True => další kroky rozhodnou, zda QSO zneplatnit, penalizovat nebo DQ log. + return $qsoTime < $start || $qsoTime > $end; + } +} diff --git a/app/Services/Evaluation/OperatingWindowService.php b/app/Services/Evaluation/OperatingWindowService.php new file mode 100644 index 0000000..bf75fd5 --- /dev/null +++ b/app/Services/Evaluation/OperatingWindowService.php @@ -0,0 +1,397 @@ += 2h mezi nimi. + * - Součet délek segmentů (end-start) <= N hodin. + * - Skóre segmentů odpovídá agregaci (body + penalizace + multiplikátory jen z okna). + * - Deterministický výběr: skóre desc, start asc, QSO desc, start log_qso_id asc. + * + * @return array{startUtc: Carbon, endUtc: Carbon, secondStartUtc: ?Carbon, secondEndUtc: ?Carbon, includedLogQsoIds: int[], qsoCount: int}|null + */ + public function pickBestOperatingWindow( + int $evaluationRunId, + int $logId, + int $hours, + EvaluationRuleSet $ruleSet + ): ?array { + $rows = QsoResult::query() + ->where('qso_results.evaluation_run_id', $evaluationRunId) + ->join('log_qsos', 'log_qsos.id', '=', 'qso_results.log_qso_id') + ->join('working_qsos', function ($join) use ($evaluationRunId) { + $join->on('working_qsos.log_qso_id', '=', 'qso_results.log_qso_id') + ->where('working_qsos.evaluation_run_id', '=', $evaluationRunId); + }) + ->where('log_qsos.log_id', $logId) + ->where('qso_results.is_valid', true) + ->whereNotNull('working_qsos.ts_utc') + ->orderBy('working_qsos.ts_utc') + ->orderBy('qso_results.log_qso_id') + ->get([ + 'qso_results.log_qso_id', + 'qso_results.points', + 'qso_results.penalty_points', + 'qso_results.error_code', + 'qso_results.error_side', + 'qso_results.is_nil', + 'qso_results.is_duplicate', + 'qso_results.is_busted_call', + 'qso_results.is_busted_rst', + 'qso_results.is_busted_exchange', + 'qso_results.is_time_out_of_window', + 'qso_results.matched_qso_id', + 'qso_results.wwl', + 'qso_results.dxcc', + 'qso_results.country', + 'qso_results.section', + 'working_qsos.band_id as band_id', + 'working_qsos.ts_utc as ts_utc', + ]); + + if ($rows->isEmpty()) { + return null; + } + + $items = []; + foreach ($rows as $row) { + $ts = Carbon::parse($row->ts_utc, 'UTC')->getTimestamp(); + $errorCode = $row->error_code; + $errorSide = $row->error_side ?? 'NONE'; + + $isNil = (bool) $row->is_nil + || in_array($errorCode, [QsoErrorCode::NOT_IN_COUNTERPART_LOG, QsoErrorCode::NO_COUNTERPART_LOG], true); + $isUnique = $errorCode === QsoErrorCode::UNIQUE; + $isDuplicate = (bool) $row->is_duplicate || $errorCode === QsoErrorCode::DUP; + $isBusted = (bool) $row->is_busted_call + || (bool) $row->is_busted_rst + || (bool) $row->is_busted_exchange + || (in_array($errorCode, [ + QsoErrorCode::BUSTED_CALL, + QsoErrorCode::BUSTED_RST, + QsoErrorCode::BUSTED_SERIAL, + QsoErrorCode::BUSTED_LOCATOR, + ], true) + && $errorSide !== 'TX'); + $isOutOfWindow = (bool) $row->is_time_out_of_window; + + $eligibleForMultiplier = false; + if ($ruleSet->usesMultipliers()) { + if ($ruleSet->multiplier_source === 'VALID_ONLY') { + $eligibleForMultiplier = ! $isNil && ! $isDuplicate && ! $isBusted && ! $isOutOfWindow; + } elseif ($ruleSet->multiplier_source === 'ALL_MATCHED') { + $eligibleForMultiplier = $row->matched_qso_id !== null + && ! $isNil + && ! $isDuplicate + && ! $isBusted + && ! $isOutOfWindow; + } + } + + $multiplierValue = null; + if ($eligibleForMultiplier) { + if ($ruleSet->multiplier_type === 'WWL') { + $multiplierValue = $row->wwl; + } elseif ($ruleSet->multiplier_type === 'DXCC') { + $multiplierValue = $row->dxcc; + } elseif ($ruleSet->multiplier_type === 'COUNTRY') { + $multiplierValue = $row->country; + } elseif ($ruleSet->multiplier_type === 'SECTION') { + $multiplierValue = $row->section; + } + } + + $bandKey = $ruleSet->multiplier_scope === 'PER_BAND' + ? (int) ($row->band_id ?? 0) + : 0; + + $items[] = [ + 'log_qso_id' => (int) $row->log_qso_id, + 'ts' => $ts, + 'points' => (int) ($row->points ?? 0), + 'penalty_points' => (int) ($row->penalty_points ?? 0), + 'multiplier_eligible' => $eligibleForMultiplier && $multiplierValue, + 'multiplier_value' => $multiplierValue, + 'multiplier_band_key' => $bandKey, + ]; + } + + $windowSeconds = $hours * 3600; + $intervals = $this->buildIntervals($items, $windowSeconds, $ruleSet); + if (! $intervals) { + return null; + } + + $bestSingle = null; + foreach ($intervals as $interval) { + $candidate = [ + 'score' => $interval['score'], + 'start_ts' => $interval['start_ts'], + 'end_ts' => $interval['end_ts'], + 'qso_count' => $interval['qso_count'], + 'start_log_qso_id' => $interval['start_log_qso_id'], + 'segment1' => $interval, + 'segment2' => null, + ]; + if ($this->isBetterCandidate($candidate, $bestSingle)) { + $bestSingle = $candidate; + } + } + + $bestPair = $this->findBestTwoSegments($intervals, $windowSeconds); + + $best = $bestSingle; + if ($this->isBetterCandidate($bestPair, $best)) { + $best = $bestPair; + } + + if (! $best || ! $best['segment1']) { + return null; + } + + $included = []; + foreach ([$best['segment1'], $best['segment2']] as $segment) { + if (! $segment) { + continue; + } + for ($i = $segment['start']; $i <= $segment['end']; $i++) { + $included[] = $items[$i]['log_qso_id']; + } + } + + $segment1 = $best['segment1']; + $segment2 = $best['segment2']; + + return [ + 'startUtc' => Carbon::createFromTimestampUTC($segment1['start_ts']), + 'endUtc' => Carbon::createFromTimestampUTC($segment1['end_ts']), + 'secondStartUtc' => $segment2 ? Carbon::createFromTimestampUTC($segment2['start_ts']) : null, + 'secondEndUtc' => $segment2 ? Carbon::createFromTimestampUTC($segment2['end_ts']) : null, + 'includedLogQsoIds' => $included, + 'qsoCount' => count($included), + ]; + } + + private function buildIntervals(array $items, int $windowSeconds, EvaluationRuleSet $ruleSet): array + { + $intervals = []; + $total = count($items); + + for ($start = 0; $start < $total; $start++) { + $baseScore = 0; + $penaltyScore = 0; + $qsoCount = 0; + $multiplierBuckets = []; + $multiplierCount = 0; + + for ($end = $start; $end < $total; $end++) { + $duration = $items[$end]['ts'] - $items[$start]['ts']; + if ($duration > $windowSeconds) { + break; + } + + $this->addItem($items[$end], $ruleSet, $baseScore, $penaltyScore, $qsoCount, $multiplierBuckets, $multiplierCount); + + $scoreBeforeMultiplier = $baseScore + $penaltyScore; + if ($ruleSet->usesMultipliers()) { + $score = $scoreBeforeMultiplier * $multiplierCount; + } else { + $score = $scoreBeforeMultiplier; + } + $score = max(0, $score); + + $intervals[] = [ + 'start' => $start, + 'end' => $end, + 'start_ts' => $items[$start]['ts'], + 'end_ts' => $items[$end]['ts'], + 'duration' => $duration, + 'score' => $score, + 'qso_count' => $qsoCount, + 'start_log_qso_id' => $items[$start]['log_qso_id'], + ]; + } + } + + return $intervals; + } + + private function findBestTwoSegments(array $intervals, int $windowSeconds): ?array + { + $gapSeconds = 2 * 3600; + $intervalsByEnd = $intervals; + usort($intervalsByEnd, fn ($a, $b) => $a['end_ts'] <=> $b['end_ts']); + + $intervalsByStart = $intervals; + usort($intervalsByStart, fn ($a, $b) => $a['start_ts'] <=> $b['start_ts']); + + $tree = new IntervalScoreTree($windowSeconds); + $best = null; + $idx = 0; + $count = count($intervalsByEnd); + + foreach ($intervalsByStart as $segment2) { + $threshold = $segment2['start_ts'] - $gapSeconds; + while ($idx < $count && $intervalsByEnd[$idx]['end_ts'] <= $threshold) { + $tree->update($intervalsByEnd[$idx]['duration'], $intervalsByEnd[$idx]); + $idx++; + } + + $remaining = $windowSeconds - $segment2['duration']; + if ($remaining < 0) { + continue; + } + $segment1 = $tree->query(0, $remaining); + if (! $segment1) { + continue; + } + + $candidate = [ + 'score' => $segment1['score'] + $segment2['score'], + 'start_ts' => $segment1['start_ts'], + 'end_ts' => $segment2['end_ts'], + 'qso_count' => $segment1['qso_count'] + $segment2['qso_count'], + 'start_log_qso_id' => $segment1['start_log_qso_id'], + 'segment1' => $segment1, + 'segment2' => $segment2, + ]; + + if ($this->isBetterCandidate($candidate, $best)) { + $best = $candidate; + } + } + + return $best; + } + + private function isBetterCandidate(?array $candidate, ?array $best): bool + { + if (! $candidate) { + return false; + } + if (! $best) { + return true; + } + if ($candidate['score'] !== $best['score']) { + return $candidate['score'] > $best['score']; + } + if ($candidate['start_ts'] !== $best['start_ts']) { + return $candidate['start_ts'] < $best['start_ts']; + } + if ($candidate['qso_count'] !== $best['qso_count']) { + return $candidate['qso_count'] > $best['qso_count']; + } + return $candidate['start_log_qso_id'] < $best['start_log_qso_id']; + } + + private function addItem( + array $item, + EvaluationRuleSet $ruleSet, + int &$baseScore, + int &$penaltyScore, + int &$qsoCount, + array &$multiplierBuckets, + int &$multiplierCount + ): void { + $baseScore += $item['points']; + $penaltyScore -= $item['penalty_points']; + $qsoCount++; + if (! $ruleSet->usesMultipliers()) { + return; + } + if ($item['multiplier_eligible']) { + $bandKey = $item['multiplier_band_key']; + $value = $item['multiplier_value']; + if (! isset($multiplierBuckets[$bandKey])) { + $multiplierBuckets[$bandKey] = []; + } + $current = $multiplierBuckets[$bandKey][$value] ?? 0; + $multiplierBuckets[$bandKey][$value] = $current + 1; + if ($current === 0) { + $multiplierCount++; + } + } + } +} + +final class IntervalScoreTree +{ + private int $size; + private array $tree; + + public function __construct(int $maxDuration) + { + $size = 1; + while ($size < $maxDuration + 1) { + $size *= 2; + } + $this->size = $size; + $this->tree = array_fill(0, $size * 2, null); + } + + public function update(int $duration, array $interval): void + { + $pos = $this->size + $duration; + if ($this->isBetter($interval, $this->tree[$pos])) { + $this->tree[$pos] = $interval; + } + $pos = intdiv($pos, 2); + while ($pos >= 1) { + $left = $this->tree[$pos * 2]; + $right = $this->tree[$pos * 2 + 1]; + $this->tree[$pos] = $this->isBetter($left, $right) ? $left : $right; + if ($pos === 1) { + break; + } + $pos = intdiv($pos, 2); + } + } + + public function query(int $left, int $right): ?array + { + $left += $this->size; + $right += $this->size; + $best = null; + while ($left <= $right) { + if ($left % 2 === 1) { + $best = $this->isBetter($this->tree[$left], $best) ? $this->tree[$left] : $best; + $left++; + } + if ($right % 2 === 0) { + $best = $this->isBetter($this->tree[$right], $best) ? $this->tree[$right] : $best; + $right--; + } + $left = intdiv($left, 2); + $right = intdiv($right, 2); + } + return $best; + } + + private function isBetter(?array $candidate, ?array $best): bool + { + if (! $candidate) { + return false; + } + if (! $best) { + return true; + } + if ($candidate['score'] !== $best['score']) { + return $candidate['score'] > $best['score']; + } + if ($candidate['start_ts'] !== $best['start_ts']) { + return $candidate['start_ts'] < $best['start_ts']; + } + if ($candidate['qso_count'] !== $best['qso_count']) { + return $candidate['qso_count'] > $best['qso_count']; + } + return $candidate['start_log_qso_id'] < $best['start_log_qso_id']; + } +} diff --git a/app/Services/Evaluation/ScoringService.php b/app/Services/Evaluation/ScoringService.php new file mode 100644 index 0000000..0eaef75 --- /dev/null +++ b/app/Services/Evaluation/ScoringService.php @@ -0,0 +1,189 @@ + stejné body, penalizace a příznaky. + * - Idempotence: opakovaný výpočet pro stejný run+group musí přepsat + * předchozí mezivýsledky bez duplicit. + * - Výkon: výpočty musí být dávkové (chunking), bez N+1 dotazů, + * s využitím indexů a minimalizací zápisů. + * - Přesnost: výpočet vzdálenosti (pokud se používá) musí být jednotný + * v celé aplikaci (stejná funkce, stejné zaokrouhlení). + */ +class ScoringService +{ + /** + * Služba je bezstavová (stateless). + * + * Závislosti (např. kalkulátor vzdálenosti z WWL, mapování multiplikátorů, + * repo/query helpery) mají být injektované přes DI container. + */ + public function __construct() + { + // + } + + /** + * Spocte zakladni body za QSO podle scoring modu a pravidel vzdalenosti. + */ + public function computeBasePoints(?float $distanceKm, EvaluationRuleSet $ruleSet): int + { + if ($ruleSet->scoring_mode === 'FIXED_POINTS') { + return (int) $ruleSet->points_per_qso; + } + + if ($distanceKm === null) { + // Bez vzdálenosti (např. chybějící lokátory) se body neudělují. + return 0; + } + + $distance = $this->applyDistanceRounding($distanceKm, $ruleSet); + $minDistance = $ruleSet->minDistanceKm(); + if ($minDistance !== null && $distance < $minDistance) { + return 0; + } + + return (int) round($distance * (float) $ruleSet->points_per_km); + } + + /** + * Aplikuje zaokrouhleni vzdalenosti. + */ + public function applyDistanceRounding(float $distanceKm, EvaluationRuleSet $ruleSet): float + { + // Jednotné zaokrouhlení vzdálenosti podle pravidel soutěže. + return match ($ruleSet->distanceRounding()) { + 'CEIL' => (float) ceil($distanceKm), + 'ROUND' => (float) round($distanceKm), + default => (float) floor($distanceKm), + }; + } + + /** + * Vrati penalizaci pro dany typ chyby. + */ + public function penaltyPointsFor(string $errorCode, EvaluationRuleSet $ruleSet): int + { + // Penalizace jsou konfigurované přímo v rulesetu, ne v kódu. + return match ($errorCode) { + 'DUP' => (int) $ruleSet->penalty_dup_points, + 'NIL' => (int) $ruleSet->penalty_nil_points, + 'BUSTED_CALL' => (int) $ruleSet->penalty_busted_call_points, + 'BUSTED_RST' => (int) $ruleSet->penalty_busted_rst_points, + 'BUSTED_EXCHANGE' => (int) $ruleSet->penalty_busted_exchange_points, + 'BUSTED_SERIAL' => (int) ($ruleSet->penalty_busted_serial_points ?? 0), + 'BUSTED_LOCATOR' => (int) ($ruleSet->penalty_busted_locator_points ?? 0), + 'OUT_OF_WINDOW' => (int) $ruleSet->penalty_out_of_window_points, + 'TIME_MISMATCH' => (int) $ruleSet->penalty_nil_points, + default => 0, + }; + } + + /** + * Rozhodne, jak nalozit s QSO mimo casove okno kola. + */ + public function outOfWindowDecision(EvaluationRuleSet $ruleSet): string + { + // Výsledek rozhoduje o tom, zda QSO zneplatnit, penalizovat nebo jen vynulovat. + return $ruleSet->out_of_window_policy ?? 'INVALID'; + } + + public function calculateDistanceKm(?string $locA, ?string $locB): ?float + { + if (! $locA || ! $locB) { + return null; + } + + // Lokátory musí být validní; jinak se distance nepočítá. + $coordA = $this->locatorToLatLon($locA); + $coordB = $this->locatorToLatLon($locB); + if (! $coordA || ! $coordB) { + return null; + } + + [$lat1, $lon1] = $coordA; + [$lat2, $lon2] = $coordB; + + $earthRadius = 6371.0; + $dLat = deg2rad($lat2 - $lat1); + $dLon = deg2rad($lon2 - $lon1); + $lat1Rad = deg2rad($lat1); + $lat2Rad = deg2rad($lat2); + + $a = sin($dLat / 2) ** 2 + + cos($lat1Rad) * cos($lat2Rad) * sin($dLon / 2) ** 2; + $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); + + return $earthRadius * $c; + } + + protected function locatorToLatLon(string $locator): ?array + { + $loc = strtoupper(trim($locator)); + $loc = preg_replace('/\s+/', '', $loc) ?? ''; + if (! preg_match('/^[A-R]{2}[0-9]{2}([A-X]{2})?$/', $loc)) { + return null; + } + + $lon = (ord($loc[0]) - 65) * 20 - 180; + $lat = (ord($loc[1]) - 65) * 10 - 90; + $lon += (int) $loc[2] * 2; + $lat += (int) $loc[3]; + + if (strlen($loc) >= 6) { + $lon += (ord($loc[4]) - 65) * (5 / 60); + $lat += (ord($loc[5]) - 65) * (2.5 / 60); + $lon += 2.5 / 60; + $lat += 1.25 / 60; + } else { + $lon += 1.0; + $lat += 0.5; + } + + return [$lat, $lon]; + } +} diff --git a/artisan b/artisan new file mode 100755 index 0000000..c35e31d --- /dev/null +++ b/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..bbf2430 --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,28 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->statefulApi(); + $middleware->append(SetLocaleFromCookie::class); + $middleware->trustProxies(at: '*', + headers: Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO + ); + }) + ->withExceptions(function (Exceptions $exceptions): void { + // + })->create(); diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 0000000..2b2f95f --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,7 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6|^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2023-10-12T05:21:21+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "laravel/fortify", + "version": "v1.31.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/fortify.git", + "reference": "38e864bd731b67b89f97a72c401e4ac604b7e1a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/fortify/zipball/38e864bd731b67b89f97a72c401e4ac604b7e1a9", + "reference": "38e864bd731b67b89f97a72c401e4ac604b7e1a9", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^3.0", + "ext-json": "*", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.1", + "pragmarx/google2fa": "^8.0", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^8.16|^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.4|^11.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Fortify\\FortifyServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Fortify\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Backend controllers and scaffolding for Laravel authentication.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/fortify/issues", + "source": "https://github.com/laravel/fortify" + }, + "time": "2025-11-07T20:17:39+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.38.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "7f3012af6059f5f64a12930701cd8caed6cf7c17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/7f3012af6059f5f64a12930701cd8caed6cf7c17", + "reference": "7f3012af6059f5f64a12930701cd8caed6cf7c17", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.7", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.7.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-11-13T02:12:47+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.7" + }, + "time": "2025-09-19T13:47:56+00:00" + }, + { + "name": "laravel/sanctum", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0", + "illuminate/contracts": "^11.0|^12.0", + "illuminate/database": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", + "php": "^8.2", + "symfony/console": "^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2025-07-09T19:45:24+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "038ce42edee619599a1debb7e81d7b3759492819" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819", + "reference": "038ce42edee619599a1debb7e81d7b3759492819", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2025-10-09T13:42:30+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.10.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.10.1" + }, + "time": "2025-01-27T14:24:01+00:00" + }, + { + "name": "league/commonmark", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2025-07-20T12:47:49+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.30.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.30.2" + }, + "time": "2025-11-10T17:13:11+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.30.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ab4f9d0d672f601b102936aa728801dd1a11968d", + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.2" + }, + "time": "2025-11-10T11:23:37+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.10.3", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-09-06T13:39:36+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.3", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004", + "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/tester": "^2.5.2", + "phpstan/phpstan-nette": "^2.0@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.3" + }, + "time": "2025-10-30T22:57:59+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.8", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/tester": "^2.5", + "phpstan/phpstan-nette": "^2.0@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.8" + }, + "time": "2025-08-06T21:43:34+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + }, + "time": "2025-10-21T19:32:17+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.3.2", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "eb61920a53057a7debd718a5b89c2178032b52c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/eb61920a53057a7debd718a5b89c2178032b52c0", + "reference": "eb61920a53057a7debd718a5b89c2178032b52c0", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.3.4" + }, + "require-dev": { + "illuminate/console": "^11.46.1", + "laravel/pint": "^1.25.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Its like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2025-10-18T11:10:27+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.4", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-08-21T11:53:16+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "v8.0.3", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" + }, + "time": "2024-09-05T11:56:40+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.14", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "95c29b3756a23855a30566b745d218bee690bef2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2", + "reference": "95c29b3756a23855a30566b745d218bee690bef2", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.14" + }, + "time": "2025-10-27T17:15:31+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.1" + }, + "time": "2025-09-04T20:59:21+00:00" + }, + { + "name": "spatie/laravel-package-tools", + "version": "1.92.7", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/f09a799850b1ed765103a4f0b4355006360c49a5", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", + "pestphp/pest": "^1.23|^2.1|^3.1", + "phpunit/php-code-coverage": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^9.5.24|^10.5|^11.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.7" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-07-17T15:46:43+00:00" + }, + { + "name": "spatie/laravel-translatable", + "version": "6.11.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-translatable.git", + "reference": "032d85b28de315310dab2048b857016f1194f68b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-translatable/zipball/032d85b28de315310dab2048b857016f1194f68b", + "reference": "032d85b28de315310dab2048b857016f1194f68b", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.0", + "spatie/laravel-package-tools": "^1.11" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.64", + "mockery/mockery": "^1.4", + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "pestphp/pest": "^1.20|^2.0|^3.0" + }, + "type": "library", + "extra": { + "aliases": { + "Translatable": "Spatie\\Translatable\\Facades\\Translatable" + }, + "laravel": { + "providers": [ + "Spatie\\Translatable\\TranslatableServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\Translatable\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Sebastian De Deyne", + "email": "sebastian@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A trait to make an Eloquent model hold translations", + "homepage": "https://github.com/spatie/laravel-translatable", + "keywords": [ + "eloquent", + "i8n", + "laravel-translatable", + "model", + "multilingual", + "spatie", + "translate" + ], + "support": { + "issues": "https://github.com/spatie/laravel-translatable/issues", + "source": "https://github.com/spatie/laravel-translatable/tree/6.11.4" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-02-20T15:51:22+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/console", + "version": "v7.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.3.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-04T01:21:42+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "84321188c4754e64273b46b406081ad9b18e8614" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/84321188c4754e64273b46b406081ad9b18e8614", + "reference": "84321188c4754e64273b46b406081ad9b18e8614", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.3.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-10-29T17:24:25+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "bbe40bfab84323d99dab491b716ff142410a92a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/bbe40bfab84323d99dab491b716ff142410a92a8", + "reference": "bbe40bfab84323d99dab491b716ff142410a92a8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^6.4|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.3.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-10-31T19:12:50+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-13T11:49:31+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-10-15T18:45:57+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.3.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/db488a62f98f7a81d5746f05eea63a74e55bb7c4", + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/clock": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.3.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-08T16:41:12+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.3.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "10b8e9b748ea95fa4539c208e2487c435d3c87ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/10b8e9b748ea95fa4539c208e2487c435d3c87ce", + "reference": "10b8e9b748ea95fa4539c208e2487c435d3c87ce", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^7.3", + "symfony/http-foundation": "^7.3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.3.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T11:38:40+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/fd497c45ba9c10c37864e19466b090dcb60a50ba", + "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^7.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-10-24T14:27:20+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-16T08:38:17+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "c97abe725f2a1a858deca629a6488c8fc20c3091" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/c97abe725f2a1a858deca629a6488c8fc20c3091", + "reference": "c97abe725f2a1a858deca629a6488c8fc20c3091", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.3.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-05T07:57:47+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f96476035142921000338bad71e5247fbc138872" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T14:36:48+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", + "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-07T11:39:36+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T19:55:54+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-27T09:00:46+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + }, + "time": "2024-12-21T16:25:41+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-04-30T23:37:27+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + } + ], + "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.14.2", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "de06de1ae1203b11976c6ca01d6a9081c8b33d45" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/de06de1ae1203b11976c6ca01d6a9081c8b33d45", + "reference": "de06de1ae1203b11976c6ca01d6a9081c8b33d45", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.3.0", + "jean85/pretty-package-versions": "^2.1.1", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^12.4.0", + "phpunit/php-file-iterator": "^6", + "phpunit/php-timer": "^8", + "phpunit/phpunit": "^12.4.1", + "sebastian/environment": "^8.0.3", + "symfony/console": "^6.4.20 || ^7.3.4", + "symfony/process": "^6.4.20 || ^7.3.4" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.31", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpstan/phpstan-strict-rules": "^2.0.7", + "symfony/filesystem": "^6.4.13 || ^7.3.2" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.14.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2025-10-24T07:20:53+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0", + "illuminate/contracts": "^10.24|^11.0|^12.0", + "illuminate/log": "^10.24|^11.0|^12.0", + "illuminate/process": "^10.24|^11.0|^12.0", + "illuminate/support": "^10.24|^11.0|^12.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.0|^10.0", + "pestphp/pest": "^2.20|^3.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2025-06-05T13:55:57+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.25.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.87.2", + "illuminate/view": "^11.46.0", + "larastan/larastan": "^3.7.1", + "laravel-zero/framework": "^11.45.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.3.1", + "pestphp/pest": "^2.36.0" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2025-09-19T02:57:12+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.48.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "1bf3b8870b72a258a3b6b5119435835ece522e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/1bf3b8870b72a258a3b6b5119435835ece522e8a", + "reference": "1bf3b8870b72a258a3b6b5119435835ece522e8a", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0", + "symfony/yaml": "^6.0|^7.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "phpstan/phpstan": "^2.0" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2025-11-09T14:46:21+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.8.2", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.1", + "nunomaduro/termwind": "^2.3.1", + "php": "^8.2.0", + "symfony/console": "^7.3.0" + }, + "conflict": { + "laravel/framework": "<11.44.2 || >=13.0.0", + "phpunit/phpunit": "<11.5.15 || >=13.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.3", + "larastan/larastan": "^3.4.2", + "laravel/framework": "^11.44.2 || ^12.18", + "laravel/pint": "^1.22.1", + "laravel/sail": "^1.43.1", + "laravel/sanctum": "^4.1.1", + "laravel/tinker": "^2.10.1", + "orchestra/testbench-core": "^9.12.0 || ^10.4", + "pestphp/pest": "^3.8.2", + "sebastian/environment": "^7.2.1 || ^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-06-25T02:12:12+00:00" + }, + { + "name": "pestphp/pest", + "version": "v4.1.3", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest.git", + "reference": "477d20a54fd9329ddfb0f8d4eb90dca7bc81b027" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest/zipball/477d20a54fd9329ddfb0f8d4eb90dca7bc81b027", + "reference": "477d20a54fd9329ddfb0f8d4eb90dca7bc81b027", + "shasum": "" + }, + "require": { + "brianium/paratest": "^7.14.0", + "nunomaduro/collision": "^8.8.2", + "nunomaduro/termwind": "^2.3.1", + "pestphp/pest-plugin": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.0", + "pestphp/pest-plugin-mutate": "^4.0.1", + "pestphp/pest-plugin-profanity": "^4.1.0", + "php": "^8.3.0", + "phpunit/phpunit": "^12.4.1", + "symfony/process": "^7.3.4" + }, + "conflict": { + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.4.1", + "sebastian/exporter": "<7.0.0", + "webmozart/assert": "<1.11.0" + }, + "require-dev": { + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-browser": "^4.1.1", + "pestphp/pest-plugin-type-coverage": "^4.0.2", + "psy/psysh": "^0.12.12" + }, + "bin": [ + "bin/pest" + ], + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "The elegant PHP Testing Framework.", + "keywords": [ + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v4.1.3" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-10-29T22:45:27+00:00" + }, + { + "name": "pestphp/pest-plugin", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.3" + }, + "conflict": { + "pestphp/pest": "<4.0.0" + }, + "require-dev": { + "composer/composer": "^2.8.10", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" + }, + "autoload": { + "psr-4": { + "Pest\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest plugin manager", + "keywords": [ + "framework", + "manager", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-08-20T12:35:58+00:00" + }, + { + "name": "pestphp/pest-plugin-arch", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "ta-tikoma/phpunit-architecture-test": "^0.8.5" + }, + "require-dev": { + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Arch\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Arch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Arch plugin for Pest PHP.", + "keywords": [ + "arch", + "architecture", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-20T13:10:51+00:00" + }, + { + "name": "pestphp/pest-plugin-laravel", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-laravel.git", + "reference": "e12a07046b826a40b1c8632fd7b80d6b8d7b628e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/e12a07046b826a40b1c8632fd7b80d6b8d7b628e", + "reference": "e12a07046b826a40b1c8632fd7b80d6b8d7b628e", + "shasum": "" + }, + "require": { + "laravel/framework": "^11.45.2|^12.25.0", + "pestphp/pest": "^4.0.0", + "php": "^8.3.0" + }, + "require-dev": { + "laravel/dusk": "^8.3.3", + "orchestra/testbench": "^9.13.0|^10.5.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Laravel\\Plugin" + ] + }, + "laravel": { + "providers": [ + "Pest\\Laravel\\PestServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Laravel Plugin", + "keywords": [ + "framework", + "laravel", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-20T12:46:37+00:00" + }, + { + "name": "pestphp/pest-plugin-mutate", + "version": "v4.0.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.6.1", + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Mutate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", + "keywords": [ + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-21T20:19:25+00:00" + }, + { + "name": "pestphp/pest-plugin-profanity", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "c37e5e2c7136ee4eae12082e7952332bc1c6600a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/c37e5e2c7136ee4eae12082e7952332bc1c6600a", + "reference": "c37e5e2c7136ee4eae12082e7952332bc1c6600a", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3" + }, + "require-dev": { + "faissaloux/pest-plugin-inside": "^1.9", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\Profanity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Profanity Plugin", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.0" + }, + "time": "2025-10-28T23:14:11+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", + "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" + }, + "time": "2025-08-01T19:43:32+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" + }, + "time": "2025-08-30T15:50:23+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.4.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", + "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.6.1", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.3.7" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.4.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-09-24T13:44:41+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:37+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "fc5413a2e6d240d2f6d9317bdf7f0a24e73de194" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc5413a2e6d240d2f6d9317bdf7f0a24e73de194", + "reference": "fc5413a2e6d240d2f6d9317bdf7f0a24e73de194", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.4.0", + "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.3", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.4-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.1" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-10-09T14:08:29+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2025-09-14T09:36:45+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dc904b4bb3ab070865fa4068cd84f3da8b945148", + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2025-08-20T11:27:00+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-08-12T14:11:56+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:16:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-29T11:29:25+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:57:12+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-27T09:00:46+00:00" + }, + { + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.5", + "source": { + "type": "git", + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "cf6fb197b676ba716837c886baca842e4db29005" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/cf6fb197b676ba716837c886baca842e4db29005", + "reference": "cf6fb197b676ba716837c886baca842e4db29005", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0" + }, + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.5" + }, + "time": "2025-04-20T20:23:40+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/d74205c497bfbca49f34d4bc4c19c17e22db4ebb", + "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-13T13:44:09+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^7.2 || ^8.0" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.12.1" + }, + "time": "2025-10-29T15:56:20+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..423eed5 --- /dev/null +++ b/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..7d1eb0d --- /dev/null +++ b/config/auth.php @@ -0,0 +1,115 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', App\Models\User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..b32aead --- /dev/null +++ b/config/cache.php @@ -0,0 +1,117 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", + | "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + +]; diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 0000000..6a1350f --- /dev/null +++ b/config/cors.php @@ -0,0 +1,34 @@ + ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => [env('FRONTEND_URL', 'http://127.0.0.1:8000'),'*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => true, + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..53dcae0 --- /dev/null +++ b/config/database.php @@ -0,0 +1,183 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..3d671bd --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/config/fortify.php b/config/fortify.php new file mode 100644 index 0000000..5bb6313 --- /dev/null +++ b/config/fortify.php @@ -0,0 +1,159 @@ + 'web', + + /* + |-------------------------------------------------------------------------- + | Fortify Password Broker + |-------------------------------------------------------------------------- + | + | Here you may specify which password broker Fortify can use when a user + | is resetting their password. This configured value should match one + | of your password brokers setup in your "auth" configuration file. + | + */ + + 'passwords' => 'users', + + /* + |-------------------------------------------------------------------------- + | Username / Email + |-------------------------------------------------------------------------- + | + | This value defines which model attribute should be considered as your + | application's "username" field. Typically, this might be the email + | address of the users but you are free to change this value here. + | + | Out of the box, Fortify expects forgot password and reset password + | requests to have a field named 'email'. If the application uses + | another name for the field you may define it below as needed. + | + */ + + 'username' => 'email', + + 'email' => 'email', + + /* + |-------------------------------------------------------------------------- + | Lowercase Usernames + |-------------------------------------------------------------------------- + | + | This value defines whether usernames should be lowercased before saving + | them in the database, as some database system string fields are case + | sensitive. You may disable this for your application if necessary. + | + */ + + 'lowercase_usernames' => true, + + /* + |-------------------------------------------------------------------------- + | Home Path + |-------------------------------------------------------------------------- + | + | Here you may configure the path where users will get redirected during + | authentication or password reset when the operations are successful + | and the user is authenticated. You are free to change this value. + | + */ + + 'home' => '/', + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Prefix / Subdomain + |-------------------------------------------------------------------------- + | + | Here you may specify which prefix Fortify will assign to all the routes + | that it registers with the application. If necessary, you may change + | subdomain under which all of the Fortify routes will be available. + | + */ + + 'prefix' => '', + + 'domain' => null, + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Fortify will assign to the routes + | that it registers with the application. If necessary, you may change + | these middleware but typically this provided default is preferred. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | By default, Fortify will throttle logins to five requests per minute for + | every email and IP address combination. However, if you would like to + | specify a custom rate limiter to call then you may specify it here. + | + */ + + 'limiters' => [ + 'login' => 'login', + 'two-factor' => 'two-factor', + ], + + /* + |-------------------------------------------------------------------------- + | Register View Routes + |-------------------------------------------------------------------------- + | + | Here you may specify if the routes returning views should be disabled as + | you may not need them when building your own application. This may be + | especially true if you're writing a custom single-page application. + | + */ + + 'views' => false, + + /* + |-------------------------------------------------------------------------- + | Features + |-------------------------------------------------------------------------- + | + | Some of the Fortify features are optional. You may disable the features + | by removing them from this array. You're free to only remove some of + | these features or you can even remove all of these if you need to. + | + */ + + 'features' => [ + Features::registration(), + Features::resetPasswords(), + // Features::emailVerification(), + Features::updateProfileInformation(), + Features::updatePasswords(), + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + // 'window' => 0, + ]), + ], + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..9e998a4 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..522b284 --- /dev/null +++ b/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..304f0e8 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,85 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : '', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..6a90eb8 --- /dev/null +++ b/config/services.php @@ -0,0 +1,38 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..bc45901 --- /dev/null +++ b/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain and all subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/database/factories/BandFactory.php b/database/factories/BandFactory.php new file mode 100644 index 0000000..830c79d --- /dev/null +++ b/database/factories/BandFactory.php @@ -0,0 +1,28 @@ + + */ +class BandFactory extends Factory +{ + protected $model = Band::class; + + public function definition(): array + { + $start = $this->faker->numberBetween(100000000, 1000000000); + $end = $start + $this->faker->numberBetween(1000000, 10000000); + + return [ + 'name' => $this->faker->randomElement(['144 MHz', '432 MHz', '1.3 GHz']), + 'order' => $this->faker->numberBetween(1, 10), + 'edi_band_begin' => $start, + 'edi_band_end' => $end, + 'has_power_category' => $this->faker->boolean(), + ]; + } +} diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php new file mode 100644 index 0000000..292c7a1 --- /dev/null +++ b/database/factories/CategoryFactory.php @@ -0,0 +1,22 @@ + + */ +class CategoryFactory extends Factory +{ + protected $model = Category::class; + + public function definition(): array + { + return [ + 'name' => strtoupper($this->faker->bothify('CAT-??')), + 'order' => $this->faker->numberBetween(1, 20), + ]; + } +} diff --git a/database/factories/ContestFactory.php b/database/factories/ContestFactory.php new file mode 100644 index 0000000..83c299d --- /dev/null +++ b/database/factories/ContestFactory.php @@ -0,0 +1,33 @@ + + */ +class ContestFactory extends Factory +{ + protected $model = Contest::class; + + public function definition(): array + { + $title = $this->faker->words(2, true); + + return [ + 'name' => [ + 'cs' => $title, + 'en' => $title, + ], + 'description' => [ + 'cs' => $this->faker->sentence(), + 'en' => $this->faker->sentence(), + ], + 'rule_set_id' => EvaluationRuleSet::factory(), + 'is_active' => true, + ]; + } +} diff --git a/database/factories/ContestParameterFactory.php b/database/factories/ContestParameterFactory.php new file mode 100644 index 0000000..13a30cd --- /dev/null +++ b/database/factories/ContestParameterFactory.php @@ -0,0 +1,34 @@ + + */ +class ContestParameterFactory extends Factory +{ + protected $model = ContestParameter::class; + + public function definition(): array + { + return [ + 'contest_id' => Contest::factory(), + 'log_type' => $this->faker->randomElement(['STANDARD', 'CHECK']), + 'ignore_slash_part' => true, + 'ignore_third_part' => true, + 'letters_in_rst' => true, + 'discard_qso_rec_diff_call' => true, + 'discard_qso_sent_diff_call' => false, + 'discard_qso_rec_diff_rst' => true, + 'discard_qso_sent_diff_rst' => false, + 'discard_qso_rec_diff_code' => true, + 'discard_qso_sent_diff_code' => false, + 'unique_qso' => true, + 'time_tolerance' => 600, + ]; + } +} diff --git a/database/factories/CountryWwlFactory.php b/database/factories/CountryWwlFactory.php new file mode 100644 index 0000000..3418606 --- /dev/null +++ b/database/factories/CountryWwlFactory.php @@ -0,0 +1,22 @@ + + */ +class CountryWwlFactory extends Factory +{ + protected $model = CountryWwl::class; + + public function definition(): array + { + return [ + 'country_name' => $this->faker->unique()->country(), + 'wwl' => strtoupper($this->faker->bothify('??##')), + ]; + } +} diff --git a/database/factories/CtyFactory.php b/database/factories/CtyFactory.php new file mode 100644 index 0000000..93f961b --- /dev/null +++ b/database/factories/CtyFactory.php @@ -0,0 +1,34 @@ + + */ +class CtyFactory extends Factory +{ + protected $model = Cty::class; + + public function definition(): array + { + $prefix = strtoupper($this->faker->unique()->bothify('T??')); + + return [ + 'country_name' => $this->faker->country(), + 'dxcc' => $this->faker->numberBetween(1, 400), + 'cq_zone' => $this->faker->numberBetween(1, 40), + 'itu_zone' => $this->faker->numberBetween(1, 90), + 'continent' => $this->faker->randomElement(['EU', 'AS', 'AF', 'NA', 'SA', 'OC', 'AN']), + 'latitude' => $this->faker->latitude(), + 'longitude' => $this->faker->longitude(), + 'time_offset' => $this->faker->randomFloat(2, -12, 12), + 'prefix' => $prefix, + 'prefix_norm' => $prefix, + 'precise' => $this->faker->boolean(), + 'source' => 'test', + ]; + } +} diff --git a/database/factories/EdiBandFactory.php b/database/factories/EdiBandFactory.php new file mode 100644 index 0000000..cba2025 --- /dev/null +++ b/database/factories/EdiBandFactory.php @@ -0,0 +1,21 @@ + + */ +class EdiBandFactory extends Factory +{ + protected $model = EdiBand::class; + + public function definition(): array + { + return [ + 'value' => $this->faker->unique()->bothify('BAND-##'), + ]; + } +} diff --git a/database/factories/EdiCategoryFactory.php b/database/factories/EdiCategoryFactory.php new file mode 100644 index 0000000..dd5ae45 --- /dev/null +++ b/database/factories/EdiCategoryFactory.php @@ -0,0 +1,21 @@ + + */ +class EdiCategoryFactory extends Factory +{ + protected $model = EdiCategory::class; + + public function definition(): array + { + return [ + 'value' => $this->faker->unique()->bothify('CAT-##'), + ]; + } +} diff --git a/database/factories/EvaluationRuleSetFactory.php b/database/factories/EvaluationRuleSetFactory.php new file mode 100644 index 0000000..7d5d269 --- /dev/null +++ b/database/factories/EvaluationRuleSetFactory.php @@ -0,0 +1,23 @@ + + */ +class EvaluationRuleSetFactory extends Factory +{ + protected $model = EvaluationRuleSet::class; + + public function definition(): array + { + return [ + 'name' => $this->faker->words(2, true), + 'code' => strtoupper($this->faker->unique()->bothify('RULE_##??')), + 'description' => $this->faker->sentence(), + ]; + } +} diff --git a/database/factories/EvaluationRunFactory.php b/database/factories/EvaluationRunFactory.php new file mode 100644 index 0000000..b7b5041 --- /dev/null +++ b/database/factories/EvaluationRunFactory.php @@ -0,0 +1,28 @@ + + */ +class EvaluationRunFactory extends Factory +{ + protected $model = EvaluationRun::class; + + public function definition(): array + { + return [ + 'round_id' => Round::factory(), + 'rule_set_id' => EvaluationRuleSet::factory(), + 'name' => $this->faker->words(2, true), + 'rules_version' => 'OFFICIAL', + 'is_official' => false, + 'notes' => null, + ]; + } +} diff --git a/database/factories/FileFactory.php b/database/factories/FileFactory.php new file mode 100644 index 0000000..b0fe9e9 --- /dev/null +++ b/database/factories/FileFactory.php @@ -0,0 +1,29 @@ + + */ +class FileFactory extends Factory +{ + protected $model = File::class; + + public function definition(): array + { + $filename = $this->faker->lexify('log-????.edi'); + + return [ + 'path' => 'uploads/test/' . $filename, + 'filename' => $filename, + 'mimetype' => 'text/plain', + 'filesize' => 10, + 'hash' => Str::random(64), + 'uploaded_by' => null, + ]; + } +} diff --git a/database/factories/LogFactory.php b/database/factories/LogFactory.php new file mode 100644 index 0000000..472b322 --- /dev/null +++ b/database/factories/LogFactory.php @@ -0,0 +1,27 @@ + + */ +class LogFactory extends Factory +{ + protected $model = Log::class; + + public function definition(): array + { + return [ + 'round_id' => Round::factory(), + 'pcall' => strtoupper($this->faker->bothify('OK#???')), + 'pwwlo' => $this->faker->randomElement(['JN79', 'JN89', 'JO60', 'JN99']), + 'psect' => $this->faker->randomElement(['A', 'B', 'C', 'D']), + 'pband' => $this->faker->randomElement(['144', '432', '1296']), + 'power_watt' => $this->faker->numberBetween(10, 1000), + ]; + } +} diff --git a/database/factories/LogOverrideFactory.php b/database/factories/LogOverrideFactory.php new file mode 100644 index 0000000..88b35ae --- /dev/null +++ b/database/factories/LogOverrideFactory.php @@ -0,0 +1,26 @@ + + */ +class LogOverrideFactory extends Factory +{ + protected $model = LogOverride::class; + + public function definition(): array + { + return [ + 'evaluation_run_id' => EvaluationRun::factory(), + 'log_id' => Log::factory(), + 'forced_log_status' => 'AUTO', + 'reason' => $this->faker->sentence(), + ]; + } +} diff --git a/database/factories/LogQsoFactory.php b/database/factories/LogQsoFactory.php new file mode 100644 index 0000000..13af72b --- /dev/null +++ b/database/factories/LogQsoFactory.php @@ -0,0 +1,30 @@ + + */ +class LogQsoFactory extends Factory +{ + protected $model = LogQso::class; + + public function definition(): array + { + return [ + 'log_id' => Log::factory(), + 'qso_index' => $this->faker->numberBetween(1, 1000), + 'time_on' => $this->faker->dateTime(), + 'band' => $this->faker->randomElement(['144', '432', '1296']), + 'freq_khz' => $this->faker->numberBetween(144000, 1296000), + 'mode' => $this->faker->randomElement(['CW', 'SSB', 'FM']), + 'my_call' => strtoupper($this->faker->bothify('OK#???')), + 'dx_call' => strtoupper($this->faker->bothify('DL#???')), + 'points' => $this->faker->numberBetween(1, 500), + ]; + } +} diff --git a/database/factories/LogResultFactory.php b/database/factories/LogResultFactory.php new file mode 100644 index 0000000..6a19097 --- /dev/null +++ b/database/factories/LogResultFactory.php @@ -0,0 +1,29 @@ + + */ +class LogResultFactory extends Factory +{ + protected $model = LogResult::class; + + public function definition(): array + { + return [ + 'evaluation_run_id' => EvaluationRun::factory(), + 'log_id' => Log::factory(), + 'claimed_qso_count' => $this->faker->numberBetween(10, 200), + 'claimed_score' => $this->faker->numberBetween(100, 5000), + 'valid_qso_count' => $this->faker->numberBetween(10, 200), + 'official_score' => $this->faker->numberBetween(100, 5000), + 'status' => 'OK', + ]; + } +} diff --git a/database/factories/NewsPostFactory.php b/database/factories/NewsPostFactory.php new file mode 100644 index 0000000..e68bc90 --- /dev/null +++ b/database/factories/NewsPostFactory.php @@ -0,0 +1,46 @@ + + */ +class NewsPostFactory extends Factory +{ + protected $model = NewsPost::class; + + public function definition(): array + { + $title = $this->faker->sentence(4); + + return [ + 'title' => [ + 'cs' => $title, + 'en' => $title, + ], + 'slug' => Str::slug($title) . '-' . $this->faker->unique()->numberBetween(100, 999), + 'content' => [ + 'cs' => $this->faker->paragraph(), + 'en' => $this->faker->paragraph(), + ], + 'excerpt' => [ + 'cs' => $this->faker->sentence(), + 'en' => $this->faker->sentence(), + ], + 'is_published' => true, + 'published_at' => now()->subDay(), + ]; + } + + public function unpublished(): static + { + return $this->state(fn () => [ + 'is_published' => false, + 'published_at' => null, + ]); + } +} diff --git a/database/factories/PowerCategoryFactory.php b/database/factories/PowerCategoryFactory.php new file mode 100644 index 0000000..e770e59 --- /dev/null +++ b/database/factories/PowerCategoryFactory.php @@ -0,0 +1,23 @@ + + */ +class PowerCategoryFactory extends Factory +{ + protected $model = PowerCategory::class; + + public function definition(): array + { + return [ + 'name' => strtoupper($this->faker->unique()->bothify('PWR-?')), + 'order' => $this->faker->numberBetween(1, 10), + 'power_level' => $this->faker->numberBetween(10, 1000), + ]; + } +} diff --git a/database/factories/QsoOverrideFactory.php b/database/factories/QsoOverrideFactory.php new file mode 100644 index 0000000..2956e93 --- /dev/null +++ b/database/factories/QsoOverrideFactory.php @@ -0,0 +1,26 @@ + + */ +class QsoOverrideFactory extends Factory +{ + protected $model = QsoOverride::class; + + public function definition(): array + { + return [ + 'evaluation_run_id' => EvaluationRun::factory(), + 'log_qso_id' => LogQso::factory(), + 'forced_status' => 'AUTO', + 'reason' => $this->faker->sentence(), + ]; + } +} diff --git a/database/factories/QsoResultFactory.php b/database/factories/QsoResultFactory.php new file mode 100644 index 0000000..4935e80 --- /dev/null +++ b/database/factories/QsoResultFactory.php @@ -0,0 +1,27 @@ + + */ +class QsoResultFactory extends Factory +{ + protected $model = QsoResult::class; + + public function definition(): array + { + return [ + 'evaluation_run_id' => EvaluationRun::factory(), + 'log_qso_id' => LogQso::factory(), + 'is_valid' => true, + 'is_operating_window_excluded' => false, + 'points' => $this->faker->numberBetween(1, 500), + ]; + } +} diff --git a/database/factories/RoundFactory.php b/database/factories/RoundFactory.php new file mode 100644 index 0000000..d834a87 --- /dev/null +++ b/database/factories/RoundFactory.php @@ -0,0 +1,41 @@ + + */ +class RoundFactory extends Factory +{ + protected $model = Round::class; + + public function definition(): array + { + $title = $this->faker->words(3, true); + $start = now()->startOfDay()->addHours(14); + $end = (clone $start)->addHours(2); + $deadline = (clone $end)->addDays(2); + + return [ + 'contest_id' => Contest::factory(), + 'rule_set_id' => EvaluationRuleSet::factory(), + 'name' => [ + 'cs' => $title, + 'en' => $title, + ], + 'description' => [ + 'cs' => $this->faker->sentence(), + 'en' => $this->faker->sentence(), + ], + 'start_time' => $start, + 'end_time' => $end, + 'logs_deadline' => $deadline, + 'is_active' => true, + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..65121c9 --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,58 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->name(), + 'email' => $this->faker->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('demodemo'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } + + public function admin(): static + { + return $this->state(fn (array $attributes) => [ + 'is_admin' => true, + ]); + } + + public function inactive(): static + { + return $this->state(fn (array $attributes) => [ + 'is_active' => false, + ]); + } +} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..bd49b20 --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,50 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->boolean('is_admin')->default(false); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..b9c106b --- /dev/null +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration'); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/database/migrations/0001_01_01_000003_create_personal_access_tokens_table.php b/database/migrations/0001_01_01_000003_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/database/migrations/0001_01_01_000003_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/database/migrations/2025_11_12_195547_create_edi_bands_table.php b/database/migrations/2025_11_12_195547_create_edi_bands_table.php new file mode 100644 index 0000000..8b97264 --- /dev/null +++ b/database/migrations/2025_11_12_195547_create_edi_bands_table.php @@ -0,0 +1,28 @@ +id(); + $table->timestamps(); + $table->string('value'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('edi_bands'); + } +}; diff --git a/database/migrations/2025_11_12_195548_create_bands_table.php b/database/migrations/2025_11_12_195548_create_bands_table.php new file mode 100644 index 0000000..c1887a9 --- /dev/null +++ b/database/migrations/2025_11_12_195548_create_bands_table.php @@ -0,0 +1,32 @@ +id(); + $table->timestamps(); + $table->string('name'); + $table->integer('order')->default(0); + $table->bigInteger('edi_band_begin'); + $table->bigInteger('edi_band_end'); + $table->boolean('has_power_category')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('bands'); + } +}; diff --git a/database/migrations/2025_11_12_195819_create_categories_table.php b/database/migrations/2025_11_12_195819_create_categories_table.php new file mode 100644 index 0000000..ac20641 --- /dev/null +++ b/database/migrations/2025_11_12_195819_create_categories_table.php @@ -0,0 +1,29 @@ +id(); + $table->timestamps(); + $table->string('name'); + $table->integer('order')->default(10); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('categories'); + } +}; diff --git a/database/migrations/2025_11_12_195836_create_power_categories_table.php b/database/migrations/2025_11_12_195836_create_power_categories_table.php new file mode 100644 index 0000000..52f6b38 --- /dev/null +++ b/database/migrations/2025_11_12_195836_create_power_categories_table.php @@ -0,0 +1,30 @@ +id(); + $table->timestamps(); + $table->string('name')->unique(); + $table->integer('order')->default(10); + $table->bigInteger('power_level')->default(0); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('power_categories'); + } +}; diff --git a/database/migrations/2025_11_12_210154_create_bands_edi_bands_table.php b/database/migrations/2025_11_12_210154_create_bands_edi_bands_table.php new file mode 100644 index 0000000..122a7d5 --- /dev/null +++ b/database/migrations/2025_11_12_210154_create_bands_edi_bands_table.php @@ -0,0 +1,31 @@ +timestamps(); + $table->unsignedBigInteger('band_id'); + $table->unsignedBigInteger('edi_band_id'); + $table->foreign('band_id')->references('id')->on('bands')->cascadeOnDelete(); + $table->foreign('edi_band_id')->references('id')->on('edi_bands')->cascadeOnDelete(); + $table->primary(['band_id', 'edi_band_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('bands_edi_bands'); + } +}; diff --git a/database/migrations/2025_11_12_213306_create_edi_categories_table.php b/database/migrations/2025_11_12_213306_create_edi_categories_table.php new file mode 100644 index 0000000..c7303f9 --- /dev/null +++ b/database/migrations/2025_11_12_213306_create_edi_categories_table.php @@ -0,0 +1,28 @@ +id(); + $table->timestamps(); + $table->string('value'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('edi_categories'); + } +}; diff --git a/database/migrations/2025_11_12_213319_create_categories_edi_categories_table.php b/database/migrations/2025_11_12_213319_create_categories_edi_categories_table.php new file mode 100644 index 0000000..ee8e842 --- /dev/null +++ b/database/migrations/2025_11_12_213319_create_categories_edi_categories_table.php @@ -0,0 +1,31 @@ +timestamps(); + $table->unsignedBigInteger('category_id'); + $table->unsignedBigInteger('edi_category_id'); + $table->foreign('category_id')->references('id')->on('categories')->cascadeOnDelete(); + $table->foreign('edi_category_id')->references('id')->on('edi_categories')->cascadeOnDelete(); + $table->primary(['category_id', 'edi_category_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('categories_edi_categories'); + } +}; diff --git a/database/migrations/2025_11_13_063129_create_cty_table.php b/database/migrations/2025_11_13_063129_create_cty_table.php new file mode 100644 index 0000000..6dcc5c0 --- /dev/null +++ b/database/migrations/2025_11_13_063129_create_cty_table.php @@ -0,0 +1,42 @@ +id(); + $table->timestamps(); + + $table->string('country_name', 150); + $table->integer('cq_zone'); + $table->integer('itu_zone'); + $table->string('continent', 2); + $table->decimal('latitude', 10, 2); + $table->decimal('longitude', 10, 2); + $table->decimal('time_offset', 10, 2); + $table->string('prefix', 25)->unique(); + $table->boolean('precise')->default(false); + $table->string('source', 25); + + $table->index('continent'); + $table->index('cq_zone'); + $table->index('itu_zone'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cty'); + } +}; diff --git a/database/migrations/2025_11_13_064116_create_countries_wwl_table.php b/database/migrations/2025_11_13_064116_create_countries_wwl_table.php new file mode 100644 index 0000000..15ab019 --- /dev/null +++ b/database/migrations/2025_11_13_064116_create_countries_wwl_table.php @@ -0,0 +1,29 @@ +timestamps(); + $table->string('country_name', 150); + $table->string('wwl', 4); + $table->primary(['country_name','wwl']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('countries_wwl'); + } +}; diff --git a/database/migrations/2025_11_13_141542_create_contests_table.php b/database/migrations/2025_11_13_141542_create_contests_table.php new file mode 100644 index 0000000..e2a3045 --- /dev/null +++ b/database/migrations/2025_11_13_141542_create_contests_table.php @@ -0,0 +1,40 @@ +id(); + $table->timestamps(); + $table->json('name'); + $table->json('description')->nullable(); + $table->string('url')->nullable(); + $table->string('evaluator')->default('Český radioklub')->nullable(); + $table->string('email')->default('vkvzavody@crk.cz')->nullable(); + $table->string('email2')->nullable(); + $table->boolean('is_mcr')->default(false)->index(); + $table->boolean('is_active')->default(false)->index(); + $table->boolean('is_test')->default(false)->index(); + $table->boolean('is_sixhr')->default(false)->index(); + $table->time('start_time')->default('14:00:00'); + $table->integer('duration')->default(24); + $table->integer('logs_deadline_days')->default(3); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contests'); + } +}; diff --git a/database/migrations/2025_11_13_141608_create_contests_power_categories_table.php b/database/migrations/2025_11_13_141608_create_contests_power_categories_table.php new file mode 100644 index 0000000..661d57a --- /dev/null +++ b/database/migrations/2025_11_13_141608_create_contests_power_categories_table.php @@ -0,0 +1,31 @@ +timestamps(); + $table->unsignedBigInteger('contest_id'); + $table->unsignedBigInteger('power_category_id'); + $table->foreign('contest_id')->references('id')->on('contests')->cascadeOnDelete(); + $table->foreign('power_category_id')->references('id')->on('power_categories')->cascadeOnDelete(); + $table->primary(['contest_id', 'power_category_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contests_power_categories'); + } +}; diff --git a/database/migrations/2025_11_13_141626_create_contests_categories_table.php b/database/migrations/2025_11_13_141626_create_contests_categories_table.php new file mode 100644 index 0000000..5fee7df --- /dev/null +++ b/database/migrations/2025_11_13_141626_create_contests_categories_table.php @@ -0,0 +1,31 @@ +timestamps(); + $table->unsignedBigInteger('contest_id'); + $table->unsignedBigInteger('category_id'); + $table->foreign('contest_id')->references('id')->on('contests')->cascadeOnDelete(); + $table->foreign('category_id')->references('id')->on('categories')->cascadeOnDelete(); + $table->primary(['contest_id', 'category_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contests_categories'); + } +}; diff --git a/database/migrations/2025_11_13_141638_create_contests_bands_table.php b/database/migrations/2025_11_13_141638_create_contests_bands_table.php new file mode 100644 index 0000000..41bca06 --- /dev/null +++ b/database/migrations/2025_11_13_141638_create_contests_bands_table.php @@ -0,0 +1,31 @@ +timestamps(); + $table->unsignedBigInteger('contest_id'); + $table->unsignedBigInteger('band_id'); + $table->foreign('contest_id')->references('id')->on('contests')->cascadeOnDelete(); + $table->foreign('band_id')->references('id')->on('bands')->cascadeOnDelete(); + $table->primary(['contest_id', 'band_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contests_bands'); + } +}; diff --git a/database/migrations/2025_11_13_173633_create_rounds_table.php b/database/migrations/2025_11_13_173633_create_rounds_table.php new file mode 100644 index 0000000..db33a67 --- /dev/null +++ b/database/migrations/2025_11_13_173633_create_rounds_table.php @@ -0,0 +1,45 @@ +id(); + $table->timestamps(); + $table->unsignedBigInteger('contest_id'); + $table->json('name'); + $table->json('description')->nullable(); + $table->dateTime('start_time'); + $table->dateTime('end_time'); + $table->dateTime('logs_deadline'); + $table->boolean('is_mcr')->default(false)->index(); + $table->boolean('is_active')->default(false)->index(); + $table->boolean('is_test')->default(false)->index(); + $table->boolean('is_sixhr')->default(false)->index(); + $table->datetime('first_check')->nullable(); + $table->datetime('second_check')->nullable(); + $table->datetime('unique_qso_check')->nullable(); + $table->datetime('third_check')->nullable(); + $table->datetime('fourth_check')->nullable(); + $table->datetime('prelimitary_results')->nullable(); + + $table->foreign('contest_id')->references('id')->on('contests')->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('rounds'); + } +}; diff --git a/database/migrations/2025_11_13_173634_create_contests_parameters_table.php b/database/migrations/2025_11_13_173634_create_contests_parameters_table.php new file mode 100644 index 0000000..adea29b --- /dev/null +++ b/database/migrations/2025_11_13_173634_create_contests_parameters_table.php @@ -0,0 +1,44 @@ +id(); + $table->timestamps(); + $table->unsignedBigInteger('contest_id'); + $table->enum('log_type', ['STANDARD', 'CHECK']); + $table->boolean('ignore_slash_part')->default(true); + $table->boolean('ignore_third_part')->default(true); + $table->boolean('letters_in_rst')->default(true); + $table->boolean('discard_qso_rec_diff_call')->default(true); + $table->boolean('discard_qso_sent_diff_call')->default(false); + $table->boolean('discard_qso_rec_diff_rst')->default(true); + $table->boolean('discard_qso_sent_diff_rst')->default(false); + $table->boolean('discard_qso_rec_diff_code')->default(true); + $table->boolean('discard_qso_sent_diff_code')->default(false); + $table->boolean('unique_qso')->default(true); + $table->integer('time_tolerance')->default(600); + + $table->foreign('contest_id') + ->references('id')->on('contests') + ->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contests_parameters'); + } +}; diff --git a/database/migrations/2025_11_13_173634_create_rounds_parameters_table.php b/database/migrations/2025_11_13_173634_create_rounds_parameters_table.php new file mode 100644 index 0000000..644a392 --- /dev/null +++ b/database/migrations/2025_11_13_173634_create_rounds_parameters_table.php @@ -0,0 +1,45 @@ +id(); // přidat + $table->timestamps(); + $table->unsignedBigInteger('round_id'); + $table->enum('log_type', ['STANDARD', 'CHECK']); + $table->boolean('ignore_slash_part')->default(true); + $table->boolean('ignore_third_part')->default(true); + $table->boolean('letters_in_rst')->default(true); + $table->boolean('discard_qso_rec_diff_call')->default(true); + $table->boolean('discard_qso_sent_diff_call')->default(false); + $table->boolean('discard_qso_rec_diff_rst')->default(true); + $table->boolean('discard_qso_sent_diff_rst')->default(false); + $table->boolean('discard_qso_rec_diff_code')->default(true); + $table->boolean('discard_qso_sent_diff_code')->default(false); + $table->boolean('unique_qso')->default(true); + $table->integer('time_tolerance')->default(600); + + $table->foreign('round_id')->references('id')->on('rounds')->cascadeOnDelete(); + + // pokud chceš jednu sadu paramů na (round_id, log_type): + // $table->unique(['round_id', 'log_type']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('rounds_parameters'); + } +}; diff --git a/database/migrations/2025_11_13_181403_create_rounds_bands_table.php b/database/migrations/2025_11_13_181403_create_rounds_bands_table.php new file mode 100644 index 0000000..8fe222d --- /dev/null +++ b/database/migrations/2025_11_13_181403_create_rounds_bands_table.php @@ -0,0 +1,31 @@ +timestamps(); + $table->unsignedBigInteger('round_id'); + $table->unsignedBigInteger('band_id'); + $table->foreign('round_id')->references('id')->on('rounds')->cascadeOnDelete(); + $table->foreign('band_id')->references('id')->on('bands')->cascadeOnDelete(); + $table->primary(['round_id', 'band_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('rounds_bands'); + } +}; diff --git a/database/migrations/2025_11_13_181429_create_rounds_categories_table.php b/database/migrations/2025_11_13_181429_create_rounds_categories_table.php new file mode 100644 index 0000000..0f65ed1 --- /dev/null +++ b/database/migrations/2025_11_13_181429_create_rounds_categories_table.php @@ -0,0 +1,31 @@ +timestamps(); + $table->unsignedBigInteger('round_id'); + $table->unsignedBigInteger('category_id'); + $table->foreign('round_id')->references('id')->on('rounds')->cascadeOnDelete(); + $table->foreign('category_id')->references('id')->on('categories')->cascadeOnDelete(); + $table->primary(['round_id', 'category_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('rounds_categories'); + } +}; diff --git a/database/migrations/2025_11_13_181441_create_rounds_power_categories_table.php b/database/migrations/2025_11_13_181441_create_rounds_power_categories_table.php new file mode 100644 index 0000000..8f53ebe --- /dev/null +++ b/database/migrations/2025_11_13_181441_create_rounds_power_categories_table.php @@ -0,0 +1,31 @@ +timestamps(); + $table->unsignedBigInteger('round_id'); + $table->unsignedBigInteger('power_category_id'); + $table->foreign('round_id')->references('id')->on('rounds')->cascadeOnDelete(); + $table->foreign('power_category_id')->references('id')->on('power_categories')->cascadeOnDelete(); + $table->primary(['round_id', 'power_category_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('rounds_power_categories'); + } +}; diff --git a/database/migrations/2025_11_15_072705_create_files_table.php b/database/migrations/2025_11_15_072705_create_files_table.php new file mode 100644 index 0000000..fe4205d --- /dev/null +++ b/database/migrations/2025_11_15_072705_create_files_table.php @@ -0,0 +1,33 @@ +id(); + $table->timestamps(); + $table->string('path'); + $table->string('filename'); + $table->string('mimetype'); + $table->unsignedBigInteger('filesize'); + $table->string('hash')->unique(); + $table->string('uploaded_by')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('files'); + } +}; diff --git a/database/migrations/2025_11_16_151949_create_logs_table.php b/database/migrations/2025_11_16_151949_create_logs_table.php new file mode 100644 index 0000000..f4482f4 --- /dev/null +++ b/database/migrations/2025_11_16_151949_create_logs_table.php @@ -0,0 +1,72 @@ +id(); + $table->timestamps(); + + // vazby + $table->foreignId('round_id') + ->constrained('rounds') + ->cascadeOnDelete(); + + $table->foreignId('file_id') + ->nullable() + ->constrained('files') + ->nullOnDelete(); + + // stav zpracování + $table->boolean('accepted')->default(false); + $table->boolean('processed')->default(false); + + // základ z EDI hlavičky (mapování na TName, TDate, PCall, PWWLo, ...) + $table->string('tname', 100)->nullable(); // TName + $table->string('tdate', 50)->nullable(); // TDate (raw) + $table->string('pcall', 20)->nullable(); // PCall + $table->string('pwwlo', 6)->nullable(); // PWWLo + $table->string('pexch', 10)->nullable(); // PExch / exchange + $table->string('psect', 10)->nullable(); // PSect + $table->string('pband', 10)->nullable(); // PBand (band label) + $table->string('pclub', 50)->nullable(); // PClub + + $table->string('country_name', 150)->nullable(); // z cty_country / RCoun + $table->string('operator_name', 100)->nullable();// RName / MOpe1 + $table->string('locator', 6)->nullable(); // QTH/locator dle logu + + // výkon a kategorie + $table->float('power_watt')->nullable(); // SPowe + $table->string('power_category', 3)->nullable(); // power_category + $table->boolean('sixhr_category')->nullable(); // sixhr_category (Y/N => bool) + + // součty z logu + $table->integer('claimed_qso_count')->nullable(); // CQSOP / qso_count + $table->integer('claimed_score')->nullable(); // CToSc + $table->string('claimed_wwl', 50)->nullable(); // CWWLs + $table->string('claimed_dxcc', 50)->nullable(); // CDXCs + + // metadata / poznámky + $table->string('ip_address', 45)->nullable(); + $table->string('remarks', 500)->nullable(); + $table->string('remarks_eval', 500)->nullable(); + + // raw hlavička pro audit/debug (volitelné, ale praktické) + $table->longText('raw_header')->nullable(); + + // indexy pro typické filtry + $table->index(['round_id', 'pcall']); + $table->index(['round_id', 'processed']); + }); + } + + public function down(): void + { + Schema::dropIfExists('logs'); + } +}; diff --git a/database/migrations/2025_11_16_151953_create_logs_qsos_table.php b/database/migrations/2025_11_16_151953_create_logs_qsos_table.php new file mode 100644 index 0000000..05dba69 --- /dev/null +++ b/database/migrations/2025_11_16_151953_create_logs_qsos_table.php @@ -0,0 +1,59 @@ +id(); + $table->timestamps(); + + $table->foreignId('log_id') + ->constrained('logs') + ->cascadeOnDelete(); + + // pořadí v logu + $table->integer('qso_index')->nullable(); + + // základní pole z QSO řádku + $table->dateTime('time_on')->nullable(); // kombinace data + času + $table->string('band', 10)->nullable(); // odvozeno z frekvence / PBand + $table->integer('freq_khz')->nullable(); // QSO frekvence v kHz + $table->string('mode', 5)->nullable(); // CW/SSB/etc. + + // naše strana + $table->string('my_call', 20)->nullable(); + $table->string('my_rst', 10)->nullable(); + $table->string('my_serial', 10)->nullable(); + $table->string('my_locator', 6)->nullable(); + + // protistanice + $table->string('dx_call', 20)->nullable(); + $table->string('dx_rst', 10)->nullable(); + $table->string('dx_serial', 10)->nullable(); + $table->string('dx_locator', 6)->nullable(); + + // vyhodnocení + $table->integer('points')->nullable(); + $table->string('wwl', 6)->nullable(); // odvozený WWL + $table->string('dxcc', 10)->nullable(); + $table->boolean('is_duplicate')->default(false); + $table->boolean('is_valid')->default(true); + + // raw QSO řádek tak, jak byl v EDI + $table->string('raw_line', 500)->nullable(); + + $table->index(['log_id', 'band']); + $table->index(['log_id', 'dx_call']); + }); + } + + public function down(): void + { + Schema::dropIfExists('log_qsos'); + } +}; diff --git a/database/migrations/2025_11_16_153325_create_evaluation_rule_sets_table.php b/database/migrations/2025_11_16_153325_create_evaluation_rule_sets_table.php new file mode 100644 index 0000000..6924d18 --- /dev/null +++ b/database/migrations/2025_11_16_153325_create_evaluation_rule_sets_table.php @@ -0,0 +1,72 @@ +id(); + $table->timestamps(); + + // identifikace rulesetu + $table->string('name', 100); + $table->string('code', 50)->unique(); // např. "IARU_VHF_2025" + $table->text('description')->nullable(); + + // základ scoringu + $table->enum('scoring_mode', ['DISTANCE', 'FIXED_POINTS']) + ->default('DISTANCE'); + + // FIXED_POINTS: body za QSO + $table->integer('points_per_qso')->default(1); + + // DISTANCE: body za km + $table->float('points_per_km')->default(1.0); + + // multiplikátory + $table->boolean('use_multipliers')->default(true); + $table->enum('multiplier_type', ['NONE', 'WWL', 'DXCC', 'SECTION', 'COUNTRY']) + ->default('WWL'); + + // politika duplicit + $table->enum('dup_qso_policy', [ + 'COUNT_ONCE', // další QSO = 0 bodů + 'ZERO_POINTS', // explicitně 0 bodů, bez penalizace + 'PENALTY', // 0 bodů + penalizace + ])->default('ZERO_POINTS'); + + // NIL / not-in-log + $table->enum('nil_qso_policy', [ + 'ZERO_POINTS', // smazat body za QSO + 'PENALTY', // smazat + penalizace + ])->default('PENALTY'); + + // busted call / exchange + $table->enum('busted_call_policy', [ + 'ZERO_POINTS', + 'PENALTY', + ])->default('PENALTY'); + + $table->enum('busted_exchange_policy', [ + 'ZERO_POINTS', + 'PENALTY', + ])->default('ZERO_POINTS'); + + // časová tolerance a pravidla duplicit jsou součástí rulesetu + $table->integer('time_tolerance_sec')->nullable(); + $table->boolean('require_unique_qso')->default(true); + + // obecné doplňkové nastavení (např. per-band multipliers, speciální bonusy) + $table->json('options')->nullable(); + }); + } + + public function down(): void + { + Schema::dropIfExists('evaluation_rule_sets'); + } +}; diff --git a/database/migrations/2025_11_16_153423_create_evaluation_runs_table.php b/database/migrations/2025_11_16_153423_create_evaluation_runs_table.php new file mode 100644 index 0000000..d80a358 --- /dev/null +++ b/database/migrations/2025_11_16_153423_create_evaluation_runs_table.php @@ -0,0 +1,35 @@ +id(); + $table->timestamps(); + + $table->foreignId('round_id') + ->constrained('rounds') + ->cascadeOnDelete(); + + $table->foreignId('rule_set_id') + ->nullable() + ->constrained('evaluation_rule_sets') + ->nullOnDelete(); + + $table->string('name', 100)->nullable(); + $table->string('rules_version', 100)->nullable(); + $table->boolean('is_official')->default(false); + $table->text('notes')->nullable(); + }); + } + + public function down(): void + { + Schema::dropIfExists('evaluation_runs'); + } +}; diff --git a/database/migrations/2025_11_16_153432_create_log_results_table.php b/database/migrations/2025_11_16_153432_create_log_results_table.php new file mode 100644 index 0000000..2f67d87 --- /dev/null +++ b/database/migrations/2025_11_16_153432_create_log_results_table.php @@ -0,0 +1,56 @@ +id(); + $table->timestamps(); + + $table->foreignId('evaluation_run_id') + ->constrained('evaluation_runs') + ->cascadeOnDelete(); + + $table->foreignId('log_id') + ->constrained('logs') + ->cascadeOnDelete(); + + // kategorizační kontext (tak jak byl pro daný běh určen) + $table->foreignId('band_id')->nullable()->constrained('bands'); + $table->foreignId('category_id')->nullable()->constrained('categories'); + $table->foreignId('power_category_id')->nullable()->constrained('power_categories'); + + // claim vs. official + $table->integer('claimed_qso_count')->nullable(); + $table->integer('claimed_score')->nullable(); + + $table->integer('valid_qso_count')->default(0); + $table->integer('dupe_qso_count')->default(0); + $table->integer('busted_qso_count')->default(0); // chybný call/rst/locator + $table->integer('other_error_qso_count')->default(0); + + $table->integer('official_score')->default(0); + $table->integer('penalty_score')->default(0); + + // pořadí v závodě / kategorii + $table->integer('rank_overall')->nullable(); + $table->integer('rank_in_category')->nullable(); + + // stav logu v evaluaci + $table->string('status', 20)->default('OK'); // např. OK / CHECK / DQ / MISSING + $table->text('status_reason')->nullable(); + + $table->unique(['evaluation_run_id', 'log_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('log_results'); + } +}; diff --git a/database/migrations/2025_11_16_153441_create_qso_results_table.php b/database/migrations/2025_11_16_153441_create_qso_results_table.php new file mode 100644 index 0000000..f4d85cc --- /dev/null +++ b/database/migrations/2025_11_16_153441_create_qso_results_table.php @@ -0,0 +1,54 @@ +id(); + $table->timestamps(); + + $table->foreignId('evaluation_run_id') + ->constrained('evaluation_runs') + ->cascadeOnDelete(); + + $table->foreignId('log_qso_id') + ->constrained('log_qsos') + ->cascadeOnDelete(); + + // výsledek QSO + $table->boolean('is_valid')->default(true); + $table->boolean('is_duplicate')->default(false); + $table->boolean('is_nil')->default(false); // neodpovídající QSO v druhém logu + $table->boolean('is_busted_call')->default(false); + $table->boolean('is_busted_rst')->default(false); + $table->boolean('is_busted_exchange')->default(false); + $table->boolean('is_time_out_of_window')->default(false); + + $table->integer('points')->default(0); + $table->double('distance_km')->nullable(); + + // multipliers + $table->string('wwl', 6)->nullable(); + $table->string('dxcc', 10)->nullable(); + + // volitelný link na odpovídající QSO v druhém logu + $table->unsignedBigInteger('matched_qso_id')->nullable(); + + // obecná chybová kategorizace + $table->string('error_code', 50)->nullable(); // např. "BUSTED_CALL", "DUP", ... + $table->text('error_detail')->nullable(); + + $table->index(['evaluation_run_id', 'log_qso_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('qso_results'); + } +}; diff --git a/database/migrations/2025_11_16_202516_add_two_factor_columns_to_users_table.php b/database/migrations/2025_11_16_202516_add_two_factor_columns_to_users_table.php new file mode 100644 index 0000000..45739ef --- /dev/null +++ b/database/migrations/2025_11_16_202516_add_two_factor_columns_to_users_table.php @@ -0,0 +1,42 @@ +text('two_factor_secret') + ->after('password') + ->nullable(); + + $table->text('two_factor_recovery_codes') + ->after('two_factor_secret') + ->nullable(); + + $table->timestamp('two_factor_confirmed_at') + ->after('two_factor_recovery_codes') + ->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn([ + 'two_factor_secret', + 'two_factor_recovery_codes', + 'two_factor_confirmed_at', + ]); + }); + } +}; diff --git a/database/migrations/2025_11_20_000001_add_round_logtype_unique_to_rounds_parameters.php b/database/migrations/2025_11_20_000001_add_round_logtype_unique_to_rounds_parameters.php new file mode 100644 index 0000000..8d4874e --- /dev/null +++ b/database/migrations/2025_11_20_000001_add_round_logtype_unique_to_rounds_parameters.php @@ -0,0 +1,28 @@ +unique(['round_id', 'log_type'], 'rounds_parameters_round_logtype_unique'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('rounds_parameters', function (Blueprint $table) { + $table->dropUnique('rounds_parameters_round_logtype_unique'); + }); + } +}; diff --git a/database/migrations/2025_11_21_000001_add_edi_fields_to_logs.php b/database/migrations/2025_11_21_000001_add_edi_fields_to_logs.php new file mode 100644 index 0000000..aa8a058 --- /dev/null +++ b/database/migrations/2025_11_21_000001_add_edi_fields_to_logs.php @@ -0,0 +1,74 @@ +string('padr1', 255)->nullable()->after('pclub'); + $table->string('padr2', 255)->nullable()->after('padr1'); + + $table->string('radr1', 255)->nullable()->after('padr2'); + $table->string('radr2', 255)->nullable()->after('radr1'); + $table->string('rpoco', 50)->nullable()->after('radr2'); + $table->string('rcity', 100)->nullable()->after('rpoco'); + $table->string('rphon', 100)->nullable()->after('rcity'); + $table->string('rhbbs', 150)->nullable()->after('rphon'); + + $table->string('mope1', 100)->nullable()->after('rhbbs'); + $table->string('mope2', 100)->nullable()->after('mope1'); + + $table->string('stxeq', 150)->nullable()->after('mope2'); + $table->string('srxeq', 150)->nullable()->after('stxeq'); + $table->string('sante', 150)->nullable()->after('srxeq'); + $table->string('santh', 50)->nullable()->after('sante'); + + $table->string('cqsos', 50)->nullable()->after('santh'); + $table->string('cqsop', 50)->nullable()->after('cqsos'); + $table->string('cwwls', 50)->nullable()->after('cqsop'); + $table->string('cwwlb', 50)->nullable()->after('cwwls'); + $table->string('cexcs', 50)->nullable()->after('cwwlb'); + $table->string('cexcb', 50)->nullable()->after('cexcs'); + $table->string('cdxcs', 50)->nullable()->after('cexcb'); + $table->string('cdxcb', 50)->nullable()->after('cdxcs'); + $table->string('ctosc', 50)->nullable()->after('cdxcb'); + $table->string('codxc', 255)->nullable()->after('ctosc'); + }); + } + + public function down(): void + { + Schema::table('logs', function (Blueprint $table) { + $table->dropColumn([ + 'padr1', + 'padr2', + 'radr1', + 'radr2', + 'rpoco', + 'rcity', + 'rphon', + 'rhbbs', + 'mope1', + 'mope2', + 'stxeq', + 'srxeq', + 'sante', + 'santh', + 'cqsos', + 'cqsop', + 'cwwls', + 'cwwlb', + 'cexcs', + 'cexcb', + 'cdxcs', + 'cdxcb', + 'ctosc', + 'codxc', + ]); + }); + } +}; diff --git a/database/migrations/2025_11_21_000002_rename_log_columns_to_edi_names.php b/database/migrations/2025_11_21_000002_rename_log_columns_to_edi_names.php new file mode 100644 index 0000000..69109ae --- /dev/null +++ b/database/migrations/2025_11_21_000002_rename_log_columns_to_edi_names.php @@ -0,0 +1,32 @@ +renameColumn('operator_name', 'rname'); + } + if (Schema::hasColumn('logs', 'country_name')) { + $table->renameColumn('country_name', 'rcoun'); + } + }); + } + + public function down(): void + { + Schema::table('logs', function (Blueprint $table) { + if (Schema::hasColumn('logs', 'rname')) { + $table->renameColumn('rname', 'operator_name'); + } + if (Schema::hasColumn('logs', 'rcoun')) { + $table->renameColumn('rcoun', 'country_name'); + } + }); + } +}; diff --git a/database/migrations/2025_11_21_000003_add_rcall_to_logs.php b/database/migrations/2025_11_21_000003_add_rcall_to_logs.php new file mode 100644 index 0000000..168f4a9 --- /dev/null +++ b/database/migrations/2025_11_21_000003_add_rcall_to_logs.php @@ -0,0 +1,26 @@ +string('rcall', 20)->nullable()->after('rname'); + } + }); + } + + public function down(): void + { + Schema::table('logs', function (Blueprint $table) { + if (Schema::hasColumn('logs', 'rcall')) { + $table->dropColumn('rcall'); + } + }); + } +}; diff --git a/database/migrations/2025_11_22_000001_update_log_qsos_for_edi.php b/database/migrations/2025_11_22_000001_update_log_qsos_for_edi.php new file mode 100644 index 0000000..65a92d2 --- /dev/null +++ b/database/migrations/2025_11_22_000001_update_log_qsos_for_edi.php @@ -0,0 +1,54 @@ +renameColumn('dx_locator', 'rx_wwl'); + } elseif (! Schema::hasColumn('log_qsos', 'rx_wwl')) { + $table->string('rx_wwl', 10)->nullable()->after('dx_call'); + } + + if (! Schema::hasColumn('log_qsos', 'mode_code')) { + $table->string('mode_code', 5)->nullable()->after('dx_call'); + } + if (! Schema::hasColumn('log_qsos', 'rx_exchange')) { + $table->string('rx_exchange', 10)->nullable()->after('dx_serial'); + } + + if (! Schema::hasColumn('log_qsos', 'new_exchange')) { + $table->boolean('new_exchange')->nullable()->after('points'); + } + if (! Schema::hasColumn('log_qsos', 'new_wwl')) { + $table->boolean('new_wwl')->nullable()->after('new_exchange'); + } + if (! Schema::hasColumn('log_qsos', 'new_dxcc')) { + $table->boolean('new_dxcc')->nullable()->after('new_wwl'); + } + if (! Schema::hasColumn('log_qsos', 'duplicate_qso')) { + $table->boolean('duplicate_qso')->nullable()->after('new_dxcc'); + } + }); + } + + public function down(): void + { + Schema::table('log_qsos', function (Blueprint $table) { + if (Schema::hasColumn('log_qsos', 'rx_wwl') && ! Schema::hasColumn('log_qsos', 'dx_locator')) { + $table->renameColumn('rx_wwl', 'dx_locator'); + } + $columns = ['mode_code', 'rx_exchange', 'new_exchange', 'new_wwl', 'new_dxcc', 'duplicate_qso']; + foreach ($columns as $col) { + if (Schema::hasColumn('log_qsos', $col)) { + $table->dropColumn($col); + } + } + }); + } +}; diff --git a/database/migrations/2025_11_30_073728_create_news_posts_table.php b/database/migrations/2025_11_30_073728_create_news_posts_table.php new file mode 100644 index 0000000..f4a0b23 --- /dev/null +++ b/database/migrations/2025_11_30_073728_create_news_posts_table.php @@ -0,0 +1,48 @@ +id(); + $table->timestamps(); + $table->json('title'); + $table->string('slug')->unique(); + + // Markdown obsah + $table->json('content'); + + // Krátké shrnutí pro listing (volitelné) + $table->json('excerpt')->nullable(); + + // Publikační stav + $table->boolean('is_published')->default(false); + $table->timestamp('published_at')->nullable(); + + // Autor + $table->foreignId('author_id') + ->nullable() + ->constrained('users') + ->nullOnDelete(); + + // Pro rychlé filtrování + $table->index(['is_published', 'published_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('news_posts'); + } +}; diff --git a/database/migrations/2025_12_24_145237_add_runtime_fields_to_evaluation_runs_table.php b/database/migrations/2025_12_24_145237_add_runtime_fields_to_evaluation_runs_table.php new file mode 100644 index 0000000..d7ae2f1 --- /dev/null +++ b/database/migrations/2025_12_24_145237_add_runtime_fields_to_evaluation_runs_table.php @@ -0,0 +1,53 @@ +string('status', 50)->nullable()->after('notes'); + $table->string('batch_id', 100)->nullable()->after('status'); + $table->string('current_step', 100)->nullable()->after('batch_id'); + $table->unsignedInteger('progress_total')->nullable()->after('current_step'); + $table->unsignedInteger('progress_done')->nullable()->after('progress_total'); + $table->json('scope')->nullable()->after('progress_done'); + $table->text('error')->nullable()->after('scope'); + $table->timestamp('started_at')->nullable()->after('error'); + $table->timestamp('finished_at')->nullable()->after('started_at'); + $table->foreignId('created_by_user_id') + ->nullable() + ->after('finished_at') + ->constrained('users') + ->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('evaluation_runs', function (Blueprint $table) { + $table->dropForeign(['created_by_user_id']); + $table->dropColumn([ + 'status', + 'batch_id', + 'current_step', + 'progress_total', + 'progress_done', + 'scope', + 'error', + 'started_at', + 'finished_at', + 'created_by_user_id', + ]); + }); + } +}; diff --git a/database/migrations/2025_12_24_145353_create_evaluation_locks_table.php b/database/migrations/2025_12_24_145353_create_evaluation_locks_table.php new file mode 100644 index 0000000..831c2b6 --- /dev/null +++ b/database/migrations/2025_12_24_145353_create_evaluation_locks_table.php @@ -0,0 +1,36 @@ +id(); + $table->timestamps(); + $table->string('key', 150)->unique(); + + $table->foreignId('evaluation_run_id') + ->nullable() + ->constrained('evaluation_runs') + ->nullOnDelete(); + + $table->timestamp('locked_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('evaluation_locks'); + } +}; diff --git a/database/migrations/2025_12_24_145604_create_evaluation_run_events_table.php b/database/migrations/2025_12_24_145604_create_evaluation_run_events_table.php new file mode 100644 index 0000000..e642381 --- /dev/null +++ b/database/migrations/2025_12_24_145604_create_evaluation_run_events_table.php @@ -0,0 +1,35 @@ +id(); + $table->timestamps(); + + $table->foreignId('evaluation_run_id') + ->constrained('evaluation_runs') + ->cascadeOnDelete(); + + $table->string('level', 50); + $table->text('message'); + $table->json('context')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('evaluation_run_events'); + } +}; diff --git a/database/migrations/2025_12_27_221000_create_qso_overrides_table.php b/database/migrations/2025_12_27_221000_create_qso_overrides_table.php new file mode 100644 index 0000000..790a684 --- /dev/null +++ b/database/migrations/2025_12_27_221000_create_qso_overrides_table.php @@ -0,0 +1,60 @@ +id(); + $table->timestamps(); + + $table->foreignId('evaluation_run_id') + ->constrained('evaluation_runs') + ->cascadeOnDelete(); + $table->foreignId('log_qso_id') + ->constrained('log_qsos') + ->cascadeOnDelete(); + $table->foreignId('forced_matched_log_qso_id') + ->nullable() + ->constrained('log_qsos') + ->nullOnDelete(); + + $table->enum('forced_status', [ + 'AUTO', + 'VALID', + 'INVALID', + 'NIL', + 'DUPLICATE', + 'BUSTED_CALL', + 'BUSTED_EXCHANGE', + 'OUT_OF_WINDOW', + ])->default('AUTO'); + + $table->double('forced_points')->nullable(); + $table->double('forced_penalty')->nullable(); + + $table->string('reason', 500)->nullable(); + $table->json('context')->nullable(); + + $table->foreignId('created_by_user_id') + ->nullable() + ->constrained('users') + ->nullOnDelete(); + + $table->unique(['evaluation_run_id', 'log_qso_id'], 'qso_overrides_run_qso_unique'); + $table->index('evaluation_run_id', 'qso_overrides_run_idx'); + $table->index('log_qso_id', 'qso_overrides_qso_idx'); + $table->index('forced_matched_log_qso_id', 'qso_overrides_matched_idx'); + $table->index('created_by_user_id', 'qso_overrides_user_idx'); + }); + } + + public function down(): void + { + Schema::dropIfExists('qso_overrides'); + } +}; diff --git a/database/migrations/2025_12_27_221100_create_log_overrides_table.php b/database/migrations/2025_12_27_221100_create_log_overrides_table.php new file mode 100644 index 0000000..8dc8849 --- /dev/null +++ b/database/migrations/2025_12_27_221100_create_log_overrides_table.php @@ -0,0 +1,62 @@ +id(); + $table->timestamps(); + + $table->foreignId('evaluation_run_id') + ->constrained('evaluation_runs') + ->cascadeOnDelete(); + $table->foreignId('log_id') + ->constrained('logs') + ->cascadeOnDelete(); + + $table->enum('forced_log_status', ['AUTO', 'OK', 'CHECK', 'DQ', 'IGNORED']) + ->default('AUTO'); + + $table->foreignId('forced_band_id') + ->nullable() + ->constrained('bands') + ->nullOnDelete(); + $table->foreignId('forced_category_id') + ->nullable() + ->constrained('categories') + ->nullOnDelete(); + $table->foreignId('forced_power_category_id') + ->nullable() + ->constrained('power_categories') + ->nullOnDelete(); + + $table->integer('forced_power_w')->nullable(); + + $table->string('reason', 500)->nullable(); + $table->json('context')->nullable(); + + $table->foreignId('created_by_user_id') + ->nullable() + ->constrained('users') + ->nullOnDelete(); + + $table->unique(['evaluation_run_id', 'log_id'], 'log_overrides_run_log_unique'); + $table->index('evaluation_run_id', 'log_overrides_run_idx'); + $table->index('log_id', 'log_overrides_log_idx'); + $table->index('created_by_user_id', 'log_overrides_user_idx'); + $table->index('forced_band_id', 'log_overrides_band_idx'); + $table->index('forced_category_id', 'log_overrides_category_idx'); + $table->index('forced_power_category_id', 'log_overrides_power_category_idx'); + }); + } + + public function down(): void + { + Schema::dropIfExists('log_overrides'); + } +}; diff --git a/database/migrations/2025_12_29_162500_add_power_category_id_to_logs_table.php b/database/migrations/2025_12_29_162500_add_power_category_id_to_logs_table.php new file mode 100644 index 0000000..0c91668 --- /dev/null +++ b/database/migrations/2025_12_29_162500_add_power_category_id_to_logs_table.php @@ -0,0 +1,26 @@ +foreignId('power_category_id') + ->nullable() + ->after('power_category') + ->constrained('power_categories') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('logs', function (Blueprint $table) { + $table->dropConstrainedForeignId('power_category_id'); + }); + } +}; diff --git a/database/migrations/2025_12_30_210500_drop_is_duplicate_from_log_qsos_table.php b/database/migrations/2025_12_30_210500_drop_is_duplicate_from_log_qsos_table.php new file mode 100644 index 0000000..ff2fd26 --- /dev/null +++ b/database/migrations/2025_12_30_210500_drop_is_duplicate_from_log_qsos_table.php @@ -0,0 +1,26 @@ +dropColumn('is_duplicate'); + } + }); + } + + public function down(): void + { + Schema::table('log_qsos', function (Blueprint $table) { + if (! Schema::hasColumn('log_qsos', 'is_duplicate')) { + $table->boolean('is_duplicate')->default(false); + } + }); + } +}; diff --git a/database/migrations/2025_12_30_220000_add_result_type_to_evaluation_runs_table.php b/database/migrations/2025_12_30_220000_add_result_type_to_evaluation_runs_table.php new file mode 100644 index 0000000..a36c48d --- /dev/null +++ b/database/migrations/2025_12_30_220000_add_result_type_to_evaluation_runs_table.php @@ -0,0 +1,28 @@ +string('result_type', 20)->nullable()->after('rules_version'); + $table->index('result_type'); + } + }); + } + + public function down(): void + { + Schema::table('evaluation_runs', function (Blueprint $table) { + if (Schema::hasColumn('evaluation_runs', 'result_type')) { + $table->dropIndex(['result_type']); + $table->dropColumn('result_type'); + } + }); + } +}; diff --git a/database/migrations/2025_12_30_220200_add_forced_sixhr_category_to_log_overrides_table.php b/database/migrations/2025_12_30_220200_add_forced_sixhr_category_to_log_overrides_table.php new file mode 100644 index 0000000..60911fc --- /dev/null +++ b/database/migrations/2025_12_30_220200_add_forced_sixhr_category_to_log_overrides_table.php @@ -0,0 +1,24 @@ +boolean('forced_sixhr_category') + ->nullable() + ->after('forced_power_category_id'); + }); + } + + public function down(): void + { + Schema::table('log_overrides', function (Blueprint $table) { + $table->dropColumn('forced_sixhr_category'); + }); + } +}; diff --git a/database/migrations/2025_12_30_220210_add_sixhr_category_to_log_results_table.php b/database/migrations/2025_12_30_220210_add_sixhr_category_to_log_results_table.php new file mode 100644 index 0000000..3639d31 --- /dev/null +++ b/database/migrations/2025_12_30_220210_add_sixhr_category_to_log_results_table.php @@ -0,0 +1,24 @@ +boolean('sixhr_category') + ->nullable() + ->after('power_category_id'); + }); + } + + public function down(): void + { + Schema::table('log_results', function (Blueprint $table) { + $table->dropColumn('sixhr_category'); + }); + } +}; diff --git a/database/migrations/2026_01_15_000001_update_psect_length_in_logs_table.php b/database/migrations/2026_01_15_000001_update_psect_length_in_logs_table.php new file mode 100644 index 0000000..565fc3f --- /dev/null +++ b/database/migrations/2026_01_15_000001_update_psect_length_in_logs_table.php @@ -0,0 +1,22 @@ +string('psect', 255)->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('logs', function (Blueprint $table) { + $table->string('psect', 10)->nullable()->change(); + }); + } +}; diff --git a/database/migrations/2026_01_15_100000_add_regex_pattern_to_edi_categories_table.php b/database/migrations/2026_01_15_100000_add_regex_pattern_to_edi_categories_table.php new file mode 100644 index 0000000..0ba8532 --- /dev/null +++ b/database/migrations/2026_01_15_100000_add_regex_pattern_to_edi_categories_table.php @@ -0,0 +1,22 @@ +string('regex_pattern', 255)->nullable()->after('value'); + }); + } + + public function down(): void + { + Schema::table('edi_categories', function (Blueprint $table) { + $table->dropColumn('regex_pattern'); + }); + } +}; diff --git a/database/migrations/2026_01_15_130000_add_ok_ranks_to_log_results_table.php b/database/migrations/2026_01_15_130000_add_ok_ranks_to_log_results_table.php new file mode 100644 index 0000000..e4dd009 --- /dev/null +++ b/database/migrations/2026_01_15_130000_add_ok_ranks_to_log_results_table.php @@ -0,0 +1,23 @@ +integer('rank_overall_ok')->nullable()->after('rank_overall'); + $table->integer('rank_in_category_ok')->nullable()->after('rank_in_category'); + }); + } + + public function down(): void + { + Schema::table('log_results', function (Blueprint $table) { + $table->dropColumn(['rank_overall_ok', 'rank_in_category_ok']); + }); + } +}; diff --git a/database/migrations/2026_01_15_140000_add_unique_run_log_qso_to_qso_results_table.php b/database/migrations/2026_01_15_140000_add_unique_run_log_qso_to_qso_results_table.php new file mode 100644 index 0000000..a1aa1cd --- /dev/null +++ b/database/migrations/2026_01_15_140000_add_unique_run_log_qso_to_qso_results_table.php @@ -0,0 +1,22 @@ +unique(['evaluation_run_id', 'log_qso_id'], 'qso_results_run_log_qso_unique'); + }); + } + + public function down(): void + { + Schema::table('qso_results', function (Blueprint $table) { + $table->dropUnique('qso_results_run_log_qso_unique'); + }); + } +}; diff --git a/database/migrations/2026_01_15_150000_add_vhf_fields_to_evaluation_rule_sets_table.php b/database/migrations/2026_01_15_150000_add_vhf_fields_to_evaluation_rule_sets_table.php new file mode 100644 index 0000000..c135185 --- /dev/null +++ b/database/migrations/2026_01_15_150000_add_vhf_fields_to_evaluation_rule_sets_table.php @@ -0,0 +1,46 @@ +integer('penalty_dup_points')->default(0); + $table->integer('penalty_nil_points')->default(0); + $table->integer('penalty_busted_call_points')->default(0); + $table->integer('penalty_busted_exchange_points')->default(0); + + $table->enum('dupe_scope', ['BAND', 'BAND_MODE'])->default('BAND'); + $table->enum('callsign_normalization', ['STRICT', 'IGNORE_SUFFIX'])->default('IGNORE_SUFFIX'); + + $table->enum('distance_rounding', ['FLOOR', 'ROUND', 'CEIL'])->default('FLOOR'); + $table->integer('min_distance_km')->nullable(); + $table->boolean('require_locators')->default(true); + + $table->enum('out_of_window_policy', ['IGNORE', 'ZERO_POINTS', 'PENALTY', 'INVALID']) + ->default('INVALID'); + }); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + $table->dropColumn([ + 'penalty_dup_points', + 'penalty_nil_points', + 'penalty_busted_call_points', + 'penalty_busted_exchange_points', + 'dupe_scope', + 'callsign_normalization', + 'distance_rounding', + 'min_distance_km', + 'require_locators', + 'out_of_window_policy', + ]); + }); + } +}; diff --git a/database/migrations/2026_01_15_160000_create_working_qsos_table.php b/database/migrations/2026_01_15_160000_create_working_qsos_table.php new file mode 100644 index 0000000..cf696e4 --- /dev/null +++ b/database/migrations/2026_01_15_160000_create_working_qsos_table.php @@ -0,0 +1,52 @@ +id(); + $table->timestamps(); + + $table->foreignId('evaluation_run_id') + ->constrained('evaluation_runs') + ->cascadeOnDelete(); + + $table->foreignId('log_qso_id') + ->constrained('log_qsos') + ->cascadeOnDelete(); + + $table->foreignId('log_id') + ->constrained('logs') + ->cascadeOnDelete(); + + $table->dateTime('ts_utc')->nullable(); + $table->string('call_norm', 32)->nullable(); + $table->string('rcall_norm', 32)->nullable(); + $table->string('loc_norm', 6)->nullable(); + $table->string('rloc_norm', 6)->nullable(); + $table->foreignId('band_id')->nullable()->constrained('bands'); + $table->string('mode', 10)->nullable(); + + $table->string('match_key', 120)->nullable(); + $table->string('dupe_key', 120)->nullable(); + + $table->boolean('out_of_window')->default(false); + $table->json('errors')->nullable(); + + $table->unique(['evaluation_run_id', 'log_qso_id']); + $table->index(['evaluation_run_id', 'log_id']); + $table->index(['evaluation_run_id', 'match_key']); + $table->index(['evaluation_run_id', 'dupe_key']); + }); + } + + public function down(): void + { + Schema::dropIfExists('working_qsos'); + } +}; diff --git a/database/migrations/2026_01_15_160100_update_qso_results_distance_km_to_double.php b/database/migrations/2026_01_15_160100_update_qso_results_distance_km_to_double.php new file mode 100644 index 0000000..02d0798 --- /dev/null +++ b/database/migrations/2026_01_15_160100_update_qso_results_distance_km_to_double.php @@ -0,0 +1,23 @@ +foreignId('rule_set_id') + ->nullable() + ->constrained('evaluation_rule_sets') + ->nullOnDelete(); + }); + + Schema::table('rounds', function (Blueprint $table) { + $table->foreignId('rule_set_id') + ->nullable() + ->constrained('evaluation_rule_sets') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('rounds', function (Blueprint $table) { + $table->dropConstrainedForeignId('rule_set_id'); + }); + + Schema::table('contests', function (Blueprint $table) { + $table->dropConstrainedForeignId('rule_set_id'); + }); + } +}; diff --git a/database/migrations/2026_01_15_170000_add_vhf_ruleset_extensions_to_evaluation_rule_sets_table.php b/database/migrations/2026_01_15_170000_add_vhf_ruleset_extensions_to_evaluation_rule_sets_table.php new file mode 100644 index 0000000..8583222 --- /dev/null +++ b/database/migrations/2026_01_15_170000_add_vhf_ruleset_extensions_to_evaluation_rule_sets_table.php @@ -0,0 +1,53 @@ +enum('exchange_type', ['SERIAL', 'WWL', 'SERIAL_WWL', 'CUSTOM'])->default('SERIAL_WWL'); + $table->boolean('exchange_requires_wwl')->default(true); + $table->boolean('exchange_requires_serial')->default(true); + $table->boolean('exchange_requires_report')->default(false); + $table->string('exchange_pattern', 200)->nullable(); + + $table->json('match_tiebreak_order')->nullable(); + $table->boolean('match_require_locator_match')->default(false); + $table->boolean('match_require_exchange_match')->default(false); + + $table->enum('multiplier_scope', ['PER_BAND', 'OVERALL'])->default('PER_BAND'); + $table->enum('multiplier_source', ['VALID_ONLY', 'ALL_MATCHED'])->default('VALID_ONLY'); + $table->enum('wwl_multiplier_level', ['LOCATOR_2', 'LOCATOR_4', 'LOCATOR_6'])->default('LOCATOR_6'); + + $table->boolean('checklog_matching')->default(true); + $table->integer('penalty_out_of_window_points')->default(0); + $table->integer('out_of_window_dq_threshold')->nullable(); + }); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + $table->dropColumn([ + 'exchange_type', + 'exchange_requires_wwl', + 'exchange_requires_serial', + 'exchange_requires_report', + 'exchange_pattern', + 'match_tiebreak_order', + 'match_require_locator_match', + 'match_require_exchange_match', + 'multiplier_scope', + 'multiplier_source', + 'wwl_multiplier_level', + 'checklog_matching', + 'penalty_out_of_window_points', + 'out_of_window_dq_threshold', + ]); + }); + } +}; diff --git a/database/migrations/2026_01_15_171000_add_multiplier_fields_to_log_results_table.php b/database/migrations/2026_01_15_171000_add_multiplier_fields_to_log_results_table.php new file mode 100644 index 0000000..e043f8b --- /dev/null +++ b/database/migrations/2026_01_15_171000_add_multiplier_fields_to_log_results_table.php @@ -0,0 +1,28 @@ +integer('base_score')->default(0); + $table->integer('multiplier_count')->default(0); + $table->integer('multiplier_score')->default(0); + }); + } + + public function down(): void + { + Schema::table('log_results', function (Blueprint $table) { + $table->dropColumn([ + 'base_score', + 'multiplier_count', + 'multiplier_score', + ]); + }); + } +}; diff --git a/database/migrations/2026_01_16_000000_add_dxcc_to_cty_table.php b/database/migrations/2026_01_16_000000_add_dxcc_to_cty_table.php new file mode 100644 index 0000000..e763677 --- /dev/null +++ b/database/migrations/2026_01_16_000000_add_dxcc_to_cty_table.php @@ -0,0 +1,24 @@ +unsignedInteger('dxcc')->nullable()->after('country_name'); + $table->index('dxcc'); + }); + } + + public function down(): void + { + Schema::table('cty', function (Blueprint $table) { + $table->dropIndex(['dxcc']); + $table->dropColumn('dxcc'); + }); + } +}; diff --git a/database/migrations/2026_01_16_000100_add_country_and_section_to_qso_results_table.php b/database/migrations/2026_01_16_000100_add_country_and_section_to_qso_results_table.php new file mode 100644 index 0000000..1a86942 --- /dev/null +++ b/database/migrations/2026_01_16_000100_add_country_and_section_to_qso_results_table.php @@ -0,0 +1,23 @@ +string('country', 100)->nullable()->after('dxcc'); + $table->string('section', 50)->nullable()->after('country'); + }); + } + + public function down(): void + { + Schema::table('qso_results', function (Blueprint $table) { + $table->dropColumn(['country', 'section']); + }); + } +}; diff --git a/database/migrations/2026_01_16_000200_update_cty_prefix_length_and_add_prefix_norm.php b/database/migrations/2026_01_16_000200_update_cty_prefix_length_and_add_prefix_norm.php new file mode 100644 index 0000000..01c26b1 --- /dev/null +++ b/database/migrations/2026_01_16_000200_update_cty_prefix_length_and_add_prefix_norm.php @@ -0,0 +1,35 @@ +string('prefix_norm', 64)->nullable()->after('prefix'); + $table->index('prefix_norm'); + }); + + $driver = DB::getDriverName(); + if (in_array($driver, ['mysql', 'mariadb'], true)) { + DB::statement('ALTER TABLE cty MODIFY prefix VARCHAR(64) NOT NULL'); + } + } + + public function down(): void + { + Schema::table('cty', function (Blueprint $table) { + $table->dropIndex(['prefix_norm']); + $table->dropColumn('prefix_norm'); + }); + + $driver = DB::getDriverName(); + if (in_array($driver, ['mysql', 'mariadb'], true)) { + DB::statement('ALTER TABLE cty MODIFY prefix VARCHAR(25) NOT NULL'); + } + } +}; diff --git a/database/migrations/2026_01_16_120100_drop_rounds_parameters_table.php b/database/migrations/2026_01_16_120100_drop_rounds_parameters_table.php new file mode 100644 index 0000000..0381ab5 --- /dev/null +++ b/database/migrations/2026_01_16_120100_drop_rounds_parameters_table.php @@ -0,0 +1,37 @@ +id(); + $table->timestamps(); + $table->unsignedBigInteger('round_id'); + $table->enum('log_type', ['STANDARD', 'CHECK']); + $table->boolean('ignore_slash_part')->default(true); + $table->boolean('ignore_third_part')->default(true); + $table->boolean('letters_in_rst')->default(true); + $table->boolean('discard_qso_rec_diff_call')->default(true); + $table->boolean('discard_qso_sent_diff_call')->default(false); + $table->boolean('discard_qso_rec_diff_rst')->default(true); + $table->boolean('discard_qso_sent_diff_rst')->default(false); + $table->boolean('discard_qso_rec_diff_code')->default(true); + $table->boolean('discard_qso_sent_diff_code')->default(false); + $table->boolean('unique_qso')->default(true); + $table->integer('time_tolerance')->default(600); + + $table->foreign('round_id')->references('id')->on('rounds')->cascadeOnDelete(); + $table->unique(['round_id', 'log_type'], 'rounds_parameters_round_logtype_unique'); + }); + } +}; diff --git a/database/migrations/2026_01_16_130000_add_penalty_points_to_qso_results_table.php b/database/migrations/2026_01_16_130000_add_penalty_points_to_qso_results_table.php new file mode 100644 index 0000000..f32072a --- /dev/null +++ b/database/migrations/2026_01_16_130000_add_penalty_points_to_qso_results_table.php @@ -0,0 +1,22 @@ +integer('penalty_points')->default(0)->after('points'); + }); + } + + public function down(): void + { + Schema::table('qso_results', function (Blueprint $table) { + $table->dropColumn('penalty_points'); + }); + } +}; diff --git a/database/migrations/2026_01_16_140000_drop_contest_and_round_from_evaluation_rule_sets_table.php b/database/migrations/2026_01_16_140000_drop_contest_and_round_from_evaluation_rule_sets_table.php new file mode 100644 index 0000000..9994e90 --- /dev/null +++ b/database/migrations/2026_01_16_140000_drop_contest_and_round_from_evaluation_rule_sets_table.php @@ -0,0 +1,66 @@ +dropForeignIfExists('evaluation_rule_sets', 'contest_id'); + $this->dropForeignIfExists('evaluation_rule_sets', 'round_id'); + + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + if (Schema::hasColumn('evaluation_rule_sets', 'contest_id')) { + $table->dropColumn('contest_id'); + } + if (Schema::hasColumn('evaluation_rule_sets', 'round_id')) { + $table->dropColumn('round_id'); + } + }); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + $table->foreignId('contest_id') + ->nullable() + ->constrained('contests') + ->cascadeOnDelete(); + $table->foreignId('round_id') + ->nullable() + ->constrained('rounds') + ->cascadeOnDelete(); + }); + } + + private function dropForeignIfExists(string $tableName, string $columnName): void + { + if (! Schema::hasColumn($tableName, $columnName)) { + return; + } + + $row = DB::selectOne( + 'SELECT CONSTRAINT_NAME as name + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + AND COLUMN_NAME = ? + AND REFERENCED_TABLE_NAME IS NOT NULL + LIMIT 1', + [$tableName, $columnName] + ); + + if (! $row || empty($row->name)) { + return; + } + + DB::statement(sprintf( + 'ALTER TABLE `%s` DROP FOREIGN KEY `%s`', + $tableName, + $row->name + )); + } +}; diff --git a/database/migrations/2026_01_16_150000_add_exchange_requires_report_to_evaluation_rule_sets_table.php b/database/migrations/2026_01_16_150000_add_exchange_requires_report_to_evaluation_rule_sets_table.php new file mode 100644 index 0000000..7266981 --- /dev/null +++ b/database/migrations/2026_01_16_150000_add_exchange_requires_report_to_evaluation_rule_sets_table.php @@ -0,0 +1,26 @@ +boolean('exchange_requires_report')->default(false); + } + }); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + if (Schema::hasColumn('evaluation_rule_sets', 'exchange_requires_report')) { + $table->dropColumn('exchange_requires_report'); + } + }); + } +}; diff --git a/database/migrations/2026_01_16_160000_add_missing_ruleset_flags_to_evaluation_rule_sets_table.php b/database/migrations/2026_01_16_160000_add_missing_ruleset_flags_to_evaluation_rule_sets_table.php new file mode 100644 index 0000000..cb81cee --- /dev/null +++ b/database/migrations/2026_01_16_160000_add_missing_ruleset_flags_to_evaluation_rule_sets_table.php @@ -0,0 +1,76 @@ +boolean('ignore_slash_part')->default(false); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'ignore_third_part')) { + $table->boolean('ignore_third_part')->default(false); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'letters_in_rst')) { + $table->boolean('letters_in_rst')->default(true); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'discard_qso_rec_diff_call')) { + $table->boolean('discard_qso_rec_diff_call')->default(false); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'discard_qso_sent_diff_call')) { + $table->boolean('discard_qso_sent_diff_call')->default(false); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'discard_qso_rec_diff_rst')) { + $table->boolean('discard_qso_rec_diff_rst')->default(false); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'discard_qso_sent_diff_rst')) { + $table->boolean('discard_qso_sent_diff_rst')->default(false); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'discard_qso_rec_diff_code')) { + $table->boolean('discard_qso_rec_diff_code')->default(false); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'discard_qso_sent_diff_code')) { + $table->boolean('discard_qso_sent_diff_code')->default(false); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'busted_rst_policy')) { + $table->string('busted_rst_policy', 20)->default('ZERO_POINTS'); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'penalty_busted_rst_points')) { + $table->integer('penalty_busted_rst_points')->default(0); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'out_of_window_dq_threshold')) { + $table->integer('out_of_window_dq_threshold')->nullable(); + } + }); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + $columns = [ + 'ignore_slash_part', + 'ignore_third_part', + 'letters_in_rst', + 'discard_qso_rec_diff_call', + 'discard_qso_sent_diff_call', + 'discard_qso_rec_diff_rst', + 'discard_qso_sent_diff_rst', + 'discard_qso_rec_diff_code', + 'discard_qso_sent_diff_code', + 'busted_rst_policy', + 'penalty_busted_rst_points', + 'out_of_window_dq_threshold', + ]; + + foreach ($columns as $column) { + if (Schema::hasColumn('evaluation_rule_sets', $column)) { + $table->dropColumn($column); + } + } + }); + } +}; diff --git a/database/migrations/2026_01_16_170000_add_time_diff_dq_rules_to_evaluation_rule_sets_table.php b/database/migrations/2026_01_16_170000_add_time_diff_dq_rules_to_evaluation_rule_sets_table.php new file mode 100644 index 0000000..9256963 --- /dev/null +++ b/database/migrations/2026_01_16_170000_add_time_diff_dq_rules_to_evaluation_rule_sets_table.php @@ -0,0 +1,36 @@ +integer('time_diff_dq_threshold_percent')->nullable(); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'time_diff_dq_threshold_sec')) { + $table->integer('time_diff_dq_threshold_sec')->nullable(); + } + }); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + $columns = [ + 'time_diff_dq_threshold_percent', + 'time_diff_dq_threshold_sec', + ]; + + foreach ($columns as $column) { + if (Schema::hasColumn('evaluation_rule_sets', $column)) { + $table->dropColumn($column); + } + } + }); + } +}; diff --git a/database/migrations/2026_01_16_170100_add_time_diff_sec_to_qso_results_table.php b/database/migrations/2026_01_16_170100_add_time_diff_sec_to_qso_results_table.php new file mode 100644 index 0000000..72a2284 --- /dev/null +++ b/database/migrations/2026_01_16_170100_add_time_diff_sec_to_qso_results_table.php @@ -0,0 +1,26 @@ +integer('time_diff_sec')->nullable()->after('distance_km'); + } + }); + } + + public function down(): void + { + Schema::table('qso_results', function (Blueprint $table) { + if (Schema::hasColumn('qso_results', 'time_diff_sec')) { + $table->dropColumn('time_diff_sec'); + } + }); + } +}; diff --git a/database/migrations/2026_01_16_171000_add_bad_qso_dq_threshold_to_evaluation_rule_sets_table.php b/database/migrations/2026_01_16_171000_add_bad_qso_dq_threshold_to_evaluation_rule_sets_table.php new file mode 100644 index 0000000..a07c4b4 --- /dev/null +++ b/database/migrations/2026_01_16_171000_add_bad_qso_dq_threshold_to_evaluation_rule_sets_table.php @@ -0,0 +1,26 @@ +integer('bad_qso_dq_threshold_percent')->nullable(); + } + }); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + if (Schema::hasColumn('evaluation_rule_sets', 'bad_qso_dq_threshold_percent')) { + $table->dropColumn('bad_qso_dq_threshold_percent'); + } + }); + } +}; diff --git a/database/migrations/2026_01_16_171001_add_evaluation_run_pointers_to_rounds_table.php b/database/migrations/2026_01_16_171001_add_evaluation_run_pointers_to_rounds_table.php new file mode 100644 index 0000000..4fedfac --- /dev/null +++ b/database/migrations/2026_01_16_171001_add_evaluation_run_pointers_to_rounds_table.php @@ -0,0 +1,53 @@ +foreignId('preliminary_evaluation_run_id') + ->nullable() + ->after('rule_set_id') + ->constrained('evaluation_runs') + ->nullOnDelete(); + } + if (! Schema::hasColumn('rounds', 'official_evaluation_run_id')) { + $table->foreignId('official_evaluation_run_id') + ->nullable() + ->after('preliminary_evaluation_run_id') + ->constrained('evaluation_runs') + ->nullOnDelete(); + } + if (! Schema::hasColumn('rounds', 'test_evaluation_run_id')) { + $table->foreignId('test_evaluation_run_id') + ->nullable() + ->after('official_evaluation_run_id') + ->constrained('evaluation_runs') + ->nullOnDelete(); + } + }); + } + + public function down(): void + { + Schema::table('rounds', function (Blueprint $table) { + if (Schema::hasColumn('rounds', 'test_evaluation_run_id')) { + $table->dropForeign(['test_evaluation_run_id']); + $table->dropColumn('test_evaluation_run_id'); + } + if (Schema::hasColumn('rounds', 'official_evaluation_run_id')) { + $table->dropForeign(['official_evaluation_run_id']); + $table->dropColumn('official_evaluation_run_id'); + } + if (Schema::hasColumn('rounds', 'preliminary_evaluation_run_id')) { + $table->dropForeign(['preliminary_evaluation_run_id']); + $table->dropColumn('preliminary_evaluation_run_id'); + } + }); + } +}; diff --git a/database/migrations/2026_01_16_190000_add_matching_fields_to_evaluation_rule_sets_table.php b/database/migrations/2026_01_16_190000_add_matching_fields_to_evaluation_rule_sets_table.php new file mode 100644 index 0000000..7c6ce44 --- /dev/null +++ b/database/migrations/2026_01_16_190000_add_matching_fields_to_evaluation_rule_sets_table.php @@ -0,0 +1,49 @@ +boolean('rst_ignore_third_char')->default(false)->after('letters_in_rst'); + $table->unsignedInteger('callsign_suffix_max_len')->nullable()->after('callsign_normalization'); + $table->unsignedInteger('callsign_levenshtein_max')->nullable()->after('callsign_suffix_max_len'); + $table->boolean('allow_time_shift_one_hour')->default(false)->after('time_tolerance_sec'); + $table->unsignedInteger('time_shift_seconds')->nullable()->after('allow_time_shift_one_hour'); + $table->string('time_mismatch_policy', 20)->nullable()->after('time_shift_seconds'); + $table->boolean('discard_qso_rec_diff_serial')->nullable()->after('discard_qso_rec_diff_code'); + $table->boolean('discard_qso_sent_diff_serial')->nullable()->after('discard_qso_sent_diff_code'); + $table->boolean('discard_qso_rec_diff_wwl')->nullable()->after('discard_qso_sent_diff_serial'); + $table->boolean('discard_qso_sent_diff_wwl')->nullable()->after('discard_qso_rec_diff_wwl'); + }); + + DB::statement('UPDATE evaluation_rule_sets SET rst_ignore_third_char = ignore_third_part WHERE rst_ignore_third_char = 0'); + DB::statement('UPDATE evaluation_rule_sets SET discard_qso_rec_diff_serial = discard_qso_rec_diff_code WHERE discard_qso_rec_diff_serial IS NULL'); + DB::statement('UPDATE evaluation_rule_sets SET discard_qso_sent_diff_serial = discard_qso_sent_diff_code WHERE discard_qso_sent_diff_serial IS NULL'); + DB::statement('UPDATE evaluation_rule_sets SET discard_qso_rec_diff_wwl = discard_qso_rec_diff_code WHERE discard_qso_rec_diff_wwl IS NULL'); + DB::statement('UPDATE evaluation_rule_sets SET discard_qso_sent_diff_wwl = discard_qso_sent_diff_code WHERE discard_qso_sent_diff_wwl IS NULL'); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + $table->dropColumn([ + 'rst_ignore_third_char', + 'callsign_suffix_max_len', + 'callsign_levenshtein_max', + 'allow_time_shift_one_hour', + 'time_shift_seconds', + 'time_mismatch_policy', + 'discard_qso_rec_diff_serial', + 'discard_qso_sent_diff_serial', + 'discard_qso_rec_diff_wwl', + 'discard_qso_sent_diff_wwl', + ]); + }); + } +}; diff --git a/database/migrations/2026_01_16_190100_add_match_fields_to_qso_results_table.php b/database/migrations/2026_01_16_190100_add_match_fields_to_qso_results_table.php new file mode 100644 index 0000000..f6722e0 --- /dev/null +++ b/database/migrations/2026_01_16_190100_add_match_fields_to_qso_results_table.php @@ -0,0 +1,23 @@ +string('match_type', 40)->nullable()->after('matched_qso_id'); + $table->json('error_flags')->nullable()->after('error_detail'); + }); + } + + public function down(): void + { + Schema::table('qso_results', function (Blueprint $table) { + $table->dropColumn(['match_type', 'error_flags']); + }); + } +}; diff --git a/database/migrations/2026_01_16_210000_add_matching_columns_to_evaluation_rule_sets_table.php b/database/migrations/2026_01_16_210000_add_matching_columns_to_evaluation_rule_sets_table.php new file mode 100644 index 0000000..c9b462e --- /dev/null +++ b/database/migrations/2026_01_16_210000_add_matching_columns_to_evaluation_rule_sets_table.php @@ -0,0 +1,38 @@ +boolean('allow_time_mismatch_pairing')->default(true)->after('time_tolerance_sec'); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'time_mismatch_max_sec')) { + $table->unsignedInteger('time_mismatch_max_sec')->nullable()->after('allow_time_mismatch_pairing'); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'dup_resolution_strategy')) { + $table->json('dup_resolution_strategy')->nullable()->after('require_unique_qso'); + } + }); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + if (Schema::hasColumn('evaluation_rule_sets', 'allow_time_mismatch_pairing')) { + $table->dropColumn('allow_time_mismatch_pairing'); + } + if (Schema::hasColumn('evaluation_rule_sets', 'time_mismatch_max_sec')) { + $table->dropColumn('time_mismatch_max_sec'); + } + if (Schema::hasColumn('evaluation_rule_sets', 'dup_resolution_strategy')) { + $table->dropColumn('dup_resolution_strategy'); + } + }); + } +}; diff --git a/database/migrations/2026_01_16_211000_add_matching_fields_to_qso_results_table.php b/database/migrations/2026_01_16_211000_add_matching_fields_to_qso_results_table.php new file mode 100644 index 0000000..4d2787c --- /dev/null +++ b/database/migrations/2026_01_16_211000_add_matching_fields_to_qso_results_table.php @@ -0,0 +1,38 @@ +unsignedBigInteger('matched_log_qso_id')->nullable()->after('matched_qso_id'); + } + if (! Schema::hasColumn('qso_results', 'match_confidence')) { + $table->string('match_confidence', 20)->nullable()->after('matched_log_qso_id'); + } + if (! Schema::hasColumn('qso_results', 'error_side')) { + $table->string('error_side', 10)->nullable()->after('error_code'); + } + }); + } + + public function down(): void + { + Schema::table('qso_results', function (Blueprint $table) { + if (Schema::hasColumn('qso_results', 'matched_log_qso_id')) { + $table->dropColumn('matched_log_qso_id'); + } + if (Schema::hasColumn('qso_results', 'match_confidence')) { + $table->dropColumn('match_confidence'); + } + if (Schema::hasColumn('qso_results', 'error_side')) { + $table->dropColumn('error_side'); + } + }); + } +}; diff --git a/database/migrations/2026_01_17_000000_add_unpaired_and_busted_policies_to_evaluation_rule_sets_table.php b/database/migrations/2026_01_17_000000_add_unpaired_and_busted_policies_to_evaluation_rule_sets_table.php new file mode 100644 index 0000000..ac5b071 --- /dev/null +++ b/database/migrations/2026_01_17_000000_add_unpaired_and_busted_policies_to_evaluation_rule_sets_table.php @@ -0,0 +1,62 @@ +string('no_counterpart_log_policy', 30)->default('PENALTY')->after('nil_qso_policy'); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'not_in_counterpart_log_policy')) { + $table->string('not_in_counterpart_log_policy', 30)->default('PENALTY')->after('no_counterpart_log_policy'); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'unique_qso_policy')) { + $table->string('unique_qso_policy', 30)->default('ZERO_POINTS')->after('not_in_counterpart_log_policy'); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'busted_serial_policy')) { + $table->string('busted_serial_policy', 30)->default('ZERO_POINTS')->after('busted_rst_policy'); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'penalty_busted_serial_points')) { + $table->integer('penalty_busted_serial_points')->default(0)->after('busted_serial_policy'); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'busted_locator_policy')) { + $table->string('busted_locator_policy', 30)->default('ZERO_POINTS')->after('penalty_busted_serial_points'); + } + if (! Schema::hasColumn('evaluation_rule_sets', 'penalty_busted_locator_points')) { + $table->integer('penalty_busted_locator_points')->default(0)->after('busted_locator_policy'); + } + }); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + if (Schema::hasColumn('evaluation_rule_sets', 'penalty_busted_locator_points')) { + $table->dropColumn('penalty_busted_locator_points'); + } + if (Schema::hasColumn('evaluation_rule_sets', 'busted_locator_policy')) { + $table->dropColumn('busted_locator_policy'); + } + if (Schema::hasColumn('evaluation_rule_sets', 'penalty_busted_serial_points')) { + $table->dropColumn('penalty_busted_serial_points'); + } + if (Schema::hasColumn('evaluation_rule_sets', 'busted_serial_policy')) { + $table->dropColumn('busted_serial_policy'); + } + if (Schema::hasColumn('evaluation_rule_sets', 'unique_qso_policy')) { + $table->dropColumn('unique_qso_policy'); + } + if (Schema::hasColumn('evaluation_rule_sets', 'not_in_counterpart_log_policy')) { + $table->dropColumn('not_in_counterpart_log_policy'); + } + if (Schema::hasColumn('evaluation_rule_sets', 'no_counterpart_log_policy')) { + $table->dropColumn('no_counterpart_log_policy'); + } + }); + } +}; diff --git a/database/migrations/2026_01_17_120000_add_discarded_fields_to_log_results_table.php b/database/migrations/2026_01_17_120000_add_discarded_fields_to_log_results_table.php new file mode 100644 index 0000000..7325e15 --- /dev/null +++ b/database/migrations/2026_01_17_120000_add_discarded_fields_to_log_results_table.php @@ -0,0 +1,34 @@ +integer('total_qso_count')->default(0); + $table->integer('discarded_qso_count')->default(0); + $table->integer('discarded_points')->default(0); + $table->decimal('discarded_qso_percent', 5, 2)->default(0); + $table->integer('unique_qso_count')->default(0); + $table->decimal('score_per_qso', 10, 2)->nullable(); + }); + } + + public function down(): void + { + Schema::table('log_results', function (Blueprint $table) { + $table->dropColumn([ + 'total_qso_count', + 'discarded_qso_count', + 'discarded_points', + 'discarded_qso_percent', + 'unique_qso_count', + 'score_per_qso', + ]); + }); + } +}; diff --git a/database/migrations/2026_02_01_090000_add_is_active_to_users_table.php b/database/migrations/2026_02_01_090000_add_is_active_to_users_table.php new file mode 100644 index 0000000..cfef9fe --- /dev/null +++ b/database/migrations/2026_02_01_090000_add_is_active_to_users_table.php @@ -0,0 +1,23 @@ +boolean('is_active')->default(true)->index(); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropIndex(['is_active']); + $table->dropColumn('is_active'); + }); + } +}; diff --git a/database/migrations/2026_02_10_120000_add_operating_window_fields_to_evaluation_rule_sets_table.php b/database/migrations/2026_02_10_120000_add_operating_window_fields_to_evaluation_rule_sets_table.php new file mode 100644 index 0000000..a10c10a --- /dev/null +++ b/database/migrations/2026_02_10_120000_add_operating_window_fields_to_evaluation_rule_sets_table.php @@ -0,0 +1,27 @@ +enum('operating_window_mode', ['NONE', 'BEST_CONTIGUOUS']) + ->default('NONE') + ->after('dup_resolution_strategy'); + $table->unsignedTinyInteger('operating_window_hours') + ->nullable() + ->after('operating_window_mode'); + }); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + $table->dropColumn(['operating_window_mode', 'operating_window_hours']); + }); + } +}; diff --git a/database/migrations/2026_02_10_120100_add_operating_window_fields_to_log_results_table.php b/database/migrations/2026_02_10_120100_add_operating_window_fields_to_log_results_table.php new file mode 100644 index 0000000..e95ea9c --- /dev/null +++ b/database/migrations/2026_02_10_120100_add_operating_window_fields_to_log_results_table.php @@ -0,0 +1,30 @@ +dateTime('operating_window_start_utc')->nullable()->after('sixhr_category'); + $table->dateTime('operating_window_end_utc')->nullable()->after('operating_window_start_utc'); + $table->unsignedTinyInteger('operating_window_hours')->nullable()->after('operating_window_end_utc'); + $table->unsignedInteger('operating_window_qso_count')->nullable()->after('operating_window_hours'); + }); + } + + public function down(): void + { + Schema::table('log_results', function (Blueprint $table) { + $table->dropColumn([ + 'operating_window_start_utc', + 'operating_window_end_utc', + 'operating_window_hours', + 'operating_window_qso_count', + ]); + }); + } +}; diff --git a/database/migrations/2026_02_10_120200_add_operating_window_excluded_to_qso_results_table.php b/database/migrations/2026_02_10_120200_add_operating_window_excluded_to_qso_results_table.php new file mode 100644 index 0000000..2f67a43 --- /dev/null +++ b/database/migrations/2026_02_10_120200_add_operating_window_excluded_to_qso_results_table.php @@ -0,0 +1,24 @@ +boolean('is_operating_window_excluded') + ->default(false) + ->after('is_time_out_of_window'); + }); + } + + public function down(): void + { + Schema::table('qso_results', function (Blueprint $table) { + $table->dropColumn('is_operating_window_excluded'); + }); + } +}; diff --git a/database/migrations/2026_02_10_120400_add_sixhr_ranking_mode_to_evaluation_rule_sets_table.php b/database/migrations/2026_02_10_120400_add_sixhr_ranking_mode_to_evaluation_rule_sets_table.php new file mode 100644 index 0000000..542db91 --- /dev/null +++ b/database/migrations/2026_02_10_120400_add_sixhr_ranking_mode_to_evaluation_rule_sets_table.php @@ -0,0 +1,24 @@ +string('sixhr_ranking_mode', 10) + ->default('IARU') + ->after('operating_window_hours'); + }); + } + + public function down(): void + { + Schema::table('evaluation_rule_sets', function (Blueprint $table) { + $table->dropColumn('sixhr_ranking_mode'); + }); + } +}; diff --git a/database/migrations/2026_02_10_120500_add_sixhr_ranking_bucket_to_log_results_table.php b/database/migrations/2026_02_10_120500_add_sixhr_ranking_bucket_to_log_results_table.php new file mode 100644 index 0000000..a946d12 --- /dev/null +++ b/database/migrations/2026_02_10_120500_add_sixhr_ranking_bucket_to_log_results_table.php @@ -0,0 +1,22 @@ +string('sixhr_ranking_bucket', 10)->nullable()->after('sixhr_category'); + }); + } + + public function down(): void + { + Schema::table('log_results', function (Blueprint $table) { + $table->dropColumn('sixhr_ranking_bucket'); + }); + } +}; diff --git a/database/migrations/2026_02_10_120600_add_operating_window_second_segment_to_log_results_table.php b/database/migrations/2026_02_10_120600_add_operating_window_second_segment_to_log_results_table.php new file mode 100644 index 0000000..aa0e6f0 --- /dev/null +++ b/database/migrations/2026_02_10_120600_add_operating_window_second_segment_to_log_results_table.php @@ -0,0 +1,30 @@ +dateTime('operating_window_2_start_utc') + ->nullable() + ->after('operating_window_end_utc'); + $table->dateTime('operating_window_2_end_utc') + ->nullable() + ->after('operating_window_2_start_utc'); + }); + } + + public function down(): void + { + Schema::table('log_results', function (Blueprint $table) { + $table->dropColumn([ + 'operating_window_2_start_utc', + 'operating_window_2_end_utc', + ]); + }); + } +}; diff --git a/database/seeders/BandEdiBandSeeder.php b/database/seeders/BandEdiBandSeeder.php new file mode 100644 index 0000000..b7fb289 --- /dev/null +++ b/database/seeders/BandEdiBandSeeder.php @@ -0,0 +1,57 @@ +truncate(); + Schema::enableForeignKeyConstraints(); + + $this->call([ + BandSeeder::class, + EdiBandSeeder::class, + ]); + + $bands_edi_bands = [ + ['band_id' => 1, 'edi_band_id' => 1], + ['band_id' => 2, 'edi_band_id' => 2], + ['band_id' => 2, 'edi_band_id' => 3], + ['band_id' => 3, 'edi_band_id' => 4], + ['band_id' => 3, 'edi_band_id' => 5], + ['band_id' => 4, 'edi_band_id' => 6], + ['band_id' => 4, 'edi_band_id' => 7], + ['band_id' => 5, 'edi_band_id' => 8], + ['band_id' => 5, 'edi_band_id' => 9], + ['band_id' => 6, 'edi_band_id' => 10], + ['band_id' => 7, 'edi_band_id' => 11], + ['band_id' => 8, 'edi_band_id' => 12], + ['band_id' => 9, 'edi_band_id' => 13], + ['band_id' => 10, 'edi_band_id' => 14], + ['band_id' => 11, 'edi_band_id' => 15], + ['band_id' => 12, 'edi_band_id' => 16], + ['band_id' => 12, 'edi_band_id' => 17], + ['band_id' => 13, 'edi_band_id' => 18], + ['band_id' => 13, 'edi_band_id' => 19], + ['band_id' => 14, 'edi_band_id' => 20], + ]; + + foreach ($bands_edi_bands as &$bands_edi_band) { + $bands_edi_band['created_at'] = now(); + $bands_edi_band['updated_at'] = now(); + }; + + DB::table('bands_edi_bands')->insert($bands_edi_bands); + } +} diff --git a/database/seeders/BandSeeder.php b/database/seeders/BandSeeder.php new file mode 100644 index 0000000..6f91e0f --- /dev/null +++ b/database/seeders/BandSeeder.php @@ -0,0 +1,132 @@ +truncate(); + Schema::enableForeignKeyConstraints(); + + $bands = [ + [ + 'name' => '50 MHz', + 'order' => 10, + 'edi_band_begin' => 50, + 'edi_band_end' => 54, + 'has_power_category' => false, + ], + [ + 'name' => '145 MHz', + 'order' => 20, + 'edi_band_begin' => 144, + 'edi_band_end' => 148, + 'has_power_category' => true, + ], + [ + 'name' => '435 MHz', + 'order' => 30, + 'edi_band_begin' => 430, + 'edi_band_end' => 440, + 'has_power_category' => true, + ], + [ + 'name' => '1,3 GHz', + 'order' => 40, + 'edi_band_begin' => 1240, + 'edi_band_end' => 1300, + 'has_power_category' => false, + ], + [ + 'name' => '2,3 GHz', + 'order' => 50, + 'edi_band_begin' => 2300, + 'edi_band_end' => 2450, + 'has_power_category' => false, + ], + [ + 'name' => '3,4 GHz', + 'order' => 60, + 'edi_band_begin' => 3400, + 'edi_band_end' => 3600, + 'has_power_category' => false, + ], + [ + 'name' => '5,7 GHz', + 'order' => 70, + 'edi_band_begin' => 5650, + 'edi_band_end' => 5850, + 'has_power_category' => false, + ], + [ + 'name' => '10 GHz', + 'order' => 80, + 'edi_band_begin' => 10000, + 'edi_band_end' => 10500, + 'has_power_category' => false, + ], + [ + 'name' => '24 GHz', + 'order' => 90, + 'edi_band_begin' => 24000, + 'edi_band_end' => 24250, + 'has_power_category' => false, + ], + [ + 'name' => '47 GHz', + 'order' => 100, + 'edi_band_begin' => 47000, + 'edi_band_end' => 47200, + 'has_power_category' => false, + ], + [ + 'name' => '76 GHz', + 'order' => 110, + 'edi_band_begin' => 75500, + 'edi_band_end' => 81000, + 'has_power_category' => false, + ], + [ + 'name' => '120 GHz', + 'order' => 120, + 'edi_band_begin' => 122250, + 'edi_band_end' => 123000, + 'has_power_category' => false, + ], + [ + 'name' => '134 GHz', + 'order' => 130, + 'edi_band_begin' => 134000, + 'edi_band_end' => 144000, + 'has_power_category' => false, + ], + [ + 'name' => '287 GHz', + 'order' => 140, + 'edi_band_begin' => 287000, + 'edi_band_end' => 287000, + 'has_power_category' => false, + ], + + ]; + + foreach ($bands as &$band) { + $band['created_at'] = now(); + $band['updated_at'] = now(); + }; + + DB::table('bands')->insert($bands); + } +} diff --git a/database/seeders/CategoriesEdiCategoriesSeeder.php b/database/seeders/CategoriesEdiCategoriesSeeder.php new file mode 100644 index 0000000..b126479 --- /dev/null +++ b/database/seeders/CategoriesEdiCategoriesSeeder.php @@ -0,0 +1,45 @@ +truncate(); + Schema::enableForeignKeyConstraints(); + + $this->call([ + CategoriesSeeder::class, + EdiCategoriesSeeder::class, + ]); + + $categories_edi_categories = [ + ['category_id' => 1, 'edi_category_id' => 1], + ['category_id' => 1, 'edi_category_id' => 2], + ['category_id' => 1, 'edi_category_id' => 3], + ['category_id' => 1, 'edi_category_id' => 4], + ['category_id' => 1, 'edi_category_id' => 5], + ['category_id' => 2, 'edi_category_id' => 6], + ['category_id' => 2, 'edi_category_id' => 7], + ['category_id' => 3, 'edi_category_id' => 8], + ['category_id' => 3, 'edi_category_id' => 9], + ]; + + foreach ($categories_edi_categories as &$categories_edi_category) { + $categories_edi_category['created_at'] = now(); + $categories_edi_category['updated_at'] = now(); + }; + + DB::table('categories_edi_categories')->insert($categories_edi_categories); + } +} diff --git a/database/seeders/CategoriesSeeder.php b/database/seeders/CategoriesSeeder.php new file mode 100644 index 0000000..0b6f36e --- /dev/null +++ b/database/seeders/CategoriesSeeder.php @@ -0,0 +1,35 @@ +truncate(); + Schema::enableForeignKeyConstraints(); + + $categories = [ + ['name' => 'SINGLE', 'order' => 10], + ['name' => 'MULTI', 'order' => 20], + ['name' => 'CHECK', 'order' => 30], + ]; + + foreach ($categories as &$category) { + $category['created_at'] = now(); + $category['updated_at'] = now(); + }; + + DB::table('categories')->insert($categories); + } +} diff --git a/database/seeders/ContestSeeder.php b/database/seeders/ContestSeeder.php new file mode 100644 index 0000000..de92142 --- /dev/null +++ b/database/seeders/ContestSeeder.php @@ -0,0 +1,97 @@ +truncate(); + DB::table('contests_bands')->truncate(); + DB::table('contests_categories')->truncate(); + DB::table('contests_power_categories')->truncate(); + DB::table('contests_parameters')->truncate(); + Schema::enableForeignKeyConstraints(); + + $bands = Band::all()->pluck('id')->toArray(); + $categories = Category::all()->pluck('id')->toArray(); + $powerCategories = PowerCategory::all()->pluck('id')->toArray(); + $defaultRuleSetId = EvaluationRuleSet::where('code', 'default_vhf_compat')->value('id'); + + // základní parametry dle vzoru (Y/N -> bool) + $baseParams = [ + 'ignore_slash_part' => true, + 'ignore_third_part' => true, + 'letters_in_rst' => true, + 'discard_qso_rec_diff_call' => true, + 'discard_qso_sent_diff_call' => false, + 'discard_qso_rec_diff_rst' => true, + 'discard_qso_sent_diff_rst' => false, + 'discard_qso_rec_diff_code' => true, + 'discard_qso_sent_diff_code' => false, + 'unique_qso' => false, + 'time_tolerance' => 600, + ]; + + for ($i = 1; $i <= 2; $i++) { + $contest = new Contest(); + + // lokalizované položky přes Spatie HasTranslations + $contest->setTranslations('name', [ + 'cs' => "VKV závod {$i}", + 'en' => "VHF Contest {$i}", + ]); + + $contest->setTranslations('description', [ + 'cs' => "Oficiální VKV závod číslo {$i}.", + 'en' => "The official VHF Contest number {$i}.", + ]); + + // nelokalizovaná data + $contest->evaluator = 'Český radioklub'; + $contest->email = 'crk@crk.cz'; + $contest->email2 = null; + $contest->is_mcr = true; + $contest->is_sixhr = $i % 2 === 0; + $contest->is_active = $i !== 5; // poslední neaktivní + $contest->start_time = '14:00:00'; + $contest->duration = 24; + $contest->logs_deadline_days = 3; + $contest->rule_set_id = $defaultRuleSetId; + + $contest->save(); + + // navázání všech pásem + $contest->bands()->attach($bands); + // navázání všech kategorií + $contest->categories()->attach($categories); + // navázání všech výkonových kategorií + $contest->powerCategories()->attach($powerCategories); + + // contest parameters pro oba log typy + foreach (['STANDARD', 'CHECK'] as $logType) { + ContestParameter::create(array_merge( + $baseParams, + [ + 'contest_id' => $contest->id, + 'log_type' => $logType, + ] + )); + } + } + } +} diff --git a/database/seeders/CountriesWwlSeeder.php b/database/seeders/CountriesWwlSeeder.php new file mode 100644 index 0000000..5fb418d --- /dev/null +++ b/database/seeders/CountriesWwlSeeder.php @@ -0,0 +1,33 @@ + $record['0'], + "wwl" => $record['1'] + ); + CountryWwl::create($wwl); + } + $heading = false; + } + fclose($input_file); + } +} diff --git a/database/seeders/CtySeeder.php b/database/seeders/CtySeeder.php new file mode 100644 index 0000000..fe0c149 --- /dev/null +++ b/database/seeders/CtySeeder.php @@ -0,0 +1,169 @@ +truncate(); + + $handle = gzopen($path, 'rb'); + if (! $handle) { + return; + } + + $dxcc = null; + $current = null; + $prefixBuffer = ''; + $batch = []; + $batchSize = 500; + $source = 'cty_wt.dat'; + $now = now(); + + $flush = function () use (&$batch) { + if (! $batch) { + return; + } + + DB::table('cty')->upsert( + $batch, + ['prefix'], + [ + 'country_name', + 'dxcc', + 'cq_zone', + 'itu_zone', + 'continent', + 'latitude', + 'longitude', + 'time_offset', + 'prefix_norm', + 'precise', + 'source', + 'updated_at', + ] + ); + + $batch = []; + }; + + $addPrefix = function (string $token, array $current, bool $preciseOverride = false) use (&$batch, $batchSize, $now, $flush) { + $rawToken = trim($token); + if ($rawToken === '') { + return; + } + + $precise = $preciseOverride || str_starts_with($rawToken, '='); + $prefix = $rawToken; + + $normToken = $rawToken; + if (str_starts_with($normToken, '=')) { + $normToken = substr($normToken, 1); + } + $normToken = trim($normToken); + $normToken = preg_split('/[\\(\\[<\\{~]/', $normToken, 2)[0] ?? ''; + $normToken = strtoupper(trim($normToken)); + if ($normToken === '') { + return; + } + $batch[] = [ + 'country_name' => $current['country_name'], + 'dxcc' => $current['dxcc'], + 'cq_zone' => $current['cq_zone'], + 'itu_zone' => $current['itu_zone'], + 'continent' => $current['continent'], + 'latitude' => $current['latitude'], + 'longitude' => $current['longitude'], + 'time_offset' => $current['time_offset'], + 'prefix' => $prefix, + 'prefix_norm' => $normToken, + 'precise' => $precise, + 'source' => $current['source'], + 'created_at' => $now, + 'updated_at' => $now, + ]; + + if (count($batch) >= $batchSize) { + $flush(); + } + }; + + while (($line = gzgets($handle)) !== false) { + $line = rtrim($line, "\r\n"); + if ($line === '') { + continue; + } + + if (str_starts_with($line, '#')) { + if (preg_match('/ADIF\\s+(\\d+)/', $line, $match)) { + $dxcc = (int) $match[1]; + } + continue; + } + + if (! preg_match('/^\\s/', $line)) { + $prefixBuffer = ''; + if (preg_match( + '/^([^:]+):\\s*([0-9]+):\\s*([0-9]+):\\s*([A-Z]{2}):\\s*([0-9\\.-]+):\\s*([0-9\\.-]+):\\s*([0-9\\.-]+):\\s*([^:]+):/', + $line, + $match + )) { + $current = [ + 'country_name' => trim($match[1]), + 'dxcc' => $dxcc, + 'cq_zone' => (int) $match[2], + 'itu_zone' => (int) $match[3], + 'continent' => trim($match[4]), + 'latitude' => (float) $match[5], + 'longitude' => (float) $match[6], + 'time_offset' => (float) $match[7], + 'source' => $source, + ]; + + $primaryPrefix = trim($match[8]); + if ($primaryPrefix !== '') { + $addPrefix($primaryPrefix, $current); + } + } else { + $current = null; + } + continue; + } + + if (! $current) { + continue; + } + + $segment = trim($line); + if ($segment === '') { + continue; + } + + $prefixBuffer .= ' ' . $segment; + + if (str_contains($segment, ';')) { + $prefixBuffer = rtrim($prefixBuffer, ';'); + $tokens = array_map('trim', explode(',', $prefixBuffer)); + foreach ($tokens as $token) { + $addPrefix($token, $current); + } + $prefixBuffer = ''; + } + } + + $flush(); + gzclose($handle); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..ec176c4 --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,42 @@ +create(); + + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@test.com', + 'is_admin' => true + ]); + + $this->call([ + BandEdiBandSeeder::class, + CategoriesEdiCategoriesSeeder::class, + PowerCategoriesSeeder::class, + CtySeeder::class, + CountriesWwlSeeder::class, + ContestSeeder::class, + RoundSeeder::class, + EvaluationRuleSetSeeder::class, + NewsPostSeeder::class + ]); + + if (env('RUN_EVAL_REGRESSION_SEEDER')) { + $this->call(EvaluationPipelineRegressionSeeder::class); + } + } +} diff --git a/database/seeders/EdiBandSeeder.php b/database/seeders/EdiBandSeeder.php new file mode 100644 index 0000000..d0357a9 --- /dev/null +++ b/database/seeders/EdiBandSeeder.php @@ -0,0 +1,52 @@ +truncate(); + Schema::enableForeignKeyConstraints(); + + $edi_bands = [ + ['value' => '50 Mhz'], //1 + ['value' => '144 Mhz'], //2 + ['value' => '145 Mhz'], //3 + ['value' => '432 Mhz'], //4 + ['value' => '435 Mhz'], //5 + ['value' => '1.2 Ghz'], //6 + ['value' => '1.3 Ghz'], //7 + ['value' => '2.3 Ghz'], //8 + ['value' => '2.4 Ghz'], //9 + ['value' => '3.4 Ghz'], //10 + ['value' => '5.7 Ghz'], //11 + ['value' => '10 Ghz'], //12 + ['value' => '24 Ghz'], //13 + ['value' => '47 Ghz'], //14 + ['value' => '76 Ghz'], //15 + ['value' => '120 Ghz'], //16 + ['value' => '122 Ghz'], //17 + ['value' => '134 Ghz'], //18 + ['value' => '144 Ghz'], //19 + ['value' => '248 Ghz'], //20 + ]; + + foreach ($edi_bands as &$edi_band) { + $edi_band['created_at'] = now(); + $edi_band['updated_at'] = now(); + }; + + DB::table('edi_bands')->insert($edi_bands); + } +} diff --git a/database/seeders/EdiCategoriesSeeder.php b/database/seeders/EdiCategoriesSeeder.php new file mode 100644 index 0000000..00f5a37 --- /dev/null +++ b/database/seeders/EdiCategoriesSeeder.php @@ -0,0 +1,44 @@ +truncate(); + Schema::enableForeignKeyConstraints(); + + $ediCategories = [ + [ + 'value' => 'SINGLE', + 'regex_pattern' => '^[\\w\\s-]*\\b(?:single|so)\\b' + ], //1 + ['value' => 'SINGLE-OP', 'regex_pattern' => '' ], //2 + ['value' => 'SO', 'regex_pattern' => '' ], //3 + ['value' => 'SINGLE QRP', 'regex_pattern' => '' ], //4 + ['value' => 'SO-LP', 'regex_pattern' => '' ], //5 + ['value' => 'MULTI', 'regex_pattern' => '^[\w\s-]*\b(?:multi|mo)\b' ], //6 + ['value' => 'MO-LP', 'regex_pattern' => '' ], //7 + ['value' => 'CHECK', 'regex_pattern' => '' ], //8 + ['value' => 'CHECKLOG', 'regex_pattern' => '' ], //9 + ]; + + foreach ($ediCategories as &$ediCategory) { + $ediCategory['created_at'] = now(); + $ediCategory['updated_at'] = now(); + }; + + DB::table('edi_categories')->insert($ediCategories); + } +} diff --git a/database/seeders/EvaluationPipelineRegressionSeeder.php b/database/seeders/EvaluationPipelineRegressionSeeder.php new file mode 100644 index 0000000..72f7c73 --- /dev/null +++ b/database/seeders/EvaluationPipelineRegressionSeeder.php @@ -0,0 +1,504 @@ + '145 MHz'], + [ + 'order' => 1, + 'edi_band_begin' => 144.0, + 'edi_band_end' => 146.0, + 'has_power_category' => true, + ] + ); + $category = Category::firstOrCreate( + ['name' => 'SINGLE'], + [ + 'order' => 1, + ] + ); + $power = PowerCategory::firstOrCreate( + ['name' => 'LP'], + [ + 'order' => 1, + 'power_level' => 100, + ] + ); + + $ruleSetUnique = EvaluationRuleSet::firstOrCreate( + ['code' => 'REG_UNIQUE'], + [ + 'name' => 'Regression UNIQUE', + 'scoring_mode' => 'FIXED_POINTS', + 'points_per_qso' => 1, + 'use_multipliers' => false, + 'dup_qso_policy' => 'ZERO_POINTS', + 'nil_qso_policy' => 'ZERO_POINTS', + 'busted_call_policy' => 'ZERO_POINTS', + 'busted_exchange_policy' => 'ZERO_POINTS', + 'time_tolerance_sec' => 60, + 'require_unique_qso' => true, + ] + ); + + $ruleSetNoUnique = EvaluationRuleSet::firstOrCreate( + ['code' => 'REG_DUP'], + [ + 'name' => 'Regression DUP', + 'scoring_mode' => 'FIXED_POINTS', + 'points_per_qso' => 1, + 'use_multipliers' => false, + 'dup_qso_policy' => 'ZERO_POINTS', + 'nil_qso_policy' => 'ZERO_POINTS', + 'busted_call_policy' => 'ZERO_POINTS', + 'busted_exchange_policy' => 'ZERO_POINTS', + 'time_tolerance_sec' => 60, + 'require_unique_qso' => false, + ] + ); + + $ruleSetTimeMismatch = EvaluationRuleSet::firstOrCreate( + ['code' => 'REG_TIME_MISMATCH'], + [ + 'name' => 'Regression TIME_MISMATCH', + 'scoring_mode' => 'FIXED_POINTS', + 'points_per_qso' => 1, + 'use_multipliers' => false, + 'dup_qso_policy' => 'ZERO_POINTS', + 'nil_qso_policy' => 'ZERO_POINTS', + 'busted_call_policy' => 'ZERO_POINTS', + 'busted_exchange_policy' => 'ZERO_POINTS', + 'time_tolerance_sec' => 60, + 'allow_time_mismatch_pairing' => true, + 'time_mismatch_max_sec' => 600, + 'time_mismatch_policy' => 'ZERO_POINTS', + ] + ); + $ruleSetFlags = EvaluationRuleSet::firstOrCreate( + ['code' => 'REG_FLAGS'], + [ + 'name' => 'Regression FLAGS', + 'scoring_mode' => 'FIXED_POINTS', + 'points_per_qso' => 1, + 'use_multipliers' => false, + 'dup_qso_policy' => 'ZERO_POINTS', + 'nil_qso_policy' => 'ZERO_POINTS', + 'busted_call_policy' => 'ZERO_POINTS', + 'busted_exchange_policy' => 'ZERO_POINTS', + 'exchange_requires_report' => true, + 'exchange_requires_serial' => true, + 'rst_ignore_third_char' => true, + 'ignore_slash_part' => true, + 'callsign_suffix_max_len' => 4, + 'callsign_levenshtein_max' => 1, + 'discard_qso_rec_diff_rst' => true, + 'discard_qso_rec_diff_serial' => true, + 'time_tolerance_sec' => 60, + ] + ); + + $contest = Contest::create([ + 'name' => ['cs' => 'Regression Contest', 'en' => 'Regression Contest'], + 'description' => ['cs' => 'Regresní sada', 'en' => 'Regression set'], + 'is_active' => false, + 'is_test' => true, + 'rule_set_id' => $ruleSetUnique->id, + ]); + $contest->bands()->sync([$band->id]); + $contest->categories()->sync([$category->id]); + $contest->powerCategories()->sync([$power->id]); + + $this->scenarioUniqueGlobal($contest, $band, $category, $power, $ruleSetUnique); + $this->scenarioUniqueNotGlobal($contest, $band, $category, $power, $ruleSetUnique); + $this->scenarioDuplicateResolution($contest, $band, $category, $power, $ruleSetNoUnique); + $this->scenarioTimeMismatchPolicy($contest, $band, $category, $power, $ruleSetTimeMismatch); + $this->scenarioFlagCoverage($contest, $band, $category, $power, $ruleSetFlags); + } + + private function scenarioUniqueGlobal( + Contest $contest, + Band $band, + Category $category, + PowerCategory $power, + EvaluationRuleSet $ruleSet + ): void { + $round = $this->makeRound($contest, $ruleSet, 'UNIQUE global'); + $this->attachRoundScopes($round, $band, $category, $power); + + $log = $this->makeLog($round, 'OK1AAA', 'JN79AB', '145'); + $qso = LogQso::create([ + 'log_id' => $log->id, + 'qso_index' => 1, + 'time_on' => Carbon::parse($round->start_time)->addMinutes(5), + 'freq_khz' => 144000, + 'my_call' => 'OK1AAA', + 'dx_call' => 'UNIQUE1', + 'my_locator' => 'JN79AB', + 'rx_wwl' => 'JN79AB', + ]); + + $run = $this->makeRun($round, $ruleSet); + $this->runPipeline($run); + + $errorCode = QsoResult::where('evaluation_run_id', $run->id) + ->where('log_qso_id', $qso->id) + ->value('error_code'); + if ($errorCode !== 'UNIQUE') { + throw new RuntimeException("Regression UNIQUE global selhal: očekáván UNIQUE, got {$errorCode}."); + } + } + + private function scenarioUniqueNotGlobal( + Contest $contest, + Band $band, + Category $category, + PowerCategory $power, + EvaluationRuleSet $ruleSet + ): void { + $round = $this->makeRound($contest, $ruleSet, 'UNIQUE not global'); + $this->attachRoundScopes($round, $band, $category, $power); + + $logA = $this->makeLog($round, 'OK1AAB', 'JN79AB', '145'); + $logB = $this->makeLog($round, 'OK1AAC', 'JN79AB', '145'); + $qsoA = LogQso::create([ + 'log_id' => $logA->id, + 'qso_index' => 1, + 'time_on' => Carbon::parse($round->start_time)->addMinutes(5), + 'freq_khz' => 144000, + 'my_call' => 'OK1AAB', + 'dx_call' => 'X2AAA', + 'my_locator' => 'JN79AB', + 'rx_wwl' => 'JN79AB', + ]); + LogQso::create([ + 'log_id' => $logB->id, + 'qso_index' => 1, + 'time_on' => Carbon::parse($round->start_time)->addMinutes(6), + 'freq_khz' => 144000, + 'my_call' => 'OK1AAC', + 'dx_call' => 'X2AAA', + 'my_locator' => 'JN79AB', + 'rx_wwl' => 'JN79AB', + ]); + + $run = $this->makeRun($round, $ruleSet); + $this->runPipeline($run); + + $errorCode = QsoResult::where('evaluation_run_id', $run->id) + ->where('log_qso_id', $qsoA->id) + ->value('error_code'); + if ($errorCode === 'UNIQUE') { + throw new RuntimeException('Regression UNIQUE not global selhal: UNIQUE nesmí vzniknout.'); + } + } + + private function scenarioDuplicateResolution( + Contest $contest, + Band $band, + Category $category, + PowerCategory $power, + EvaluationRuleSet $ruleSet + ): void { + $round = $this->makeRound($contest, $ruleSet, 'DUP resolution'); + $this->attachRoundScopes($round, $band, $category, $power); + + $log = $this->makeLog($round, 'OK1DUP', 'JN79AB', '145'); + LogQso::create([ + 'log_id' => $log->id, + 'qso_index' => 1, + 'time_on' => Carbon::parse($round->start_time)->addMinutes(5), + 'freq_khz' => 144000, + 'my_call' => 'OK1DUP', + 'dx_call' => 'DUP1', + 'my_locator' => 'JN79AB', + 'rx_wwl' => 'JN79AB', + ]); + LogQso::create([ + 'log_id' => $log->id, + 'qso_index' => 2, + 'time_on' => Carbon::parse($round->start_time)->addMinutes(6), + 'freq_khz' => 144000, + 'my_call' => 'OK1DUP', + 'dx_call' => 'DUP1', + 'my_locator' => 'JN79AB', + 'rx_wwl' => 'JN79AB', + ]); + + $run = $this->makeRun($round, $ruleSet); + $this->runPipeline($run); + + $dupCount = QsoResult::where('evaluation_run_id', $run->id) + ->where('error_code', 'DUP') + ->count(); + if ($dupCount !== 1) { + throw new RuntimeException("Regression DUP selhal: očekáván 1 DUP, got {$dupCount}."); + } + } + + private function scenarioTimeMismatchPolicy( + Contest $contest, + Band $band, + Category $category, + PowerCategory $power, + EvaluationRuleSet $ruleSet + ): void { + $round = $this->makeRound($contest, $ruleSet, 'TIME_MISMATCH policy'); + $this->attachRoundScopes($round, $band, $category, $power); + + $logA = $this->makeLog($round, 'OK1TMA', 'JN79AB', '145'); + $logB = $this->makeLog($round, 'OK1TMB', 'JN79AB', '145'); + + $timeA = Carbon::parse($round->start_time)->addMinutes(5); + $timeB = Carbon::parse($round->start_time)->addMinutes(9); + + $qsoA = LogQso::create([ + 'log_id' => $logA->id, + 'qso_index' => 1, + 'time_on' => $timeA, + 'freq_khz' => 144000, + 'my_call' => 'OK1TMA', + 'dx_call' => 'OK1TMB', + 'my_serial' => '001', + 'dx_serial' => '002', + 'my_locator' => 'JN79AB', + 'rx_wwl' => 'JN79AB', + ]); + LogQso::create([ + 'log_id' => $logB->id, + 'qso_index' => 1, + 'time_on' => $timeB, + 'freq_khz' => 144000, + 'my_call' => 'OK1TMB', + 'dx_call' => 'OK1TMA', + 'my_serial' => '002', + 'dx_serial' => '001', + 'my_locator' => 'JN79AB', + 'rx_wwl' => 'JN79AB', + ]); + + $run = $this->makeRun($round, $ruleSet); + $this->runPipeline($run); + + $result = QsoResult::where('evaluation_run_id', $run->id) + ->where('log_qso_id', $qsoA->id) + ->first(); + if (! $result || $result->error_code !== 'TIME_MISMATCH') { + throw new RuntimeException('Regression TIME_MISMATCH selhal: očekáván TIME_MISMATCH.'); + } + if (! $result->is_valid || (int) $result->points !== 0) { + throw new RuntimeException('Regression TIME_MISMATCH selhal: očekáván validní QSO s 0 body.'); + } + } + + private function scenarioFlagCoverage( + Contest $contest, + Band $band, + Category $category, + PowerCategory $power, + EvaluationRuleSet $ruleSet + ): void { + $round = $this->makeRound($contest, $ruleSet, 'FLAGS coverage'); + $this->attachRoundScopes($round, $band, $category, $power); + + $logSuffix = $this->makeLog($round, 'OK1AAA/P', 'JN79AB', '145'); + $qsoFuzzySource = LogQso::create([ + 'log_id' => $logSuffix->id, + 'qso_index' => 1, + 'time_on' => Carbon::parse($round->start_time)->addMinutes(2), + 'freq_khz' => 144000, + 'my_call' => 'OK1AAA/P', + 'dx_call' => 'OK1AAB', + 'my_rst' => '599', + 'dx_rst' => '599', + 'my_serial' => '001', + 'dx_serial' => '002', + 'my_locator' => 'JN79AB', + 'rx_wwl' => 'JN79AB', + ]); + + $logFuzzy = $this->makeLog($round, 'OK1AAB', 'JN79AB', '145'); + $qsoFuzzy = LogQso::create([ + 'log_id' => $logFuzzy->id, + 'qso_index' => 1, + 'time_on' => Carbon::parse($round->start_time)->addMinutes(2), + 'freq_khz' => 144000, + 'my_call' => 'OK1AAB', + 'dx_call' => 'OK1AAX', + 'my_rst' => '599', + 'dx_rst' => '599', + 'my_serial' => '002', + 'dx_serial' => '001', + 'my_locator' => 'JN79AB', + 'rx_wwl' => 'JN79AB', + ]); + $logSerial = $this->makeLog($round, 'OK1SER', 'JN79AB', '145'); + $qsoSerialMismatch = LogQso::create([ + 'log_id' => $logSuffix->id, + 'qso_index' => 2, + 'time_on' => Carbon::parse($round->start_time)->addMinutes(3), + 'freq_khz' => 144000, + 'my_call' => 'OK1AAA/P', + 'dx_call' => 'OK1SER', + 'my_rst' => '599', + 'dx_rst' => '599', + 'my_serial' => '005', + 'dx_serial' => '006', + 'my_locator' => 'JN79AB', + 'rx_wwl' => 'JN79AB', + ]); + LogQso::create([ + 'log_id' => $logSerial->id, + 'qso_index' => 1, + 'time_on' => Carbon::parse($round->start_time)->addMinutes(3), + 'freq_khz' => 144000, + 'my_call' => 'OK1SER', + 'dx_call' => 'OK1AAA', + 'my_rst' => '599', + 'dx_rst' => '599', + 'my_serial' => '007', + 'dx_serial' => '008', + 'my_locator' => 'JN79AB', + 'rx_wwl' => 'JN79AB', + ]); + + $run = $this->makeRun($round, $ruleSet); + $this->runPipeline($run); + + $suffixNorm = \App\Models\WorkingQso::where('evaluation_run_id', $run->id) + ->where('log_id', $logSuffix->id) + ->value('call_norm'); + if ($suffixNorm !== 'OK1AAA') { + throw new RuntimeException('Regression FLAGS selhal: callsign suffix nebyl normalizován.'); + } + + $fuzzyResult = QsoResult::where('evaluation_run_id', $run->id) + ->where('log_qso_id', $qsoFuzzy->id) + ->first(); + if (! $fuzzyResult || $fuzzyResult->match_type !== 'MATCH_FUZZY_CALL_1') { + throw new RuntimeException('Regression FLAGS selhal: callsign_levenshtein_max neovlivňuje matching.'); + } + + if ($fuzzyResult->error_code === 'BUSTED_RST') { + throw new RuntimeException('Regression FLAGS selhal: rst_ignore_third_char nebylo zohledněno.'); + } + + $serialResult = QsoResult::where('evaluation_run_id', $run->id) + ->where('log_qso_id', $qsoSerialMismatch->id) + ->where('error_code', 'BUSTED_SERIAL') + ->first(); + if (! $serialResult) { + throw new RuntimeException('Regression FLAGS selhal: discard_qso_rec_diff_serial nebylo zohledněno.'); + } + } + + private function runPipeline(EvaluationRun $run): void + { + (new PrepareRunJob($run->id))->handle(); + (new BuildWorkingSetJob($run->id))->handle(); + + $run = EvaluationRun::find($run->id); + $group = $run && isset($run->scope['groups'][0]) + ? $run->scope['groups'][0] + : [ + 'key' => 'b0', + 'band_id' => null, + 'category_id' => null, + 'power_category_id' => null, + ]; + if (! ($group['band_id'] ?? null)) { + $workingBandId = WorkingQso::where('evaluation_run_id', $run->id) + ->whereNotNull('band_id') + ->value('band_id'); + if ($workingBandId) { + $group['band_id'] = (int) $workingBandId; + $group['key'] = 'b' . $workingBandId; + } + } + $groupKey = $group['key'] ?? 'b0'; + + (new MatchQsoGroupJob($run->id, $groupKey, $group, 1))->handle(); + (new MatchQsoGroupJob($run->id, $groupKey, $group, 2))->handle(); + (new UnpairedClassificationJob($run->id))->handle(); + (new DuplicateResolutionJob($run->id))->handle(); + (new ScoreGroupJob($run->id, $groupKey, $group))->handle(); + } + + private function makeRound(Contest $contest, EvaluationRuleSet $ruleSet, string $label): Round + { + $start = now()->subDays(1); + $end = now()->subDays(1)->addHours(5); + + return Round::create([ + 'contest_id' => $contest->id, + 'rule_set_id' => $ruleSet->id, + 'name' => ['cs' => "Regrese {$label}", 'en' => "Regression {$label}"], + 'description' => ['cs' => 'Regresní běh', 'en' => 'Regression run'], + 'is_test' => true, + 'is_active' => false, + 'start_time' => $start, + 'end_time' => $end, + 'logs_deadline' => now(), + ]); + } + + private function attachRoundScopes(Round $round, Band $band, Category $category, PowerCategory $power): void + { + $round->bands()->sync([$band->id]); + $round->categories()->sync([$category->id]); + $round->powerCategories()->sync([$power->id]); + } + + private function makeLog(Round $round, string $pcall, string $pwwlo, string $pband): Log + { + return Log::create([ + 'round_id' => $round->id, + 'pcall' => $pcall, + 'pwwlo' => $pwwlo, + 'pband' => $pband, + 'power_category_id' => PowerCategory::first()?->id, + ]); + } + + private function makeRun(Round $round, EvaluationRuleSet $ruleSet): EvaluationRun + { + return EvaluationRun::create([ + 'round_id' => $round->id, + 'rule_set_id' => $ruleSet->id, + 'status' => 'PENDING', + 'current_step' => 'seed', + 'is_official' => false, + 'result_type' => 'TEST', + ]); + } +} diff --git a/database/seeders/EvaluationRuleSetSeeder.php b/database/seeders/EvaluationRuleSetSeeder.php new file mode 100644 index 0000000..2b11067 --- /dev/null +++ b/database/seeders/EvaluationRuleSetSeeder.php @@ -0,0 +1,107 @@ + 'default_vhf_compat'], + [ + 'name' => 'Default VHF (compat)', + 'description' => 'Permisivní profil – maximalizuje matching, mismatchy řeší policy ve scoringu.', + 'scoring_mode' => 'DISTANCE', + 'points_per_qso' => 1, + 'points_per_km' => 1.0, + 'use_multipliers' => false, + 'multiplier_type' => 'WWL', + 'dup_qso_policy' => 'ZERO_POINTS', + 'nil_qso_policy' => 'ZERO_POINTS', + 'no_counterpart_log_policy' => 'FLAG_ONLY', + 'not_in_counterpart_log_policy' => 'ZERO_POINTS', + 'unique_qso_policy' => 'FLAG_ONLY', + 'busted_call_policy' => 'ZERO_POINTS', + 'busted_exchange_policy' => 'ZERO_POINTS', + 'busted_serial_policy' => 'ZERO_POINTS', + 'busted_locator_policy' => 'ZERO_POINTS', + 'busted_rst_policy' => 'ZERO_POINTS', + 'penalty_busted_rst_points' => 0, + 'penalty_dup_points' => 0, + 'penalty_nil_points' => 0, + 'penalty_busted_call_points' => 0, + 'penalty_busted_exchange_points' => 0, + 'penalty_busted_serial_points' => 0, + 'penalty_busted_locator_points' => 0, + 'dupe_scope' => 'BAND', + 'callsign_normalization' => 'IGNORE_SUFFIX', + 'distance_rounding' => 'CEIL', + 'min_distance_km' => 1, + 'require_locators' => false, + 'out_of_window_policy' => 'INVALID', + 'penalty_out_of_window_points' => 0, + 'exchange_type' => 'SERIAL_WWL', + 'exchange_requires_wwl' => true, + 'exchange_requires_serial' => true, + 'exchange_requires_report' => true, + 'exchange_pattern' => null, + 'ignore_slash_part' => true, + 'ignore_third_part' => true, + 'letters_in_rst' => false, + 'rst_ignore_third_char' => true, + 'discard_qso_rec_diff_call' => true, + 'discard_qso_sent_diff_call' => false, + 'discard_qso_rec_diff_rst' => true, + 'discard_qso_sent_diff_rst' => false, + 'discard_qso_rec_diff_code' => true, + 'discard_qso_sent_diff_code' => false, + 'discard_qso_rec_diff_serial' => true, + 'discard_qso_sent_diff_serial' => false, + 'discard_qso_rec_diff_wwl' => true, + 'discard_qso_sent_diff_wwl' => false, + 'match_tiebreak_order' => [ + 'time_diff', + 'exchange_match', + 'locator_match', + 'report_match', + 'log_qso_id', + ], + 'match_require_locator_match' => false, + 'match_require_exchange_match' => false, + 'multiplier_scope' => 'PER_BAND', + 'multiplier_source' => 'VALID_ONLY', + 'wwl_multiplier_level' => 'LOCATOR_6', + 'checklog_matching' => true, + 'out_of_window_dq_threshold' => 600, + 'time_diff_dq_threshold_percent' => 30, + 'time_diff_dq_threshold_sec' => 600, + 'bad_qso_dq_threshold_percent' => 30, + 'time_tolerance_sec' => 600, + 'require_unique_qso' => true, + 'allow_time_mismatch_pairing' => false, + 'time_mismatch_max_sec' => null, + 'allow_time_shift_one_hour' => true, + 'time_shift_seconds' => 3600, + 'time_mismatch_policy' => 'ZERO_POINTS', + 'callsign_suffix_max_len' => 4, + 'callsign_levenshtein_max' => 0, + 'dup_resolution_strategy' => [ + 'paired_first', + 'ok_first', + 'earlier_time', + 'lower_id', + ], + 'operating_window_mode' => 'NONE', + 'operating_window_hours' => null, + 'sixhr_ranking_mode' => 'IARU', + 'options' => null, + ] + ); + } +} diff --git a/database/seeders/NewsPostSeeder.php b/database/seeders/NewsPostSeeder.php new file mode 100644 index 0000000..4de6b54 --- /dev/null +++ b/database/seeders/NewsPostSeeder.php @@ -0,0 +1,136 @@ + [ + 'cs' => 'Vývojová verze nového vyhodnocovacího systému VKV závodů', + 'en' => 'Development version of the new VHF contest evaluation system', + ], + 'content' => [ + 'cs' => << << [ + 'cs' => 'Nový systém pro vyhodnocování VKV závodů je ve vývoji.', + 'en' => 'The new VHF contest evaluation system is in development.', + ], + 'is_published' => true, + 'published_at' => now()->subDays(7), + ], + [ + 'title' => [ + 'cs' => 'Plánovaná údržba systému', + 'en' => 'Planned system maintenance', + ], + 'content' => [ + 'cs' => << << [ + 'cs' => 'Plánovaná odstávka systému 15. 12. 2025 mezi 22:00–23:00 CET.', + 'en' => 'Planned downtime on 15 Dec 2025 between 22:00–23:00 CET.', + ], + 'is_published' => false, + 'published_at' => now()->subDays(3), + ], + [ + 'title' => [ + 'cs' => 'Testovací novinka pouze v češtině', + 'en' => 'Test news item (Czech only translation)', + ], + 'content' => [ + 'cs' => << << [ + 'cs' => 'Testovací novinka pro ověření lokalizace.', + 'en' => 'Test news item for localization checks.', + ], + 'is_published' => false, + 'published_at' => now()->subDay(), + ], + ]; + + foreach ($items as $item) { + // slug vygenerujeme z českého titulku (nebo fallback na en) + $titleForSlug = $item['title']['cs'] + ?? $item['title']['en'] + ?? reset($item['title']); + + $slug = Str::slug($titleForSlug); + + $news = new NewsPost(); + + // lokalizované položky nastavíme explicitně jako překlady + $news->setTranslations('title', $item['title']); + $news->setTranslations('content', $item['content']); + $news->setTranslations('excerpt', $item['excerpt']); + + $news->slug = $slug; + $news->is_published = $item['is_published']; + $news->published_at = $item['published_at']; + $news->author_id = null; // nebo nějaký konkrétní user_id + + $news->save(); + } + } +} diff --git a/database/seeders/PowerCategoriesSeeder.php b/database/seeders/PowerCategoriesSeeder.php new file mode 100644 index 0000000..966be14 --- /dev/null +++ b/database/seeders/PowerCategoriesSeeder.php @@ -0,0 +1,36 @@ +truncate(); + Schema::enableForeignKeyConstraints(); + + $powerCategories = [ + ['name' => 'LP', 'order' => 10, 'power_level' => 100], + ['name' => 'QRP', 'order' => 20, 'power_level' => 5], + ['name' => 'N', 'order' => 30, 'power_level' => 10], + ['name' => 'A', 'order' => 40, 'power_level' => 100000000], + ]; + + foreach ($powerCategories as &$powerCategory) { + $powerCategory['created_at'] = now(); + $powerCategory['updated_at'] = now(); + }; + + DB::table('power_categories')->insert($powerCategories); + } +} diff --git a/database/seeders/RoundSeeder.php b/database/seeders/RoundSeeder.php new file mode 100644 index 0000000..37421a4 --- /dev/null +++ b/database/seeders/RoundSeeder.php @@ -0,0 +1,88 @@ +truncate(); + DB::table('rounds_bands')->truncate(); + DB::table('rounds_categories')->truncate(); + DB::table('rounds_power_categories')->truncate(); + DB::table('evaluation_runs')->truncate(); + Schema::enableForeignKeyConstraints(); + + $bands = Band::all()->pluck('id')->toArray(); + + $contests = Contest::with(['categories', 'powerCategories'])->get(); + if ($contests->isEmpty()) { + // contest se zakládá přes ContestSeeder – pokud chybí, nic neseedujeme + return; + } + + $globalStart = now()->startOfMonth()->setTime(14, 0); + + foreach ($contests as $contestIndex => $contest) { + $baseStart = (clone $globalStart)->addMonths($contestIndex * 2); + + for ($i = 0; $i < 2; $i++) { + $start = (clone $baseStart)->addMonths($i); + $end = (clone $start)->addHours(24); + $deadline = (clone $end)->addDays(3); + + $round = new Round(); + + $round->contest_id = $contest->id; + $round->rule_set_id = $contest->rule_set_id; + + $round->setTranslations('name', [ + 'cs' => 'Kolo ' . ($i + 1) . ' – ' . $contest->getTranslation('name', 'cs'), + 'en' => 'Round ' . ($i + 1) . ' – ' . $contest->getTranslation('name', 'en'), + ]); + + $round->setTranslations('description', [ + 'cs' => 'Testovací kolo číslo ' . ($i + 1) . ' pro závod ' . $contest->getTranslation('name', 'cs'), + 'en' => 'Test round number ' . ($i + 1) . ' for contest ' . $contest->getTranslation('name', 'en'), + ]); + + $round->is_active = $i < 8; // poslední dvě kola jako neaktivní + $round->is_test = $i === 0; // první kolo jako testovací + $round->is_sixhr = $i % 2 === 1; // lichá kola jako 6h + + $round->start_time = $start; + $round->end_time = $end; + $round->logs_deadline = $deadline; + + $round->save(); + + // napojení pásem stejné jako u contestu (všechna dostupná) + if (!empty($bands)) { + $round->bands()->attach($bands); + } + // napojení kategorií stejné jako u contestu + $categoryIds = $contest->categories->pluck('id')->toArray(); + if (!empty($categoryIds)) { + $round->categories()->attach($categoryIds); + } + // výkonové kategorie stejně jako u contestu + $powerCategoryIds = $contest->powerCategories->pluck('id')->toArray(); + if (!empty($powerCategoryIds)) { + $round->powerCategories()->attach($powerCategoryIds); + } + + } + } + } +} diff --git a/database/seeders/countries_wwl.csv b/database/seeders/countries_wwl.csv new file mode 100644 index 0000000..2d58d25 --- /dev/null +++ b/database/seeders/countries_wwl.csv @@ -0,0 +1,934 @@ +"country_name";"wwl" +African Italy;IM56 +African Italy;IM66 +Aland Islands;JP90 +Aland Islands;KO09 +Aland Islands;KP00 +Albania;JM99 +Albania;JN90 +Albania;JN91 +Albania;JN92 +Albania;KM09 +Albania;KN00 +Albania;KN01 +Albania;KN02 +Andorra;JN02 +Austria;JN46 +Austria;JN47 +Austria;JN56 +Austria;JN57 +Austria;JN66 +Austria;JN67 +Austria;JN68 +Austria;JN76 +Austria;JN77 +Austria;JN78 +Austria;JN79 +Austria;JN86 +Austria;JN87 +Austria;JN88 +Balearic Islands;JM08 +Balearic Islands;JM09 +Balearic Islands;JM19 +Balearic Islands;JM29 +Balearic Islands;JN10 +Balearic Islands;JN20 +Belarus;KO11 +Belarus;KO12 +Belarus;KO13 +Belarus;KO21 +Belarus;KO22 +Belarus;KO23 +Belarus;KO24 +Belarus;KO31 +Belarus;KO32 +Belarus;KO33 +Belarus;KO34 +Belarus;KO35 +Belarus;KO36 +Belarus;KO41 +Belarus;KO42 +Belarus;KO43 +Belarus;KO44 +Belarus;KO45 +Belarus;KO46 +Belarus;KO51 +Belarus;KO52 +Belarus;KO53 +Belarus;KO54 +Belarus;KO55 +Belarus;KO63 +Belgium;JN29 +Belgium;JO10 +Belgium;JO11 +Belgium;JO20 +Belgium;JO21 +Belgium;JO30 +Bosnia-Herzegovina;JN74 +Bosnia-Herzegovina;JN75 +Bosnia-Herzegovina;JN82 +Bosnia-Herzegovina;JN83 +Bosnia-Herzegovina;JN84 +Bosnia-Herzegovina;JN85 +Bosnia-Herzegovina;JN92 +Bosnia-Herzegovina;JN93 +Bosnia-Herzegovina;JN94 +Bosnia-Herzegovina;JN95 +Bulgaria;KN11 +Bulgaria;KN12 +Bulgaria;KN13 +Bulgaria;KN14 +Bulgaria;KN21 +Bulgaria;KN22 +Bulgaria;KN23 +Bulgaria;KN31 +Bulgaria;KN32 +Bulgaria;KN33 +Bulgaria;KN34 +Bulgaria;KN41 +Bulgaria;KN42 +Bulgaria;KN43 +Ceuta & Melilla;IM75 +Ceuta & Melilla;IM85 +Corsica;JN41 +Corsica;JN42 +Corsica;JN43 +Crete;KM15 +Crete;KM24 +Crete;KM25 +Crete;KM34 +Crete;KM35 +Croatia;JN64 +Croatia;JN65 +Croatia;JN73 +Croatia;JN74 +Croatia;JN75 +Croatia;JN76 +Croatia;JN82 +Croatia;JN83 +Croatia;JN84 +Croatia;JN85 +Croatia;JN86 +Croatia;JN92 +Croatia;JN94 +Croatia;JN95 +Cyprus;KM64 +Cyprus;KM65 +Cyprus;KM74 +Cyprus;KM75 +Czech Republic;JN68 +Czech Republic;JN69 +Czech Republic;JN78 +Czech Republic;JN79 +Czech Republic;JN88 +Czech Republic;JN89 +Czech Republic;JN99 +Czech Republic;JO60 +Czech Republic;JO70 +Czech Republic;JO71 +Czech Republic;JO80 +Czech Republic;JO90 +Denmark;JO44 +Denmark;JO45 +Denmark;JO46 +Denmark;JO47 +Denmark;JO54 +Denmark;JO55 +Denmark;JO56 +Denmark;JO57 +Denmark;JO64 +Denmark;JO65 +Denmark;JO66 +Denmark;JO75 +Dodecanese;KM35 +England;IN69 +England;IN79 +England;IN89 +England;IO70 +England;IO71 +England;IO80 +England;IO81 +England;IO82 +England;IO83 +England;IO84 +England;IO85 +England;IO90 +England;IO91 +England;IO92 +England;IO93 +England;IO94 +England;IO95 +England;JO00 +England;JO01 +England;JO02 +England;JO03 +Estonia;KO07 +Estonia;KO08 +Estonia;KO17 +Estonia;KO18 +Estonia;KO19 +Estonia;KO27 +Estonia;KO28 +Estonia;KO29 +Estonia;KO37 +Estonia;KO38 +Estonia;KO39 +Estonia;KO49 +European Russia;KN84 +European Russia;KN85 +European Russia;KN86 +European Russia;KN93 +European Russia;KN94 +European Russia;KN95 +European Russia;KN96 +European Russia;KN97 +European Russia;KN98 +European Russia;KN99 +European Russia;KO36 +European Russia;KO37 +European Russia;KO38 +European Russia;KO39 +European Russia;KO45 +European Russia;KO46 +European Russia;KO47 +European Russia;KO48 +European Russia;KO49 +European Russia;KO52 +European Russia;KO53 +European Russia;KO54 +European Russia;KO55 +European Russia;KO56 +European Russia;KO57 +European Russia;KO58 +European Russia;KO59 +European Russia;KO62 +European Russia;KO63 +European Russia;KO64 +European Russia;KO65 +European Russia;KO67 +European Russia;KO68 +European Russia;KO69 +European Russia;KO70 +European Russia;KO71 +European Russia;KO72 +European Russia;KO73 +European Russia;KO74 +European Russia;KO75 +European Russia;KO76 +European Russia;KO77 +European Russia;KO78 +European Russia;KO79 +European Russia;KO80 +European Russia;KO81 +European Russia;KO82 +European Russia;KO83 +European Russia;KO84 +European Russia;KO85 +European Russia;KO86 +European Russia;KO87 +European Russia;KO88 +European Russia;KO89 +European Russia;KO90 +European Russia;KO91 +European Russia;KO92 +European Russia;KO93 +European Russia;KO94 +European Russia;KO95 +European Russia;KO96 +European Russia;KO97 +European Russia;KO98 +European Russia;KO99 +European Russia;LN03 +European Russia;LN04 +European Russia;LN05 +European Russia;LN06 +European Russia;LN07 +European Russia;LN08 +European Russia;LN09 +European Russia;LN12 +European Russia;LN13 +European Russia;LN14 +European Russia;LN15 +European Russia;LN16 +European Russia;LN17 +European Russia;LN18 +European Russia;LN19 +European Russia;LN22 +European Russia;LN23 +European Russia;LN24 +European Russia;LN25 +European Russia;LN26 +European Russia;LN27 +European Russia;LN28 +European Russia;LN29 +European Russia;LN31 +European Russia;LN32 +European Russia;LN33 +European Russia;LN34 +European Russia;LN35 +European Russia;LN36 +European Russia;LN37 +European Russia;LN38 +European Russia;LN39 +European Russia;LN41 +European Russia;LN42 +European Russia;LN43 +European Russia;LN44 +European Russia;LN45 +European Russia;LN46 +European Russia;LN47 +European Russia;LN48 +European Russia;LN49 +European Russia;LO00 +European Russia;LO01 +European Russia;LO02 +European Russia;LO03 +European Russia;LO04 +European Russia;LO05 +European Russia;LO06 +European Russia;LO07 +European Russia;LO08 +European Russia;LO09 +European Russia;LO10 +European Russia;LO11 +European Russia;LO12 +European Russia;LO13 +European Russia;LO14 +European Russia;LO15 +European Russia;LO16 +European Russia;LO17 +European Russia;LO18 +European Russia;LO19 +European Russia;LO20 +European Russia;LO21 +European Russia;LO22 +European Russia;LO23 +European Russia;LO24 +European Russia;LO25 +European Russia;LO26 +European Russia;LO27 +European Russia;LO28 +European Russia;LO29 +European Russia;LO30 +European Russia;LO31 +European Russia;LO32 +European Russia;LO33 +European Russia;LO34 +European Russia;LO35 +European Russia;LO36 +European Russia;LO37 +European Russia;LO38 +European Russia;LO39 +European Russia;LO40 +European Russia;LO41 +European Russia;LO42 +European Russia;LO43 +European Russia;LO44 +European Russia;LO45 +European Russia;LO46 +European Russia;LO47 +European Russia;LO48 +European Russia;LO49 +Faroe Islands;IP61 +Faroe Islands;IP62 +Fed. Rep. of Germany;JN37 +Fed. Rep. of Germany;JN38 +Fed. Rep. of Germany;JN39 +Fed. Rep. of Germany;JN47 +Fed. Rep. of Germany;JN48 +Fed. Rep. of Germany;JN49 +Fed. Rep. of Germany;JN57 +Fed. Rep. of Germany;JN58 +Fed. Rep. of Germany;JN59 +Fed. Rep. of Germany;JN67 +Fed. Rep. of Germany;JN68 +Fed. Rep. of Germany;JN69 +Fed. Rep. of Germany;JO20 +Fed. Rep. of Germany;JO21 +Fed. Rep. of Germany;JO30 +Fed. Rep. of Germany;JO31 +Fed. Rep. of Germany;JO32 +Fed. Rep. of Germany;JO33 +Fed. Rep. of Germany;JO34 +Fed. Rep. of Germany;JO40 +Fed. Rep. of Germany;JO41 +Fed. Rep. of Germany;JO42 +Fed. Rep. of Germany;JO43 +Fed. Rep. of Germany;JO44 +Fed. Rep. of Germany;JO50 +Fed. Rep. of Germany;JO51 +Fed. Rep. of Germany;JO52 +Fed. Rep. of Germany;JO53 +Fed. Rep. of Germany;JO54 +Fed. Rep. of Germany;JO60 +Fed. Rep. of Germany;JO61 +Fed. Rep. of Germany;JO62 +Fed. Rep. of Germany;JO63 +Fed. Rep. of Germany;JO64 +Fed. Rep. of Germany;JO70 +Fed. Rep. of Germany;JO71 +Fed. Rep. of Germany;JO72 +Fed. Rep. of Germany;JO73 +Fed. Rep. of Germany;JO74 +Finland;JP90 +Finland;KN09 +Finland;KO19 +Finland;KO29 +Finland;KP00 +Finland;KP01 +Finland;KP02 +Finland;KP03 +Finland;KP08 +Finland;KP09 +Finland;KP10 +Finland;KP11 +Finland;KP12 +Finland;KP13 +Finland;KP14 +Finland;KP16 +Finland;KP17 +Finland;KP18 +Finland;KP20 +Finland;KP21 +Finland;KP22 +Finland;KP23 +Finland;KP24 +Finland;KP25 +Finland;KP26 +Finland;KP27 +Finland;KP28 +Finland;KP29 +Finland;KP30 +Finland;KP31 +Finland;KP32 +Finland;KP33 +Finland;KP34 +Finland;KP35 +Finland;KP36 +Finland;KP37 +Finland;KP38 +Finland;KP39 +Finland;KP40 +Finland;KP41 +Finland;KP42 +Finland;KP43 +Finland;KP44 +Finland;KP46 +Finland;KP47 +Finland;KP48 +Finland;KP49 +Finland;KP51 +Finland;KP52 +Finland;KP53 +Finland;KP54 +Finland;KP55 +Finland;KP56 +Finland;KP57 +Finland;KQ30 +Finland;KQ40 +France;IN77 +France;IN78 +France;IN86 +France;IN87 +France;IN88 +France;IN92 +France;IN93 +France;IN94 +France;IN95 +France;IN96 +France;IN97 +France;IN98 +France;IN99 +France;JN02 +France;JN03 +France;JN04 +France;JN05 +France;JN06 +France;JN07 +France;JN08 +France;JN09 +France;JN12 +France;JN13 +France;JN14 +France;JN15 +France;JN16 +France;JN17 +France;JN18 +France;JN19 +France;JN23 +France;JN24 +France;JN25 +France;JN26 +France;JN27 +France;JN28 +France;JN29 +France;JN32 +France;JN33 +France;JN34 +France;JN35 +France;JN36 +France;JN37 +France;JN38 +France;JN39 +France;JN48 +France;JN49 +France;JO00 +France;JO10 +France;JO11 +France;JO20 +Gibraltar;IM76 +Greece;JM99 +Greece;KM06 +Greece;KM07 +Greece;KM08 +Greece;KM09 +Greece;KM15 +Greece;KM16 +Greece;KM17 +Greece;KM18 +Greece;KM19 +Greece;KM25 +Greece;KM26 +Greece;KM27 +Greece;KM28 +Greece;KM29 +Greece;KN00 +Greece;KN01 +Greece;KN10 +Greece;KN11 +Greece;KN20 +Greece;KN21 +Greece;KN30 +Greece;KN31 +Guernsey;IN89 +Hungary;JN85 +Hungary;JN86 +Hungary;JN87 +Hungary;JN88 +Hungary;JN95 +Hungary;JN96 +Hungary;JN97 +Hungary;JN98 +Hungary;KN06 +Hungary;KN07 +Hungary;KN08 +Hungary;KN17 +Hungary;KN18 +Ireland;IO41 +Ireland;IO42 +Ireland;IO43 +Ireland;IO44 +Ireland;IO51 +Ireland;IO52 +Ireland;IO53 +Ireland;IO54 +Ireland;IO55 +Ireland;IO61 +Ireland;IO62 +Ireland;IO63 +Ireland;IO64 +Ireland;IO65 +Ireland;IO72 +Isle of Man;IO74 +Italy;JM68 +Italy;JM77 +Italy;JM78 +Italy;JM79 +Italy;JM87 +Italy;JM88 +Italy;JM89 +Italy;JM99 +Italy;JN33 +Italy;JN34 +Italy;JN35 +Italy;JN40 +Italy;JN43 +Italy;JN44 +Italy;JN45 +Italy;JN46 +Italy;JN51 +Italy;JN52 +Italy;JN53 +Italy;JN54 +Italy;JN55 +Italy;JN56 +Italy;JN57 +Italy;JN60 +Italy;JN61 +Italy;JN62 +Italy;JN63 +Italy;JN64 +Italy;JN65 +Italy;JN66 +Italy;JN67 +Italy;JN70 +Italy;JN71 +Italy;JN72 +Italy;JN80 +Italy;JN81 +Italy;JN90 +ITU HQ;JN36 +Jan Mayen;IO50 +Jan Mayen;IO60 +Jan Mayen;IO61 +Jersey;IN89 +Kaliningrad;JO94 +Kaliningrad;KO04 +Kaliningrad;KO05 +Kaliningrad;KO14 +Kaliningrad;KO15 +Latvia;KO06 +Latvia;KO07 +Latvia;KO16 +Latvia;KO17 +Latvia;KO27 +Latvia;KO28 +Latvia;KO35 +Latvia;KO36 +Latvia;KO37 +Latvia;KO46 +Liechtenstein;JN47 +Lithuania;KO05 +Lithuania;KO06 +Lithuania;KO13 +Lithuania;KO14 +Lithuania;KO15 +Lithuania;KO16 +Lithuania;KO23 +Lithuania;KO24 +Lithuania;KO25 +Lithuania;KO26 +Lithuania;KO34 +Lithuania;KO35 +Luxembourg;JN29 +Luxembourg;JN39 +Luxembourg;JO20 +Luxembourg;JO30 +Macedonia;KN00 +Macedonia;KN01 +Macedonia;KN02 +Macedonia;KN11 +Macedonia;KN12 +Malta;JM75 +Malta;JM76 +Moldova;KN37 +Moldova;KN38 +Moldova;KN45 +Moldova;KN46 +Moldova;KN47 +Moldova;KN48 +Moldova;KN56 +Monaco;JN33 +Montenegro;JN91 +Montenegro;JN92 +Montenegro;JN93 +Montenegro;KN02 +Montenegro;KN03 +Netherlands;JO11 +Netherlands;JO20 +Netherlands;JO21 +Netherlands;JO22 +Netherlands;JO23 +Netherlands;JO30 +Netherlands;JO31 +Netherlands;JO32 +Netherlands;JO33 +Northern Ireland;IO54 +Northern Ireland;IO64 +Northern Ireland;IO65 +Northern Ireland;IO74 +Northern Ireland;IO75 +Poland;JN89 +Poland;JN99 +Poland;JO70 +Poland;JO71 +Poland;JO72 +Poland;JO73 +Poland;JO74 +Poland;JO80 +Poland;JO81 +Poland;JO82 +Poland;JO83 +Poland;JO84 +Poland;JO90 +Poland;JO91 +Poland;JO92 +Poland;JO93 +Poland;JO94 +Poland;KN09 +Poland;KN19 +Poland;KO00 +Poland;KO01 +Poland;KO02 +Poland;KO03 +Poland;KO04 +Poland;KO10 +Poland;KO11 +Poland;KO12 +Poland;KO13 +Poland;KO14 +Poland;KO20 +Portugal;IM56 +Portugal;IM57 +Portugal;IM58 +Portugal;IM59 +Portugal;IM66 +Portugal;IM67 +Portugal;IM68 +Portugal;IM69 +Portugal;IN50 +Portugal;IN51 +Portugal;IN52 +Portugal;IN60 +Portugal;IN61 +Romania;KN04 +Romania;KN05 +Romania;KN06 +Romania;KN07 +Romania;KN13 +Romania;KN14 +Romania;KN15 +Romania;KN16 +Romania;KN17 +Romania;KN18 +Romania;KN23 +Romania;KN24 +Romania;KN25 +Romania;KN26 +Romania;KN27 +Romania;KN33 +Romania;KN34 +Romania;KN35 +Romania;KN36 +Romania;KN37 +Romania;KN38 +Romania;KN43 +Romania;KN44 +Romania;KN45 +Romania;KN46 +Romania;KN47 +Sardinia;JM48 +Sardinia;JM49 +Sardinia;JN40 +Sardinia;JN41 +Scotland;IO65 +Scotland;IO66 +Scotland;IO67 +Scotland;IO68 +Scotland;IO74 +Scotland;IO75 +Scotland;IO76 +Scotland;IO77 +Scotland;IO78 +Scotland;IO84 +Scotland;IO85 +Scotland;IO86 +Scotland;IO87 +Scotland;IO88 +Scotland;IO89 +Scotland;IO97 +Scotland;IO99 +Scotland;IP90 +Serbia;JN93 +Serbia;JN94 +Serbia;JN95 +Serbia;JN96 +Serbia;KN01 +Serbia;KN02 +Serbia;KN03 +Serbia;KN04 +Serbia;KN05 +Serbia;KN06 +Serbia;KN12 +Serbia;KN13 +Serbia;KN14 +Sicily;JM67 +Sicily;JM68 +Sicily;JM76 +Sicily;JM77 +Sicily;JM78 +Slovak Republic;JN87 +Slovak Republic;JN88 +Slovak Republic;JN89 +Slovak Republic;JN97 +Slovak Republic;JN98 +Slovak Republic;JN99 +Slovak Republic;KN08 +Slovak Republic;KN09 +Slovak Republic;KN18 +Slovak Republic;KN19 +Slovenia;JN64 +Slovenia;JN65 +Slovenia;JN66 +Slovenia;JN73 +Slovenia;JN74 +Slovenia;JN75 +Slovenia;JN76 +Slovenia;JN82 +Slovenia;JN83 +Slovenia;JN84 +Slovenia;JN85 +Slovenia;JN86 +Slovenia;JN92 +Spain;IM66 +Spain;IM67 +Spain;IM68 +Spain;IM69 +Spain;IM76 +Spain;IM77 +Spain;IM78 +Spain;IM79 +Spain;IM86 +Spain;IM87 +Spain;IM88 +Spain;IM89 +Spain;IM96 +Spain;IM97 +Spain;IM98 +Spain;IM99 +Spain;IN51 +Spain;IN52 +Spain;IN53 +Spain;IN60 +Spain;IN61 +Spain;IN62 +Spain;IN63 +Spain;IN70 +Spain;IN71 +Spain;IN72 +Spain;IN73 +Spain;IN80 +Spain;IN81 +Spain;IN82 +Spain;IN83 +Spain;IN90 +Spain;IN91 +Spain;IN92 +Spain;IN93 +Spain;JM08 +Spain;JM09 +Spain;JN00 +Spain;JN01 +Spain;JN02 +Spain;JN11 +Spain;JN12 +Svalbard;JQ58 +Svalbard;JQ59 +Svalbard;JQ67 +Svalbard;JQ68 +Svalbard;JQ69 +Svalbard;JQ76 +Svalbard;JQ77 +Svalbard;JQ78 +Svalbard;JQ79 +Svalbard;JQ86 +Svalbard;JQ87 +Svalbard;JQ88 +Svalbard;JQ89 +Svalbard;JQ97 +Svalbard;JQ98 +Svalbard;JQ99 +Svalbard;JR80 +Svalbard;JR90 +Svalbard;KQ07 +Svalbard;KQ08 +Svalbard;KQ09 +Svalbard;KQ17 +Svalbard;KQ18 +Svalbard;KQ19 +Svalbard;KQ26 +Svalbard;KQ27 +Svalbard;KQ29 +Svalbard;KQ38 +Svalbard;KQ39 +Svalbard;KQ48 +Svalbard;KQ49 +Svalbard;KR00 +Svalbard;KR06 +Svalbard;KR07 +Svalbard;KR10 +Svalbard;KR20 +Svalbard;KR30 +Sweden;JO57 +Sweden;JO58 +Sweden;JO59 +Sweden;JO65 +Sweden;JO66 +Sweden;JO67 +Sweden;JO68 +Sweden;JO69 +Sweden;JO74 +Sweden;JO75 +Sweden;JO76 +Sweden;JO77 +Sweden;JO78 +Sweden;JO79 +Sweden;JO86 +Sweden;JO87 +Sweden;JO88 +Sweden;JO89 +Sweden;JO96 +Sweden;JO97 +Sweden;JO98 +Sweden;JO99 +Sweden;JP53 +Sweden;JP60 +Sweden;JP61 +Sweden;JP62 +Sweden;JP63 +Sweden;JP64 +Sweden;JP70 +Sweden;JP71 +Sweden;JP72 +Sweden;JP73 +Sweden;JP74 +Sweden;JP75 +Sweden;JP76 +Sweden;JP80 +Sweden;JP81 +Sweden;JP82 +Sweden;JP83 +Sweden;JP84 +Sweden;JP85 +Sweden;JP86 +Sweden;JP87 +Sweden;JP88 +Sweden;JP90 +Sweden;JP92 +Sweden;JP93 +Sweden;JP94 +Sweden;JP95 +Sweden;JP96 +Sweden;JP97 +Sweden;JP98 +Sweden;KP03 +Sweden;KP04 +Sweden;KP05 +Sweden;KP06 +Sweden;KP07 +Sweden;KP08 +Sweden;KP09 +Sweden;KP15 +Sweden;KP16 +Sweden;KP17 +Sweden;KP18 +Sweden;KP25 +Switzerland;JN26 +Switzerland;JN35 +Switzerland;JN36 +Switzerland;JN37 +Switzerland;JN45 +Switzerland;JN46 +Switzerland;JN47 +Switzerland;JN56 +UK Base Areas on Cyprus;KM64 +UK Base Areas on Cyprus;KM65 +Vatican City;JN61 +Vienna Intl Ctr;JN88 +Wales;IO71 +Wales;IO72 +Wales;IO73 +Wales;IO81 +Wales;IO82 +Wales;IO83 +Wales;IO93 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..328d2bd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,74 @@ +# docker-compose.yml + +services: + php: + build: + context: ./src/vkv + dockerfile: Dockerfile + target: app + image: vkv-php:latest + container_name: "vkv-php" + hostname: "vkv-php" + restart: no + environment: + APP_ENV: production + APP_DEBUG: "false" + env_file: + - .env + volumes: + - /home/data/vkv/storage:/var/www/html/storage + depends_on: + - db + + nginx: + #image: nginx:1.21.3 + build: + context: ./src/vkv + dockerfile: Dockerfile + target: nginx + image: vkv-web:latest + container_name: "vkv-web" + hostname: "vkv-web" + restart: no + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + #- ./src/vkv/public:/var/www/html/public:ro + depends_on: + - php + + db: + image: mariadb:10.9 + hostname: "vkv-db" + restart: no + environment: + MYSQL_ROOT_PASSWORD_FILE: "/run/secrets/mysql_root_password" + MYSQL_DATABASE: "vkv" + MYSQL_USER: "vkv" + MYSQL_PASSWORD_FILE: "/run/secrets/mysql_password" + secrets: + - mysql_password + - mysql_root_password + volumes: + - "/home/data/vkv/mariadb:/var/lib/mysql" + command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci --innodb-flush-log-at-trx-commit=0 + + queue: + build: + context: ./src/vkv + dockerfile: Dockerfile + target: app + container_name: "vkv-queue" + hostname: "vkv-queue" + env_file: + - .env + command: ["php", "artisan", "queue:work", "--queue=evaluation,default", "--sleep=1", "--tries=1"] + volumes: + - /home/data/vkv/storage:/var/www/html/storage + depends_on: + - db + +secrets: + mysql_root_password: + file: ./secrets/mysql_root_password + mysql_password: + file: ./secrets/mysql_password diff --git a/docker/app/entrypoint.sh b/docker/app/entrypoint.sh new file mode 100644 index 0000000..40c6908 --- /dev/null +++ b/docker/app/entrypoint.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# docker/app/entrypoint.sh +set -euo pipefail + +# Pokud nechceš migrovat/cachovat při startu, tyto bloky odstraň. +# V produkci často dává smysl mít to řízené deploy procesem. + +php artisan config:clear >/dev/null 2>&1 || true +php artisan route:clear >/dev/null 2>&1 || true +php artisan view:clear >/dev/null 2>&1 || true + +# Cache pro production (vyžaduje korektní .env) +php artisan config:cache || true +php artisan route:cache || true +php artisan view:cache || true +php artisan event:cache || true + +# Migrace (pokud chceš automaticky; jinak vypnout) +php artisan migrate --force || true + +exec "$@" diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..5054766 --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,28 @@ +# docker/nginx/default.conf +server { + listen 80; + server_name _; + root /var/www/html/public; + index index.php; + + client_max_body_size 50m; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + try_files $uri =404; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_pass app:9000; + fastcgi_read_timeout 300; + } + + location ~* \.(?:css|js|map|png|jpg|jpeg|gif|svg|ico|woff2?)$ { + expires 30d; + access_log off; + try_files $uri =404; + } +} diff --git a/docker/php/opcache.ini b/docker/php/opcache.ini new file mode 100644 index 0000000..c49123e --- /dev/null +++ b/docker/php/opcache.ini @@ -0,0 +1,8 @@ +; docker/php/opcache.ini +opcache.enable=1 +opcache.enable_cli=0 +opcache.memory_consumption=256 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=20000 +opcache.validate_timestamps=0 +opcache.jit=0 diff --git a/docker/php/php.ini b/docker/php/php.ini new file mode 100644 index 0000000..682224c --- /dev/null +++ b/docker/php/php.ini @@ -0,0 +1,6 @@ +; docker/php/php.ini +memory_limit=512M +upload_max_filesize=50M +post_max_size=50M +max_execution_time=300 +date.timezone=Europe/Prague diff --git a/hero.ts b/hero.ts new file mode 100644 index 0000000..dbce534 --- /dev/null +++ b/hero.ts @@ -0,0 +1,3 @@ +import { heroui } from "@heroui/react"; +export default heroui(); + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..02a7d6f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8425 @@ +{ + "name": "vkv", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.1.0", + "@fortawesome/free-brands-svg-icons": "^7.1.0", + "@heroui/react": "^2.8.5", + "@heroui/use-theme": "^2.1.10", + "@react-aria/visually-hidden": "^3.8.28", + "@react-stately/data": "^3.15.0", + "@tailwindcss/typography": "^0.5.19", + "@vitejs/plugin-react": "^5.1.1", + "file-saver": "^2.0.5", + "fortawesome": "^0.0.1-security", + "framer-motion": "^12.23.24", + "i18next": "^25.6.3", + "next": "^16.0.9", + "next-themes": "^0.4.6", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-i18next": "^16.3.5", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.9.6", + "zustand": "^5.0.8" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.9.3", + "vite": "^7.0.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.0.tgz", + "integrity": "sha512-Z82FDl1ByxqPEPrAYYeTQVlx2FSHPe1qwX465c+96IRS3fTdSYRoJcRxg3g2fEG5I69z1dSEWQlNRRr0/677mg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz", + "integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", + "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.1.0.tgz", + "integrity": "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@heroui/accordion": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/@heroui/accordion/-/accordion-2.2.26.tgz", + "integrity": "sha512-hTOyxt8sQqRHDyz6M4g0eRIICwQQy+03zFXPbDv7DQINMyZLwAjIZhtZBjSa3N+nnyJ4YBCxBlQr4zFJChD9aw==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.26", + "@heroui/divider": "2.2.21", + "@heroui/dom-animation": "2.1.10", + "@heroui/framer-utils": "2.1.25", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-aria-accordion": "2.2.19", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-stately/tree": "3.9.4", + "@react-types/accordion": "3.0.0-alpha.26", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/alert": { + "version": "2.2.29", + "resolved": "https://registry.npmjs.org/@heroui/alert/-/alert-2.2.29.tgz", + "integrity": "sha512-poPE5fwK4CQO2s3AcLcdVyde4FU8NAJKn8YpUEcoP/Kfn8i8nuHoRKMTj5Ofs/0W/y4ysABajsgKPydPNzUupA==", + "license": "MIT", + "dependencies": { + "@heroui/button": "2.2.29", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@react-stately/utils": "3.11.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/aria-utils": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/@heroui/aria-utils/-/aria-utils-2.2.26.tgz", + "integrity": "sha512-FUrI92sy3s3JnZPBLmGH4UaT6nMrWCr2ksxGdL86eTc9S+QbUtiGgMw4SFMTsvjH175q8Cbl67/276kK0WHpOw==", + "license": "MIT", + "dependencies": { + "@heroui/system": "2.4.25", + "@react-aria/utils": "3.32.0", + "@react-stately/collections": "3.12.8", + "@react-types/overlays": "3.9.2", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/autocomplete": { + "version": "2.3.31", + "resolved": "https://registry.npmjs.org/@heroui/autocomplete/-/autocomplete-2.3.31.tgz", + "integrity": "sha512-xRA3mttbLSiSU9rJSm1N3+liHcLEUUCiGdKRkCa89yZwcrD9N1mg6FaTrn099W0/obHZ30r36Nmfx8z3Z7Cnfw==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.26", + "@heroui/button": "2.2.29", + "@heroui/form": "2.1.29", + "@heroui/input": "2.4.30", + "@heroui/listbox": "2.3.28", + "@heroui/popover": "2.3.29", + "@heroui/react-utils": "2.1.14", + "@heroui/scroll-shadow": "2.3.19", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-safe-layout-effect": "2.1.8", + "@react-aria/combobox": "3.14.1", + "@react-aria/i18n": "3.12.14", + "@react-stately/combobox": "3.12.1", + "@react-types/combobox": "3.13.10", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/avatar": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/@heroui/avatar/-/avatar-2.2.24.tgz", + "integrity": "sha512-GuocEjSrxM6lHlLjrpJam5MJzKzprXtJjVOnXAcOzbWN8VKSUbYvvnf4mMtb3ckfVAE8AwF9vX9S9LwH1kk9/w==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-image": "2.1.13", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/badge": { + "version": "2.2.18", + "resolved": "https://registry.npmjs.org/@heroui/badge/-/badge-2.2.18.tgz", + "integrity": "sha512-OfGove8YJ9oDrdugzq05FC15ZKD5nzqe+thPZ+1SY1LZorJQjZvqSD9QnoEH1nG7fu2IdH6pYJy3sZ/b6Vj5Kg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.23", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/breadcrumbs": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/@heroui/breadcrumbs/-/breadcrumbs-2.2.24.tgz", + "integrity": "sha512-O4M+FrqmAyBB0kfUjBN8PyuVfMMuMRg8B6dl7U+DxFyzfc3TmgtI9t2rIrnnNKj/EA3s/FEv9iaPcb02W6Fp5A==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@react-aria/breadcrumbs": "3.5.30", + "@react-aria/focus": "3.21.3", + "@react-types/breadcrumbs": "3.7.17" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/button": { + "version": "2.2.29", + "resolved": "https://registry.npmjs.org/@heroui/button/-/button-2.2.29.tgz", + "integrity": "sha512-F8cWp6V1/dJIeLOj0Cb9fA8luwzVKI3RUMUmx4zLo0C90cctRzssAMlg6eQ+SBz2NQxCYxMff8mtxMri1wrizg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/ripple": "2.2.21", + "@heroui/shared-utils": "2.1.12", + "@heroui/spinner": "2.2.26", + "@heroui/use-aria-button": "2.2.21", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/calendar": { + "version": "2.2.29", + "resolved": "https://registry.npmjs.org/@heroui/calendar/-/calendar-2.2.29.tgz", + "integrity": "sha512-poDlzOIB30sWSG+xxgNUwiSM90JmGHxq8w9ggVW460BChMAxPSA0IXZXF8fXWjReblSKHu50yS+Z2/koFJDl8Q==", + "license": "MIT", + "dependencies": { + "@heroui/button": "2.2.29", + "@heroui/dom-animation": "2.1.10", + "@heroui/framer-utils": "2.1.25", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-aria-button": "2.2.21", + "@internationalized/date": "3.10.1", + "@react-aria/calendar": "3.9.3", + "@react-aria/focus": "3.21.3", + "@react-aria/i18n": "3.12.14", + "@react-aria/interactions": "3.26.0", + "@react-aria/visually-hidden": "3.8.29", + "@react-stately/calendar": "3.9.1", + "@react-stately/utils": "3.11.0", + "@react-types/button": "3.14.1", + "@react-types/calendar": "3.8.1", + "@react-types/shared": "3.32.1", + "scroll-into-view-if-needed": "3.0.10" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/card": { + "version": "2.2.27", + "resolved": "https://registry.npmjs.org/@heroui/card/-/card-2.2.27.tgz", + "integrity": "sha512-UP9IuKYzGCjwBaocv8eiusOi1SheV6Pn37r05N6Hrqd8DKvs2Ebgye3hGRZ3z3MKRsqFKAyhG+3tdDIjVs3J/Q==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/ripple": "2.2.21", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-aria-button": "2.2.21", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/checkbox": { + "version": "2.3.29", + "resolved": "https://registry.npmjs.org/@heroui/checkbox/-/checkbox-2.3.29.tgz", + "integrity": "sha512-KcI2hAv/lsW427KEtcIq5GFILmRNiPaj9em5QaDrGUYQeJkO29slOGG8M8YAWvF4e3rRzBa9Xfzjp1D51d/OGA==", + "license": "MIT", + "dependencies": { + "@heroui/form": "2.1.29", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-callback-ref": "2.1.8", + "@heroui/use-safe-layout-effect": "2.1.8", + "@react-aria/checkbox": "3.16.3", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-stately/checkbox": "3.7.3", + "@react-stately/toggle": "3.9.3", + "@react-types/checkbox": "3.10.2", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/chip": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/@heroui/chip/-/chip-2.2.24.tgz", + "integrity": "sha512-QdJPQroHKGO+ZgZVlnhlhnAwE46Sm23UlHuFiW6cFIRVuARxHo/K+M/KXpjUEAP659EOtMyS1CzIVhDzuqHuSg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/code": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/@heroui/code/-/code-2.2.22.tgz", + "integrity": "sha512-i3pDe5Mzzh04jVx0gFwi2NMtCmsYfIRhLvkebXQcmfUDYl0+IGRJLcBsrWoOzes0pE/s7yyv+yJ/VhoU8F5jcg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/system-rsc": "2.3.21" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.23", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/date-input": { + "version": "2.3.29", + "resolved": "https://registry.npmjs.org/@heroui/date-input/-/date-input-2.3.29.tgz", + "integrity": "sha512-ADjmqCJWERjd0BYIwCNgA16IJQ+k7K+Y//ht0OKx4wWU2hMrug0MD9nhymecuCuP7Fa6xIU55+ucZ1qSmesNmg==", + "license": "MIT", + "dependencies": { + "@heroui/form": "2.1.29", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@internationalized/date": "3.10.1", + "@react-aria/datepicker": "3.15.3", + "@react-aria/i18n": "3.12.14", + "@react-stately/datepicker": "3.15.3", + "@react-types/datepicker": "3.13.3", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/date-picker": { + "version": "2.3.30", + "resolved": "https://registry.npmjs.org/@heroui/date-picker/-/date-picker-2.3.30.tgz", + "integrity": "sha512-NBdo1KkaCkFLRMTrzQoAB02qUP/FxEVffFgCUeTwAxQCKb76gnGYfOVKIbxZHleBmQtpaaIl7LlLpjo08qtgFA==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.26", + "@heroui/button": "2.2.29", + "@heroui/calendar": "2.2.29", + "@heroui/date-input": "2.3.29", + "@heroui/form": "2.1.29", + "@heroui/popover": "2.3.29", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@internationalized/date": "3.10.1", + "@react-aria/datepicker": "3.15.3", + "@react-aria/i18n": "3.12.14", + "@react-stately/datepicker": "3.15.3", + "@react-stately/utils": "3.11.0", + "@react-types/datepicker": "3.13.3", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/divider": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/@heroui/divider/-/divider-2.2.21.tgz", + "integrity": "sha512-aVvl8/3fWUc+/fHbg+hD/0wrkoMKmXG0yRgyNrJSeu0pkRwhb0eD4ZjnBK1pCYqnstoltNE33J8ko/sU+WlmPw==", + "license": "MIT", + "dependencies": { + "@heroui/react-rsc-utils": "2.1.9", + "@heroui/system-rsc": "2.3.21", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.23", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/dom-animation": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@heroui/dom-animation/-/dom-animation-2.1.10.tgz", + "integrity": "sha512-dt+0xdVPbORwNvFT5pnqV2ULLlSgOJeqlg/DMo97s9RWeD6rD4VedNY90c8C9meqWqGegQYBQ9ztsfX32mGEPA==", + "license": "MIT", + "peerDependencies": { + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1" + } + }, + "node_modules/@heroui/drawer": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/@heroui/drawer/-/drawer-2.2.26.tgz", + "integrity": "sha512-XTWKsmYX7/35kOJkidSuuDEbgZqQPv7iJhDvfgVgM1NXX0913CA+Q/Lnl2D7LHFIXs/lhXaV2Z/KWNlbUnBHfQ==", + "license": "MIT", + "dependencies": { + "@heroui/framer-utils": "2.1.25", + "@heroui/modal": "2.2.26", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/dropdown": { + "version": "2.3.29", + "resolved": "https://registry.npmjs.org/@heroui/dropdown/-/dropdown-2.3.29.tgz", + "integrity": "sha512-QJxA9SgzThrP8mQJQwrlS+PBITn9ig/pXylVgodZbAMbHJ3E/OgTFeAbYTmoxYAlzSLs/0+SfTdm0vI83zrcmA==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.26", + "@heroui/menu": "2.2.28", + "@heroui/popover": "2.3.29", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@react-aria/focus": "3.21.3", + "@react-aria/menu": "3.19.4", + "@react-stately/menu": "3.9.9", + "@react-types/menu": "3.10.5" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/form": { + "version": "2.1.29", + "resolved": "https://registry.npmjs.org/@heroui/form/-/form-2.1.29.tgz", + "integrity": "sha512-bWkd9SK+uuZN6gDVy/p9ccrpUryEjW4Y6y1EDaAsXYV8E9o/7JwIoWyZ0oxfskk1CS5TCHpKIYlb7mkdCeYmkA==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.12", + "@heroui/system": "2.4.25", + "@heroui/theme": "2.4.25", + "@react-stately/form": "3.2.2", + "@react-types/form": "3.7.16", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@heroui/framer-utils": { + "version": "2.1.25", + "resolved": "https://registry.npmjs.org/@heroui/framer-utils/-/framer-utils-2.1.25.tgz", + "integrity": "sha512-uH55w1g0UuzPB9/2XfTFq/JiJG+Vxp4N5hAAw0/G4R/kFo4YYdtPafmYyL1Qcpi37LgbLdLP6w4dQejLmzR0Mg==", + "license": "MIT", + "dependencies": { + "@heroui/system": "2.4.25", + "@heroui/use-measure": "2.1.8" + }, + "peerDependencies": { + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/image": { + "version": "2.2.18", + "resolved": "https://registry.npmjs.org/@heroui/image/-/image-2.2.18.tgz", + "integrity": "sha512-hrvj/hDM0+Khb9EqstZOPeO0vIGZvhrJWPMxk7a6i2PqhWWQI+ws+nrwsG5XqAkwE4mqqf9Uw8EMfIG1XE5YYg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-image": "2.1.13" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.23", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/input": { + "version": "2.4.30", + "resolved": "https://registry.npmjs.org/@heroui/input/-/input-2.4.30.tgz", + "integrity": "sha512-dTtQaZ21PofBIyWCnbysw2zpb5V8g6xu4mrZWO5faXt/bUjQLqmv3Y4MI1ghkWL6d0DB2xx0Z0I+U7LYMvoD4g==", + "license": "MIT", + "dependencies": { + "@heroui/form": "2.1.29", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-safe-layout-effect": "2.1.8", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/textfield": "3.18.3", + "@react-stately/utils": "3.11.0", + "@react-types/shared": "3.32.1", + "@react-types/textfield": "3.12.6", + "react-textarea-autosize": "^8.5.3" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/input-otp": { + "version": "2.1.29", + "resolved": "https://registry.npmjs.org/@heroui/input-otp/-/input-otp-2.1.29.tgz", + "integrity": "sha512-N3vejZl7+4VYazUS0/JZYBTGjUvstYBz9Bo4ArYye7zC20XkM84j3+Ox664UyNTdLu3Fcr7cO0dv4MVo2vJu7Q==", + "license": "MIT", + "dependencies": { + "@heroui/form": "2.1.29", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-form-reset": "2.0.1", + "@react-aria/focus": "3.21.3", + "@react-aria/form": "3.1.3", + "@react-stately/form": "3.2.2", + "@react-stately/utils": "3.11.0", + "@react-types/textfield": "3.12.6", + "input-otp": "1.4.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@heroui/kbd": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/@heroui/kbd/-/kbd-2.2.23.tgz", + "integrity": "sha512-nKL1Kl044l1Xsk4U8Nib3wFD2NlZCZo6kdqiqUv+DchOo4s3BJcxWSWqHn6fDVmHNyj3DFMYDvA2f/geMasaHQ==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/system-rsc": "2.3.21" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.23", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/link": { + "version": "2.2.25", + "resolved": "https://registry.npmjs.org/@heroui/link/-/link-2.2.25.tgz", + "integrity": "sha512-6hJpMrmHzmVkhze3523xe9PygCjiOHIu0t9p2LRG/kyWrTGx6LZRiufyIHEwZPVm2xp1Wu39UqPwBIkHoGkrag==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-aria-link": "2.2.22", + "@react-aria/focus": "3.21.3", + "@react-types/link": "3.6.5" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/listbox": { + "version": "2.3.28", + "resolved": "https://registry.npmjs.org/@heroui/listbox/-/listbox-2.3.28.tgz", + "integrity": "sha512-uONT4NOSYYSOYDtjuMvK13vUYNXspZw+1QpvVSd+Vaq0WcPvEfgoLI/3Kwu4lHPyfoOlE58vCpY7Hfqx/FTQjg==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.26", + "@heroui/divider": "2.2.21", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-is-mobile": "2.2.12", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/listbox": "3.15.1", + "@react-stately/list": "3.13.2", + "@react-types/shared": "3.32.1", + "@tanstack/react-virtual": "3.11.3" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/menu": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/@heroui/menu/-/menu-2.2.28.tgz", + "integrity": "sha512-54RdjC9cJsdksozy8ZZSoeixFDzbrdCU8qKttg1KYttAUaZzYm853VBwCLYsooIioeCXgrITqNy/NFjQcqx6Fg==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.26", + "@heroui/divider": "2.2.21", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-is-mobile": "2.2.12", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/menu": "3.19.4", + "@react-stately/tree": "3.9.4", + "@react-types/menu": "3.10.5", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/modal": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/@heroui/modal/-/modal-2.2.26.tgz", + "integrity": "sha512-NpnjTEweNExUb3pZWr17u15N1OHbBac4QY4aObwcbIJZKsInLU8NbuLbwyRw9nwAshGSf2FdnQ6dhmDZqwNqfA==", + "license": "MIT", + "dependencies": { + "@heroui/dom-animation": "2.1.10", + "@heroui/framer-utils": "2.1.25", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-aria-button": "2.2.21", + "@heroui/use-aria-modal-overlay": "2.2.20", + "@heroui/use-disclosure": "2.2.18", + "@heroui/use-draggable": "2.1.19", + "@heroui/use-viewport-size": "2.0.1", + "@react-aria/dialog": "3.5.32", + "@react-aria/focus": "3.21.3", + "@react-aria/overlays": "3.31.0", + "@react-stately/overlays": "3.6.21" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/navbar": { + "version": "2.2.27", + "resolved": "https://registry.npmjs.org/@heroui/navbar/-/navbar-2.2.27.tgz", + "integrity": "sha512-O2G7kavGDuGbPtpzGMci7YmV8Kf7BOxQ6k7xqnwxWivWX2MdvDNyR+ca60FPPdQL14zH+KfrQmQpoPxgxr79pw==", + "license": "MIT", + "dependencies": { + "@heroui/dom-animation": "2.1.10", + "@heroui/framer-utils": "2.1.25", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-resize": "2.1.8", + "@heroui/use-scroll-position": "2.1.8", + "@react-aria/button": "3.14.3", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/overlays": "3.31.0", + "@react-stately/toggle": "3.9.3", + "@react-stately/utils": "3.11.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/number-input": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/@heroui/number-input/-/number-input-2.0.20.tgz", + "integrity": "sha512-WnSleY9eBRPhZIz4qVi1pYSkxMqNXEZLQgZaiMVbKdkeR9M2ASMo0Qv8+tLMT3KwRaxAu53BvQjp/hz8VADx1Q==", + "license": "MIT", + "dependencies": { + "@heroui/button": "2.2.29", + "@heroui/form": "2.1.29", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-safe-layout-effect": "2.1.8", + "@react-aria/focus": "3.21.3", + "@react-aria/i18n": "3.12.14", + "@react-aria/interactions": "3.26.0", + "@react-aria/numberfield": "3.12.3", + "@react-stately/numberfield": "3.10.3", + "@react-types/button": "3.14.1", + "@react-types/numberfield": "3.8.16", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/pagination": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/@heroui/pagination/-/pagination-2.2.26.tgz", + "integrity": "sha512-Ta70RAMo223BDFw3fAvYew1PauQ+b38Xa0zWnj5mkkrYrLXk7sjomunNtlUFPKkr0B8Dpuu67tp9a8AlmI1z8A==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-intersection-observer": "2.2.14", + "@heroui/use-pagination": "2.2.19", + "@react-aria/focus": "3.21.3", + "@react-aria/i18n": "3.12.14", + "@react-aria/interactions": "3.26.0", + "@react-aria/utils": "3.32.0", + "scroll-into-view-if-needed": "3.0.10" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/popover": { + "version": "2.3.29", + "resolved": "https://registry.npmjs.org/@heroui/popover/-/popover-2.3.29.tgz", + "integrity": "sha512-ldEV2iJ8dHUxvEGSlARdqU7V/9Nr6X+AJmImEUHXASiDKnJ2GdiMyIuyx4eIC2cbldJ94W2dRUlJ1rt8Pvsm4w==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.26", + "@heroui/button": "2.2.29", + "@heroui/dom-animation": "2.1.10", + "@heroui/framer-utils": "2.1.25", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-aria-button": "2.2.21", + "@heroui/use-aria-overlay": "2.0.5", + "@heroui/use-safe-layout-effect": "2.1.8", + "@react-aria/dialog": "3.5.32", + "@react-aria/focus": "3.21.3", + "@react-aria/overlays": "3.31.0", + "@react-stately/overlays": "3.6.21", + "@react-types/overlays": "3.9.2" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/progress": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/@heroui/progress/-/progress-2.2.24.tgz", + "integrity": "sha512-1wGF1tSBx35//7+15dw06j1AB7+FhJiGYIH8hBefDSRD0U16htwXVxoVBk6v4Vd/yfpvVQTktA5fiT+Sl4XQlQ==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-is-mounted": "2.1.8", + "@react-aria/progress": "3.4.28", + "@react-types/progress": "3.5.16" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/radio": { + "version": "2.3.29", + "resolved": "https://registry.npmjs.org/@heroui/radio/-/radio-2.3.29.tgz", + "integrity": "sha512-0nj6ws7R1yX5yh4plEjvRLbri6vRG6ogWDU9tJIb6D3vqxv7Lmpdna3+V+fdGdz4uvQp3YQebOY+UE3fCak/Ow==", + "license": "MIT", + "dependencies": { + "@heroui/form": "2.1.29", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/radio": "3.12.3", + "@react-aria/visually-hidden": "3.8.29", + "@react-stately/radio": "3.11.3", + "@react-types/radio": "3.9.2", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/react": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/@heroui/react/-/react-2.8.7.tgz", + "integrity": "sha512-0PkjyvjXQpsjNz9P6q3YmJEWO7F+cyQjc2Ts7HqfvnOWPKAX3zAVnExq8d4Bdwnpp1F6TZpuC0woowVq6Fj4Fw==", + "license": "MIT", + "dependencies": { + "@heroui/accordion": "2.2.26", + "@heroui/alert": "2.2.29", + "@heroui/autocomplete": "2.3.31", + "@heroui/avatar": "2.2.24", + "@heroui/badge": "2.2.18", + "@heroui/breadcrumbs": "2.2.24", + "@heroui/button": "2.2.29", + "@heroui/calendar": "2.2.29", + "@heroui/card": "2.2.27", + "@heroui/checkbox": "2.3.29", + "@heroui/chip": "2.2.24", + "@heroui/code": "2.2.22", + "@heroui/date-input": "2.3.29", + "@heroui/date-picker": "2.3.30", + "@heroui/divider": "2.2.21", + "@heroui/drawer": "2.2.26", + "@heroui/dropdown": "2.3.29", + "@heroui/form": "2.1.29", + "@heroui/framer-utils": "2.1.25", + "@heroui/image": "2.2.18", + "@heroui/input": "2.4.30", + "@heroui/input-otp": "2.1.29", + "@heroui/kbd": "2.2.23", + "@heroui/link": "2.2.25", + "@heroui/listbox": "2.3.28", + "@heroui/menu": "2.2.28", + "@heroui/modal": "2.2.26", + "@heroui/navbar": "2.2.27", + "@heroui/number-input": "2.0.20", + "@heroui/pagination": "2.2.26", + "@heroui/popover": "2.3.29", + "@heroui/progress": "2.2.24", + "@heroui/radio": "2.3.29", + "@heroui/ripple": "2.2.21", + "@heroui/scroll-shadow": "2.3.19", + "@heroui/select": "2.4.30", + "@heroui/skeleton": "2.2.18", + "@heroui/slider": "2.4.26", + "@heroui/snippet": "2.2.30", + "@heroui/spacer": "2.2.22", + "@heroui/spinner": "2.2.26", + "@heroui/switch": "2.2.26", + "@heroui/system": "2.4.25", + "@heroui/table": "2.2.29", + "@heroui/tabs": "2.2.26", + "@heroui/theme": "2.4.25", + "@heroui/toast": "2.0.19", + "@heroui/tooltip": "2.2.26", + "@heroui/user": "2.2.24", + "@react-aria/visually-hidden": "3.8.29" + }, + "peerDependencies": { + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/react-rsc-utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@heroui/react-rsc-utils/-/react-rsc-utils-2.1.9.tgz", + "integrity": "sha512-e77OEjNCmQxE9/pnLDDb93qWkX58/CcgIqdNAczT/zUP+a48NxGq2A2WRimvc1uviwaNL2StriE2DmyZPyYW7Q==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/react-utils": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/@heroui/react-utils/-/react-utils-2.1.14.tgz", + "integrity": "sha512-hhKklYKy9sRH52C9A8P0jWQ79W4MkIvOnKBIuxEMHhigjfracy0o0lMnAUdEsJni4oZKVJYqNGdQl+UVgcmeDA==", + "license": "MIT", + "dependencies": { + "@heroui/react-rsc-utils": "2.1.9", + "@heroui/shared-utils": "2.1.12" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/ripple": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/@heroui/ripple/-/ripple-2.2.21.tgz", + "integrity": "sha512-wairSq9LnhbIqTCJmUlJAQURQ1wcRK/L8pjg2s3R/XnvZlPXHy4ZzfphiwIlTI21z/f6tH3arxv/g1uXd1RY0g==", + "license": "MIT", + "dependencies": { + "@heroui/dom-animation": "2.1.10", + "@heroui/shared-utils": "2.1.12" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.23", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/scroll-shadow": { + "version": "2.3.19", + "resolved": "https://registry.npmjs.org/@heroui/scroll-shadow/-/scroll-shadow-2.3.19.tgz", + "integrity": "sha512-y5mdBlhiITVrFnQTDqEphYj7p5pHqoFSFtVuRRvl9wUec2lMxEpD85uMGsfL8OgQTKIAqGh2s6M360+VJm7ajQ==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-data-scroll-overflow": "2.2.13" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.23", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/select": { + "version": "2.4.30", + "resolved": "https://registry.npmjs.org/@heroui/select/-/select-2.4.30.tgz", + "integrity": "sha512-laGM9ib4E/wxWu0T5/85yQZaKaT9HYP2hqy+xpD0HrKOZxBEINyTIbPJCq3cB9LiM6qqJJk/2A3rRjKNSj1Law==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.26", + "@heroui/form": "2.1.29", + "@heroui/listbox": "2.3.28", + "@heroui/popover": "2.3.29", + "@heroui/react-utils": "2.1.14", + "@heroui/scroll-shadow": "2.3.19", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@heroui/spinner": "2.2.26", + "@heroui/use-aria-button": "2.2.21", + "@heroui/use-aria-multiselect": "2.4.20", + "@heroui/use-form-reset": "2.0.1", + "@heroui/use-safe-layout-effect": "2.1.8", + "@react-aria/focus": "3.21.3", + "@react-aria/form": "3.1.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/overlays": "3.31.0", + "@react-aria/visually-hidden": "3.8.29", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/shared-icons": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@heroui/shared-icons/-/shared-icons-2.1.10.tgz", + "integrity": "sha512-ePo60GjEpM0SEyZBGOeySsLueNDCqLsVL79Fq+5BphzlrBAcaKY7kUp74964ImtkXvknTxAWzuuTr3kCRqj6jg==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/shared-utils": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@heroui/shared-utils/-/shared-utils-2.1.12.tgz", + "integrity": "sha512-0iCnxVAkIPtrHQo26Qa5g0UTqMTpugTbClNOrEPsrQuyRAq7Syux998cPwGlneTfB5E5xcU3LiEdA9GUyeK2cQ==", + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/@heroui/skeleton": { + "version": "2.2.18", + "resolved": "https://registry.npmjs.org/@heroui/skeleton/-/skeleton-2.2.18.tgz", + "integrity": "sha512-7AjU5kjk9rqrKP9mWQiAVj0dow4/vbK5/ejh4jqdb3DZm7bM2+DGzfnQPiS0c2eWR606CgOuuoImpwDS82HJtA==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.12" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.23", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/slider": { + "version": "2.4.26", + "resolved": "https://registry.npmjs.org/@heroui/slider/-/slider-2.4.26.tgz", + "integrity": "sha512-hsJOyNjixw8QK5DC9yMWSOg9abbRuXRXao0ZxQH+/xM8F59eb5xZaqopbN7aFmBP7G28Tfge4i36vE8TsK2Q/g==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/tooltip": "2.2.26", + "@react-aria/focus": "3.21.3", + "@react-aria/i18n": "3.12.14", + "@react-aria/interactions": "3.26.0", + "@react-aria/slider": "3.8.3", + "@react-aria/visually-hidden": "3.8.29", + "@react-stately/slider": "3.7.3" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/snippet": { + "version": "2.2.30", + "resolved": "https://registry.npmjs.org/@heroui/snippet/-/snippet-2.2.30.tgz", + "integrity": "sha512-o/fNVh4jtYAH8/2F6uU7pFdJiCCWZYN0LaPC57dRo8FNxL6+kcxt13Lp+sCBVKEDnuBmMtlL1prjMedX7VqzfQ==", + "license": "MIT", + "dependencies": { + "@heroui/button": "2.2.29", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@heroui/tooltip": "2.2.26", + "@heroui/use-clipboard": "2.1.9", + "@react-aria/focus": "3.21.3" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/spacer": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/@heroui/spacer/-/spacer-2.2.22.tgz", + "integrity": "sha512-BJ7RauvSY3gx10ntqZkCcyTy9K2FS4AeeryQUE9RgkMKQxP4t5TbeYLPEyomjWK+cCL/ERQCCruW16D3vKyWmw==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/system-rsc": "2.3.21" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.23", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/spinner": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/@heroui/spinner/-/spinner-2.2.26.tgz", + "integrity": "sha512-AtZhUO+IrZwahdQ5FOVptOZRMz7Z51gDUuj1K3pEJvOiKW+zvqab9BHYW9A09nd7qMH+DMM/41PQJbZg+eOHzg==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.12", + "@heroui/system": "2.4.25", + "@heroui/system-rsc": "2.3.21" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/switch": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/@heroui/switch/-/switch-2.2.26.tgz", + "integrity": "sha512-c/FCzromB+ww8AObgA0H//jOrhxyn0MllWVeEwMXac7O6z/N4B+fJ8dLCu/vu1zchySFLuDq/PaETEMJ7hKW4A==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-safe-layout-effect": "2.1.8", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/switch": "3.7.9", + "@react-aria/visually-hidden": "3.8.29", + "@react-stately/toggle": "3.9.3" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/system": { + "version": "2.4.25", + "resolved": "https://registry.npmjs.org/@heroui/system/-/system-2.4.25.tgz", + "integrity": "sha512-F6UUoGTQ+Qas5wYkCzLjXE7u74Z9ygO0u0+dkTW7zCaY7ds65CcmvZ/ahKz2ES3Tk6TNks1MJSyaQ9rFLs8AqA==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/system-rsc": "2.3.21", + "@react-aria/i18n": "3.12.14", + "@react-aria/overlays": "3.31.0", + "@react-aria/utils": "3.32.0" + }, + "peerDependencies": { + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/system-rsc": { + "version": "2.3.21", + "resolved": "https://registry.npmjs.org/@heroui/system-rsc/-/system-rsc-2.3.21.tgz", + "integrity": "sha512-icB7njbNgkI3dcfZhY5LP7VFspaVgWL1lcg9Q7uJMAaj6gGFqqSSnHkSMwpR9AGLxVRKTHey0TUx8CeZDe8XDw==", + "license": "MIT", + "dependencies": { + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.23", + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/table": { + "version": "2.2.29", + "resolved": "https://registry.npmjs.org/@heroui/table/-/table-2.2.29.tgz", + "integrity": "sha512-/YLP1+cSSiolj1kvU6YSge4BNvwqT7yDom8YebBHCjidwOBbORGHh6HJ9btVk2GUzdTh57N9vErh9VCEuz5/DA==", + "license": "MIT", + "dependencies": { + "@heroui/checkbox": "2.3.29", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/table": "3.17.9", + "@react-aria/visually-hidden": "3.8.29", + "@react-stately/table": "3.15.2", + "@react-stately/virtualizer": "4.4.4", + "@react-types/grid": "3.3.6", + "@react-types/table": "3.13.4", + "@tanstack/react-virtual": "3.11.3" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/tabs": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/@heroui/tabs/-/tabs-2.2.26.tgz", + "integrity": "sha512-RK5TjDI2KY1i/zyO/zzwkiDnQEYxcXSu9QCigNLcCZ6SXq0J3n83FC5Vv91kFwU9aTRuwdxIHv5KzV7D8Xe14w==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.26", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-is-mounted": "2.1.8", + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/tabs": "3.10.9", + "@react-stately/tabs": "3.8.7", + "@react-types/shared": "3.32.1", + "scroll-into-view-if-needed": "3.0.10" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/theme": { + "version": "2.4.25", + "resolved": "https://registry.npmjs.org/@heroui/theme/-/theme-2.4.25.tgz", + "integrity": "sha512-nTptYhO1V9rMoh9SJDnMfaSmFuoXvbem1UuwgHcraRtqy/TIVBPqv26JEGzSoUCL194TDGOJpqrpMuab/PdXcw==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.12", + "color": "^4.2.3", + "color2k": "^2.0.3", + "deepmerge": "4.3.1", + "flat": "^5.0.2", + "tailwind-merge": "3.4.0", + "tailwind-variants": "3.2.2" + }, + "peerDependencies": { + "tailwindcss": ">=4.0.0" + } + }, + "node_modules/@heroui/toast": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/@heroui/toast/-/toast-2.0.19.tgz", + "integrity": "sha512-KUl/vIMoZQxpjLPxx57XKh39Ai1CyPqm+1Pn20xOVk0fV+2SqC7OW6xivwwBnS7rysx8JfMHwNVJUHTU0o9K1Q==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/shared-icons": "2.1.10", + "@heroui/shared-utils": "2.1.12", + "@heroui/spinner": "2.2.26", + "@heroui/use-is-mobile": "2.2.12", + "@react-aria/interactions": "3.26.0", + "@react-aria/toast": "3.0.9", + "@react-stately/toast": "3.1.2" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/tooltip": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/@heroui/tooltip/-/tooltip-2.2.26.tgz", + "integrity": "sha512-VERreBoBAjqFLvJmBMVvRCYGxr+nq8gcncC14ewWqCzWwv/WQm9wVSqHLnwCtZelOz7ofaDDqvxmGjMqzMnqFw==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.26", + "@heroui/dom-animation": "2.1.10", + "@heroui/framer-utils": "2.1.25", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@heroui/use-aria-overlay": "2.0.5", + "@heroui/use-safe-layout-effect": "2.1.8", + "@react-aria/overlays": "3.31.0", + "@react-aria/tooltip": "3.9.0", + "@react-stately/tooltip": "3.5.9", + "@react-types/overlays": "3.9.2", + "@react-types/tooltip": "3.5.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-aria-accordion": { + "version": "2.2.19", + "resolved": "https://registry.npmjs.org/@heroui/use-aria-accordion/-/use-aria-accordion-2.2.19.tgz", + "integrity": "sha512-4HGY2zr+MIzRtIO9epFQGeU7VeGqhCotxxXzscfwxLfEeHBJwQvMAsu7yrUQ/uyMGvSiohHlJRgIsuT1xzxH1Q==", + "license": "MIT", + "dependencies": { + "@react-aria/button": "3.14.3", + "@react-aria/focus": "3.21.3", + "@react-aria/selection": "3.27.0", + "@react-stately/tree": "3.9.4", + "@react-types/accordion": "3.0.0-alpha.26", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-aria-button": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/@heroui/use-aria-button/-/use-aria-button-2.2.21.tgz", + "integrity": "sha512-8Lhjt1xoDpjhqvEbFC21NEgU89p7Z+MAzrDyoF1eYUn/w4ahhBgcQStP6WicLfx50tOE10WDpPBq72tah/O+ww==", + "license": "MIT", + "dependencies": { + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/utils": "3.32.0", + "@react-types/button": "3.14.1", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-aria-link": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/@heroui/use-aria-link/-/use-aria-link-2.2.22.tgz", + "integrity": "sha512-T7wESiV9IBqe5MILMZ1pL+GIWxyPVj7ag/KUhZUH3v/dm94m+f2Ua7rXxzI+hj2H51s189YP+Eb1PagHMDrfPg==", + "license": "MIT", + "dependencies": { + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/utils": "3.32.0", + "@react-types/link": "3.6.5", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-aria-modal-overlay": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/@heroui/use-aria-modal-overlay/-/use-aria-modal-overlay-2.2.20.tgz", + "integrity": "sha512-AIYfpnpiRVJm3InKlroGqQSZ1hjBI0Y5oMhMrXuQqrySsMKzMye3zMcEBWf8dEho1l+/U0dgNIUJFbkEFsOc8w==", + "license": "MIT", + "dependencies": { + "@heroui/use-aria-overlay": "2.0.5", + "@react-aria/overlays": "3.31.0", + "@react-aria/utils": "3.32.0", + "@react-stately/overlays": "3.6.21" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-aria-multiselect": { + "version": "2.4.20", + "resolved": "https://registry.npmjs.org/@heroui/use-aria-multiselect/-/use-aria-multiselect-2.4.20.tgz", + "integrity": "sha512-Tvbk2AaWfGYgL6Sn9SwsI+nSOcaD1e3wWGPEqHzeFgoSV6cT7oLY70TODD/HyTF+LKYPtYUbAenxDd80Z5j+Eg==", + "license": "MIT", + "dependencies": { + "@react-aria/i18n": "3.12.14", + "@react-aria/interactions": "3.26.0", + "@react-aria/label": "3.7.23", + "@react-aria/listbox": "3.15.1", + "@react-aria/menu": "3.19.4", + "@react-aria/selection": "3.27.0", + "@react-aria/utils": "3.32.0", + "@react-stately/form": "3.2.2", + "@react-stately/list": "3.13.2", + "@react-stately/menu": "3.9.9", + "@react-types/button": "3.14.1", + "@react-types/overlays": "3.9.2", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-aria-overlay": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@heroui/use-aria-overlay/-/use-aria-overlay-2.0.5.tgz", + "integrity": "sha512-2g1HxRoDzGAqIkW7s09WEXg+SAWslh+ZkIuixNAqsA60FHSAzQtGCNpbE2yFeMrukhbmRfS8t3hT2JVZVAXG7w==", + "license": "MIT", + "dependencies": { + "@react-aria/focus": "3.21.3", + "@react-aria/interactions": "3.26.0", + "@react-aria/overlays": "3.31.0", + "@react-types/shared": "3.32.1" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@heroui/use-callback-ref": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@heroui/use-callback-ref/-/use-callback-ref-2.1.8.tgz", + "integrity": "sha512-D1JDo9YyFAprYpLID97xxQvf86NvyWLay30BeVVZT9kWmar6O9MbCRc7ACi7Ngko60beonj6+amTWkTm7QuY/Q==", + "license": "MIT", + "dependencies": { + "@heroui/use-safe-layout-effect": "2.1.8" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-clipboard": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@heroui/use-clipboard/-/use-clipboard-2.1.9.tgz", + "integrity": "sha512-lkBq5RpXHiPvk1BXKJG8gMM0f7jRMIGnxAXDjAUzZyXKBuWLoM+XlaUWmZHtmkkjVFMX1L4vzA+vxi9rZbenEQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-data-scroll-overflow": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/@heroui/use-data-scroll-overflow/-/use-data-scroll-overflow-2.2.13.tgz", + "integrity": "sha512-zboLXO1pgYdzMUahDcVt5jf+l1jAQ/D9dFqr7AxWLfn6tn7/EgY0f6xIrgWDgJnM0U3hKxVeY13pAeB4AFTqTw==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.12" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-disclosure": { + "version": "2.2.18", + "resolved": "https://registry.npmjs.org/@heroui/use-disclosure/-/use-disclosure-2.2.18.tgz", + "integrity": "sha512-aR/4oITXOyt8uze9EdfL/b2j8pg75dc92Q8FfoT17MibD6nKI1VmQDA+9CAtUwuKq6rSrEGqc14muO3GYpTH4g==", + "license": "MIT", + "dependencies": { + "@heroui/use-callback-ref": "2.1.8", + "@react-aria/utils": "3.32.0", + "@react-stately/utils": "3.11.0" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-draggable": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/@heroui/use-draggable/-/use-draggable-2.1.19.tgz", + "integrity": "sha512-pk0Oe4QLcjr1gndcuvq+8z6eoM+v3lvbmEDxbsEjeST9AwykfmT/60X+xrPFtCkfYldYXk1UxBPiGwrFs0lscQ==", + "license": "MIT", + "dependencies": { + "@react-aria/interactions": "3.26.0" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-form-reset": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@heroui/use-form-reset/-/use-form-reset-2.0.1.tgz", + "integrity": "sha512-6slKWiLtVfgZnVeHVkM9eXgjwI07u0CUaLt2kQpfKPqTSTGfbHgCYJFduijtThhTdKBhdH6HCmzTcnbVlAxBXw==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-image": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@heroui/use-image/-/use-image-2.1.13.tgz", + "integrity": "sha512-NLApz+xin2bKHEXr+eSrtB0lN8geKP5VOea5QGbOCiHq4DBXu4QctpRkSfCHGIQzWdBVaLPoV+5wd0lR2S2Egg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.14", + "@heroui/use-safe-layout-effect": "2.1.8" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-intersection-observer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@heroui/use-intersection-observer/-/use-intersection-observer-2.2.14.tgz", + "integrity": "sha512-qYJeMk4cTsF+xIckRctazCgWQ4BVOpJu+bhhkB1NrN+MItx19Lcb7ksOqMdN5AiSf85HzDcAEPIQ9w9RBlt5sg==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-is-mobile": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@heroui/use-is-mobile/-/use-is-mobile-2.2.12.tgz", + "integrity": "sha512-2UKa4v1xbvFwerWKoMTrg4q9ZfP9MVIVfCl1a7JuKQlXq3jcyV6z1as5bZ41pCsTOT+wUVOFnlr6rzzQwT9ZOA==", + "license": "MIT", + "dependencies": { + "@react-aria/ssr": "3.9.10" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-is-mounted": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@heroui/use-is-mounted/-/use-is-mounted-2.1.8.tgz", + "integrity": "sha512-DO/Th1vD4Uy8KGhd17oGlNA4wtdg91dzga+VMpmt94gSZe1WjsangFwoUBxF2uhlzwensCX9voye3kerP/lskg==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-measure": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@heroui/use-measure/-/use-measure-2.1.8.tgz", + "integrity": "sha512-GjT9tIgluqYMZWfAX6+FFdRQBqyHeuqUMGzAXMTH9kBXHU0U5C5XU2c8WFORkNDoZIg1h13h1QdV+Vy4LE1dEA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-pagination": { + "version": "2.2.19", + "resolved": "https://registry.npmjs.org/@heroui/use-pagination/-/use-pagination-2.2.19.tgz", + "integrity": "sha512-0VLyxge+rPBexK7xoLgPwCC8ngh9vIgHEtS+sRvulcEy4grG9EvZWUfMpMeiboFc5Ku2l5u+D9jYkaV06EY4Rw==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.12", + "@react-aria/i18n": "3.12.14" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-resize": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@heroui/use-resize/-/use-resize-2.1.8.tgz", + "integrity": "sha512-htF3DND5GmrSiMGnzRbISeKcH+BqhQ/NcsP9sBTIl7ewvFaWiDhEDiUHdJxflmJGd/c5qZq2nYQM/uluaqIkKA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-safe-layout-effect": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@heroui/use-safe-layout-effect/-/use-safe-layout-effect-2.1.8.tgz", + "integrity": "sha512-wbnZxVWCYqk10XRMu0veSOiVsEnLcmGUmJiapqgaz0fF8XcpSScmqjTSoWjHIEWaHjQZ6xr+oscD761D6QJN+Q==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-scroll-position": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@heroui/use-scroll-position/-/use-scroll-position-2.1.8.tgz", + "integrity": "sha512-NxanHKObxVfWaPpNRyBR8v7RfokxrzcHyTyQfbgQgAGYGHTMaOGkJGqF8kBzInc3zJi+F0zbX7Nb0QjUgsLNUQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-theme": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@heroui/use-theme/-/use-theme-2.1.10.tgz", + "integrity": "sha512-JM9Y56bzHMU0rEz1K86Tnli2oRqQY3/NYNOHXbVXQtP+ZB2eabm9Cbl15bPZzSskpEsD5oKnbccFV4dCYhenmw==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-viewport-size": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@heroui/use-viewport-size/-/use-viewport-size-2.0.1.tgz", + "integrity": "sha512-blv8BEB/QdLePLWODPRzRS2eELJ2eyHbdOIADbL0KcfLzOUEg9EiuVk90hcSUDAFqYiJ3YZ5Z0up8sdPcR8Y7g==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/user": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/@heroui/user/-/user-2.2.24.tgz", + "integrity": "sha512-SH8MlILc1Nn7lBvbvsqNok6H36+FrhT9VIQlKwzzX/tidr15LRK74F1k8UPV7PBAxDKxQ0FRCictCXI8dN9lcQ==", + "license": "MIT", + "dependencies": { + "@heroui/avatar": "2.2.24", + "@heroui/react-utils": "2.1.14", + "@heroui/shared-utils": "2.1.12", + "@react-aria/focus": "3.21.3" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.18", + "@heroui/theme": ">=2.4.24", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@internationalized/date": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.1.tgz", + "integrity": "sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/message": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.8.tgz", + "integrity": "sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "intl-messageformat": "^10.1.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz", + "integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/string": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.7.tgz", + "integrity": "sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz", + "integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz", + "integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz", + "integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz", + "integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz", + "integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz", + "integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz", + "integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz", + "integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz", + "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-aria/breadcrumbs": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.30.tgz", + "integrity": "sha512-DZymglA70SwvDJA7GB147sUexvdDy6vWcriGrlEHhMMzBLhGB30I5J96R4pPzURLxXISrWFH56KC5rRgIqsqqg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.14", + "@react-aria/link": "^3.8.7", + "@react-aria/utils": "^3.32.0", + "@react-types/breadcrumbs": "^3.7.17", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/button": { + "version": "3.14.3", + "resolved": "https://registry.npmjs.org/@react-aria/button/-/button-3.14.3.tgz", + "integrity": "sha512-iJTuEECs9im7TwrCRZ0dvuwp8Gao0+I1IuYs1LQvJQgKLpgRH2/6jAiqb2bdAcoAjdbaMs7Xe0xUwURpVNkEyA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.26.0", + "@react-aria/toolbar": "3.0.0-beta.22", + "@react-aria/utils": "^3.32.0", + "@react-stately/toggle": "^3.9.3", + "@react-types/button": "^3.14.1", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/calendar": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@react-aria/calendar/-/calendar-3.9.3.tgz", + "integrity": "sha512-F12UQ4zd8GIxpJxs9GAHzDD9Lby2hESHm0LF5tjsYBIOBJc5K7ICeeE5UqLMBPzgnEP5nfh1CKS8KhCB0mS7PA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.10.1", + "@react-aria/i18n": "^3.12.14", + "@react-aria/interactions": "^3.26.0", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/utils": "^3.32.0", + "@react-stately/calendar": "^3.9.1", + "@react-types/button": "^3.14.1", + "@react-types/calendar": "^3.8.1", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/checkbox": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@react-aria/checkbox/-/checkbox-3.16.3.tgz", + "integrity": "sha512-2p1haCUtERo5XavBAWNaX//dryNVnOOWfSKyzLs4UiCZR/NL0ttN+Nu/i445q0ipjLqZ6bBJtx0g0NNrubbU7Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/form": "^3.1.3", + "@react-aria/interactions": "^3.26.0", + "@react-aria/label": "^3.7.23", + "@react-aria/toggle": "^3.12.3", + "@react-aria/utils": "^3.32.0", + "@react-stately/checkbox": "^3.7.3", + "@react-stately/form": "^3.2.2", + "@react-stately/toggle": "^3.9.3", + "@react-types/checkbox": "^3.10.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/combobox": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/@react-aria/combobox/-/combobox-3.14.1.tgz", + "integrity": "sha512-wuP/4UQrGsYXLw1Gk8G/FcnUlHuoViA9G6w3LhtUgu5Q3E5DvASJalxej3NtyYU+4w4epD1gJidzosAL0rf8Ug==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.3", + "@react-aria/i18n": "^3.12.14", + "@react-aria/listbox": "^3.15.1", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/menu": "^3.19.4", + "@react-aria/overlays": "^3.31.0", + "@react-aria/selection": "^3.27.0", + "@react-aria/textfield": "^3.18.3", + "@react-aria/utils": "^3.32.0", + "@react-stately/collections": "^3.12.8", + "@react-stately/combobox": "^3.12.1", + "@react-stately/form": "^3.2.2", + "@react-types/button": "^3.14.1", + "@react-types/combobox": "^3.13.10", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/datepicker": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@react-aria/datepicker/-/datepicker-3.15.3.tgz", + "integrity": "sha512-0KkLYeLs+IubHXb879n8dzzKU/NWcxC9DXtv7M/ofL7vAvMSTmaceYJcMW+2gGYhJVpyYz8B6bk0W7kTxgB3jg==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.10.1", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/focus": "^3.21.3", + "@react-aria/form": "^3.1.3", + "@react-aria/i18n": "^3.12.14", + "@react-aria/interactions": "^3.26.0", + "@react-aria/label": "^3.7.23", + "@react-aria/spinbutton": "^3.7.0", + "@react-aria/utils": "^3.32.0", + "@react-stately/datepicker": "^3.15.3", + "@react-stately/form": "^3.2.2", + "@react-types/button": "^3.14.1", + "@react-types/calendar": "^3.8.1", + "@react-types/datepicker": "^3.13.3", + "@react-types/dialog": "^3.5.22", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/dialog": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@react-aria/dialog/-/dialog-3.5.32.tgz", + "integrity": "sha512-2puMjsJS2FtB8LiFuQDAdBSU4dt3lqdJn4FWt/8GL6l91RZBqp2Dnm5Obuee6rV2duNJZcSAUWsQZ/S1iW8Y2g==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.26.0", + "@react-aria/overlays": "^3.31.0", + "@react-aria/utils": "^3.32.0", + "@react-types/dialog": "^3.5.22", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/focus": { + "version": "3.21.3", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.3.tgz", + "integrity": "sha512-FsquWvjSCwC2/sBk4b+OqJyONETUIXQ2vM0YdPAuC+QFQh2DT6TIBo6dOZVSezlhudDla69xFBd6JvCFq1AbUw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.26.0", + "@react-aria/utils": "^3.32.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/form": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@react-aria/form/-/form-3.1.3.tgz", + "integrity": "sha512-HAKnPjMiqTxoGLVbfZyGYcZQ1uu6aSeCi9ODmtZuKM5DWZZnTUjDmM1i2L6IXvF+d1kjyApyJC7VTbKZ8AI77g==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.26.0", + "@react-aria/utils": "^3.32.0", + "@react-stately/form": "^3.2.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid": { + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.14.6.tgz", + "integrity": "sha512-xagBKHNPu4Ovt/I5He7T/oIEq82MDMSrRi5Sw3oxSCwwtZpv+7eyKRSrFz9vrNUzNgWCcx5VHLE660bLdeVNDQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.3", + "@react-aria/i18n": "^3.12.14", + "@react-aria/interactions": "^3.26.0", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/selection": "^3.27.0", + "@react-aria/utils": "^3.32.0", + "@react-stately/collections": "^3.12.8", + "@react-stately/grid": "^3.11.7", + "@react-stately/selection": "^3.20.7", + "@react-types/checkbox": "^3.10.2", + "@react-types/grid": "^3.3.6", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/i18n": { + "version": "3.12.14", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.14.tgz", + "integrity": "sha512-zYvs1FlLamFD49uneX3i5mPHrAsB3OjVpSWApTcPw8ydxOaphQDp/Q1aqrbcxlrQCcxZdXWHuvLlbkNR4+8jzw==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.10.1", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.32.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.26.0.tgz", + "integrity": "sha512-AAEcHiltjfbmP1i9iaVw34Mb7kbkiHpYdqieWufldh4aplWgsF11YQZOfaCJW4QoR2ML4Zzoa9nfFwLXA52R7Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.32.0", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/label": { + "version": "3.7.23", + "resolved": "https://registry.npmjs.org/@react-aria/label/-/label-3.7.23.tgz", + "integrity": "sha512-dRkuCJfsyBHPTq3WOJVHNRvNyQL4cRRLELmjYfUX9/jQKIsUW2l71YnUHZTRCSn2ZjhdAcdwq96fNcQo0hncBQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/utils": "^3.32.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/landmark": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@react-aria/landmark/-/landmark-3.0.8.tgz", + "integrity": "sha512-xuY8kYxCrF9C0h0Pj2lZHoxCidNfQ/SrkYWXuiN+LuBTJGCmPVif93gt7TklQ0rKJ+pKJsUgh8AC0pgwI3QP7A==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/utils": "^3.32.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/link": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.8.7.tgz", + "integrity": "sha512-TOC6Hf/x3N0P8SLR1KD/dGiJ9PmwAq8H57RiwbFbdINnG/HIvIQr5MxGTjwBvOOWcJu9brgWL5HkQaZK7Q/4Yw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.26.0", + "@react-aria/utils": "^3.32.0", + "@react-types/link": "^3.6.5", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/listbox": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/@react-aria/listbox/-/listbox-3.15.1.tgz", + "integrity": "sha512-81iDLFhmPXvLOtkI0SKzgrngfzwfR2o9oFDAYRfpYCOxgT7jjh8SaB4wCteJXRiMwymRGmgyTvD4yxWTluEeXA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.26.0", + "@react-aria/label": "^3.7.23", + "@react-aria/selection": "^3.27.0", + "@react-aria/utils": "^3.32.0", + "@react-stately/collections": "^3.12.8", + "@react-stately/list": "^3.13.2", + "@react-types/listbox": "^3.7.4", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/live-announcer": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@react-aria/live-announcer/-/live-announcer-3.4.4.tgz", + "integrity": "sha512-PTTBIjNRnrdJOIRTDGNifY2d//kA7GUAwRFJNOEwSNG4FW+Bq9awqLiflw0JkpyB0VNIwou6lqKPHZVLsGWOXA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/menu": { + "version": "3.19.4", + "resolved": "https://registry.npmjs.org/@react-aria/menu/-/menu-3.19.4.tgz", + "integrity": "sha512-0A0DUEkEvZynmaD3zktHavM+EmgZSR/ht+g1ExS2jXe73CegA+dbSRfPl9eIKcHxaRrWOV96qMj2pTf0yWTBDg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.3", + "@react-aria/i18n": "^3.12.14", + "@react-aria/interactions": "^3.26.0", + "@react-aria/overlays": "^3.31.0", + "@react-aria/selection": "^3.27.0", + "@react-aria/utils": "^3.32.0", + "@react-stately/collections": "^3.12.8", + "@react-stately/menu": "^3.9.9", + "@react-stately/selection": "^3.20.7", + "@react-stately/tree": "^3.9.4", + "@react-types/button": "^3.14.1", + "@react-types/menu": "^3.10.5", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/numberfield": { + "version": "3.12.3", + "resolved": "https://registry.npmjs.org/@react-aria/numberfield/-/numberfield-3.12.3.tgz", + "integrity": "sha512-70LRXWPEuj2X8mbQXUx6l6We+RGs49Kb+2eUiSSLArHK4RvTWJWEfSjHL5IHHJ+j2AkbORdryD7SR3gcXSX+5w==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.14", + "@react-aria/interactions": "^3.26.0", + "@react-aria/spinbutton": "^3.7.0", + "@react-aria/textfield": "^3.18.3", + "@react-aria/utils": "^3.32.0", + "@react-stately/form": "^3.2.2", + "@react-stately/numberfield": "^3.10.3", + "@react-types/button": "^3.14.1", + "@react-types/numberfield": "^3.8.16", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/overlays": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@react-aria/overlays/-/overlays-3.31.0.tgz", + "integrity": "sha512-Vq41X1s8XheGIhGbbuqRJslJEX08qmMVX//dwuBaFX9T18mMR04tumKOMxp8Lz+vqwdGLvjNUYDMcgolL+AMjw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.3", + "@react-aria/i18n": "^3.12.14", + "@react-aria/interactions": "^3.26.0", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.32.0", + "@react-aria/visually-hidden": "^3.8.29", + "@react-stately/overlays": "^3.6.21", + "@react-types/button": "^3.14.1", + "@react-types/overlays": "^3.9.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/progress": { + "version": "3.4.28", + "resolved": "https://registry.npmjs.org/@react-aria/progress/-/progress-3.4.28.tgz", + "integrity": "sha512-3NUUAu+rwf1M7pau9WFkrxe/PlBPiqCl/1maGU7iufVveHnz+SVVqXdNkjYx+WkPE0ViwG86Zx6OU4AYJ1pjNw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.14", + "@react-aria/label": "^3.7.23", + "@react-aria/utils": "^3.32.0", + "@react-types/progress": "^3.5.16", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/radio": { + "version": "3.12.3", + "resolved": "https://registry.npmjs.org/@react-aria/radio/-/radio-3.12.3.tgz", + "integrity": "sha512-noucVX++9J3VYWg7dB+r09NVX8UZSR1TWUMCbT/MffzhltOsmiLJVvgJ0uEeeVRuu3+ZM63jOshrzG89anX4TQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.3", + "@react-aria/form": "^3.1.3", + "@react-aria/i18n": "^3.12.14", + "@react-aria/interactions": "^3.26.0", + "@react-aria/label": "^3.7.23", + "@react-aria/utils": "^3.32.0", + "@react-stately/radio": "^3.11.3", + "@react-types/radio": "^3.9.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/selection": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.27.0.tgz", + "integrity": "sha512-4zgreuCu4QM4t2U7aF3mbMvIKCEkTEo6h6nGJvbyZALZ/eFtLTvUiV8/5CGDJRLGvgMvi3XxUeF9PZbpk5nMJg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.3", + "@react-aria/i18n": "^3.12.14", + "@react-aria/interactions": "^3.26.0", + "@react-aria/utils": "^3.32.0", + "@react-stately/selection": "^3.20.7", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/slider": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/@react-aria/slider/-/slider-3.8.3.tgz", + "integrity": "sha512-tOZVH+wLt3ik0C3wyuXqHL9fvnQ5S+/tHMYB7z8aZV5cEe36Gt4efBILphlA7ChkL/RvpHGK2AGpEGxvuEQIuQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.14", + "@react-aria/interactions": "^3.26.0", + "@react-aria/label": "^3.7.23", + "@react-aria/utils": "^3.32.0", + "@react-stately/slider": "^3.7.3", + "@react-types/shared": "^3.32.1", + "@react-types/slider": "^3.8.2", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/spinbutton": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.7.0.tgz", + "integrity": "sha512-FOyH94BZp+jNhUJuZqXSubQZDNQEJyW/J19/gwCxQvQvxAP79dhDFshh1UtrL4EjbjIflmaOes+sH/XEHUnJVA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.14", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/utils": "^3.32.0", + "@react-types/button": "^3.14.1", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/switch": { + "version": "3.7.9", + "resolved": "https://registry.npmjs.org/@react-aria/switch/-/switch-3.7.9.tgz", + "integrity": "sha512-RZtuFRXews0PBx8Fc2R/kqaIARD5YIM5uYtmwnWfY7y5bEsBGONxp0d+m2vDyY7yk+VNpVFBdwewY9GbZmH1CA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/toggle": "^3.12.3", + "@react-stately/toggle": "^3.9.3", + "@react-types/shared": "^3.32.1", + "@react-types/switch": "^3.5.15", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/table": { + "version": "3.17.9", + "resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.17.9.tgz", + "integrity": "sha512-Jby561E1YfzoRgtp+RQuhDz4vnxlcqol9RTgQQ7FWXC2IcN9Pny1COU34LkA1cL9VeB9LJ0+qfMhGw4aAwaUmw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.3", + "@react-aria/grid": "^3.14.6", + "@react-aria/i18n": "^3.12.14", + "@react-aria/interactions": "^3.26.0", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/utils": "^3.32.0", + "@react-aria/visually-hidden": "^3.8.29", + "@react-stately/collections": "^3.12.8", + "@react-stately/flags": "^3.1.2", + "@react-stately/table": "^3.15.2", + "@react-types/checkbox": "^3.10.2", + "@react-types/grid": "^3.3.6", + "@react-types/shared": "^3.32.1", + "@react-types/table": "^3.13.4", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tabs": { + "version": "3.10.9", + "resolved": "https://registry.npmjs.org/@react-aria/tabs/-/tabs-3.10.9.tgz", + "integrity": "sha512-2+FNd7Ohr3hrEgYrKdZW0FWbgybzTVZft6tw95oQ2+9PnjdDVdtzHliI+8HY8jzb4hTf4bU7O8n+s/HBlCBSIw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.3", + "@react-aria/i18n": "^3.12.14", + "@react-aria/selection": "^3.27.0", + "@react-aria/utils": "^3.32.0", + "@react-stately/tabs": "^3.8.7", + "@react-types/shared": "^3.32.1", + "@react-types/tabs": "^3.3.20", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/textfield": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/@react-aria/textfield/-/textfield-3.18.3.tgz", + "integrity": "sha512-ehiSHOKuKCwPdxFe7wGE0QJlSeeJR4iJuH+OdsYVlZzYbl9J/uAdGbpsj/zPhNtBo1g/Td76U8TtTlYRZ8lUZw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/form": "^3.1.3", + "@react-aria/interactions": "^3.26.0", + "@react-aria/label": "^3.7.23", + "@react-aria/utils": "^3.32.0", + "@react-stately/form": "^3.2.2", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.32.1", + "@react-types/textfield": "^3.12.6", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toast": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@react-aria/toast/-/toast-3.0.9.tgz", + "integrity": "sha512-2sRitczXl5VEwyq97o8TVvq3bIqLA7EfA7dhDPkYlHGa4T1vzKkhNqgkskKd9+Tw7gqeFRFjnokh+es9jkM11g==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.14", + "@react-aria/interactions": "^3.26.0", + "@react-aria/landmark": "^3.0.8", + "@react-aria/utils": "^3.32.0", + "@react-stately/toast": "^3.1.2", + "@react-types/button": "^3.14.1", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toggle": { + "version": "3.12.3", + "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.12.3.tgz", + "integrity": "sha512-mciUbeVP99fRObnH5qLFrkKXX+5VKeV6BhFJlmz1eo3ltR/0xZKnUcycA2CGzmqtB70w09CAhr8NMEnpNH8dwQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.26.0", + "@react-aria/utils": "^3.32.0", + "@react-stately/toggle": "^3.9.3", + "@react-types/checkbox": "^3.10.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toolbar": { + "version": "3.0.0-beta.22", + "resolved": "https://registry.npmjs.org/@react-aria/toolbar/-/toolbar-3.0.0-beta.22.tgz", + "integrity": "sha512-Q1gOj6N4vzvpGrIoNAxpUudEQP82UgQACENH/bcH8FnEMbSP7DHvVfDhj7GTU6ldMXO2cjqLhiidoUK53gkCiA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.3", + "@react-aria/i18n": "^3.12.14", + "@react-aria/utils": "^3.32.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tooltip": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@react-aria/tooltip/-/tooltip-3.9.0.tgz", + "integrity": "sha512-2O1DXEV8/+DeUq9dIlAfaNa7lSG+7FCZDuF+sNiPYnZM6tgFOrsId26uMF5EuwpVfOvXSSGnq0+6Ma2On7mZPg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.26.0", + "@react-aria/utils": "^3.32.0", + "@react-stately/tooltip": "^3.5.9", + "@react-types/shared": "^3.32.1", + "@react-types/tooltip": "^3.5.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.32.0.tgz", + "integrity": "sha512-/7Rud06+HVBIlTwmwmJa2W8xVtgxgzm0+kLbuFooZRzKDON6hhozS1dOMR/YLMxyJOaYOTpImcP4vRR9gL1hEg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/visually-hidden": { + "version": "3.8.29", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.29.tgz", + "integrity": "sha512-1joCP+MHBLd+YA6Gb08nMFfDBhOF0Kh1gR1SA8zoxEB5RMfQEEkufIB8k0GGwvHGSCK3gFyO8UAVsD0+rRYEyg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.26.0", + "@react-aria/utils": "^3.32.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/calendar": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.9.1.tgz", + "integrity": "sha512-q0Q8fivpQa1rcLg5daUVxwVj1smCp1VnpX9A5Q5PkI9lH9x+xdS0Y6eOqb8Ih3TKBDkx9/oEZonOX7RYNIzSig==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.10.1", + "@react-stately/utils": "^3.11.0", + "@react-types/calendar": "^3.8.1", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/checkbox": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@react-stately/checkbox/-/checkbox-3.7.3.tgz", + "integrity": "sha512-ve2K+uWT+NRM1JMn+tkWJDP2iBAaWvbZ0TbSXs371IUcTWaNW61HygZ+UFOB/frAZGloazEKGqAsX5XjFpgB9w==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/form": "^3.2.2", + "@react-stately/utils": "^3.11.0", + "@react-types/checkbox": "^3.10.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/collections": { + "version": "3.12.8", + "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.12.8.tgz", + "integrity": "sha512-AceJYLLXt1Y2XIcOPi6LEJSs4G/ubeYW3LqOCQbhfIgMaNqKfQMIfagDnPeJX9FVmPFSlgoCBxb1pTJW2vjCAQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/combobox": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@react-stately/combobox/-/combobox-3.12.1.tgz", + "integrity": "sha512-RwfTTYgKJ9raIY+7grZ5DbfVRSO5pDjo/ur2VN/28LZzM0eOQrLFQ00vpBmY7/R64sHRpcXLDxpz5cqpKCdvTw==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.8", + "@react-stately/form": "^3.2.2", + "@react-stately/list": "^3.13.2", + "@react-stately/overlays": "^3.6.21", + "@react-stately/utils": "^3.11.0", + "@react-types/combobox": "^3.13.10", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/data": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@react-stately/data/-/data-3.15.0.tgz", + "integrity": "sha512-ocP39NQQkrbtHVCPsqltNncpEHaONyYX/8s2UK9xeLRc+55NtDI2RZDKTUf/mi6H2SHxzEwLMQH8hWtEwC55mQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/datepicker": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@react-stately/datepicker/-/datepicker-3.15.3.tgz", + "integrity": "sha512-RDYoz1R/EkCyxHYewb58T7DngU3gl6CnQL7xiWiDlayPnstGaanoQ3yCZGJaIQwR8PrKdNbQwXF9NlSmj8iCOw==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.10.1", + "@internationalized/string": "^3.2.7", + "@react-stately/form": "^3.2.2", + "@react-stately/overlays": "^3.6.21", + "@react-stately/utils": "^3.11.0", + "@react-types/datepicker": "^3.13.3", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/form": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@react-stately/form/-/form-3.2.2.tgz", + "integrity": "sha512-soAheOd7oaTO6eNs6LXnfn0tTqvOoe3zN9FvtIhhrErKz9XPc5sUmh3QWwR45+zKbitOi1HOjfA/gifKhZcfWw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/grid": { + "version": "3.11.7", + "resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.11.7.tgz", + "integrity": "sha512-SqzBSxUTFZKLZicfXDK+M0A3gh07AYK1pmU/otcq2cjZ0nSC4CceKijQ2GBZnl+YGcGHI1RgkhpLP6ZioMYctQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.8", + "@react-stately/selection": "^3.20.7", + "@react-types/grid": "^3.3.6", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/list": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.13.2.tgz", + "integrity": "sha512-dGFALuQWNNOkv7W12qSsXLF4mJHLeWeK2hVvdyj4SI8Vxku+BOfaVKuW3sn3mNiixI1dM/7FY2ip4kK+kv27vw==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.8", + "@react-stately/selection": "^3.20.7", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/menu": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/@react-stately/menu/-/menu-3.9.9.tgz", + "integrity": "sha512-moW5JANxMxPilfR0SygpCWCZe7Ef09oadgzTZthRymNRv0PXVS9ad4wd1EkwuMvPH/n0uZLZE2s8hNyFDgyqPA==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/overlays": "^3.6.21", + "@react-types/menu": "^3.10.5", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/numberfield": { + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@react-stately/numberfield/-/numberfield-3.10.3.tgz", + "integrity": "sha512-40g/oyVcWoEaLqkr61KuHZzQVLLXFi3oa2K8XLnb6o+859SM4TX3XPNqL6eNQjXSKoJO5Hlgpqhee9j+VDbGog==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/number": "^3.6.5", + "@react-stately/form": "^3.2.2", + "@react-stately/utils": "^3.11.0", + "@react-types/numberfield": "^3.8.16", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/overlays": { + "version": "3.6.21", + "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.21.tgz", + "integrity": "sha512-7f25H1PS2g+SNvuWPEW30pSGqYNHxesCP4w+1RcV/XV1oQI7oP5Ji2WfI0QsJEFc9wP/ZO1pyjHNKpfLI3O88g==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.11.0", + "@react-types/overlays": "^3.9.2", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/radio": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/@react-stately/radio/-/radio-3.11.3.tgz", + "integrity": "sha512-8+Cy0azV1aBWKcBfGHi3nBa285lAS6XhmVw2LfEwxq8DeVKTbJAaCHHwvDoclxDiOAnqzE0pio0QMD8rYISt9g==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/form": "^3.2.2", + "@react-stately/utils": "^3.11.0", + "@react-types/radio": "^3.9.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/selection": { + "version": "3.20.7", + "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.20.7.tgz", + "integrity": "sha512-NkiRsNCfORBIHNF1bCavh4Vvj+Yd5NffE10iXtaFuhF249NlxLynJZmkcVCqNP9taC2pBIHX00+9tcBgxhG+mA==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.8", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/slider": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@react-stately/slider/-/slider-3.7.3.tgz", + "integrity": "sha512-9QGnQNXFAH52BzxtU7weyOV/VV7/so6uIvE8VOHfc6QR3GMBM/kJvqBCTWZfQ0pxDIsRagBQDD/tjB09ixTOzg==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.32.1", + "@react-types/slider": "^3.8.2", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/table": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/@react-stately/table/-/table-3.15.2.tgz", + "integrity": "sha512-vgEArBN5ocqsQdeORBj6xk8acu5iFnd/CyXEQKl0R5RyuYuw0ms8UmFHvs8Fv1HONehPYg+XR4QPliDFPX8R9A==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.8", + "@react-stately/flags": "^3.1.2", + "@react-stately/grid": "^3.11.7", + "@react-stately/selection": "^3.20.7", + "@react-stately/utils": "^3.11.0", + "@react-types/grid": "^3.3.6", + "@react-types/shared": "^3.32.1", + "@react-types/table": "^3.13.4", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tabs": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@react-stately/tabs/-/tabs-3.8.7.tgz", + "integrity": "sha512-ETZEzg7s9F2SCvisZ2cCpLx6XBHqdvVgDGU5l3C3s9zBKBr6lgyLFt61IdGW8XXZRUvw4mMGT6tGQbXeGvR0Wg==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/list": "^3.13.2", + "@react-types/shared": "^3.32.1", + "@react-types/tabs": "^3.3.20", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/toast": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/toast/-/toast-3.1.2.tgz", + "integrity": "sha512-HiInm7bck32khFBHZThTQaAF6e6/qm57F4mYRWdTq8IVeGDzpkbUYibnLxRhk0UZ5ybc6me+nqqPkG/lVmM42Q==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/toggle": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.9.3.tgz", + "integrity": "sha512-G6aA/aTnid/6dQ9dxNEd7/JqzRmVkVYYpOAP+l02hepiuSmFwLu4nE98i4YFBQqFZ5b4l01gMrS90JGL7HrNmw==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.11.0", + "@react-types/checkbox": "^3.10.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tooltip": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@react-stately/tooltip/-/tooltip-3.5.9.tgz", + "integrity": "sha512-YwqtxFqQFfJtbeh+axHVGAfz9XHf73UaBndHxSbVM/T5c1PfI2yOB39T2FOU5fskZ2VMO3qTDhiXmFgGbGYSfQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/overlays": "^3.6.21", + "@react-types/tooltip": "^3.5.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tree": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/@react-stately/tree/-/tree-3.9.4.tgz", + "integrity": "sha512-Re1fdEiR0hHPcEda+7ecw+52lgGfFW0MAEDzFg9I6J/t8STQSP+1YC0VVVkv2xRrkLbKLPqggNKgmD8nggecnw==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.8", + "@react-stately/selection": "^3.20.7", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz", + "integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/virtualizer": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@react-stately/virtualizer/-/virtualizer-4.4.4.tgz", + "integrity": "sha512-ri8giqXSZOrznZDCCOE4U36wSkOhy+hrFK7yo/YVcpxTqqp3d3eisfKMqbDsgqBW+XTHycTU/xeAf0u9NqrfpQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/accordion": { + "version": "3.0.0-alpha.26", + "resolved": "https://registry.npmjs.org/@react-types/accordion/-/accordion-3.0.0-alpha.26.tgz", + "integrity": "sha512-OXf/kXcD2vFlEnkcZy/GG+a/1xO9BN7Uh3/5/Ceuj9z2E/WwD55YwU3GFM5zzkZ4+DMkdowHnZX37XnmbyD3Mg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/breadcrumbs": { + "version": "3.7.17", + "resolved": "https://registry.npmjs.org/@react-types/breadcrumbs/-/breadcrumbs-3.7.17.tgz", + "integrity": "sha512-IhvVTcfli5o/UDlGACXxjlor2afGlMQA8pNR3faH0bBUay1Fmm3IWktVw9Xwmk+KraV2RTAg9e+E6p8DOQZfiw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/link": "^3.6.5", + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/button": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.14.1.tgz", + "integrity": "sha512-D8C4IEwKB7zEtiWYVJ3WE/5HDcWlze9mLWQ5hfsBfpePyWCgO3bT/+wjb/7pJvcAocrkXo90QrMm85LcpBtrpg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/calendar": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.8.1.tgz", + "integrity": "sha512-B0UuitMP7YkArBAQldwSZSNL2WwazNGCG+lp6yEDj831NrH9e36/jcjv1rObQ9ZMS6uDX9LXu5C8V5RFwGQabA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.10.1", + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/checkbox": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.10.2.tgz", + "integrity": "sha512-ktPkl6ZfIdGS1tIaGSU/2S5Agf2NvXI9qAgtdMDNva0oLyAZ4RLQb6WecPvofw1J7YKXu0VA5Mu7nlX+FM2weQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/combobox": { + "version": "3.13.10", + "resolved": "https://registry.npmjs.org/@react-types/combobox/-/combobox-3.13.10.tgz", + "integrity": "sha512-Wo4iix++ID6JzoH9eD7ddGUlirQiGpN/VQc3iFjnaTXiJ/cj3v+1oGsDGCZZTklTVeUMU7SRBfMhMgxHHIYLXA==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/datepicker": { + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/@react-types/datepicker/-/datepicker-3.13.3.tgz", + "integrity": "sha512-OTRa3banGxcUQKRTLUzr0zTVUMUL+Az1BWARCYQ+8Z/dlkYXYUW0fnS5I0pUEqihgai15KxiY13U0gAqbNSfcA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.10.1", + "@react-types/calendar": "^3.8.1", + "@react-types/overlays": "^3.9.2", + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/dialog": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.22.tgz", + "integrity": "sha512-smSvzOcqKE196rWk0oqJDnz+ox5JM5+OT0PmmJXiUD4q7P5g32O6W5Bg7hMIFUI9clBtngo8kLaX2iMg+GqAzg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/overlays": "^3.9.2", + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/form": { + "version": "3.7.16", + "resolved": "https://registry.npmjs.org/@react-types/form/-/form-3.7.16.tgz", + "integrity": "sha512-Sb7KJoWEaQ/e4XIY+xRbjKvbP1luome98ZXevpD+zVSyGjEcfIroebizP6K1yMHCWP/043xH6GUkgEqWPoVGjg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/grid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.3.6.tgz", + "integrity": "sha512-vIZJlYTii2n1We9nAugXwM2wpcpsC6JigJFBd6vGhStRdRWRoU4yv1Gc98Usbx0FQ/J7GLVIgeG8+1VMTKBdxw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/link": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@react-types/link/-/link-3.6.5.tgz", + "integrity": "sha512-+I2s3XWBEvLrzts0GnNeA84mUkwo+a7kLUWoaJkW0TOBDG7my95HFYxF9WnqKye7NgpOkCqz4s3oW96xPdIniQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/listbox": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@react-types/listbox/-/listbox-3.7.4.tgz", + "integrity": "sha512-p4YEpTl/VQGrqVE8GIfqTS5LkT5jtjDTbVeZgrkPnX/fiPhsfbTPiZ6g0FNap4+aOGJFGEEZUv2q4vx+rCORww==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/menu": { + "version": "3.10.5", + "resolved": "https://registry.npmjs.org/@react-types/menu/-/menu-3.10.5.tgz", + "integrity": "sha512-HBTrKll2hm0VKJNM4ubIv1L9MNo8JuOnm2G3M+wXvb6EYIyDNxxJkhjsqsGpUXJdAOSkacHBDcNh2HsZABNX4A==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/overlays": "^3.9.2", + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/numberfield": { + "version": "3.8.16", + "resolved": "https://registry.npmjs.org/@react-types/numberfield/-/numberfield-3.8.16.tgz", + "integrity": "sha512-945F0GsD7K2T293YXhap+2Runl3tZWbnhadXVHFWLbqIKKONZFSZTfLKxQcbFr+bQXr2uh1bVJhYcOiS1l5M+A==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/overlays": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.9.2.tgz", + "integrity": "sha512-Q0cRPcBGzNGmC8dBuHyoPR7N3057KTS5g+vZfQ53k8WwmilXBtemFJPLsogJbspuewQ/QJ3o2HYsp2pne7/iNw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/progress": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@react-types/progress/-/progress-3.5.16.tgz", + "integrity": "sha512-I9tSdCFfvQ7gHJtm90VAKgwdTWXQgVNvLRStEc0z9h+bXBxdvZb+QuiRPERChwFQ9VkK4p4rDqaFo69nDqWkpw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/radio": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@react-types/radio/-/radio-3.9.2.tgz", + "integrity": "sha512-3UcJXu37JrTkRyP4GJPDBU7NmDTInrEdOe+bVzA1j4EegzdkJmLBkLg5cLDAbpiEHB+xIsvbJdx6dxeMuc+H3g==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/slider": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@react-types/slider/-/slider-3.8.2.tgz", + "integrity": "sha512-MQYZP76OEOYe7/yA2To+Dl0LNb0cKKnvh5JtvNvDnAvEprn1RuLiay8Oi/rTtXmc2KmBa4VdTcsXsmkbbkeN2Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/switch": { + "version": "3.5.15", + "resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.5.15.tgz", + "integrity": "sha512-r/ouGWQmIeHyYSP1e5luET+oiR7N7cLrAlWsrAfYRWHxqXOSNQloQnZJ3PLHrKFT02fsrQhx2rHaK2LfKeyN3A==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/table": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.13.4.tgz", + "integrity": "sha512-I/DYiZQl6aNbMmjk90J9SOhkzVDZvyA3Vn3wMWCiajkMNjvubFhTfda5DDf2SgFP5l0Yh6TGGH5XumRv9LqL5Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/grid": "^3.3.6", + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/tabs": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/@react-types/tabs/-/tabs-3.3.20.tgz", + "integrity": "sha512-Kjq4PypapdMOVPAQgaFIKH65Kr3YnRvaxBGd6RYizTsqYImQhXoGj6B4lBpjYy4KhfRd4dYS82frHqTGKmBYiA==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/textfield": { + "version": "3.12.6", + "resolved": "https://registry.npmjs.org/@react-types/textfield/-/textfield-3.12.6.tgz", + "integrity": "sha512-hpEVKE+M3uUkTjw2WrX1NrH/B3rqDJFUa+ViNK2eVranLY4ZwFqbqaYXSzHupOF3ecSjJJv2C103JrwFvx6TPQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/tooltip": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@react-types/tooltip/-/tooltip-3.5.0.tgz", + "integrity": "sha512-o/m1wlKlOD2sLb9vZLWdVkD5LFLHBMLGeeK/bhyUtp0IEdUeKy0ZRTS7pa/A50trov9RvdbzLK79xG8nKNxHew==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/overlays": "^3.9.2", + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.3.tgz", + "integrity": "sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.11.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.3.tgz", + "integrity": "sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", + "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color2k": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.3.tgz", + "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fortawesome": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fortawesome/-/fortawesome-0.0.1-security.tgz", + "integrity": "sha512-o/pwn9ZSnveFLYP0d5IejSwz0rpGMDQsBaYVvR8WXSvnxILIwh7IUJK0P90QMF4cnIyouVF2BGrz0LEVSyzimA==" + }, + "node_modules/framer-motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", + "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/i18next": { + "version": "25.7.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz", + "integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/input-otp": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz", + "integrity": "sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz", + "integrity": "sha512-zQuvzWfUKQu9oNVi1o0RZAJCwhGsdhx4NEOyrVQwJHaWDseGP9tl7XUPLY2T8Cj6+IrZ6lmyxlR1KC8unf3RLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^7.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", + "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==", + "license": "MIT", + "dependencies": { + "@next/env": "16.1.1", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.1.1", + "@next/swc-darwin-x64": "16.1.1", + "@next/swc-linux-arm64-gnu": "16.1.1", + "@next/swc-linux-arm64-musl": "16.1.1", + "@next/swc-linux-x64-gnu": "16.1.1", + "@next/swc-linux-x64-musl": "16.1.1", + "@next/swc-win32-arm64-msvc": "16.1.1", + "@next/swc-win32-x64-msvc": "16.1.1", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-i18next": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.1.tgz", + "integrity": "sha512-Hks6UIRZWW4c+qDAnx1csVsCGYeIR4MoBGQgJ+NUoNnO6qLxXuf8zu0xdcinyXUORgGzCdRsexxO1Xzv3sTdnw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz", + "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.11.0.tgz", + "integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==", + "license": "MIT", + "dependencies": { + "react-router": "7.11.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.0.10.tgz", + "integrity": "sha512-t44QCeDKAPf1mtQH3fYpWz8IM/DyvHLjs8wUvvwMYxk5moOqCzrMSxK6HQVD0QVmVjXFavoFIPRVrMuJPKAvtg==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", + "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", + "license": "MIT", + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwind-merge": ">=3.0.0", + "tailwindcss": "*" + }, + "peerDependenciesMeta": { + "tailwind-merge": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zustand": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", + "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c906c05 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.9.3", + "vite": "^7.0.7" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.1.0", + "@fortawesome/free-brands-svg-icons": "^7.1.0", + "@heroui/react": "^2.8.5", + "@heroui/use-theme": "^2.1.10", + "@react-aria/visually-hidden": "^3.8.28", + "@react-stately/data": "^3.15.0", + "@tailwindcss/typography": "^0.5.19", + "@vitejs/plugin-react": "^5.1.1", + "file-saver": "^2.0.5", + "fortawesome": "^0.0.1-security", + "framer-motion": "^12.23.24", + "i18next": "^25.6.3", + "next": "^16.0.9", + "next-themes": "^0.4.6", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-i18next": "^16.3.5", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.9.6", + "zustand": "^5.0.8" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..c4a3078 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,36 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/resources/css/app.css b/resources/css/app.css new file mode 100644 index 0000000..a761263 --- /dev/null +++ b/resources/css/app.css @@ -0,0 +1,8 @@ +@source "../**/*.{js,ts,jsx,tsx}" +@import "tailwindcss/preflight"; +@import 'tailwindcss'; +@plugin "@tailwindcss/typography"; + +@plugin '../../hero.ts'; +@source '../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'; +@custom-variant dark (&:is(.dark *)); diff --git a/resources/docs/EvaluationRuleSet.md b/resources/docs/EvaluationRuleSet.md new file mode 100644 index 0000000..6911123 --- /dev/null +++ b/resources/docs/EvaluationRuleSet.md @@ -0,0 +1,171 @@ +# EvaluationRuleSet - pravidla vyhodnocení pro rozhodčí + +Tento dokument popisuje vyhodnocovací proces a jednotlivá nastavení rulesetu +(`EvaluationRuleSet`). Slouží jako nápověda a vysvětlení dopadu na +skóre a výsledky. + +## Stručný popis vyhodnocovacího procesu + +1. **Prepare** - příprava běhu, kontrola locku, vyčištění staging tabulek. +2. **ParseLogs** - parsování EDI souboru, naplnění `logs` a `log_qsos`. +3. **BuildWorkingSet** - normalizace volacích znaků, lokátorů, příprava `working_qsos`. +4. **Match** - párování QSO mezi logy, detekce neshod a typy chyb. +5. **UnpairedClassification** - NIL/NO_COUNTERPART/UNIQUE pro nenapárovaná QSO. +6. **DuplicateResolution** - výběr přeživších u duplicit podle strategie. +7. **Score** - bodování, aplikace policy, penalizací a multiplikátorů. +8. **Aggregate** - součty za log (score, counts), výpočet metrik. +9. **ApplyLogOverrides** - ruční zásahy do log_results. +10. **RecalculateRanks** - přepočet pořadí. +11. **Finalize** - uzavření běhu a uvolnění locku. + +## Politiky (společné chování) + +Používané policy hodnoty: +- **INVALID**: `is_valid=false`, body 0. +- **ZERO_POINTS**: `is_valid=true`, body 0. +- **FLAG_ONLY**: `is_valid=true`, body beze změny (jen flag). +- **PENALTY**: `is_valid=true`, body 0 (u některých chyb body zůstávají) + penalizace. + +Poznámka: `valid_qso_count` se počítá podle `is_valid`. + +## Nastavení rulesetu a dopad (kde se uplatňuje) + +### Skóre a body +- `scoring_mode` (DISTANCE / FIXED_POINTS): určuje typ bodování. + Uplatnění: scoring. +- `points_per_qso`: fixní body za QSO (FIXED_POINTS). + Uplatnění: scoring. +- `points_per_km`: body za km (DISTANCE). + Uplatnění: scoring. +- `distance_rounding` (FLOOR/ROUND/CEIL): zaokrouhlení vzdálenosti. + Uplatnění: scoring. +- `min_distance_km`: minimální vzdálenost pro bodované QSO. + Uplatnění: scoring. + +### Multiplikátory +- `use_multipliers`: zapíná multiplikátory. + Uplatnění: scoring + agregace. +- `multiplier_type` (WWL/DXCC/SECTION/COUNTRY/NONE): typ multiplikátoru. + Uplatnění: scoring + agregace. +- `multiplier_scope` (PER_BAND/OVERALL): scope multiplikátoru. + Uplatnění: agregace. +- `multiplier_source` (VALID_ONLY/ALL_MATCHED): z čeho se počítají. + Uplatnění: agregace. +- `wwl_multiplier_level` (LOCATOR_2/4/6): délka WWL multiplikátoru. + Uplatnění: scoring. + +### Error policy a penalizace +- `dup_qso_policy`, `nil_qso_policy`: policy pro DUP/NIL. + Uplatnění: scoring. +- `no_counterpart_log_policy`, `not_in_counterpart_log_policy`, `unique_qso_policy`, + `time_mismatch_policy`: policy pro NIL/UNIQUE/TIME_MISMATCH. + Uplatnění: scoring. +- `busted_call_policy`, `busted_exchange_policy`, `busted_serial_policy`, + `busted_locator_policy`, `busted_rst_policy`: policy pro BUSTED chyby. + Uplatnění: scoring. +- `penalty_*_points`: velikost penalizací pro jednotlivé chyby. + Uplatnění: scoring. +- `out_of_window_policy`, `penalty_out_of_window_points`: chování mimo časové okno. + Uplatnění: scoring. + +### Matching (párování QSO) +- `time_tolerance_sec`: tolerance času pro match. + Uplatnění: matching. +- `allow_time_shift_one_hour`, `time_shift_seconds`: povolený časový posun. + Uplatnění: matching. +- `allow_time_mismatch_pairing`, `time_mismatch_max_sec`: párování mimo toleranci. + Uplatnění: matching. +- `callsign_normalization` (STRICT/IGNORE_SUFFIX), + `ignore_slash_part`, `ignore_third_part`, + `callsign_suffix_max_len`, `callsign_levenshtein_max`: + normalizace volacích znaků a fuzzy match. + Uplatnění: matching. +- `match_tiebreak_order`: pořadí tiebreak kritérií (time_diff, exchange_match, ...). + Uplatnění: matching (volba nejlepšího kandidáta). +- `match_require_locator_match`, `match_require_exchange_match`: + vyžadování shody lokátoru/exchange pro match. + Uplatnění: matching. +- `exchange_type`, `exchange_requires_*`, `exchange_pattern`: + definice a kontrola exchange. + Uplatnění: matching. +- `letters_in_rst`, `rst_ignore_third_char`: normalizace RST. + Uplatnění: matching. +- `discard_qso_*`: určuje, zda se neshoda označí jako BUSTED a s jakou stranou (RX/TX). + Uplatnění: matching. +- `checklog_matching`: zahrnout CHECK logy do matchingu. + Uplatnění: matching. + +### Duplicity a unikátní QSO +- `dupe_scope` (BAND/BAND_MODE): klíč pro duplicity. + Uplatnění: working set + duplicity. +- `dup_resolution_strategy`: pořadí pravidel pro výběr přeživších DUP. + Uplatnění: duplicate resolution. +- `require_unique_qso`: zapíná detekci UNIQUE. + Uplatnění: unpaired klasifikace. + +### DQ limity (log-level) +- `out_of_window_dq_threshold`: DQ při nadlimitních QSO mimo okno. + Uplatnění: agregace. +- `time_diff_dq_threshold_percent`, `time_diff_dq_threshold_sec`: DQ při časovém rozptylu. + Uplatnění: agregace. +- `bad_qso_dq_threshold_percent`: DQ při nadlimitním % chybných QSO. + Uplatnění: agregace. + +### 6H operating window +- `operating_window_mode` (NONE/BEST_CONTIGUOUS): zapíná 6H operating window. + Uplatnění: agregace (vybere nejlepší 6H okno pro log). +- `operating_window_hours`: délka 6H okna (aktuálně pevně 6 h). + Uplatnění: agregace. +- `sixhr_ranking_mode` (IARU/CRK): způsob pořadí pro 6H. + Uplatnění: přepočet pořadí (IARU = jedna společná 6H tabulka bez SO/MO, CRK = odděleně SO/MO). + +Poznámka: Pro IARU se 6H okno vybírá jako max. 2 segmenty s pauzou >= 2 h, součet délek <= 6 h. + +### Options (JSON) +- `options`: fallback hodnoty, pokud není vyplněn sloupec. + Uplatnění: napříč matching/scoring (viz metody `getOption`/`get*` v modelu). + +## Výchozí ruleset (default_vhf_compat) + +Zdroj: `database/seeders/EvaluationRuleSetSeeder.php` + +Nastavení (zkrácené na podstatné hodnoty): +- Profil: **Default VHF (compat)**, permisivní matching. +- Scoring: `scoring_mode=DISTANCE`, `points_per_qso=1`, `points_per_km=1.0` +- Multiplikátory: `use_multipliers=false`, `multiplier_type=WWL` +- Policy: + - `dup_qso_policy=ZERO_POINTS` + - `nil_qso_policy=ZERO_POINTS` + - `no_counterpart_log_policy=FLAG_ONLY` + - `not_in_counterpart_log_policy=ZERO_POINTS` + - `unique_qso_policy=FLAG_ONLY` + - `busted_*_policy=ZERO_POINTS` + - `time_mismatch_policy=ZERO_POINTS` + - penalizace vše 0 +- Matching: + - `time_tolerance_sec=600` + - `allow_time_shift_one_hour=true`, `time_shift_seconds=3600` + - `allow_time_mismatch_pairing=false` + - `callsign_normalization=IGNORE_SUFFIX` + - `ignore_slash_part=true`, `ignore_third_part=true` + - `letters_in_rst=false`, `rst_ignore_third_char=true` + - `match_require_locator_match=false`, `match_require_exchange_match=false` + - `match_tiebreak_order=[time_diff, exchange_match, locator_match, report_match, log_qso_id]` + - `discard_qso_rec_diff_*=true`, `discard_qso_sent_diff_*=false` +- Duplicity: + - `dupe_scope=BAND` + - `dup_resolution_strategy=[paired_first, ok_first, earlier_time, lower_id]` +- Exchange: + - `exchange_type=SERIAL_WWL` + - `exchange_requires_wwl=true`, `exchange_requires_serial=true`, `exchange_requires_report=true` +- DQ limity: + - `out_of_window_dq_threshold=600` + - `time_diff_dq_threshold_percent=30` + - `time_diff_dq_threshold_sec=600` + - `bad_qso_dq_threshold_percent=30` + +## Poznámky k interpretaci výsledků + +- `valid_qso_count` = počet QSO s `is_valid=true`. +- `discarded_qso_count` = počet QSO s `is_valid=false`. +- Penalizace se uplatňuje v `penalty_score` a odčítá se od `base_score`. diff --git a/resources/docs/prezentace_pipeline.md b/resources/docs/prezentace_pipeline.md new file mode 100644 index 0000000..05cd630 --- /dev/null +++ b/resources/docs/prezentace_pipeline.md @@ -0,0 +1,149 @@ +## 1) "Manažerský souhrn" + +- Pipeline je vícekroková, deterministická a auditovaná přes `EvaluationRunEvent`. +- Pořadí: Prepare → Parse → Build working set → Match → Unpaired → Duplicity → Score → Aggregate → Overrides → Ranks → Finalize. +- Kritické kontroly: párování QSO (včetně chyb exchange), klasifikace nenapárovaných (NIL/NO_COUNTERPART/UNIQUE), duplicity, out-of-window. +- Body se počítají podle rulesetu: FIXED_POINTS nebo DISTANCE; politika chyb rozhoduje o validitě, bodech a penalizacích. +- Systém má tři „čekací“ body pro ruční kontrolu (input, matching, score) a podporuje ruční override logů/QSO. + +## 2) Podrobný popis pipeline (technicky) + +### Spuštění a příprava + +1) **StartEvaluationRunJob / EvaluationCoordinator** + - Kontrola locku pro dané kolo (`evaluation:round:{round_id}`), přechod do `RUNNING`. + - Spuštění řetězce jobů (prepare → parse logs). + +2) **PrepareRunJob** + - Vyčistí staging data (`qso_results`, `log_results`, `working_qsos`) pro daný run. + - Sestaví scope skupin (band/category/power) a uloží do `evaluation_runs.scope`. + - Vytvoří skeleton `log_results` pro všechny logy (včetně aplikace log overrides na band/kategorii/power). + - Zapisuje auditní eventy. + +### Parsování vstupů a working set + +3) **DispatchParseLogsJobsJob → ParseLogJob** + - Pro každý log načte EDI a naplní `logs` + `log_qsos`. + - Průběžný progress, chybné soubory jsou hlášeny jako eventy. + - Pro `rules_version=CLAIMED` se aktualizují deklarované výsledky. + +4) **DispatchBuildWorkingSetJobsJob → BuildWorkingSetLogJob** + - Vytvoří `working_qsos` (normalizace callsignů, lokátorů, band, match_key, dupe_key). + - Kontroly: + - validace lokátoru (invalid → error v `working_qsos.errors`), + - out-of-window podle času kola, + - normalizace a klíče pro matching/duplikace. + - Logy s override `IGNORED` se vynechají. + - Po dokončení: **PauseEvaluationRunJob → WAITING_REVIEW_INPUT**. + +### Matching a klasifikace nenapárovaných + +5) **DispatchMatchJobsJob → MatchQsoBucketJob (PASS 1 + PASS 2)** + - Matching běží v „bucketu“ (band + call_norm), dvě fáze: + - PASS 1: pouze exact shody. + - PASS 2: u zbylých QSO povolí fuzzy shody dle rulesetu (tolerance, time shift, Levenshtein). + - Kontroly během matchingu: + - časová tolerance (`time_tolerance_sec`), + - mismatch exchange (callsign/RST/serial/locator) dle `discard_qso_*`, + - time mismatch se jen označí, validita se řeší až ve scoringu, + - ruční QSO override může vynutit match nebo status. + - Výstup: `qso_results` s `matched_log_qso_id`, `error_code`, `error_side`, `match_type`. + +6) **DispatchUnpairedJobsJob → UnpairedClassificationBucketJob** + - Pro nenapárované QSO určí: + - `NOT_IN_COUNTERPART_LOG` (protistanice log má), + - `NO_COUNTERPART_LOG` (protistanice log nemá), + - `UNIQUE` (pokud je zapnuté `require_unique_qso`). + - Nastaví `error_code`, `is_nil`, `is_valid=false` (validita se následně řeší policy ve scoringu). + +7) **DuplicateResolutionJob** + - Zpracuje duplicity v rámci logu podle `dupe_scope` (BAND/BAND_MODE). + - Strategie výběru „přeživšího“ QSO: `dup_resolution_strategy` (typicky paired_first → ok_first → earlier_time → lower_id). + - Non‑survivory se označí `DUP` a připraví se na policy v bodech. + - Po dokončení: **PauseEvaluationRunJob → WAITING_REVIEW_MATCH**. + +### Bodování a agregace + +8) **DispatchScoreJobsJob → ScoreGroupJob** + - Pro každé QSO spočte základní body: + - `FIXED_POINTS`: `points_per_qso` + - `DISTANCE`: vzdálenost z lokátorů × `points_per_km` + - vzdálenost se zaokrouhluje (`distance_rounding`) a respektuje `min_distance_km`. + - Policy rozhodnutí (error_code → policy): + - `INVALID` → `is_valid=false` + - `ZERO_POINTS` → 0 bodů + - `FLAG_ONLY` → body beze změny + - `PENALTY` → 0 bodů + penalizace (u `BUSTED_RST` může body ponechat) + - Aplikuje se out‑of‑window policy (`out_of_window_policy`). + - Výsledky: `points`, `penalty_points`, multiplikátor (WWL/DXCC/SECTION/COUNTRY) pro pozdější agregaci. + +9) **DispatchAggregateResultsJobsJob → AggregateLogResultsJob** + - Sečte `base_score` (validní QSO) a `penalty_score` (odečet penalizací). + - Pokud jsou multiplikátory aktivní: `multiplier_score = (base + penalty) × multiplier_count`. + - `official_score = max(0, multiplier_score)`. + - Počítá statistiky: valid/dupe/busted/unique/out-of-window/invalid, `score_per_qso`. + - DQ kontroly: + - `out_of_window_dq_threshold`, + - `time_diff_dq_threshold_percent` + `time_diff_dq_threshold_sec`, + - `bad_qso_dq_threshold_percent`. + - 6H: volí nejlepší operating window (pokud zapnuto). + - Navazuje **ApplyLogOverridesJob** + **RecalculateOfficialRanksJob**. + - Po dokončení: **PauseEvaluationRunJob → WAITING_REVIEW_SCORE**. + +10) **FinalizeRunJob** + - Znov evidentně aplikuje overrides a přepočítá pořadí (pro jistotu). + - Uzavře běh (`SUCCEEDED`) a uvolní lock. + +### Poznámky k ručním zásahům + +- **Log overrides**: lze vynutit status/band/kategorii/power/6H; log lze i ignorovat. +- **QSO overrides**: lze vynutit match nebo stav QSO, případně body. +- Tři „čekací“ stavy jsou určeny pro ruční kontrolu a zásah rozhodčího. + +## 3) Návrh prezentace (alfa verze a sběr feedbacku) + +### Doporučená struktura prezentace (slide deck) + +1) **Proč nový vyhodnocovač** – cíl: determinismus, auditovatelnost, pravidla v rulesetu. +2) **Pipeline v 1 minutě** – jediný graf s pořadím kroků + tři kontrolní „pause“. +3) **Matching principy** – 2 passy, tolerance času, co je „busted“ vs „time mismatch“. +4) **NIL/NO_COUNTERPART/UNIQUE** – kdy co vzniká a proč. +5) **Duplicity** – strategie přeživších, důvod pro samostatný krok. +6) **Bodování** – FIXED vs DISTANCE, validita až ve scoringu, policy + penalizace. +7) **Agregace a DQ** – multiplikátory, 6H window, limity DQ. +8) **UI a ruční zásahy** – ukázka overrides (log/QSO) + auditní eventy. +9) **Co umí alfa a co ještě ne** – rizika, co je stabilní, co se bude dolaďovat. +10) **Co od vás potřebujeme** – seznam dotazů, sběr pravidel a příkladů. + +### Co získat + +- Preferované policy pro NIL/DUP/BUSTED/TIME_MISMATCH (validita vs penalizace). +- Reálné hranice tolerancí (čas, matching pravidla, tiebreak priority). +- Pravidla pro 6H: reálná očekávání, jak interpretovat okno a pořadí. +- DQ prahy (out-of-window, bad QSO %, time diff). +- Seznam typických problémových situací z minulých ročníků. + +### Otázky, na které se zeptat + +- Co je v praxi považováno za „přijatelnou“ odchylku času? +- Kdy je chyba „busted“ vs jen „flag“ (nechat body)? +- Jak zacházet s unikátními QSO (UNIQUE) v různých soutěžích? +- Jaké ruční zásahy děláte dnes nejčastěji a proč? +- Jak má vypadat finální výstup pro rozhodčí (tabulka, export, log detail)? + + +## Slovníček pojmů a stavů + +- **OK**: QSO bez chyb, standardni vstup do bodovani. +- **NIL**: nenaprovane QSO; protistanice se v matchingu nenasla. +- **NO_COUNTERPART_LOG**: protistanice nema zadny log; QSO je klasifikovano jako NIL. +- **NOT_IN_COUNTERPART_LOG**: protistanice log ma, ale konkretni QSO v nem chybi; QSO je klasifikovano jako NIL. +- **UNIQUE**: jediny zaznam o spojeni s danou protistanici v danem scope; pouziva se, pokud je zapnute `require_unique_qso`. +- **DUP**: duplicitni QSO v ramci logu podle `dupe_scope`; pouze "prezivsi" QSO boduje. +- **BUSTED_CALL**: neshoda volaciho znaku (callsign); urcuje se strana chyby (RX/TX). +- **BUSTED_RST**: neshoda RST reportu (pokud je RST soucasti exchange). +- **BUSTED_SERIAL**: neshoda serialu (nebo casti exchange, ktera se mapuje na serial). +- **BUSTED_LOCATOR**: neshoda lokatoru (WWL). +- **TIME_MISMATCH**: QSO sparovano, ale casovy rozdil mimo toleranci; resi se policy ve scoringu. +- **OUT_OF_WINDOW**: QSO mimo casove okno kola; bodovani urcuje `out_of_window_policy`. +- **ERROR_SIDE (RX/TX/NONE)**: kdo udelal chybu (prijem/vysilani/neurceno); ovlivnuje penalizace. diff --git a/resources/icons/Icons.tsx b/resources/icons/Icons.tsx new file mode 100644 index 0000000..8ba8786 --- /dev/null +++ b/resources/icons/Icons.tsx @@ -0,0 +1,100 @@ +import * as React from "react"; + +import { IconSvgProps } from "./types"; + +export const MoonFilledIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + +); + +export const SunFilledIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + +); + +export const HeartFilledIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + +); + +export const SearchIcon = (props: IconSvgProps) => ( + +); diff --git a/resources/icons/types.ts b/resources/icons/types.ts new file mode 100644 index 0000000..cece4a4 --- /dev/null +++ b/resources/icons/types.ts @@ -0,0 +1,5 @@ +import { SVGProps } from "react"; + +export type IconSvgProps = SVGProps & { + size?: number; +}; diff --git a/resources/js/Header.tsx b/resources/js/Header.tsx new file mode 100644 index 0000000..4fe04af --- /dev/null +++ b/resources/js/Header.tsx @@ -0,0 +1,82 @@ +'use client' + +import {MouseEvent} from 'react' +import {Link} from 'react-router-dom' +import axios from 'axios' +import ThemeSwitch from '@/components/ThemeSwitch' +import { useTranslation } from 'react-i18next'; +import LanguageSwitcher from '@/components/LanguageSwitcher'; +import { useUserStore } from '@/stores/userStore' + + + +// https://reactrouter.com/start/declarative/navigating + +export default function Header() { + const { t } = useTranslation('common') + const user = useUserStore((s) => s.user); + const clearUser = useUserStore((s) => s.clearUser); + const isAuthenticated = Boolean(user); + const isAdmin = Boolean(user?.is_admin); + + const handleLogout = async (event: MouseEvent) => { + event.preventDefault() + try { + await axios.post( + '/logout', + {}, + { + withCredentials: true, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }, + ) + clearUser() + window.location.href = '/' + } catch (error) { + console.error(t("logout_failed"), error) + } + } + + return ( +
+ +
+ ) +} diff --git a/resources/js/Tail.tsx b/resources/js/Tail.tsx new file mode 100644 index 0000000..61bb8b8 --- /dev/null +++ b/resources/js/Tail.tsx @@ -0,0 +1,27 @@ +'use client' + +import { useTranslation } from "react-i18next"; + +export default function Tail() { + const { t } = useTranslation("common"); + const year = new Date().getFullYear(); + + return ( + + ) +} diff --git a/resources/js/VkvApp.tsx b/resources/js/VkvApp.tsx new file mode 100644 index 0000000..f50d44a --- /dev/null +++ b/resources/js/VkvApp.tsx @@ -0,0 +1,26 @@ +// resources/js/VkvApp.tsx +import {HeroUIProvider} from '@heroui/react' +import {useHref, useNavigate, useRoutes} from 'react-router-dom' +import routes from './routes' +import Header from './Header' +import Tail from './Tail' +import AppErrorBoundary from './components/AppErrorBoundary' + +export default function VkvApp() { + const navigate = useNavigate() + const routeElement = useRoutes(routes) + + return ( + +
+
+
+ + {routeElement} + +
+ +
+
+ ) +} diff --git a/resources/js/app.tsx b/resources/js/app.tsx new file mode 100644 index 0000000..1af983a --- /dev/null +++ b/resources/js/app.tsx @@ -0,0 +1,25 @@ +import './i18n'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import type {NavigateOptions} from "react-router-dom"; +import {BrowserRouter} from "react-router-dom"; +import VkvApp from './VkvApp' + +const rootElement = document.getElementById('root') +if (!rootElement) { + throw new Error('Root element not found') +} + +declare module "@react-types/shared" { + interface RouterConfig { + routerOptions: NavigateOptions; + } +} + +ReactDOM.createRoot(rootElement).render( + + + + + , +); diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js new file mode 100644 index 0000000..5f1390b --- /dev/null +++ b/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/resources/js/components/AppBreadcrumbs.tsx b/resources/js/components/AppBreadcrumbs.tsx new file mode 100644 index 0000000..ed2607e --- /dev/null +++ b/resources/js/components/AppBreadcrumbs.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { Breadcrumbs, BreadcrumbItem } from "@heroui/react"; +import { useContestStore } from "@/stores/contestStore"; +import { useLanguageStore } from "@/stores/languageStore"; +import { useNavigate } from "react-router-dom"; + +type AppBreadcrumbsProps = { + extra?: { label: string; href?: string }[]; +}; + +export default function AppBreadcrumbs({ extra = [] }: AppBreadcrumbsProps) { + const selectedContest = useContestStore((s) => s.selectedContest); + const selectedRound = useContestStore((s) => s.selectedRound); + const setSelectedRound = useContestStore((s) => s.setSelectedRound); + const clearSelection = useContestStore((s) => s.clearSelection); + const setSelectedContest = useContestStore((s) => s.setSelectedContest); + const locale = useLanguageStore((s) => s.locale); + const navigate = useNavigate(); + + const resolveLabel = (value: any): string => { + if (!value) return ""; + if (typeof value === "string") return value; + if (typeof value === "object") { + if (value[locale]) return value[locale]; + if (value["en"]) return value["en"]; + const first = Object.values(value)[0]; + return typeof first === "string" ? first : ""; + } + return String(value); + }; + + const crumbs = [ + { label: "Home", href: "/", level: "home" as const }, + { label: "Závody", href: "/contests", level: "contests" as const }, + ...(selectedContest + ? [{ + label: resolveLabel(selectedContest.name), + href: `/contests/${selectedContest.id}`, + level: "contest" as const, + contest: selectedContest, + }] + : []), + ...(selectedRound + ? [{ + label: resolveLabel(selectedRound.name), + href: `/contests/${selectedContest?.id ?? ""}/rounds/${selectedRound.id}`, + level: "round" as const, + }] + : []), + ...extra, + ]; + + const handleNavigate = (href?: string, level?: "home" | "contests" | "contest" | "round", contest?: any) => { + if (!href) return; + + // při kliknutí na vyšší úroveň vyčisti nižší selekce + if (level === "home") { + clearSelection(); + } else if (level === "contests") { + setSelectedRound(null); + } else if (level === "contest") { + setSelectedRound(null); + if (contest) { + setSelectedContest(contest); + } + } + + navigate(href); + }; + + return ( + + {crumbs.map((c, idx) => ( + handleNavigate(c.href, c.level as any, (c as any).contest)} + > + {c.label} + + ))} + + ); +} diff --git a/resources/js/components/AppErrorBoundary.tsx b/resources/js/components/AppErrorBoundary.tsx new file mode 100644 index 0000000..d86aca9 --- /dev/null +++ b/resources/js/components/AppErrorBoundary.tsx @@ -0,0 +1,35 @@ +import React from "react"; + +type AppErrorBoundaryProps = { + children: React.ReactNode; +}; + +type AppErrorBoundaryState = { + hasError: boolean; +}; + +export default class AppErrorBoundary extends React.Component { + state: AppErrorBoundaryState = { + hasError: false, + }; + + static getDerivedStateFromError() { + return { hasError: true }; + } + + componentDidCatch(error: unknown, info: React.ErrorInfo) { + console.error("AppErrorBoundary caught an error", error, info); + } + + render() { + if (this.state.hasError) { + return ( +
+ Došlo k chybě při vykreslování stránky. +
+ ); + } + + return this.props.children; + } +} diff --git a/resources/js/components/ContestCreateForm.tsx b/resources/js/components/ContestCreateForm.tsx new file mode 100644 index 0000000..428149a --- /dev/null +++ b/resources/js/components/ContestCreateForm.tsx @@ -0,0 +1,502 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { Button, Input, Switch, Textarea } from "@heroui/react"; +import { useContestRefreshStore } from "@/stores/contestRefreshStore"; +import { useTranslation } from "react-i18next"; + +type TranslationPayload = { + cs?: string; + en?: string; +}; + +type ContestFromApi = { + id: number; + name: string | TranslationPayload; + description?: string | TranslationPayload | null; + bands?: { id: number; name: string }[]; + categories?: { id: number; name: string }[]; + power_categories?: { id: number; name: string }[]; + rule_set_id?: number | null; + + url?: string | null; + evaluator?: string | null; + email?: string | null; + email2?: string | null; + + is_mcr: boolean; + is_sixhr: boolean; + is_active: boolean; + is_test: boolean; + + start_time: string | null; + duration: number; + logs_deadline_days: number; +}; + +type ContestFormMode = "create" | "edit"; + +type ContestCreateFormProps = { + mode?: ContestFormMode; // default "create" + contest?: ContestFromApi | null; // pro edit + onCreated?: (contest: ContestFromApi) => void; + onUpdated?: (contest: ContestFromApi) => void; +}; + +type Option = { id: number; name: string; code?: string | null }; + +const buildTranslationPayload = (cs: string, en: string): TranslationPayload => { + const trimmedCs = cs.trim(); + const trimmedEn = en.trim(); + + // nic nevyplněno → prázdný objekt + if (!trimmedCs && !trimmedEn) { + return {}; + } + + // oba jazyky vyplněné → použij obě hodnoty + if (trimmedCs && trimmedEn) { + return { + cs: trimmedCs, + en: trimmedEn, + }; + } + + // vyplněný jen jeden jazyk → použij ho pro oba + const value = trimmedCs || trimmedEn; + + return { + cs: value, + en: value, + }; +}; + +const extractTranslations = ( + field: string | TranslationPayload | null | undefined +): TranslationPayload => { + if (!field) return {}; + + if (typeof field === "string") { + return { cs: field }; + } + + return field; +}; + +const normalizeStartTime = (time: string): string | undefined => { + if (!time.trim()) return undefined; + return time.length === 5 ? `${time}:00` : time; +}; + +export default function ContestCreateForm({ + mode = "create", + contest, + onCreated, + onUpdated, +}: ContestCreateFormProps) { + const { t } = useTranslation("common"); + + const isEdit = mode === "edit" && contest != null; + + const [nameCs, setNameCs] = useState(""); + const [nameEn, setNameEn] = useState(""); + const [descriptionCs, setDescriptionCs] = useState(""); + const [descriptionEn, setDescriptionEn] = useState(""); + + const [url, setUrl] = useState(""); + const [evaluator, setEvaluator] = useState(""); + const [email, setEmail] = useState(""); + const [email2, setEmail2] = useState(""); + + const [startTime, setStartTime] = useState(""); + const [durationHours, setDurationHours] = useState("24"); + const [deadlineDays, setDeadlineDays] = useState("3"); + + const [isMcr, setIsMcr] = useState(false); + const [isSixHr, setIsSixHr] = useState(false); + const [isActive, setIsActive] = useState(true); + + const [availableBands, setAvailableBands] = useState([]); + const [availableCategories, setAvailableCategories] = useState([]); + const [availablePowerCategories, setAvailablePowerCategories] = useState([]); + const [availableRuleSets, setAvailableRuleSets] = useState([]); + const [selectedRuleSetId, setSelectedRuleSetId] = useState(null); + + const [selectedBandIds, setSelectedBandIds] = useState([]); + const [selectedCategoryIds, setSelectedCategoryIds] = useState([]); + const [selectedPowerCategoryIds, setSelectedPowerCategoryIds] = useState([]); + + const [loadingOptions, setLoadingOptions] = useState(false); + + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const [submitting, setSubmitting] = useState(false); + + // načti volby pro checkboxy + useEffect(() => { + let active = true; + + (async () => { + try { + setLoadingOptions(true); + const [bandsRes, categoriesRes, powerCatsRes, ruleSetsRes] = await Promise.all([ + axios.get("/api/bands", { headers: { Accept: "application/json" } }), + axios.get("/api/categories", { headers: { Accept: "application/json" } }), + axios.get("/api/power-categories", { headers: { Accept: "application/json" } }), + axios.get("/api/evaluation-rule-sets", { headers: { Accept: "application/json" } }), + ]); + + if (!active) return; + + const normalize = (res: any): Option[] => + Array.isArray(res.data ?? res) ? (res.data ?? res) : []; + + setAvailableBands(normalize(bandsRes.data ?? bandsRes)); + setAvailableCategories(normalize(categoriesRes.data ?? categoriesRes)); + setAvailablePowerCategories(normalize(powerCatsRes.data ?? powerCatsRes)); + const normalizedRuleSets = normalize(ruleSetsRes.data ?? ruleSetsRes); + setAvailableRuleSets(normalizedRuleSets); + if (!isEdit && !selectedRuleSetId) { + const defaultRuleSet = normalizedRuleSets.find((item) => item.code === "default_vhf_compat"); + if (defaultRuleSet) { + setSelectedRuleSetId(defaultRuleSet.id); + } + } + } catch { + if (!active) return; + setError("Nepodařilo se načíst seznam pásem/kategorií."); + } finally { + if (active) setLoadingOptions(false); + } + })(); + + return () => { + active = false; + }; + }, []); + + React.useEffect(() => { + if (!isEdit || !contest) return; + + const name = extractTranslations(contest.name); + const desc = extractTranslations(contest.description ?? null); + + setNameCs(name.cs ?? ""); + setNameEn(name.en ?? ""); + setDescriptionCs(desc.cs ?? ""); + setDescriptionEn(desc.en ?? ""); + + setUrl(contest.url ?? ""); + setEvaluator(contest.evaluator ?? ""); + setEmail(contest.email ?? ""); + setEmail2(contest.email2 ?? ""); + + setStartTime(contest.start_time ?? ""); + setDurationHours(String(contest.duration ?? 24)); + setDeadlineDays(String(contest.logs_deadline_days ?? 3)); + + setIsMcr(!!contest.is_mcr); + setIsSixHr(!!contest.is_sixhr); + setIsActive(!!contest.is_active); + + setSelectedBandIds(contest.bands?.map((b) => b.id) ?? []); + setSelectedCategoryIds(contest.categories?.map((c) => c.id) ?? []); + setSelectedPowerCategoryIds(contest.power_categories?.map((p) => p.id) ?? []); + setSelectedRuleSetId(contest.rule_set_id ?? null); + + setError(null); + setSuccess(null); + }, [isEdit, contest]); + + const triggerRefresh = useContestRefreshStore((s) => s.triggerRefresh); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setError(null); + setSuccess(null); + + const namePayload = buildTranslationPayload(nameCs, nameEn); + if (Object.keys(namePayload).length === 0) { + setError("Vyplň alespoň jeden překlad názvu závodu."); + return; + } + + const descriptionPayload = buildTranslationPayload(descriptionCs, descriptionEn); + + const payload: Record = { + name: namePayload, + is_mcr: isMcr, + is_sixhr: isSixHr, + is_active: isActive, + }; + + if (Object.keys(descriptionPayload).length > 0) { + payload.description = descriptionPayload; + } + if (url.trim()) payload.url = url.trim(); + if (evaluator.trim()) payload.evaluator = evaluator.trim(); + if (email.trim()) payload.email = email.trim(); + if (email2.trim()) payload.email2 = email2.trim(); + + const normalizedStartTime = normalizeStartTime(startTime); + if (normalizedStartTime) payload.start_time = normalizedStartTime; + + const durationNumber = durationHours.trim() ? Number(durationHours) : undefined; + const deadlineNumber = deadlineDays.trim() ? Number(deadlineDays) : undefined; + + if (durationNumber !== undefined && Number.isNaN(durationNumber)) { + setError(t("Délka závodu musí být číslo.")); + return; + } + + if (deadlineNumber !== undefined && Number.isNaN(deadlineNumber)) { + setError(t("Uzávěrka logů musí být číslo.")); + return; + } + + if (durationNumber !== undefined) payload.duration = durationNumber; + if (deadlineNumber !== undefined) payload.logs_deadline_days = deadlineNumber; + + payload.band_ids = selectedBandIds; + payload.category_ids = selectedCategoryIds; + payload.power_category_ids = selectedPowerCategoryIds; + if (selectedRuleSetId) payload.rule_set_id = selectedRuleSetId; + + try { + setSubmitting(true); + await axios.get("/sanctum/csrf-cookie", { withCredentials: true }); + + let response; + + if (isEdit && contest) { + response = await axios.put(`/api/contests/${contest.id}`, payload, { + headers: { + Accept: "application/json", + }, + withCredentials: true, + withXSRFToken: true, + }); + + setSuccess("Závod byl upraven."); + triggerRefresh(); + onUpdated?.(response.data); + } else { + response = await axios.post("/api/contests", payload, { + headers: { + Accept: "application/json", + }, + withCredentials: true, + withXSRFToken: true, + }); + + setSuccess("Závod byl vytvořen."); + triggerRefresh(); + onCreated?.(response.data); + } + } catch (err) { + if (axios.isAxiosError(err)) { + const apiErrors = + err.response?.data?.errors ?? + err.response?.data?.message ?? + "Nepodařilo se uložit závod."; + setError( + typeof apiErrors === "string" + ? apiErrors + : "Nepodařilo se uložit závod." + ); + } else { + setError("Nepodařilo se uložit závod."); + } + } finally { + setSubmitting(false); + } + }; + + const toggleId = (id: number, list: number[], setter: (v: number[]) => void) => { + setter(list.includes(id) ? list.filter((x) => x !== id) : [...list, id]); + }; + + return ( +
+
+ + +
+ +
+