From fefc2c8b5cd40760b1a10da00732d776b68f5258 Mon Sep 17 00:00:00 2001 From: Franco Pellicciotti Date: Wed, 8 Jul 2026 10:43:39 -0400 Subject: [PATCH] New apps Added --- .HA_VERSION | 2 +- .../integrations/asusrouter/dark_logo.png | Bin 0 -> 7911 bytes .../brands/integrations/blink/dark_logo.png | Bin 0 -> 15466 bytes .cache/brands/integrations/blink/logo.png | Bin 0 -> 15466 bytes .../integrations/climate_scheduler/logo.png | Bin 0 -> 13114 bytes .cache/brands/integrations/generic/logo.png | Bin 0 -> 924 bytes .../logo.png | Bin 0 -> 37470 bytes .../integrations/google_travel_time/logo.png | Bin 0 -> 69515 bytes .cache/brands/integrations/hassio/logo.png | Bin 0 -> 18349 bytes .../intelligent_heating_pilot/dark_logo.png | Bin 0 -> 75889 bytes .../intelligent_heating_pilot/logo.png | Bin 0 -> 77624 bytes .../maintenance_supporter/logo.png | Bin 0 -> 65127 bytes .cache/brands/integrations/met/logo.png | Bin 0 -> 22063 bytes .../integrations/midea_ac_lan/dark_logo.png | Bin 0 -> 28356 bytes .../integrations/nest_protect/dark_logo.png | Bin 0 -> 15439 bytes .../integrations/scheduler/dark_logo.png | Bin 0 -> 1153 bytes .cache/brands/integrations/scheduler/logo.png | Bin 0 -> 1395 bytes .../integrations/waze_travel_time/logo.png | Bin 0 -> 34996 bytes .ha_run.lock | 2 +- automations.yaml | 515 +- ...Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml | 1882 ++ .../dimmer_control_4_magic_cube.yaml | 153 + configuration.yaml | 10 +- custom_components/album_slideshow/__init__.py | 431 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 19438 bytes .../__pycache__/button.cpython-314.pyc | Bin 0 -> 5073 bytes .../__pycache__/camera.cpython-314.pyc | Bin 0 -> 51685 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 0 -> 10314 bytes .../__pycache__/const.cpython-314.pyc | Bin 0 -> 2713 bytes .../__pycache__/coordinator.cpython-314.pyc | Bin 0 -> 60265 bytes .../google_scraper.cpython-314.pyc | Bin 0 -> 22822 bytes .../image_processing.cpython-314.pyc | Bin 0 -> 15148 bytes .../__pycache__/number.cpython-314.pyc | Bin 0 -> 13314 bytes .../__pycache__/playlist.cpython-314.pyc | Bin 0 -> 7426 bytes .../__pycache__/select.cpython-314.pyc | Bin 0 -> 17141 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 0 -> 10113 bytes .../__pycache__/store.cpython-314.pyc | Bin 0 -> 2632 bytes .../__pycache__/switch.cpython-314.pyc | Bin 0 -> 4658 bytes .../__pycache__/text.cpython-314.pyc | Bin 0 -> 5175 bytes .../album_slideshow/brand/logo.png | Bin 0 -> 22113 bytes custom_components/album_slideshow/button.py | 68 + custom_components/album_slideshow/camera.py | 922 + .../album_slideshow/config_flow.py | 194 + custom_components/album_slideshow/const.py | 94 + .../album_slideshow/coordinator.py | 1241 + .../album_slideshow/google_scraper.py | 472 + .../album_slideshow/image_processing.py | 277 + .../album_slideshow/manifest.json | 12 + custom_components/album_slideshow/number.py | 174 + custom_components/album_slideshow/playlist.py | 138 + custom_components/album_slideshow/select.py | 241 + custom_components/album_slideshow/sensor.py | 150 + .../album_slideshow/services.yaml | 21 + custom_components/album_slideshow/store.py | 55 + .../album_slideshow/strings.json | 45 + custom_components/album_slideshow/switch.py | 55 + custom_components/album_slideshow/text.py | 60 + .../album_slideshow/translations/en.json | 45 + .../www/album-slideshow-card.js | 1763 ++ .../climate_scheduler/__init__.py | 405 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 18783 bytes .../__pycache__/climate.cpython-314.pyc | Bin 0 -> 25540 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 0 -> 2086 bytes .../__pycache__/const.cpython-314.pyc | Bin 0 -> 629 bytes .../__pycache__/coordinator.cpython-314.pyc | Bin 0 -> 52546 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 0 -> 21428 bytes .../__pycache__/services.cpython-314.pyc | Bin 0 -> 98381 bytes .../__pycache__/storage.cpython-314.pyc | Bin 0 -> 96782 bytes .../__pycache__/switch.cpython-314.pyc | Bin 0 -> 20465 bytes .../climate_scheduler/climate.py | 481 + .../climate_scheduler/config_flow.py | 28 + custom_components/climate_scheduler/const.py | 24 + .../climate_scheduler/coordinator.py | 1127 + .../climate_scheduler/frontend/.version | 1 + .../climate_scheduler/frontend/app.js | 7579 ++++++ .../frontend/climate-dialog.js | 1065 + .../frontend/climate-scheduler-card.js | 311 + .../climate_scheduler/frontend/ha-api.js | 673 + .../frontend/keyframe-timeline.js | 2225 ++ .../climate_scheduler/frontend/panel.js | 598 + .../climate_scheduler/frontend/styles.css | 2235 ++ .../climate_scheduler/frontend/utils.js | 185 + .../climate_scheduler/manifest.json | 20 + custom_components/climate_scheduler/sensor.py | 452 + .../climate_scheduler/services.py | 1909 ++ .../climate_scheduler/services.yaml | 499 + .../climate_scheduler/storage.py | 1882 ++ custom_components/climate_scheduler/switch.py | 421 + .../climate_scheduler/translations/en.json | 151 + .../climate_scheduler/translations/sk.json | 151 + .../intelligent_heating_pilot/__init__.py | 446 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 22086 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 0 -> 20167 bytes .../__pycache__/const.cpython-314.pyc | Bin 0 -> 3900 bytes .../heating_application.cpython-314.pyc | Bin 0 -> 33616 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 0 -> 35333 bytes .../__pycache__/switch.cpython-314.pyc | Bin 0 -> 5151 bytes .../__pycache__/view.cpython-314.pyc | Bin 0 -> 1537 bytes .../application/__init__.py | 7 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 364 bytes ...ng_cycle_lifecycle_manager.cpython-314.pyc | Bin 0 -> 67586 bytes ..._lifecycle_manager_factory.cpython-314.pyc | Bin 0 -> 8684 bytes .../lhs_lifecycle_manager.cpython-314.pyc | Bin 0 -> 34228 bytes ..._lifecycle_manager_factory.cpython-314.pyc | Bin 0 -> 5081 bytes .../__pycache__/orchestrator.cpython-314.pyc | Bin 0 -> 8726 bytes .../heating_cycle_lifecycle_manager.py | 1458 + ...heating_cycle_lifecycle_manager_factory.py | 191 + .../application/lhs_lifecycle_manager.py | 752 + .../lhs_lifecycle_manager_factory.py | 113 + .../application/orchestrator.py | 185 + .../application/use_cases/__init__.py | 24 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 1026 bytes ...late_anticipation_use_case.cpython-314.pyc | Bin 0 -> 12404 bytes ...ck_overshoot_risk_use_case.cpython-314.pyc | Bin 0 -> 6434 bytes ...ontrol_preheating_use_case.cpython-314.pyc | Bin 0 -> 6176 bytes ...ticipation_action_use_case.cpython-314.pyc | Bin 0 -> 17739 bytes ...update_cache_data_use_case.cpython-314.pyc | Bin 0 -> 6891 bytes .../calculate_anticipation_use_case.py | 314 + .../check_overshoot_risk_use_case.py | 133 + .../use_cases/control_preheating_use_case.py | 137 + .../schedule_anticipation_action_use_case.py | 381 + .../use_cases/update_cache_data_use_case.py | 156 + .../intelligent_heating_pilot/config_flow.py | 583 + .../intelligent_heating_pilot/const.py | 103 + .../domain/__init__.py | 10 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 540 bytes .../__pycache__/constants.cpython-314.pyc | Bin 0 -> 1136 bytes .../domain/constants.py | 33 + .../domain/entities/__init__.py | 9 + .../domain/entities/heating_pilot.py | 96 + .../domain/interfaces/__init__.py | 35 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 1364 bytes ...mate_data_reader_interface.cpython-314.pyc | Bin 0 -> 3831 bytes .../context_reader_interface.cpython-314.pyc | Bin 0 -> 2676 bytes ...ecision_strategy_interface.cpython-314.pyc | Bin 0 -> 2281 bytes ...ce_config_reader_interface.cpython-314.pyc | Bin 0 -> 7643 bytes ...attribute_mapper_interface.cpython-314.pyc | Bin 0 -> 3422 bytes ...vironment_reader_interface.cpython-314.pyc | Bin 0 -> 2506 bytes ...ng_cycle_service_interface.cpython-314.pyc | Bin 0 -> 2049 bytes ...ng_cycle_storage_interface.cpython-314.pyc | Bin 0 -> 6006 bytes ...cal_data_adapter_interface.cpython-314.pyc | Bin 0 -> 3656 bytes .../lhs_storage_interface.cpython-314.pyc | Bin 0 -> 5859 bytes ...eduler_commander_interface.cpython-314.pyc | Bin 0 -> 2061 bytes ...scheduler_reader_interface.cpython-314.pyc | Bin 0 -> 2004 bytes .../timer_scheduler.cpython-314.pyc | Bin 0 -> 1764 bytes .../climate_data_reader_interface.py | 72 + .../interfaces/context_reader_interface.py | 39 + .../interfaces/decision_strategy_interface.py | 52 + .../device_config_reader_interface.py | 154 + .../entity_attribute_mapper_interface.py | 80 + .../environment_reader_interface.py | 52 + .../heating_cycle_service_interface.py | 37 + .../heating_cycle_storage_interface.py | 131 + .../historical_data_adapter_interface.py | 78 + .../interfaces/lhs_storage_interface.py | 109 + .../scheduler_commander_interface.py | 37 + .../interfaces/scheduler_reader_interface.py | 38 + .../domain/interfaces/timer_scheduler.py | 36 + .../domain/services/__init__.py | 17 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 726 bytes ...ual_lhs_calculator_service.cpython-314.pyc | Bin 0 -> 6306 bytes ...d_time_calculation_service.cpython-314.pyc | Bin 0 -> 3138 bytes ...tion_date_range_calculator.cpython-314.pyc | Bin 0 -> 4927 bytes ...bal_lhs_calculator_service.cpython-314.pyc | Bin 0 -> 2927 bytes .../heating_cycle_service.cpython-314.pyc | Bin 0 -> 37351 bytes .../prediction_service.cpython-314.pyc | Bin 0 -> 7729 bytes .../contextual_lhs_calculator_service.py | 134 + .../services/dead_time_calculation_service.py | 66 + .../extraction_date_range_calculator.py | 115 + .../services/global_lhs_calculator_service.py | 69 + .../domain/services/heating_cycle_service.py | 1022 + .../domain/services/ml_decision_strategy.py | 159 + .../domain/services/prediction_service.py | 248 + .../services/simple_decision_strategy.py | 201 + .../domain/value_objects/__init__.py | 32 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 1110 bytes .../entity_attribute_mapping.cpython-314.pyc | Bin 0 -> 7067 bytes .../environment_state.cpython-314.pyc | Bin 0 -> 2682 bytes .../__pycache__/heating.cpython-314.pyc | Bin 0 -> 11910 bytes .../heating_cycle_cache_data.cpython-314.pyc | Bin 0 -> 5678 bytes .../historical_data.cpython-314.pyc | Bin 0 -> 2749 bytes .../lhs_cache_entry.cpython-314.pyc | Bin 0 -> 1424 bytes .../prediction_result.cpython-314.pyc | Bin 0 -> 2262 bytes .../recording_extraction_task.cpython-314.pyc | Bin 0 -> 3487 bytes .../scheduled_timeslot.cpython-314.pyc | Bin 0 -> 1848 bytes .../__pycache__/slope_data.cpython-314.pyc | Bin 0 -> 1762 bytes .../value_objects/contextual_lhs_data.py | 55 + .../value_objects/entity_attribute_mapping.py | 160 + .../domain/value_objects/environment_state.py | 49 + .../domain/value_objects/heating.py | 169 + .../value_objects/heating_cycle_cache_data.py | 101 + .../domain/value_objects/historical_data.py | 58 + .../domain/value_objects/lhs_cache_entry.py | 24 + .../domain/value_objects/prediction_result.py | 42 + .../recording_extraction_task.py | 58 + .../value_objects/scheduled_timeslot.py | 32 + .../domain/value_objects/slope_data.py | 32 + .../heating_application.py | 740 + .../infrastructure/__init__.py | 5 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 390 bytes .../__pycache__/event_bridge.cpython-314.pyc | Bin 0 -> 21524 bytes .../recorder_queue.cpython-314.pyc | Bin 0 -> 4860 bytes .../__pycache__/vtherm_compat.cpython-314.pyc | Bin 0 -> 2793 bytes .../infrastructure/adapters/__init__.py | 33 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 1228 bytes ...se_entity_attribute_mapper.cpython-314.pyc | Bin 0 -> 8612 bytes .../base_ha_storage.cpython-314.pyc | Bin 0 -> 7555 bytes .../climate_commander.cpython-314.pyc | Bin 0 -> 4386 bytes .../climate_data_reader.cpython-314.pyc | Bin 0 -> 21313 bytes .../context_reader.cpython-314.pyc | Bin 0 -> 3677 bytes .../device_config_reader.cpython-314.pyc | Bin 0 -> 9571 bytes ..._attribute_mapper_registry.cpython-314.pyc | Bin 0 -> 6217 bytes .../environment_reader.cpython-314.pyc | Bin 0 -> 5651 bytes ...c_climate_attribute_mapper.cpython-314.pyc | Bin 0 -> 2731 bytes .../heating_cycle_storage.cpython-314.pyc | Bin 0 -> 22657 bytes .../__pycache__/lhs_storage.cpython-314.pyc | Bin 0 -> 14681 bytes ...recording_extraction_queue.cpython-314.pyc | Bin 0 -> 18295 bytes .../scheduler_commander.cpython-314.pyc | Bin 0 -> 5830 bytes .../scheduler_reader.cpython-314.pyc | Bin 0 -> 22442 bytes .../sensor_data_reader.cpython-314.pyc | Bin 0 -> 8913 bytes .../timer_scheduler.cpython-314.pyc | Bin 0 -> 3297 bytes .../__pycache__/utils.cpython-314.pyc | Bin 0 -> 1181 bytes .../vtherm_attribute_mapper.cpython-314.pyc | Bin 0 -> 3095 bytes .../weather_data_reader.cpython-314.pyc | Bin 0 -> 9689 bytes .../adapters/base_entity_attribute_mapper.py | 213 + .../adapters/base_ha_storage.py | 160 + .../adapters/climate_commander.py | 87 + .../adapters/climate_data_reader.py | 565 + .../infrastructure/adapters/context_reader.py | 56 + .../adapters/device_config_reader.py | 217 + .../entity_attribute_mapper_registry.py | 154 + .../adapters/entity_attribute_validator.py | 135 + .../adapters/environment_reader.py | 130 + .../generic_climate_attribute_mapper.py | 81 + .../adapters/heating_cycle_storage.py | 502 + .../infrastructure/adapters/lhs_storage.py | 287 + .../adapters/recording_extraction_queue.py | 406 + .../adapters/scheduler_commander.py | 139 + .../adapters/scheduler_reader.py | 538 + .../adapters/sensor_data_reader.py | 241 + .../adapters/timer_scheduler.py | 68 + .../infrastructure/adapters/utils.py | 23 + .../adapters/vtherm_attribute_mapper.py | 109 + .../adapters/weather_data_reader.py | 263 + .../decision_strategy_factory.py | 77 + .../infrastructure/event_bridge.py | 457 + .../infrastructure/recorder_queue.py | 104 + .../infrastructure/rest_api.py | 13 + .../infrastructure/vtherm_compat.py | 82 + .../intelligent_heating_pilot/logo.png | Bin 0 -> 262591 bytes .../intelligent_heating_pilot/manifest.json | 15 + .../intelligent_heating_pilot/py.typed | 0 .../intelligent_heating_pilot/sensor.py | 639 + .../intelligent_heating_pilot/services.yaml | 47 + .../intelligent_heating_pilot/strings.json | 81 + .../intelligent_heating_pilot/switch.py | 82 + .../translations/en.json | 62 + .../translations/fr.json | 73 + .../utils/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 168 bytes .../config_helpers.cpython-314.pyc | Bin 0 -> 1236 bytes .../utils/config_helpers.py | 24 + .../intelligent_heating_pilot/view.py | 33 + .../maintenance_supporter/__init__.py | 1448 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 73153 bytes .../__pycache__/binary_sensor.cpython-314.pyc | Bin 0 -> 9157 bytes .../__pycache__/button.cpython-314.pyc | Bin 0 -> 7122 bytes .../__pycache__/calendar.cpython-314.pyc | Bin 0 -> 23835 bytes .../__pycache__/condition.cpython-314.pyc | Bin 0 -> 2192 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 0 -> 60016 bytes .../config_flow_helpers.cpython-314.pyc | Bin 0 -> 12244 bytes ...config_flow_options_global.cpython-314.pyc | Bin 0 -> 41821 bytes .../config_flow_trigger.cpython-314.pyc | Bin 0 -> 48044 bytes .../__pycache__/const.cpython-314.pyc | Bin 0 -> 16689 bytes .../__pycache__/coordinator.cpython-314.pyc | Bin 0 -> 60917 bytes .../__pycache__/diagnostics.cpython-314.pyc | Bin 0 -> 10306 bytes .../__pycache__/logbook.cpython-314.pyc | Bin 0 -> 12851 bytes .../__pycache__/panel.cpython-314.pyc | Bin 0 -> 5123 bytes .../__pycache__/repairs.cpython-314.pyc | Bin 0 -> 34819 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 0 -> 32074 bytes .../__pycache__/storage.cpython-314.pyc | Bin 0 -> 20932 bytes .../__pycache__/templates.cpython-314.pyc | Bin 0 -> 9371 bytes .../templates_i18n.cpython-314.pyc | Bin 0 -> 45580 bytes .../__pycache__/todo.cpython-314.pyc | Bin 0 -> 9384 bytes .../__pycache__/trigger.cpython-314.pyc | Bin 0 -> 2954 bytes .../__pycache__/views.cpython-314.pyc | Bin 0 -> 16783 bytes .../maintenance_supporter/binary_sensor.py | 175 + .../maintenance_supporter/button.py | 122 + .../maintenance_supporter/calendar.py | 531 + .../maintenance_supporter/condition.py | 46 + .../maintenance_supporter/conditions.yaml | 23 + .../maintenance_supporter/config_flow.py | 1060 + .../config_flow_helpers.py | 208 + .../config_flow_options.py | 13 + .../config_flow_options_global.py | 835 + .../config_flow_options_task.py | 30 + .../config_flow_options_task_adaptive.py | 182 + .../config_flow_options_task_add.py | 312 + .../config_flow_options_task_base.py | 203 + .../config_flow_options_task_crud.py | 531 + .../config_flow_options_task_object.py | 141 + .../config_flow_options_task_trigger.py | 437 + .../config_flow_trigger.py | 1102 + .../maintenance_supporter/const.py | 577 + .../maintenance_supporter/coordinator.py | 1273 + .../maintenance_supporter/diagnostics.py | 220 + .../maintenance_supporter/entity/__init__.py | 7 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 365 bytes .../__pycache__/entity_base.cpython-314.pyc | Bin 0 -> 5235 bytes .../summary_coordinator.cpython-314.pyc | Bin 0 -> 7694 bytes .../entity/entity_base.py | 89 + .../entity/summary_coordinator.py | 103 + .../entity/triggers/__init__.py | 189 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 8303 bytes .../__pycache__/base_trigger.cpython-314.pyc | Bin 0 -> 14559 bytes .../__pycache__/compound.cpython-314.pyc | Bin 0 -> 16033 bytes .../__pycache__/counter.cpython-314.pyc | Bin 0 -> 8148 bytes .../__pycache__/runtime.cpython-314.pyc | Bin 0 -> 16850 bytes .../__pycache__/state_change.cpython-314.pyc | Bin 0 -> 9768 bytes .../__pycache__/threshold.cpython-314.pyc | Bin 0 -> 9628 bytes .../entity/triggers/base_trigger.py | 309 + .../entity/triggers/compound.py | 282 + .../entity/triggers/counter.py | 161 + .../entity/triggers/runtime.py | 324 + .../entity/triggers/state_change.py | 200 + .../entity/triggers/threshold.py | 176 + .../maintenance_supporter/export.py | 232 + .../frontend-src/__tests__/_panel-utils.ts | 105 + .../frontend-src/__tests__/_test-utils.ts | 171 + .../__tests__/calendar-bucket.test.ts | 465 + .../__tests__/calendar-card.test.ts | 120 + .../__tests__/card-sort-due-date.test.ts | 86 + .../__tests__/complete-dialog.test.ts | 169 + .../dialog-no-lazy-load-elements.test.ts | 164 + .../__tests__/documents-section.test.ts | 216 + .../frontend-src/__tests__/download.test.ts | 61 + .../__tests__/filter-outliers.test.ts | 39 + .../__tests__/format-recurrence.test.ts | 66 + .../__tests__/group-dialog-sort.test.ts | 109 + .../__tests__/i18n-loader.test.ts | 63 + .../frontend-src/__tests__/interval.test.ts | 47 + .../__tests__/ll-custom-payload.test.ts | 85 + .../__tests__/object-columns.test.ts | 54 + .../__tests__/panel-deeplink.test.ts | 111 + .../__tests__/panel-shell.test.ts | 188 + .../settings-view-notify-picker.test.ts | 97 + .../__tests__/settings-view-print-qr.test.ts | 182 + .../__tests__/settings-view-vacation.test.ts | 147 + .../__tests__/storage-section-card.test.ts | 117 + .../__tests__/task-detail-renderer.test.ts | 211 + .../task-dialog-calendar-kinds.test.ts | 126 + .../task-dialog-completion-actions.test.ts | 280 + .../__tests__/task-dialog-compound.test.ts | 97 + .../task-dialog-interval-hydration.test.ts | 86 + .../task-dialog-recovery-flag.test.ts | 87 + .../__tests__/task-documents.test.ts | 125 + .../__tests__/trigger-chart.test.ts | 120 + .../__tests__/trigger-section.test.ts | 106 + .../__tests__/vacation-section-card.test.ts | 107 + .../__tests__/virtual-window.test.ts | 90 + .../frontend-src/__tests__/warranty.test.ts | 61 + .../frontend-src/calendar-styles.ts | 204 + .../capture-readme-screenshots.mjs | 547 + .../capture-v1041-screenshots.mjs | 250 + .../frontend-src/capture-v131-screenshots.mjs | 299 + .../frontend-src/capture-v14-screenshots.mjs | 259 + .../components/budget-section-card.ts | 271 + .../components/complete-dialog.ts | 396 + .../frontend-src/components/confirm-dialog.ts | 170 + .../components/documents-section.ts | 643 + .../frontend-src/components/group-dialog.ts | 244 + .../components/groups-section-card.ts | 340 + .../components/history-edit-dialog.ts | 259 + .../frontend-src/components/history-photo.ts | 74 + .../frontend-src/components/ms-textfield.ts | 116 + .../frontend-src/components/object-dialog.ts | 289 + .../components/object-quick-actions-dialog.ts | 320 + .../frontend-src/components/qr-dialog.ts | 413 + .../components/seasonal-overrides-dialog.ts | 199 + .../components/section-card-shared-styles.ts | 77 + .../frontend-src/components/settings-view.ts | 1782 ++ .../components/storage-section-card.ts | 361 + .../frontend-src/components/task-dialog.ts | 1791 ++ .../frontend-src/components/task-documents.ts | 339 + .../components/task-quick-actions-dialog.ts | 815 + .../frontend-src/components/trigger-chart.ts | 382 + .../components/vacation-section-card.ts | 327 + .../frontend-src/deeplink_test.mjs | 50 + .../frontend-src/dialog-mount.ts | 300 + .../frontend-src/e2e-checklist-preview.mjs | 116 + .../frontend-src/e2e-checklist-visibility.mjs | 178 + .../frontend-src/e2e-error-messages.mjs | 98 + .../frontend-src/e2e-helpers.mjs | 127 + .../frontend-src/e2e-issues-30-31.mjs | 292 + .../frontend-src/e2e-new-features.mjs | 292 + .../frontend-src/e2e-schedule-time.mjs | 143 + .../frontend-src/e2e-test.mjs | 197 + .../frontend-src/esbuild.mjs | 93 + .../frontend-src/helpers/calendar-bucket.ts | 387 + .../frontend-src/helpers/download.ts | 50 + .../frontend-src/helpers/format-bytes.ts | 7 + .../frontend-src/helpers/interval.ts | 41 + .../frontend-src/helpers/object-columns.ts | 65 + .../frontend-src/helpers/report.ts | 110 + .../frontend-src/helpers/virtual-window.ts | 69 + .../frontend-src/helpers/warranty.ts | 41 + .../frontend-src/helpers/worksheet.ts | 157 + .../frontend-src/locales/cs.json | 696 + .../frontend-src/locales/da.json | 696 + .../frontend-src/locales/de.json | 696 + .../frontend-src/locales/en.json | 696 + .../frontend-src/locales/es.json | 696 + .../frontend-src/locales/fi.json | 696 + .../frontend-src/locales/fr.json | 696 + .../frontend-src/locales/hi.json | 696 + .../frontend-src/locales/it.json | 696 + .../frontend-src/locales/ja.json | 696 + .../frontend-src/locales/nb.json | 696 + .../frontend-src/locales/nl.json | 696 + .../frontend-src/locales/pl.json | 696 + .../frontend-src/locales/pt.json | 696 + .../frontend-src/locales/ru.json | 696 + .../frontend-src/locales/sv.json | 696 + .../frontend-src/locales/uk.json | 696 + .../frontend-src/locales/zh.json | 696 + .../frontend-src/maintenance-calendar-card.ts | 586 + .../frontend-src/maintenance-card-editor.ts | 278 + .../frontend-src/maintenance-card.ts | 419 + .../maintenance-dashboard-strategy.ts | 1174 + .../frontend-src/maintenance-panel.ts | 2903 ++ .../frontend-src/maintenance-strategy-shim.ts | 348 + .../frontend-src/package-lock.json | 5611 ++++ .../frontend-src/package.json | 27 + .../frontend-src/panel-styles.ts | 1393 + .../frontend-src/qr_test.mjs | 106 + .../frontend-src/renderers/chart-utils.ts | 90 + .../frontend-src/renderers/charts.ts | 149 + .../frontend-src/renderers/history.ts | 122 + .../frontend-src/renderers/prediction.ts | 60 + .../frontend-src/renderers/progress.ts | 189 + .../frontend-src/renderers/recommendation.ts | 49 + .../frontend-src/renderers/seasonal.ts | 111 + .../frontend-src/renderers/sparkline.ts | 361 + .../frontend-src/renderers/task-detail.ts | 442 + .../frontend-src/renderers/weibull.ts | 187 + .../frontend-src/screenshot-section-cards.mjs | 251 + .../frontend-src/screenshot.mjs | 88 + .../frontend-src/statistics-service.ts | 223 + .../frontend-src/styles.ts | 1393 + .../frontend-src/test-entity-attributes.mjs | 112 + .../frontend-src/tsconfig.json | 19 + .../frontend-src/types.ts | 421 + .../frontend-src/user-service.ts | 125 + .../frontend-src/verify-history-edit-e2e.mjs | 275 + .../frontend-src/verify-kpi-click.mjs | 224 + .../verify-section-cards-write.mjs | 116 + .../frontend-src/verify-strategy-picker.mjs | 153 + .../frontend-src/verify-strategy.mjs | 382 + .../frontend-src/web-test-runner.config.mjs | 41 + .../frontend-src/ws-errors.ts | 160 + .../frontend/__init__.py | 105 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 3742 bytes .../frontend/locales/cs.json | 696 + .../frontend/locales/da.json | 696 + .../frontend/locales/de.json | 696 + .../frontend/locales/en.json | 696 + .../frontend/locales/es.json | 696 + .../frontend/locales/fi.json | 696 + .../frontend/locales/fr.json | 696 + .../frontend/locales/hi.json | 696 + .../frontend/locales/it.json | 696 + .../frontend/locales/ja.json | 696 + .../frontend/locales/nb.json | 696 + .../frontend/locales/nl.json | 696 + .../frontend/locales/pl.json | 696 + .../frontend/locales/pt.json | 696 + .../frontend/locales/ru.json | 696 + .../frontend/locales/sv.json | 696 + .../frontend/locales/uk.json | 696 + .../frontend/locales/zh.json | 696 + .../frontend/maintenance-calendar-card.js | 1514 ++ .../frontend/maintenance-card.js | 3676 +++ .../frontend/maintenance-panel.js | 7779 ++++++ .../frontend/maintenance-strategy-shim.js | 1 + .../chunks/budget-section-card-V4BTQUYP.js | 125 + .../strategy/chunks/chunk-2LLDFX26.js | 1171 + .../strategy/chunks/chunk-6EEEWEML.js | 59 + .../strategy/chunks/chunk-LO2NM3CE.js | 1 + .../strategy/chunks/dialog-mount-RTTXPFO3.js | 2203 ++ .../chunks/groups-section-card-U36TKQBF.js | 136 + .../chunks/vacation-section-card-YFJ6JUPY.js | 121 + .../maintenance-dashboard-strategy.js | 24 + .../frontend/vendor/pdf.min.mjs | 21 + .../frontend/vendor/pdf.worker.min.mjs | 21 + .../maintenance_supporter/helpers/__init__.py | 3 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 285 bytes .../action_listener.cpython-314.pyc | Bin 0 -> 6226 bytes .../__pycache__/aggregate.cpython-314.pyc | Bin 0 -> 5113 bytes .../helpers/__pycache__/dates.cpython-314.pyc | Bin 0 -> 9812 bytes .../__pycache__/documents.cpython-314.pyc | Bin 0 -> 30574 bytes .../entity_analyzer.cpython-314.pyc | Bin 0 -> 9598 bytes .../__pycache__/entity_rename.cpython-314.pyc | Bin 0 -> 8934 bytes .../global_options.cpython-314.pyc | Bin 0 -> 3716 bytes .../helpers/__pycache__/i18n.cpython-314.pyc | Bin 0 -> 1337 bytes .../notification_manager.cpython-314.pyc | Bin 0 -> 62232 bytes .../notify_targets.cpython-314.pyc | Bin 0 -> 2701 bytes .../helpers/__pycache__/pause.cpython-314.pyc | Bin 0 -> 4217 bytes .../__pycache__/permissions.cpython-314.pyc | Bin 0 -> 5960 bytes .../__pycache__/qr_generator.cpython-314.pyc | Bin 0 -> 8279 bytes .../__pycache__/qrcodegen.cpython-314.pyc | Bin 0 -> 50344 bytes .../__pycache__/retention.cpython-314.pyc | Bin 0 -> 10544 bytes .../__pycache__/sanitize.cpython-314.pyc | Bin 0 -> 13146 bytes .../__pycache__/schedule.cpython-314.pyc | Bin 0 -> 21261 bytes .../settings_registry.cpython-314.pyc | Bin 0 -> 7903 bytes .../__pycache__/status.cpython-314.pyc | Bin 0 -> 2481 bytes .../__pycache__/task_fields.cpython-314.pyc | Bin 0 -> 2021 bytes .../threshold_calculator.cpython-314.pyc | Bin 0 -> 6238 bytes .../__pycache__/vacation.cpython-314.pyc | Bin 0 -> 16497 bytes .../__pycache__/workday.cpython-314.pyc | Bin 0 -> 8654 bytes .../helpers/action_listener.py | 110 + .../helpers/aggregate.py | 85 + .../helpers/csv_handler.py | 326 + .../maintenance_supporter/helpers/dates.py | 177 + .../helpers/documents.py | 513 + .../helpers/entity_analyzer.py | 213 + .../helpers/entity_attributes.py | 239 + .../helpers/entity_rename.py | 175 + .../helpers/global_options.py | 69 + .../maintenance_supporter/helpers/i18n.py | 19 + .../helpers/interval_analyzer.py | 687 + .../helpers/notification_manager.py | 1308 + .../helpers/notify_targets.py | 39 + .../maintenance_supporter/helpers/pause.py | 79 + .../helpers/permissions.py | 94 + .../helpers/qr_generator.py | 171 + .../helpers/qrcodegen.py | 700 + .../helpers/retention.py | 206 + .../maintenance_supporter/helpers/sanitize.py | 320 + .../maintenance_supporter/helpers/schedule.py | 444 + .../helpers/sensor_predictor.py | 637 + .../helpers/settings_registry.py | 176 + .../maintenance_supporter/helpers/status.py | 50 + .../helpers/task_fields.py | 44 + .../helpers/threshold_calculator.py | 131 + .../helpers/trigger_fallback.py | 201 + .../maintenance_supporter/helpers/vacation.py | 325 + .../maintenance_supporter/helpers/workday.py | 166 + .../maintenance_supporter/icons.json | 20 + .../maintenance_supporter/logbook.py | 271 + .../maintenance_supporter/manifest.json | 26 + .../maintenance_supporter/models/__init__.py | 14 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 529 bytes .../maintenance_object.cpython-314.pyc | Bin 0 -> 5037 bytes .../maintenance_task.cpython-314.pyc | Bin 0 -> 26475 bytes .../maintenance_type.cpython-314.pyc | Bin 0 -> 3085 bytes .../models/maintenance_object.py | 95 + .../models/maintenance_task.py | 546 + .../models/maintenance_type.py | 86 + .../maintenance_supporter/panel.py | 89 + .../maintenance_supporter/py.typed | 0 .../maintenance_supporter/quality_scale.yaml | 153 + .../maintenance_supporter/repairs.py | 648 + .../maintenance_supporter/sensor.py | 599 + .../maintenance_supporter/services.yaml | 347 + .../maintenance_supporter/storage.py | 390 + .../maintenance_supporter/strings.json | 1726 ++ .../maintenance_supporter/templates.py | 286 + .../maintenance_supporter/templates_i18n.py | 1071 + .../maintenance_supporter/todo.py | 173 + .../translations/cs.json | 1726 ++ .../translations/da.json | 1726 ++ .../translations/de.json | 1726 ++ .../translations/en.json | 1726 ++ .../translations/es.json | 1726 ++ .../translations/fi.json | 1726 ++ .../translations/fr.json | 1726 ++ .../translations/hi.json | 1726 ++ .../translations/it.json | 1726 ++ .../translations/ja.json | 1726 ++ .../translations/nb.json | 1726 ++ .../translations/nl.json | 1726 ++ .../translations/pl.json | 1726 ++ .../translations/pt.json | 1726 ++ .../translations/ru.json | 1726 ++ .../translations/sv.json | 1726 ++ .../translations/uk.json | 1726 ++ .../translations/zh-Hans.json | 1726 ++ .../maintenance_supporter/trigger.py | 56 + .../maintenance_supporter/triggers.yaml | 23 + .../maintenance_supporter/views.py | 251 + .../websocket/__init__.py | 519 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 23599 bytes .../__pycache__/analysis.cpython-314.pyc | Bin 0 -> 12878 bytes .../__pycache__/dashboard.cpython-314.pyc | Bin 0 -> 25933 bytes .../__pycache__/documents.cpython-314.pyc | Bin 0 -> 13158 bytes .../__pycache__/groups.cpython-314.pyc | Bin 0 -> 8318 bytes .../websocket/__pycache__/io.cpython-314.pyc | Bin 0 -> 29225 bytes .../__pycache__/objects.cpython-314.pyc | Bin 0 -> 45695 bytes .../__pycache__/tags.cpython-314.pyc | Bin 0 -> 2930 bytes .../__pycache__/tasks.cpython-314.pyc | Bin 0 -> 1723 bytes .../__pycache__/tasks_actions.cpython-314.pyc | Bin 0 -> 14844 bytes .../__pycache__/tasks_crud.cpython-314.pyc | Bin 0 -> 34236 bytes .../__pycache__/tasks_history.cpython-314.pyc | Bin 0 -> 6477 bytes .../tasks_lifecycle.cpython-314.pyc | Bin 0 -> 9040 bytes .../__pycache__/tasks_persist.cpython-314.pyc | Bin 0 -> 8441 bytes .../tasks_validation.cpython-314.pyc | Bin 0 -> 10170 bytes .../__pycache__/users.cpython-314.pyc | Bin 0 -> 7375 bytes .../__pycache__/vacation.cpython-314.pyc | Bin 0 -> 11145 bytes .../websocket/analysis.py | 271 + .../websocket/dashboard.py | 593 + .../websocket/documents.py | 248 + .../maintenance_supporter/websocket/groups.py | 181 + .../maintenance_supporter/websocket/io.py | 690 + .../websocket/objects.py | 929 + .../maintenance_supporter/websocket/tags.py | 52 + .../maintenance_supporter/websocket/tasks.py | 72 + .../websocket/tasks_actions.py | 280 + .../websocket/tasks_crud.py | 682 + .../websocket/tasks_history.py | 161 + .../websocket/tasks_lifecycle.py | 190 + .../websocket/tasks_persist.py | 194 + .../websocket/tasks_validation.py | 249 + .../maintenance_supporter/websocket/users.py | 152 + .../websocket/vacation.py | 234 + .../__pycache__/__init__.cpython-314.pyc | Bin 15758 -> 15758 bytes .../__pycache__/binary_sensor.cpython-314.pyc | Bin 3553 -> 3553 bytes .../__pycache__/climate.cpython-314.pyc | Bin 45470 -> 45415 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 45427 -> 45427 bytes .../__pycache__/const.cpython-314.pyc | Bin 1461 -> 1461 bytes .../__pycache__/diagnostics.cpython-314.pyc | Bin 1221 -> 1221 bytes .../__pycache__/fan.cpython-314.pyc | Bin 23228 -> 23228 bytes .../__pycache__/humidifier.cpython-314.pyc | Bin 10982 -> 10883 bytes .../__pycache__/light.cpython-314.pyc | Bin 13319 -> 13319 bytes .../__pycache__/lock.cpython-314.pyc | Bin 4184 -> 4184 bytes .../__pycache__/midea_devices.cpython-314.pyc | Bin 75864 -> 79861 bytes .../__pycache__/midea_entity.cpython-314.pyc | Bin 6815 -> 6815 bytes .../__pycache__/number.cpython-314.pyc | Bin 6728 -> 6728 bytes .../__pycache__/select.cpython-314.pyc | Bin 4548 -> 4548 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 5042 -> 5042 bytes .../__pycache__/switch.cpython-314.pyc | Bin 3858 -> 3858 bytes .../__pycache__/water_heater.cpython-314.pyc | Bin 32610 -> 32555 bytes custom_components/midea_ac_lan/climate.py | 4 +- custom_components/midea_ac_lan/humidifier.py | 4 +- custom_components/midea_ac_lan/manifest.json | 4 +- .../midea_ac_lan/midea_devices.py | 148 +- .../midea_ac_lan/translations/de.json | 12 + .../midea_ac_lan/translations/en.json | 60 + .../midea_ac_lan/translations/es.json | 12 + .../midea_ac_lan/translations/fr.json | 12 + .../midea_ac_lan/translations/hu.json | 12 + .../midea_ac_lan/translations/ru.json | 12 + .../midea_ac_lan/translations/sk.json | 12 + .../midea_ac_lan/translations/zh-Hans.json | 15 + .../midea_ac_lan/water_heater.py | 4 +- custom_components/pirateweather/__init__.py | 213 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 9059 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 0 -> 15130 bytes .../__pycache__/const.cpython-314.pyc | Bin 0 -> 10017 bytes .../forecast_models.cpython-314.pyc | Bin 0 -> 11110 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 0 -> 42319 bytes .../__pycache__/weather.cpython-314.pyc | Bin 0 -> 23963 bytes ...weather_update_coordinator.cpython-314.pyc | Bin 0 -> 5760 bytes .../pirateweather/config_flow.py | 335 + custom_components/pirateweather/const.py | 358 + .../pirateweather/forecast_models.py | 188 + custom_components/pirateweather/manifest.json | 14 + custom_components/pirateweather/sensor.py | 1454 + .../pirateweather/translations/cs.json | 64 + .../pirateweather/translations/de.json | 65 + .../pirateweather/translations/en.json | 65 + .../pirateweather/translations/fr.json | 65 + .../pirateweather/translations/nl.json | 65 + .../pirateweather/translations/pl.json | 64 + .../pirateweather/translations/sk.json | 64 + custom_components/pirateweather/weather.py | 471 + .../weather_update_coordinator.py | 115 + custom_components/powercalc/__init__.py | 10 +- .../__pycache__/__init__.cpython-314.pyc | Bin 31644 -> 31641 bytes .../__pycache__/common.cpython-314.pyc | Bin 8201 -> 8995 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 42191 -> 43568 bytes .../__pycache__/const.cpython-314.pyc | Bin 13907 -> 14126 bytes .../device_binding.cpython-314.pyc | Bin 6351 -> 6351 bytes .../__pycache__/diagnostics.cpython-314.pyc | Bin 3506 -> 3506 bytes .../__pycache__/discovery.cpython-314.pyc | Bin 37320 -> 37319 bytes .../__pycache__/errors.cpython-314.pyc | Bin 3964 -> 3964 bytes .../__pycache__/helpers.cpython-314.pyc | Bin 18753 -> 18753 bytes .../__pycache__/migrate.cpython-314.pyc | Bin 10884 -> 10884 bytes .../__pycache__/repairs.cpython-314.pyc | Bin 4675 -> 4675 bytes .../__pycache__/select.cpython-314.pyc | Bin 4956 -> 4956 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 54751 -> 54751 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 156 -> 156 bytes .../__pycache__/analytics.cpython-314.pyc | Bin 17889 -> 17889 bytes custom_components/powercalc/common.py | 42 +- custom_components/powercalc/config_flow.py | 68 +- .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 160 bytes .../__pycache__/global_config.cpython-314.pyc | Bin 5216 -> 5216 bytes custom_components/powercalc/const.py | 4 + custom_components/powercalc/discovery.py | 1 - .../__pycache__/__init__.cpython-314.pyc | Bin 153 -> 153 bytes .../__pycache__/outlier.cpython-314.pyc | Bin 3439 -> 3439 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 158 -> 158 bytes .../__pycache__/common.cpython-314.pyc | Bin 9526 -> 9526 bytes .../dynamic_field_builder.cpython-314.pyc | Bin 2253 -> 2253 bytes .../profile_preview.cpython-314.pyc | Bin 11278 -> 11278 bytes .../__pycache__/schema.cpython-314.pyc | Bin 5206 -> 5206 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 164 -> 164 bytes .../__pycache__/daily_energy.cpython-314.pyc | Bin 8719 -> 8719 bytes .../global_configuration.cpython-314.pyc | Bin 17944 -> 19125 bytes .../flows/__pycache__/group.cpython-314.pyc | Bin 29408 -> 29408 bytes .../flows/__pycache__/library.cpython-314.pyc | Bin 38349 -> 38349 bytes .../__pycache__/real_power.cpython-314.pyc | Bin 4356 -> 4356 bytes .../__pycache__/virtual_power.cpython-314.pyc | Bin 36776 -> 36776 bytes .../flow_helper/flows/global_configuration.py | 33 +- .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 160 bytes .../__pycache__/filter.cpython-314.pyc | Bin 30964 -> 30964 bytes .../__pycache__/include.cpython-314.pyc | Bin 8749 -> 8749 bytes custom_components/powercalc/manifest.json | 2 +- .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 160 bytes .../__pycache__/error.cpython-314.pyc | Bin 994 -> 994 bytes .../__pycache__/factory.cpython-314.pyc | Bin 3226 -> 3226 bytes .../__pycache__/library.cpython-314.pyc | Bin 20368 -> 20368 bytes .../__pycache__/power_profile.cpython-314.pyc | Bin 30353 -> 30834 bytes .../sub_profile_selector.cpython-314.pyc | Bin 18625 -> 18625 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 167 -> 167 bytes .../__pycache__/composite.cpython-314.pyc | Bin 6311 -> 6311 bytes .../loader/__pycache__/local.cpython-314.pyc | Bin 14604 -> 14604 bytes .../__pycache__/protocol.cpython-314.pyc | Bin 3890 -> 3890 bytes .../loader/__pycache__/remote.cpython-314.pyc | Bin 34171 -> 34171 bytes .../powercalc/power_profile/power_profile.py | 11 +- .../__pycache__/__init__.cpython-314.pyc | Bin 154 -> 154 bytes .../__pycache__/abstract.cpython-314.pyc | Bin 6956 -> 6956 bytes .../__pycache__/daily_energy.cpython-314.pyc | Bin 16399 -> 16399 bytes .../__pycache__/energy.cpython-314.pyc | Bin 17679 -> 17679 bytes .../sensors/__pycache__/power.cpython-314.pyc | Bin 51534 -> 51534 bytes .../__pycache__/utility_meter.cpython-314.pyc | Bin 11591 -> 11591 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 160 bytes .../config_entry_utils.cpython-314.pyc | Bin 9307 -> 9307 bytes .../group/__pycache__/custom.cpython-314.pyc | Bin 57110 -> 57110 bytes .../group/__pycache__/domain.cpython-314.pyc | Bin 1848 -> 1848 bytes .../group/__pycache__/factory.cpython-314.pyc | Bin 3465 -> 3465 bytes .../group/__pycache__/standby.cpython-314.pyc | Bin 4944 -> 4944 bytes .../__pycache__/subtract.cpython-314.pyc | Bin 5871 -> 5871 bytes .../tracked_untracked.cpython-314.pyc | Bin 14159 -> 14159 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 154 -> 154 bytes .../gui_configuration.cpython-314.pyc | Bin 2275 -> 2275 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 155 -> 155 bytes .../__pycache__/composite.cpython-314.pyc | Bin 15367 -> 15367 bytes .../__pycache__/factory.cpython-314.pyc | Bin 15483 -> 15483 bytes .../__pycache__/fixed.cpython-314.pyc | Bin 5588 -> 5588 bytes .../__pycache__/linear.cpython-314.pyc | Bin 17088 -> 17088 bytes .../strategy/__pycache__/lut.cpython-314.pyc | Bin 25657 -> 26053 bytes .../__pycache__/multi_switch.cpython-314.pyc | Bin 5054 -> 5054 bytes .../__pycache__/playbook.cpython-314.pyc | Bin 18986 -> 18986 bytes .../__pycache__/selector.cpython-314.pyc | Bin 1908 -> 1908 bytes .../strategy_interface.cpython-314.pyc | Bin 3770 -> 3770 bytes .../strategy/__pycache__/wled.cpython-314.pyc | Bin 6794 -> 6794 bytes custom_components/powercalc/strategy/lut.py | 13 +- .../powercalc/translations/cz.json | 2 +- .../powercalc/translations/de.json | 2 +- .../powercalc/translations/es.json | 2 +- .../powercalc/translations/fr.json | 2 +- .../powercalc/translations/it.json | 2 +- .../powercalc/translations/nl.json | 2 +- .../powercalc/translations/pl.json | 2 +- .../powercalc/translations/pt-BR.json | 2 +- .../powercalc/translations/pt.json | 2 +- .../powercalc/translations/ro.json | 2 +- .../powercalc/translations/ru.json | 2 +- .../powercalc/translations/sk.json | 2 +- .../powercalc/translations/sv.json | 2 +- custom_components/scheduler/__init__.py | 446 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 27232 bytes .../__pycache__/actions.cpython-314.pyc | Bin 0 -> 30357 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 0 -> 1628 bytes .../__pycache__/const.cpython-314.pyc | Bin 0 -> 8167 bytes .../__pycache__/store.cpython-314.pyc | Bin 0 -> 23171 bytes .../__pycache__/switch.cpython-314.pyc | Bin 0 -> 29624 bytes .../__pycache__/timer.cpython-314.pyc | Bin 0 -> 26606 bytes .../__pycache__/websockets.cpython-314.pyc | Bin 0 -> 13262 bytes custom_components/scheduler/actions.py | 629 + custom_components/scheduler/config_flow.py | 26 + custom_components/scheduler/const.py | 220 + custom_components/scheduler/manifest.json | 12 + custom_components/scheduler/services.yaml | 180 + custom_components/scheduler/store.py | 376 + custom_components/scheduler/switch.py | 539 + custom_components/scheduler/timer.py | 509 + custom_components/scheduler/websockets.py | 272 + custom_components/taskmate/__init__.py | 1531 ++ .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 91346 bytes .../__pycache__/binary_sensor.cpython-314.pyc | Bin 0 -> 5642 bytes .../__pycache__/button.cpython-314.pyc | Bin 0 -> 14932 bytes .../__pycache__/calendar.cpython-314.pyc | Bin 0 -> 13685 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 0 -> 2852 bytes .../__pycache__/const.cpython-314.pyc | Bin 0 -> 23665 bytes .../coord_assignments.cpython-314.pyc | Bin 0 -> 31708 bytes .../__pycache__/coord_avatars.cpython-314.pyc | Bin 0 -> 8254 bytes .../__pycache__/coord_badges.cpython-314.pyc | Bin 0 -> 17532 bytes .../coord_calendar.cpython-314.pyc | Bin 0 -> 23069 bytes .../coord_challenges.cpython-314.pyc | Bin 0 -> 12678 bytes .../__pycache__/coord_chores.cpython-314.pyc | Bin 0 -> 81413 bytes .../coord_mandatory.cpython-314.pyc | Bin 0 -> 25601 bytes .../coord_notifications.cpython-314.pyc | Bin 0 -> 38321 bytes .../__pycache__/coord_points.cpython-314.pyc | Bin 0 -> 58994 bytes .../__pycache__/coord_quests.cpython-314.pyc | Bin 0 -> 11086 bytes .../__pycache__/coord_rewards.cpython-314.pyc | Bin 0 -> 34521 bytes .../coord_templates.cpython-314.pyc | Bin 0 -> 9310 bytes .../__pycache__/coord_timed.cpython-314.pyc | Bin 0 -> 13506 bytes .../__pycache__/coordinator.cpython-314.pyc | Bin 0 -> 55327 bytes .../__pycache__/frontend.cpython-314.pyc | Bin 0 -> 9642 bytes .../__pycache__/http_calendar.cpython-314.pyc | Bin 0 -> 5114 bytes .../__pycache__/http_photos.cpython-314.pyc | Bin 0 -> 9264 bytes .../taskmate/__pycache__/ics.cpython-314.pyc | Bin 0 -> 8068 bytes .../__pycache__/intents.cpython-314.pyc | Bin 0 -> 7216 bytes .../__pycache__/models.cpython-314.pyc | Bin 0 -> 62228 bytes .../__pycache__/number.cpython-314.pyc | Bin 0 -> 5571 bytes .../__pycache__/panel.cpython-314.pyc | Bin 0 -> 3883 bytes .../__pycache__/photos.cpython-314.pyc | Bin 0 -> 10788 bytes .../__pycache__/select.cpython-314.pyc | Bin 0 -> 5013 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 0 -> 75211 bytes .../__pycache__/storage.cpython-314.pyc | Bin 0 -> 111575 bytes .../__pycache__/templates.cpython-314.pyc | Bin 0 -> 5810 bytes .../taskmate/__pycache__/todo.cpython-314.pyc | Bin 0 -> 6024 bytes .../__pycache__/websocket.cpython-314.pyc | Bin 0 -> 128519 bytes custom_components/taskmate/binary_sensor.py | 90 + .../taskmate/brand/dark_logo.png | Bin 0 -> 23937 bytes .../taskmate/brand/dark_logo@2x.png | Bin 0 -> 26300 bytes custom_components/taskmate/brand/logo.png | Bin 0 -> 22549 bytes custom_components/taskmate/brand/logo@2x.png | Bin 0 -> 26266 bytes custom_components/taskmate/button.py | 251 + custom_components/taskmate/calendar.py | 232 + custom_components/taskmate/config_flow.py | 58 + custom_components/taskmate/const.py | 390 + .../taskmate/coord_assignments.py | 584 + custom_components/taskmate/coord_avatars.py | 115 + custom_components/taskmate/coord_badges.py | 323 + custom_components/taskmate/coord_calendar.py | 385 + .../taskmate/coord_challenges.py | 165 + custom_components/taskmate/coord_chores.py | 1483 ++ custom_components/taskmate/coord_mandatory.py | 344 + .../taskmate/coord_notifications.py | 625 + custom_components/taskmate/coord_points.py | 1042 + custom_components/taskmate/coord_quests.py | 156 + custom_components/taskmate/coord_rewards.py | 589 + custom_components/taskmate/coord_templates.py | 134 + custom_components/taskmate/coord_timed.py | 224 + custom_components/taskmate/coordinator.py | 796 + custom_components/taskmate/frontend.py | 245 + custom_components/taskmate/http_calendar.py | 76 + custom_components/taskmate/http_photos.py | 141 + custom_components/taskmate/icon.svg | 35 + custom_components/taskmate/ics.py | 127 + custom_components/taskmate/intents.py | 104 + custom_components/taskmate/manifest.json | 21 + custom_components/taskmate/models.py | 1112 + custom_components/taskmate/number.py | 73 + custom_components/taskmate/panel.py | 68 + custom_components/taskmate/photos.py | 203 + custom_components/taskmate/select.py | 63 + custom_components/taskmate/sensor.py | 1288 + custom_components/taskmate/services.yaml | 983 + custom_components/taskmate/storage.py | 1390 + custom_components/taskmate/strings.json | 884 + custom_components/taskmate/templates.py | 90 + custom_components/taskmate/todo.py | 80 + .../taskmate/translations/de.json | 884 + .../taskmate/translations/en-GB.json | 884 + .../taskmate/translations/en.json | 884 + .../taskmate/translations/fr.json | 884 + .../taskmate/translations/nb.json | 884 + .../taskmate/translations/nn.json | 884 + .../taskmate/translations/pt-BR.json | 884 + .../taskmate/translations/pt.json | 884 + custom_components/taskmate/websocket.py | 2093 ++ custom_components/taskmate/www/fart1.mp3 | Bin 0 -> 63740 bytes custom_components/taskmate/www/fart10.mp3 | Bin 0 -> 9910 bytes custom_components/taskmate/www/fart2.mp3 | Bin 0 -> 10986 bytes custom_components/taskmate/www/fart3.mp3 | Bin 0 -> 8898 bytes custom_components/taskmate/www/fart4.mp3 | Bin 0 -> 151908 bytes custom_components/taskmate/www/fart5.mp3 | Bin 0 -> 51108 bytes custom_components/taskmate/www/fart6.mp3 | Bin 0 -> 32868 bytes custom_components/taskmate/www/fart7.mp3 | Bin 0 -> 26148 bytes custom_components/taskmate/www/fart8.mp3 | Bin 0 -> 24228 bytes custom_components/taskmate/www/fart9.mp3 | Bin 0 -> 25188 bytes .../taskmate/www/locales/de.json | 1539 ++ .../taskmate/www/locales/en-GB.json | 1539 ++ .../taskmate/www/locales/en.json | 1539 ++ .../taskmate/www/locales/fr.json | 1539 ++ .../taskmate/www/locales/nb.json | 1539 ++ .../taskmate/www/locales/nn.json | 1539 ++ .../taskmate/www/locales/pt-BR.json | 1539 ++ .../taskmate/www/locales/pt.json | 1539 ++ .../taskmate/www/taskmate-activity-card.js | 1343 + .../taskmate/www/taskmate-approvals-card.js | 1586 ++ .../taskmate/www/taskmate-attr-resolver.js | 167 + .../taskmate/www/taskmate-badges-card.js | 778 + .../taskmate/www/taskmate-bonuses-card.js | 29 + .../taskmate/www/taskmate-calendar-card.js | 923 + .../taskmate/www/taskmate-child-card.js | 4271 +++ .../taskmate/www/taskmate-config-sounds.js | 647 + .../taskmate/www/taskmate-design.js | 394 + .../taskmate/www/taskmate-family-goal-card.js | 110 + .../taskmate/www/taskmate-graph-card.js | 1094 + .../taskmate/www/taskmate-incentive-card.js | 1138 + .../taskmate/www/taskmate-leaderboard-card.js | 935 + .../taskmate/www/taskmate-localize.js | 149 + .../taskmate/www/taskmate-overview-card.js | 996 + .../taskmate/www/taskmate-panel.js | 6647 +++++ .../www/taskmate-parent-dashboard-card.js | 1512 ++ .../taskmate/www/taskmate-penalties-card.js | 29 + .../www/taskmate-photo-gallery-card.js | 171 + .../taskmate/www/taskmate-points-card.js | 1405 + .../www/taskmate-points-display-card.js | 1259 + .../taskmate/www/taskmate-reorder-card.js | 1325 + .../www/taskmate-reward-progress-card.js | 996 + .../taskmate/www/taskmate-rewards-card.js | 2419 ++ .../taskmate/www/taskmate-streak-card.js | 811 + .../taskmate/www/taskmate-weekly-card.js | 898 + .../thermostat_timeline/__init__.py | 3461 +++ .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 219031 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 0 -> 1643 bytes .../__pycache__/const.cpython-314.pyc | Bin 0 -> 336 bytes .../__pycache__/frontend.cpython-314.pyc | Bin 0 -> 12087 bytes .../thermostat_timeline/config_flow.py | 17 + .../thermostat_timeline/const.py | 5 + .../thermostat_timeline/frontend.py | 238 + .../thermostat_timeline/manifest.json | 20 + .../thermostat_timeline/services.yaml | 202 + .../thermostat_timeline/translations/en.json | 13 + .../www/thermostat-pro-timeline.js | 22112 ++++++++++++++++ .../__pycache__/__init__.cpython-314.pyc | Bin 10578 -> 10578 bytes .../alarm_control_panel.cpython-314.pyc | Bin 11003 -> 11003 bytes .../__pycache__/button.cpython-314.pyc | Bin 18544 -> 18544 bytes .../__pycache__/camera.cpython-314.pyc | Bin 30155 -> 30155 bytes .../__pycache__/climate.cpython-314.pyc | Bin 27526 -> 27526 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 9823 -> 9823 bytes .../wyzeapi/__pycache__/const.cpython-314.pyc | Bin 1146 -> 1146 bytes .../__pycache__/coordinator.cpython-314.pyc | Bin 12543 -> 13007 bytes .../wyzeapi/__pycache__/cover.cpython-314.pyc | Bin 9389 -> 9389 bytes .../wyzeapi/__pycache__/light.cpython-314.pyc | Bin 28180 -> 28289 bytes .../wyzeapi/__pycache__/lock.cpython-314.pyc | Bin 15583 -> 15851 bytes .../__pycache__/number.cpython-314.pyc | Bin 10707 -> 10707 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 35472 -> 35472 bytes .../wyzeapi/__pycache__/siren.cpython-314.pyc | Bin 8627 -> 8627 bytes .../__pycache__/switch.cpython-314.pyc | Bin 38863 -> 38863 bytes .../__pycache__/token_manager.cpython-314.pyc | Bin 3958 -> 3958 bytes .../__pycache__/ydble_utils.cpython-314.pyc | Bin 7728 -> 7728 bytes custom_components/wyzeapi/coordinator.py | 8 + custom_components/wyzeapi/light.py | 5 +- custom_components/wyzeapi/lock.py | 6 +- custom_components/wyzeapi/manifest.json | 2 +- custom_components/wyzeapi/switch.py | 2 +- go2rtc-1.9.9 | Bin 0 -> 5941448 bytes go2rtc.yaml | 3 + scripts.yaml | 8 + telink_sensor.js | 14 + www/NestLiving.jpg | Bin 0 -> 2689 bytes www/blink.jpg | Bin 0 -> 139485 bytes www/blinkTree.jpg | Bin 0 -> 152018 bytes www/blink_Front.jpg | Bin 0 -> 203810 bytes www/blink_backyard_shed_snapshot.jpg | Bin 0 -> 167248 bytes www/blink_backyard_snapshot.jpg | Bin 0 -> 167248 bytes .../apexcharts-card/apexcharts-card.js | 1023 + .../apexcharts-card/apexcharts-card.js.gz | Bin 0 -> 278811 bytes .../atomic-calendar-revive.js | 49 + .../atomic-calendar-revive.js.gz | Bin 0 -> 66156 bytes www/community/button-card/button-card.js | 56 + www/community/button-card/button-card.js.gz | Bin 0 -> 45488 bytes .../clock-weather-card/clock-weather-card.js | 251 + .../clock-weather-card.js.gz | Bin 0 -> 56399 bytes .../skylight-calendar-card.js | 16634 ++++++++++++ .../skylight-calendar-card.js.gz | Bin 0 -> 116184 bytes www/community/ha-treemap-card/treemap-card.js | 3116 +++ .../ha-treemap-card/treemap-card.js.gz | Bin 0 -> 28061 bytes www/community/homie-dashboard/config.js | 616 + .../homie-dashboard/homie-dashboard.html | 21657 +++++++++++++++ .../lovelace-layout-card/layout-card.js | 1 + .../lovelace-layout-card/layout-card.js.gz | Bin 0 -> 19106 bytes .../lovelace-layout-card/rollup.config.js | 22 + .../lovelace-layout-card/rollup.config.js.gz | Bin 0 -> 331 bytes .../multiple-entity-row.js | 2 + .../multiple-entity-row.js.gz | Bin 0 -> 12003 bytes .../thermostat-pro-timeline.js | 19795 ++++++++++++++ .../thermostat-pro-timeline.js.gz | Bin 0 -> 221937 bytes www/community/lovelace-wallpanel/wallpanel.js | 1 + .../lovelace-wallpanel/wallpanel.js.gz | Bin 0 -> 67553 bytes .../mini-graph-card/mini-graph-card-bundle.js | 1 + .../mini-graph-card-bundle.js.gz | Bin 0 -> 38126 bytes .../platinum-weather-card/a-clear-day.svg | 131 + .../platinum-weather-card/a-clear-night.svg | 159 + .../platinum-weather-card/a-cloudy-1-day.svg | 178 + .../a-cloudy-1-night.svg | 206 + .../platinum-weather-card/a-cloudy-2-day.svg | 178 + .../a-cloudy-2-night.svg | 206 + .../platinum-weather-card/a-cloudy-3-day.svg | 178 + .../a-cloudy-3-night.svg | 206 + .../platinum-weather-card/a-cloudy.svg | 113 + .../platinum-weather-card/a-dust.svg | 30 + .../platinum-weather-card/a-fog-day.svg | 244 + .../platinum-weather-card/a-fog-night.svg | 309 + www/community/platinum-weather-card/a-fog.svg | 172 + .../platinum-weather-card/a-frost-day.svg | 204 + .../platinum-weather-card/a-frost-night.svg | 269 + .../platinum-weather-card/a-frost.svg | 130 + .../platinum-weather-card/a-hail.svg | 134 + .../platinum-weather-card/a-haze-day.svg | 244 + .../platinum-weather-card/a-haze-night.svg | 309 + .../platinum-weather-card/a-haze.svg | 172 + .../platinum-weather-card/a-hurricane.svg | 90 + .../a-isolated-thunderstorms-day.svg | 374 + .../a-isolated-thunderstorms-night.svg | 401 + .../a-isolated-thunderstorms.svg | 265 + .../a-rain-and-sleet-mix.svg | 141 + .../a-rain-and-snow-mix.svg | 195 + .../platinum-weather-card/a-rainy-1-day.svg | 179 + .../platinum-weather-card/a-rainy-1-night.svg | 243 + .../platinum-weather-card/a-rainy-1.svg | 106 + .../platinum-weather-card/a-rainy-2-day.svg | 204 + .../platinum-weather-card/a-rainy-2-night.svg | 256 + .../platinum-weather-card/a-rainy-2.svg | 131 + .../platinum-weather-card/a-rainy-3-day.svg | 206 + .../platinum-weather-card/a-rainy-3-night.svg | 270 + .../platinum-weather-card/a-rainy-3.svg | 133 + .../a-scattered-thunderstorms-day.svg | 374 + .../a-scattered-thunderstorms-night.svg | 283 + .../a-scattered-thunderstorms.svg | 265 + .../a-severe-thunderstorm.svg | 307 + .../a-snow-and-sleet-mix.svg | 227 + .../platinum-weather-card/a-snowy-1-day.svg | 241 + .../platinum-weather-card/a-snowy-1-night.svg | 269 + .../platinum-weather-card/a-snowy-1.svg | 132 + .../platinum-weather-card/a-snowy-2-day.svg | 273 + .../platinum-weather-card/a-snowy-2-night.svg | 301 + .../platinum-weather-card/a-snowy-2.svg | 160 + .../platinum-weather-card/a-snowy-3-day.svg | 334 + .../platinum-weather-card/a-snowy-3-night.svg | 361 + .../platinum-weather-card/a-snowy-3.svg | 224 + .../platinum-weather-card/a-thunderstorms.svg | 260 + .../platinum-weather-card/a-tornado.svg | 367 + .../a-tropical-storm.svg | 50 + .../platinum-weather-card/a-unknown.svg | 17 + .../platinum-weather-card/a-wind.svg | 32 + .../platinum-weather-card/editor-92788164.js | 1582 ++ .../editor-92788164.js.gz | Bin 0 -> 56654 bytes .../platinum-weather-card/fire-ban-black.svg | 74 + .../platinum-weather-card/fire-ban-white.svg | 81 + .../platinum-weather-card-df6d26e2.js | 922 + .../platinum-weather-card-df6d26e2.js.gz | Bin 0 -> 24917 bytes .../platinum-weather-card.js | 1 + .../platinum-weather-card.js.gz | Bin 0 -> 109 bytes .../platinum-weather-card/s-clear-day.svg | 44 + .../platinum-weather-card/s-clear-night.svg | 34 + .../platinum-weather-card/s-cloudy-1-day.svg | 49 + .../s-cloudy-1-night.svg | 39 + .../platinum-weather-card/s-cloudy-2-day.svg | 49 + .../s-cloudy-2-night.svg | 39 + .../platinum-weather-card/s-cloudy-3-day.svg | 49 + .../s-cloudy-3-night.svg | 39 + .../platinum-weather-card/s-cloudy.svg | 29 + .../platinum-weather-card/s-dust.svg | 27 + .../platinum-weather-card/s-fog-day.svg | 51 + .../platinum-weather-card/s-fog-night.svg | 41 + www/community/platinum-weather-card/s-fog.svg | 26 + .../platinum-weather-card/s-frost-day.svg | 51 + .../platinum-weather-card/s-frost-night.svg | 41 + .../platinum-weather-card/s-frost.svg | 26 + .../platinum-weather-card/s-hail.svg | 30 + .../platinum-weather-card/s-haze-day.svg | 51 + .../platinum-weather-card/s-haze-night.svg | 41 + .../platinum-weather-card/s-haze.svg | 26 + .../platinum-weather-card/s-hurricane.svg | 30 + .../s-isolated-thunderstorms-day.svg | 54 + .../s-isolated-thunderstorms-night.svg | 44 + .../s-isolated-thunderstorms.svg | 29 + .../s-rain-and-sleet-mix.svg | 36 + .../s-rain-and-snow-mix.svg | 36 + .../platinum-weather-card/s-rainy-1-day.svg | 53 + .../platinum-weather-card/s-rainy-1-night.svg | 43 + .../platinum-weather-card/s-rainy-1.svg | 28 + .../platinum-weather-card/s-rainy-2-day.svg | 54 + .../platinum-weather-card/s-rainy-2-night.svg | 44 + .../platinum-weather-card/s-rainy-2.svg | 29 + .../platinum-weather-card/s-rainy-3-day.svg | 55 + .../platinum-weather-card/s-rainy-3-night.svg | 45 + .../platinum-weather-card/s-rainy-3.svg | 30 + .../s-scattered-thunderstorms-day.svg | 54 + .../s-scattered-thunderstorms-night.svg | 44 + .../s-scattered-thunderstorms.svg | 29 + .../s-severe-thunderstorm.svg | 41 + .../s-snow-and-sleet-mix.svg | 47 + .../platinum-weather-card/s-snowy-1-day.svg | 57 + .../platinum-weather-card/s-snowy-1-night.svg | 47 + .../platinum-weather-card/s-snowy-1.svg | 32 + .../platinum-weather-card/s-snowy-2-day.svg | 65 + .../platinum-weather-card/s-snowy-2-night.svg | 55 + .../platinum-weather-card/s-snowy-2.svg | 40 + .../platinum-weather-card/s-snowy-3-day.svg | 73 + .../platinum-weather-card/s-snowy-3-night.svg | 63 + .../platinum-weather-card/s-snowy-3.svg | 48 + .../platinum-weather-card/s-thunderstorms.svg | 35 + .../platinum-weather-card/s-tornado.svg | 43 + .../s-tropical-storm.svg | 30 + .../platinum-weather-card/s-unknown.svg | 17 + .../platinum-weather-card/s-wind.svg | 27 + .../scheduler-card/scheduler-card.js | 2917 ++ .../scheduler-card/scheduler-card.js.gz | Bin 0 -> 98207 bytes .../sensor-bar-card-plus.js | 9551 +++++++ .../sensor-bar-card-plus.js.gz | Bin 0 -> 68231 bytes www/commute_snapshot.jpg | Bin 0 -> 83242 bytes www/nest.jpg | Bin 0 -> 2689 bytes www/temp/nest_snapshot.jpg | Bin 0 -> 2689 bytes www/thermostat-pro-timeline.js | 22112 ++++++++++++++++ zigbee2mqtt/configuration.yaml | 13 +- zigbee2mqtt/coordinator_backup.json | 46 +- 1114 files changed, 406637 insertions(+), 154 deletions(-) create mode 100644 .cache/brands/integrations/asusrouter/dark_logo.png create mode 100644 .cache/brands/integrations/blink/dark_logo.png create mode 100644 .cache/brands/integrations/blink/logo.png create mode 100644 .cache/brands/integrations/climate_scheduler/logo.png create mode 100644 .cache/brands/integrations/generic/logo.png create mode 100644 .cache/brands/integrations/google_generative_ai_conversation/logo.png create mode 100644 .cache/brands/integrations/google_travel_time/logo.png create mode 100644 .cache/brands/integrations/hassio/logo.png create mode 100644 .cache/brands/integrations/intelligent_heating_pilot/dark_logo.png create mode 100644 .cache/brands/integrations/intelligent_heating_pilot/logo.png create mode 100644 .cache/brands/integrations/maintenance_supporter/logo.png create mode 100644 .cache/brands/integrations/met/logo.png create mode 100644 .cache/brands/integrations/midea_ac_lan/dark_logo.png create mode 100644 .cache/brands/integrations/nest_protect/dark_logo.png create mode 100644 .cache/brands/integrations/scheduler/dark_logo.png create mode 100644 .cache/brands/integrations/scheduler/logo.png create mode 100644 .cache/brands/integrations/waze_travel_time/logo.png create mode 100644 blueprints/automation/SirGoodenough/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml create mode 100644 blueprints/script/SirGoodenough/dimmer_control_4_magic_cube.yaml create mode 100644 custom_components/album_slideshow/__init__.py create mode 100644 custom_components/album_slideshow/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/button.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/camera.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/config_flow.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/const.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/coordinator.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/google_scraper.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/image_processing.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/number.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/playlist.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/select.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/sensor.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/store.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/switch.cpython-314.pyc create mode 100644 custom_components/album_slideshow/__pycache__/text.cpython-314.pyc create mode 100644 custom_components/album_slideshow/brand/logo.png create mode 100644 custom_components/album_slideshow/button.py create mode 100644 custom_components/album_slideshow/camera.py create mode 100644 custom_components/album_slideshow/config_flow.py create mode 100644 custom_components/album_slideshow/const.py create mode 100644 custom_components/album_slideshow/coordinator.py create mode 100644 custom_components/album_slideshow/google_scraper.py create mode 100644 custom_components/album_slideshow/image_processing.py create mode 100644 custom_components/album_slideshow/manifest.json create mode 100644 custom_components/album_slideshow/number.py create mode 100644 custom_components/album_slideshow/playlist.py create mode 100644 custom_components/album_slideshow/select.py create mode 100644 custom_components/album_slideshow/sensor.py create mode 100644 custom_components/album_slideshow/services.yaml create mode 100644 custom_components/album_slideshow/store.py create mode 100644 custom_components/album_slideshow/strings.json create mode 100644 custom_components/album_slideshow/switch.py create mode 100644 custom_components/album_slideshow/text.py create mode 100644 custom_components/album_slideshow/translations/en.json create mode 100644 custom_components/album_slideshow/www/album-slideshow-card.js create mode 100644 custom_components/climate_scheduler/__init__.py create mode 100644 custom_components/climate_scheduler/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/climate_scheduler/__pycache__/climate.cpython-314.pyc create mode 100644 custom_components/climate_scheduler/__pycache__/config_flow.cpython-314.pyc create mode 100644 custom_components/climate_scheduler/__pycache__/const.cpython-314.pyc create mode 100644 custom_components/climate_scheduler/__pycache__/coordinator.cpython-314.pyc create mode 100644 custom_components/climate_scheduler/__pycache__/sensor.cpython-314.pyc create mode 100644 custom_components/climate_scheduler/__pycache__/services.cpython-314.pyc create mode 100644 custom_components/climate_scheduler/__pycache__/storage.cpython-314.pyc create mode 100644 custom_components/climate_scheduler/__pycache__/switch.cpython-314.pyc create mode 100644 custom_components/climate_scheduler/climate.py create mode 100644 custom_components/climate_scheduler/config_flow.py create mode 100644 custom_components/climate_scheduler/const.py create mode 100644 custom_components/climate_scheduler/coordinator.py create mode 100644 custom_components/climate_scheduler/frontend/.version create mode 100644 custom_components/climate_scheduler/frontend/app.js create mode 100644 custom_components/climate_scheduler/frontend/climate-dialog.js create mode 100644 custom_components/climate_scheduler/frontend/climate-scheduler-card.js create mode 100644 custom_components/climate_scheduler/frontend/ha-api.js create mode 100644 custom_components/climate_scheduler/frontend/keyframe-timeline.js create mode 100644 custom_components/climate_scheduler/frontend/panel.js create mode 100644 custom_components/climate_scheduler/frontend/styles.css create mode 100644 custom_components/climate_scheduler/frontend/utils.js create mode 100644 custom_components/climate_scheduler/manifest.json create mode 100644 custom_components/climate_scheduler/sensor.py create mode 100644 custom_components/climate_scheduler/services.py create mode 100644 custom_components/climate_scheduler/services.yaml create mode 100644 custom_components/climate_scheduler/storage.py create mode 100644 custom_components/climate_scheduler/switch.py create mode 100644 custom_components/climate_scheduler/translations/en.json create mode 100644 custom_components/climate_scheduler/translations/sk.json create mode 100644 custom_components/intelligent_heating_pilot/__init__.py create mode 100644 custom_components/intelligent_heating_pilot/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/__pycache__/config_flow.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/__pycache__/const.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/__pycache__/heating_application.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/__pycache__/sensor.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/__pycache__/switch.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/__pycache__/view.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/__init__.py create mode 100644 custom_components/intelligent_heating_pilot/application/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/__pycache__/heating_cycle_lifecycle_manager.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/__pycache__/heating_cycle_lifecycle_manager_factory.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/__pycache__/lhs_lifecycle_manager.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/__pycache__/lhs_lifecycle_manager_factory.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/__pycache__/orchestrator.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/heating_cycle_lifecycle_manager.py create mode 100644 custom_components/intelligent_heating_pilot/application/heating_cycle_lifecycle_manager_factory.py create mode 100644 custom_components/intelligent_heating_pilot/application/lhs_lifecycle_manager.py create mode 100644 custom_components/intelligent_heating_pilot/application/lhs_lifecycle_manager_factory.py create mode 100644 custom_components/intelligent_heating_pilot/application/orchestrator.py create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/__init__.py create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/__pycache__/calculate_anticipation_use_case.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/__pycache__/check_overshoot_risk_use_case.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/__pycache__/control_preheating_use_case.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/__pycache__/schedule_anticipation_action_use_case.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/__pycache__/update_cache_data_use_case.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/calculate_anticipation_use_case.py create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/check_overshoot_risk_use_case.py create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/control_preheating_use_case.py create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/schedule_anticipation_action_use_case.py create mode 100644 custom_components/intelligent_heating_pilot/application/use_cases/update_cache_data_use_case.py create mode 100644 custom_components/intelligent_heating_pilot/config_flow.py create mode 100644 custom_components/intelligent_heating_pilot/const.py create mode 100644 custom_components/intelligent_heating_pilot/domain/__init__.py create mode 100644 custom_components/intelligent_heating_pilot/domain/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/__pycache__/constants.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/constants.py create mode 100644 custom_components/intelligent_heating_pilot/domain/entities/__init__.py create mode 100644 custom_components/intelligent_heating_pilot/domain/entities/heating_pilot.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__init__.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/climate_data_reader_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/context_reader_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/decision_strategy_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/device_config_reader_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/entity_attribute_mapper_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/environment_reader_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/heating_cycle_service_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/heating_cycle_storage_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/historical_data_adapter_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/lhs_storage_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/scheduler_commander_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/scheduler_reader_interface.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/__pycache__/timer_scheduler.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/climate_data_reader_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/context_reader_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/decision_strategy_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/device_config_reader_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/entity_attribute_mapper_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/environment_reader_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/heating_cycle_service_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/heating_cycle_storage_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/historical_data_adapter_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/lhs_storage_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/scheduler_commander_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/scheduler_reader_interface.py create mode 100644 custom_components/intelligent_heating_pilot/domain/interfaces/timer_scheduler.py create mode 100644 custom_components/intelligent_heating_pilot/domain/services/__init__.py create mode 100644 custom_components/intelligent_heating_pilot/domain/services/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/services/__pycache__/contextual_lhs_calculator_service.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/services/__pycache__/dead_time_calculation_service.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/services/__pycache__/extraction_date_range_calculator.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/services/__pycache__/global_lhs_calculator_service.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/services/__pycache__/heating_cycle_service.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/services/__pycache__/prediction_service.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/services/contextual_lhs_calculator_service.py create mode 100644 custom_components/intelligent_heating_pilot/domain/services/dead_time_calculation_service.py create mode 100644 custom_components/intelligent_heating_pilot/domain/services/extraction_date_range_calculator.py create mode 100644 custom_components/intelligent_heating_pilot/domain/services/global_lhs_calculator_service.py create mode 100644 custom_components/intelligent_heating_pilot/domain/services/heating_cycle_service.py create mode 100644 custom_components/intelligent_heating_pilot/domain/services/ml_decision_strategy.py create mode 100644 custom_components/intelligent_heating_pilot/domain/services/prediction_service.py create mode 100644 custom_components/intelligent_heating_pilot/domain/services/simple_decision_strategy.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__init__.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__pycache__/entity_attribute_mapping.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__pycache__/environment_state.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__pycache__/heating.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__pycache__/heating_cycle_cache_data.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__pycache__/historical_data.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__pycache__/lhs_cache_entry.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__pycache__/prediction_result.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__pycache__/recording_extraction_task.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__pycache__/scheduled_timeslot.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/__pycache__/slope_data.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/contextual_lhs_data.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/entity_attribute_mapping.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/environment_state.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/heating.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/heating_cycle_cache_data.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/historical_data.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/lhs_cache_entry.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/prediction_result.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/recording_extraction_task.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/scheduled_timeslot.py create mode 100644 custom_components/intelligent_heating_pilot/domain/value_objects/slope_data.py create mode 100644 custom_components/intelligent_heating_pilot/heating_application.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/__init__.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/__pycache__/event_bridge.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/__pycache__/recorder_queue.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/__pycache__/vtherm_compat.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__init__.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/base_entity_attribute_mapper.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/base_ha_storage.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/climate_commander.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/climate_data_reader.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/context_reader.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/device_config_reader.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/entity_attribute_mapper_registry.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/environment_reader.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/generic_climate_attribute_mapper.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/heating_cycle_storage.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/lhs_storage.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/recording_extraction_queue.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/scheduler_commander.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/scheduler_reader.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/sensor_data_reader.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/timer_scheduler.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/utils.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/vtherm_attribute_mapper.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/__pycache__/weather_data_reader.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/base_entity_attribute_mapper.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/base_ha_storage.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/climate_commander.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/climate_data_reader.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/context_reader.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/device_config_reader.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/entity_attribute_mapper_registry.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/entity_attribute_validator.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/environment_reader.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/generic_climate_attribute_mapper.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/heating_cycle_storage.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/lhs_storage.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/recording_extraction_queue.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/scheduler_commander.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/scheduler_reader.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/sensor_data_reader.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/timer_scheduler.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/utils.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/vtherm_attribute_mapper.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/adapters/weather_data_reader.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/decision_strategy_factory.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/event_bridge.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/recorder_queue.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/rest_api.py create mode 100644 custom_components/intelligent_heating_pilot/infrastructure/vtherm_compat.py create mode 100644 custom_components/intelligent_heating_pilot/logo.png create mode 100644 custom_components/intelligent_heating_pilot/manifest.json create mode 100644 custom_components/intelligent_heating_pilot/py.typed create mode 100644 custom_components/intelligent_heating_pilot/sensor.py create mode 100644 custom_components/intelligent_heating_pilot/services.yaml create mode 100644 custom_components/intelligent_heating_pilot/strings.json create mode 100644 custom_components/intelligent_heating_pilot/switch.py create mode 100644 custom_components/intelligent_heating_pilot/translations/en.json create mode 100644 custom_components/intelligent_heating_pilot/translations/fr.json create mode 100644 custom_components/intelligent_heating_pilot/utils/__init__.py create mode 100644 custom_components/intelligent_heating_pilot/utils/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/utils/__pycache__/config_helpers.cpython-314.pyc create mode 100644 custom_components/intelligent_heating_pilot/utils/config_helpers.py create mode 100644 custom_components/intelligent_heating_pilot/view.py create mode 100644 custom_components/maintenance_supporter/__init__.py create mode 100644 custom_components/maintenance_supporter/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/binary_sensor.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/button.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/calendar.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/condition.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/config_flow.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/config_flow_helpers.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/config_flow_options_global.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/config_flow_trigger.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/const.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/coordinator.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/diagnostics.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/logbook.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/panel.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/repairs.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/sensor.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/storage.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/templates.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/templates_i18n.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/todo.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/trigger.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/__pycache__/views.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/binary_sensor.py create mode 100644 custom_components/maintenance_supporter/button.py create mode 100644 custom_components/maintenance_supporter/calendar.py create mode 100644 custom_components/maintenance_supporter/condition.py create mode 100644 custom_components/maintenance_supporter/conditions.yaml create mode 100644 custom_components/maintenance_supporter/config_flow.py create mode 100644 custom_components/maintenance_supporter/config_flow_helpers.py create mode 100644 custom_components/maintenance_supporter/config_flow_options.py create mode 100644 custom_components/maintenance_supporter/config_flow_options_global.py create mode 100644 custom_components/maintenance_supporter/config_flow_options_task.py create mode 100644 custom_components/maintenance_supporter/config_flow_options_task_adaptive.py create mode 100644 custom_components/maintenance_supporter/config_flow_options_task_add.py create mode 100644 custom_components/maintenance_supporter/config_flow_options_task_base.py create mode 100644 custom_components/maintenance_supporter/config_flow_options_task_crud.py create mode 100644 custom_components/maintenance_supporter/config_flow_options_task_object.py create mode 100644 custom_components/maintenance_supporter/config_flow_options_task_trigger.py create mode 100644 custom_components/maintenance_supporter/config_flow_trigger.py create mode 100644 custom_components/maintenance_supporter/const.py create mode 100644 custom_components/maintenance_supporter/coordinator.py create mode 100644 custom_components/maintenance_supporter/diagnostics.py create mode 100644 custom_components/maintenance_supporter/entity/__init__.py create mode 100644 custom_components/maintenance_supporter/entity/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/entity/__pycache__/entity_base.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/entity/__pycache__/summary_coordinator.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/entity/entity_base.py create mode 100644 custom_components/maintenance_supporter/entity/summary_coordinator.py create mode 100644 custom_components/maintenance_supporter/entity/triggers/__init__.py create mode 100644 custom_components/maintenance_supporter/entity/triggers/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/entity/triggers/__pycache__/base_trigger.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/entity/triggers/__pycache__/compound.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/entity/triggers/__pycache__/counter.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/entity/triggers/__pycache__/runtime.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/entity/triggers/__pycache__/state_change.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/entity/triggers/__pycache__/threshold.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/entity/triggers/base_trigger.py create mode 100644 custom_components/maintenance_supporter/entity/triggers/compound.py create mode 100644 custom_components/maintenance_supporter/entity/triggers/counter.py create mode 100644 custom_components/maintenance_supporter/entity/triggers/runtime.py create mode 100644 custom_components/maintenance_supporter/entity/triggers/state_change.py create mode 100644 custom_components/maintenance_supporter/entity/triggers/threshold.py create mode 100644 custom_components/maintenance_supporter/export.py create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/_panel-utils.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/_test-utils.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/calendar-bucket.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/calendar-card.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/card-sort-due-date.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/complete-dialog.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/dialog-no-lazy-load-elements.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/documents-section.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/download.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/filter-outliers.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/format-recurrence.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/group-dialog-sort.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/i18n-loader.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/interval.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/ll-custom-payload.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/object-columns.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/panel-deeplink.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/panel-shell.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/settings-view-notify-picker.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/settings-view-print-qr.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/settings-view-vacation.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/storage-section-card.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-renderer.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-calendar-kinds.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-completion-actions.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-compound.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-interval-hydration.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-recovery-flag.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/task-documents.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/trigger-chart.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/trigger-section.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/vacation-section-card.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/virtual-window.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/warranty.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/calendar-styles.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/capture-readme-screenshots.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/capture-v1041-screenshots.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/capture-v131-screenshots.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/capture-v14-screenshots.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/components/budget-section-card.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/complete-dialog.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/confirm-dialog.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/documents-section.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/group-dialog.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/groups-section-card.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/history-edit-dialog.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/history-photo.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/ms-textfield.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/object-dialog.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/object-quick-actions-dialog.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/qr-dialog.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/seasonal-overrides-dialog.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/section-card-shared-styles.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/settings-view.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/storage-section-card.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/task-documents.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/task-quick-actions-dialog.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/trigger-chart.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/vacation-section-card.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/deeplink_test.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/dialog-mount.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/e2e-checklist-preview.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/e2e-checklist-visibility.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/e2e-error-messages.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/e2e-helpers.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/e2e-issues-30-31.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/e2e-new-features.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/e2e-schedule-time.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/e2e-test.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/esbuild.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/helpers/calendar-bucket.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/helpers/download.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/helpers/format-bytes.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/helpers/interval.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/helpers/object-columns.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/helpers/report.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/helpers/virtual-window.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/helpers/warranty.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/helpers/worksheet.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/cs.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/da.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/de.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/en.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/es.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/fi.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/fr.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/hi.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/it.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/ja.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/nb.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/nl.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/pl.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/pt.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/ru.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/sv.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/uk.json create mode 100644 custom_components/maintenance_supporter/frontend-src/locales/zh.json create mode 100644 custom_components/maintenance_supporter/frontend-src/maintenance-calendar-card.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/maintenance-card-editor.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/maintenance-card.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/maintenance-dashboard-strategy.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/maintenance-strategy-shim.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/package-lock.json create mode 100644 custom_components/maintenance_supporter/frontend-src/package.json create mode 100644 custom_components/maintenance_supporter/frontend-src/panel-styles.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/qr_test.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/renderers/chart-utils.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/renderers/charts.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/renderers/history.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/renderers/prediction.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/renderers/progress.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/renderers/recommendation.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/renderers/seasonal.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/renderers/sparkline.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/renderers/task-detail.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/renderers/weibull.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/screenshot-section-cards.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/screenshot.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/statistics-service.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/styles.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/test-entity-attributes.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/tsconfig.json create mode 100644 custom_components/maintenance_supporter/frontend-src/types.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/user-service.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/verify-history-edit-e2e.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/verify-kpi-click.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/verify-section-cards-write.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/verify-strategy-picker.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/verify-strategy.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/web-test-runner.config.mjs create mode 100644 custom_components/maintenance_supporter/frontend-src/ws-errors.ts create mode 100644 custom_components/maintenance_supporter/frontend/__init__.py create mode 100644 custom_components/maintenance_supporter/frontend/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/frontend/locales/cs.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/da.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/de.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/en.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/es.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/fi.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/fr.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/hi.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/it.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/ja.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/nb.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/nl.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/pl.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/pt.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/ru.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/sv.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/uk.json create mode 100644 custom_components/maintenance_supporter/frontend/locales/zh.json create mode 100644 custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js create mode 100644 custom_components/maintenance_supporter/frontend/maintenance-card.js create mode 100644 custom_components/maintenance_supporter/frontend/maintenance-panel.js create mode 100644 custom_components/maintenance_supporter/frontend/maintenance-strategy-shim.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/budget-section-card-V4BTQUYP.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-2LLDFX26.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-6EEEWEML.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-LO2NM3CE.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/dialog-mount-RTTXPFO3.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/groups-section-card-U36TKQBF.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/vacation-section-card-YFJ6JUPY.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/maintenance-dashboard-strategy.js create mode 100644 custom_components/maintenance_supporter/frontend/vendor/pdf.min.mjs create mode 100644 custom_components/maintenance_supporter/frontend/vendor/pdf.worker.min.mjs create mode 100644 custom_components/maintenance_supporter/helpers/__init__.py create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/action_listener.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/aggregate.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/dates.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/documents.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/entity_analyzer.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/entity_rename.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/global_options.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/i18n.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/notification_manager.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/notify_targets.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/pause.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/permissions.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/qr_generator.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/qrcodegen.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/retention.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/sanitize.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/schedule.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/settings_registry.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/status.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/task_fields.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/threshold_calculator.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/vacation.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/workday.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/helpers/action_listener.py create mode 100644 custom_components/maintenance_supporter/helpers/aggregate.py create mode 100644 custom_components/maintenance_supporter/helpers/csv_handler.py create mode 100644 custom_components/maintenance_supporter/helpers/dates.py create mode 100644 custom_components/maintenance_supporter/helpers/documents.py create mode 100644 custom_components/maintenance_supporter/helpers/entity_analyzer.py create mode 100644 custom_components/maintenance_supporter/helpers/entity_attributes.py create mode 100644 custom_components/maintenance_supporter/helpers/entity_rename.py create mode 100644 custom_components/maintenance_supporter/helpers/global_options.py create mode 100644 custom_components/maintenance_supporter/helpers/i18n.py create mode 100644 custom_components/maintenance_supporter/helpers/interval_analyzer.py create mode 100644 custom_components/maintenance_supporter/helpers/notification_manager.py create mode 100644 custom_components/maintenance_supporter/helpers/notify_targets.py create mode 100644 custom_components/maintenance_supporter/helpers/pause.py create mode 100644 custom_components/maintenance_supporter/helpers/permissions.py create mode 100644 custom_components/maintenance_supporter/helpers/qr_generator.py create mode 100644 custom_components/maintenance_supporter/helpers/qrcodegen.py create mode 100644 custom_components/maintenance_supporter/helpers/retention.py create mode 100644 custom_components/maintenance_supporter/helpers/sanitize.py create mode 100644 custom_components/maintenance_supporter/helpers/schedule.py create mode 100644 custom_components/maintenance_supporter/helpers/sensor_predictor.py create mode 100644 custom_components/maintenance_supporter/helpers/settings_registry.py create mode 100644 custom_components/maintenance_supporter/helpers/status.py create mode 100644 custom_components/maintenance_supporter/helpers/task_fields.py create mode 100644 custom_components/maintenance_supporter/helpers/threshold_calculator.py create mode 100644 custom_components/maintenance_supporter/helpers/trigger_fallback.py create mode 100644 custom_components/maintenance_supporter/helpers/vacation.py create mode 100644 custom_components/maintenance_supporter/helpers/workday.py create mode 100644 custom_components/maintenance_supporter/icons.json create mode 100644 custom_components/maintenance_supporter/logbook.py create mode 100644 custom_components/maintenance_supporter/manifest.json create mode 100644 custom_components/maintenance_supporter/models/__init__.py create mode 100644 custom_components/maintenance_supporter/models/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/models/__pycache__/maintenance_object.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/models/__pycache__/maintenance_task.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/models/__pycache__/maintenance_type.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/models/maintenance_object.py create mode 100644 custom_components/maintenance_supporter/models/maintenance_task.py create mode 100644 custom_components/maintenance_supporter/models/maintenance_type.py create mode 100644 custom_components/maintenance_supporter/panel.py create mode 100644 custom_components/maintenance_supporter/py.typed create mode 100644 custom_components/maintenance_supporter/quality_scale.yaml create mode 100644 custom_components/maintenance_supporter/repairs.py create mode 100644 custom_components/maintenance_supporter/sensor.py create mode 100644 custom_components/maintenance_supporter/services.yaml create mode 100644 custom_components/maintenance_supporter/storage.py create mode 100644 custom_components/maintenance_supporter/strings.json create mode 100644 custom_components/maintenance_supporter/templates.py create mode 100644 custom_components/maintenance_supporter/templates_i18n.py create mode 100644 custom_components/maintenance_supporter/todo.py create mode 100644 custom_components/maintenance_supporter/translations/cs.json create mode 100644 custom_components/maintenance_supporter/translations/da.json create mode 100644 custom_components/maintenance_supporter/translations/de.json create mode 100644 custom_components/maintenance_supporter/translations/en.json create mode 100644 custom_components/maintenance_supporter/translations/es.json create mode 100644 custom_components/maintenance_supporter/translations/fi.json create mode 100644 custom_components/maintenance_supporter/translations/fr.json create mode 100644 custom_components/maintenance_supporter/translations/hi.json create mode 100644 custom_components/maintenance_supporter/translations/it.json create mode 100644 custom_components/maintenance_supporter/translations/ja.json create mode 100644 custom_components/maintenance_supporter/translations/nb.json create mode 100644 custom_components/maintenance_supporter/translations/nl.json create mode 100644 custom_components/maintenance_supporter/translations/pl.json create mode 100644 custom_components/maintenance_supporter/translations/pt.json create mode 100644 custom_components/maintenance_supporter/translations/ru.json create mode 100644 custom_components/maintenance_supporter/translations/sv.json create mode 100644 custom_components/maintenance_supporter/translations/uk.json create mode 100644 custom_components/maintenance_supporter/translations/zh-Hans.json create mode 100644 custom_components/maintenance_supporter/trigger.py create mode 100644 custom_components/maintenance_supporter/triggers.yaml create mode 100644 custom_components/maintenance_supporter/views.py create mode 100644 custom_components/maintenance_supporter/websocket/__init__.py create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/analysis.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/dashboard.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/documents.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/groups.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/io.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/objects.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/tags.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/tasks.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/tasks_actions.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/tasks_crud.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/tasks_history.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/tasks_lifecycle.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/tasks_persist.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/tasks_validation.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/users.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/__pycache__/vacation.cpython-314.pyc create mode 100644 custom_components/maintenance_supporter/websocket/analysis.py create mode 100644 custom_components/maintenance_supporter/websocket/dashboard.py create mode 100644 custom_components/maintenance_supporter/websocket/documents.py create mode 100644 custom_components/maintenance_supporter/websocket/groups.py create mode 100644 custom_components/maintenance_supporter/websocket/io.py create mode 100644 custom_components/maintenance_supporter/websocket/objects.py create mode 100644 custom_components/maintenance_supporter/websocket/tags.py create mode 100644 custom_components/maintenance_supporter/websocket/tasks.py create mode 100644 custom_components/maintenance_supporter/websocket/tasks_actions.py create mode 100644 custom_components/maintenance_supporter/websocket/tasks_crud.py create mode 100644 custom_components/maintenance_supporter/websocket/tasks_history.py create mode 100644 custom_components/maintenance_supporter/websocket/tasks_lifecycle.py create mode 100644 custom_components/maintenance_supporter/websocket/tasks_persist.py create mode 100644 custom_components/maintenance_supporter/websocket/tasks_validation.py create mode 100644 custom_components/maintenance_supporter/websocket/users.py create mode 100644 custom_components/maintenance_supporter/websocket/vacation.py create mode 100644 custom_components/pirateweather/__init__.py create mode 100644 custom_components/pirateweather/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/pirateweather/__pycache__/config_flow.cpython-314.pyc create mode 100644 custom_components/pirateweather/__pycache__/const.cpython-314.pyc create mode 100644 custom_components/pirateweather/__pycache__/forecast_models.cpython-314.pyc create mode 100644 custom_components/pirateweather/__pycache__/sensor.cpython-314.pyc create mode 100644 custom_components/pirateweather/__pycache__/weather.cpython-314.pyc create mode 100644 custom_components/pirateweather/__pycache__/weather_update_coordinator.cpython-314.pyc create mode 100644 custom_components/pirateweather/config_flow.py create mode 100644 custom_components/pirateweather/const.py create mode 100644 custom_components/pirateweather/forecast_models.py create mode 100644 custom_components/pirateweather/manifest.json create mode 100644 custom_components/pirateweather/sensor.py create mode 100644 custom_components/pirateweather/translations/cs.json create mode 100644 custom_components/pirateweather/translations/de.json create mode 100644 custom_components/pirateweather/translations/en.json create mode 100644 custom_components/pirateweather/translations/fr.json create mode 100644 custom_components/pirateweather/translations/nl.json create mode 100644 custom_components/pirateweather/translations/pl.json create mode 100644 custom_components/pirateweather/translations/sk.json create mode 100644 custom_components/pirateweather/weather.py create mode 100644 custom_components/pirateweather/weather_update_coordinator.py create mode 100644 custom_components/scheduler/__init__.py create mode 100644 custom_components/scheduler/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/scheduler/__pycache__/actions.cpython-314.pyc create mode 100644 custom_components/scheduler/__pycache__/config_flow.cpython-314.pyc create mode 100644 custom_components/scheduler/__pycache__/const.cpython-314.pyc create mode 100644 custom_components/scheduler/__pycache__/store.cpython-314.pyc create mode 100644 custom_components/scheduler/__pycache__/switch.cpython-314.pyc create mode 100644 custom_components/scheduler/__pycache__/timer.cpython-314.pyc create mode 100644 custom_components/scheduler/__pycache__/websockets.cpython-314.pyc create mode 100644 custom_components/scheduler/actions.py create mode 100644 custom_components/scheduler/config_flow.py create mode 100644 custom_components/scheduler/const.py create mode 100644 custom_components/scheduler/manifest.json create mode 100644 custom_components/scheduler/services.yaml create mode 100644 custom_components/scheduler/store.py create mode 100644 custom_components/scheduler/switch.py create mode 100644 custom_components/scheduler/timer.py create mode 100644 custom_components/scheduler/websockets.py create mode 100644 custom_components/taskmate/__init__.py create mode 100644 custom_components/taskmate/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/binary_sensor.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/button.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/calendar.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/config_flow.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/const.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_assignments.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_avatars.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_badges.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_calendar.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_challenges.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_chores.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_mandatory.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_notifications.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_points.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_quests.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_rewards.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_templates.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coord_timed.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/coordinator.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/frontend.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/http_calendar.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/http_photos.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/ics.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/intents.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/models.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/number.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/panel.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/photos.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/select.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/sensor.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/storage.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/templates.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/todo.cpython-314.pyc create mode 100644 custom_components/taskmate/__pycache__/websocket.cpython-314.pyc create mode 100644 custom_components/taskmate/binary_sensor.py create mode 100644 custom_components/taskmate/brand/dark_logo.png create mode 100644 custom_components/taskmate/brand/dark_logo@2x.png create mode 100644 custom_components/taskmate/brand/logo.png create mode 100644 custom_components/taskmate/brand/logo@2x.png create mode 100644 custom_components/taskmate/button.py create mode 100644 custom_components/taskmate/calendar.py create mode 100644 custom_components/taskmate/config_flow.py create mode 100644 custom_components/taskmate/const.py create mode 100644 custom_components/taskmate/coord_assignments.py create mode 100644 custom_components/taskmate/coord_avatars.py create mode 100644 custom_components/taskmate/coord_badges.py create mode 100644 custom_components/taskmate/coord_calendar.py create mode 100644 custom_components/taskmate/coord_challenges.py create mode 100644 custom_components/taskmate/coord_chores.py create mode 100644 custom_components/taskmate/coord_mandatory.py create mode 100644 custom_components/taskmate/coord_notifications.py create mode 100644 custom_components/taskmate/coord_points.py create mode 100644 custom_components/taskmate/coord_quests.py create mode 100644 custom_components/taskmate/coord_rewards.py create mode 100644 custom_components/taskmate/coord_templates.py create mode 100644 custom_components/taskmate/coord_timed.py create mode 100644 custom_components/taskmate/coordinator.py create mode 100644 custom_components/taskmate/frontend.py create mode 100644 custom_components/taskmate/http_calendar.py create mode 100644 custom_components/taskmate/http_photos.py create mode 100644 custom_components/taskmate/icon.svg create mode 100644 custom_components/taskmate/ics.py create mode 100644 custom_components/taskmate/intents.py create mode 100644 custom_components/taskmate/manifest.json create mode 100644 custom_components/taskmate/models.py create mode 100644 custom_components/taskmate/number.py create mode 100644 custom_components/taskmate/panel.py create mode 100644 custom_components/taskmate/photos.py create mode 100644 custom_components/taskmate/select.py create mode 100644 custom_components/taskmate/sensor.py create mode 100644 custom_components/taskmate/services.yaml create mode 100644 custom_components/taskmate/storage.py create mode 100644 custom_components/taskmate/strings.json create mode 100644 custom_components/taskmate/templates.py create mode 100644 custom_components/taskmate/todo.py create mode 100644 custom_components/taskmate/translations/de.json create mode 100644 custom_components/taskmate/translations/en-GB.json create mode 100644 custom_components/taskmate/translations/en.json create mode 100644 custom_components/taskmate/translations/fr.json create mode 100644 custom_components/taskmate/translations/nb.json create mode 100644 custom_components/taskmate/translations/nn.json create mode 100644 custom_components/taskmate/translations/pt-BR.json create mode 100644 custom_components/taskmate/translations/pt.json create mode 100644 custom_components/taskmate/websocket.py create mode 100644 custom_components/taskmate/www/fart1.mp3 create mode 100644 custom_components/taskmate/www/fart10.mp3 create mode 100644 custom_components/taskmate/www/fart2.mp3 create mode 100644 custom_components/taskmate/www/fart3.mp3 create mode 100644 custom_components/taskmate/www/fart4.mp3 create mode 100644 custom_components/taskmate/www/fart5.mp3 create mode 100644 custom_components/taskmate/www/fart6.mp3 create mode 100644 custom_components/taskmate/www/fart7.mp3 create mode 100644 custom_components/taskmate/www/fart8.mp3 create mode 100644 custom_components/taskmate/www/fart9.mp3 create mode 100644 custom_components/taskmate/www/locales/de.json create mode 100644 custom_components/taskmate/www/locales/en-GB.json create mode 100644 custom_components/taskmate/www/locales/en.json create mode 100644 custom_components/taskmate/www/locales/fr.json create mode 100644 custom_components/taskmate/www/locales/nb.json create mode 100644 custom_components/taskmate/www/locales/nn.json create mode 100644 custom_components/taskmate/www/locales/pt-BR.json create mode 100644 custom_components/taskmate/www/locales/pt.json create mode 100644 custom_components/taskmate/www/taskmate-activity-card.js create mode 100644 custom_components/taskmate/www/taskmate-approvals-card.js create mode 100644 custom_components/taskmate/www/taskmate-attr-resolver.js create mode 100644 custom_components/taskmate/www/taskmate-badges-card.js create mode 100644 custom_components/taskmate/www/taskmate-bonuses-card.js create mode 100644 custom_components/taskmate/www/taskmate-calendar-card.js create mode 100644 custom_components/taskmate/www/taskmate-child-card.js create mode 100644 custom_components/taskmate/www/taskmate-config-sounds.js create mode 100644 custom_components/taskmate/www/taskmate-design.js create mode 100644 custom_components/taskmate/www/taskmate-family-goal-card.js create mode 100644 custom_components/taskmate/www/taskmate-graph-card.js create mode 100644 custom_components/taskmate/www/taskmate-incentive-card.js create mode 100644 custom_components/taskmate/www/taskmate-leaderboard-card.js create mode 100644 custom_components/taskmate/www/taskmate-localize.js create mode 100644 custom_components/taskmate/www/taskmate-overview-card.js create mode 100644 custom_components/taskmate/www/taskmate-panel.js create mode 100644 custom_components/taskmate/www/taskmate-parent-dashboard-card.js create mode 100644 custom_components/taskmate/www/taskmate-penalties-card.js create mode 100644 custom_components/taskmate/www/taskmate-photo-gallery-card.js create mode 100644 custom_components/taskmate/www/taskmate-points-card.js create mode 100644 custom_components/taskmate/www/taskmate-points-display-card.js create mode 100644 custom_components/taskmate/www/taskmate-reorder-card.js create mode 100644 custom_components/taskmate/www/taskmate-reward-progress-card.js create mode 100644 custom_components/taskmate/www/taskmate-rewards-card.js create mode 100644 custom_components/taskmate/www/taskmate-streak-card.js create mode 100644 custom_components/taskmate/www/taskmate-weekly-card.js create mode 100644 custom_components/thermostat_timeline/__init__.py create mode 100644 custom_components/thermostat_timeline/__pycache__/__init__.cpython-314.pyc create mode 100644 custom_components/thermostat_timeline/__pycache__/config_flow.cpython-314.pyc create mode 100644 custom_components/thermostat_timeline/__pycache__/const.cpython-314.pyc create mode 100644 custom_components/thermostat_timeline/__pycache__/frontend.cpython-314.pyc create mode 100644 custom_components/thermostat_timeline/config_flow.py create mode 100644 custom_components/thermostat_timeline/const.py create mode 100644 custom_components/thermostat_timeline/frontend.py create mode 100644 custom_components/thermostat_timeline/manifest.json create mode 100644 custom_components/thermostat_timeline/services.yaml create mode 100644 custom_components/thermostat_timeline/translations/en.json create mode 100644 custom_components/thermostat_timeline/www/thermostat-pro-timeline.js create mode 100755 go2rtc-1.9.9 create mode 100644 go2rtc.yaml create mode 100644 telink_sensor.js create mode 100644 www/NestLiving.jpg create mode 100644 www/blink.jpg create mode 100644 www/blinkTree.jpg create mode 100644 www/blink_Front.jpg create mode 100644 www/blink_backyard_shed_snapshot.jpg create mode 100644 www/blink_backyard_snapshot.jpg create mode 100644 www/community/apexcharts-card/apexcharts-card.js create mode 100644 www/community/apexcharts-card/apexcharts-card.js.gz create mode 100644 www/community/atomic-calendar-revive/atomic-calendar-revive.js create mode 100644 www/community/atomic-calendar-revive/atomic-calendar-revive.js.gz create mode 100644 www/community/button-card/button-card.js create mode 100644 www/community/button-card/button-card.js.gz create mode 100644 www/community/clock-weather-card/clock-weather-card.js create mode 100644 www/community/clock-weather-card/clock-weather-card.js.gz create mode 100644 www/community/daylight-calendar-card/skylight-calendar-card.js create mode 100644 www/community/daylight-calendar-card/skylight-calendar-card.js.gz create mode 100644 www/community/ha-treemap-card/treemap-card.js create mode 100644 www/community/ha-treemap-card/treemap-card.js.gz create mode 100644 www/community/homie-dashboard/config.js create mode 100644 www/community/homie-dashboard/homie-dashboard.html create mode 100644 www/community/lovelace-layout-card/layout-card.js create mode 100644 www/community/lovelace-layout-card/layout-card.js.gz create mode 100644 www/community/lovelace-layout-card/rollup.config.js create mode 100644 www/community/lovelace-layout-card/rollup.config.js.gz create mode 100644 www/community/lovelace-multiple-entity-row/multiple-entity-row.js create mode 100644 www/community/lovelace-multiple-entity-row/multiple-entity-row.js.gz create mode 100644 www/community/lovelace-thermostat-pro-timeline/thermostat-pro-timeline.js create mode 100644 www/community/lovelace-thermostat-pro-timeline/thermostat-pro-timeline.js.gz create mode 100644 www/community/lovelace-wallpanel/wallpanel.js create mode 100644 www/community/lovelace-wallpanel/wallpanel.js.gz create mode 100644 www/community/mini-graph-card/mini-graph-card-bundle.js create mode 100644 www/community/mini-graph-card/mini-graph-card-bundle.js.gz create mode 100644 www/community/platinum-weather-card/a-clear-day.svg create mode 100644 www/community/platinum-weather-card/a-clear-night.svg create mode 100644 www/community/platinum-weather-card/a-cloudy-1-day.svg create mode 100644 www/community/platinum-weather-card/a-cloudy-1-night.svg create mode 100644 www/community/platinum-weather-card/a-cloudy-2-day.svg create mode 100644 www/community/platinum-weather-card/a-cloudy-2-night.svg create mode 100644 www/community/platinum-weather-card/a-cloudy-3-day.svg create mode 100644 www/community/platinum-weather-card/a-cloudy-3-night.svg create mode 100644 www/community/platinum-weather-card/a-cloudy.svg create mode 100644 www/community/platinum-weather-card/a-dust.svg create mode 100644 www/community/platinum-weather-card/a-fog-day.svg create mode 100644 www/community/platinum-weather-card/a-fog-night.svg create mode 100644 www/community/platinum-weather-card/a-fog.svg create mode 100644 www/community/platinum-weather-card/a-frost-day.svg create mode 100644 www/community/platinum-weather-card/a-frost-night.svg create mode 100644 www/community/platinum-weather-card/a-frost.svg create mode 100644 www/community/platinum-weather-card/a-hail.svg create mode 100644 www/community/platinum-weather-card/a-haze-day.svg create mode 100644 www/community/platinum-weather-card/a-haze-night.svg create mode 100644 www/community/platinum-weather-card/a-haze.svg create mode 100644 www/community/platinum-weather-card/a-hurricane.svg create mode 100644 www/community/platinum-weather-card/a-isolated-thunderstorms-day.svg create mode 100644 www/community/platinum-weather-card/a-isolated-thunderstorms-night.svg create mode 100644 www/community/platinum-weather-card/a-isolated-thunderstorms.svg create mode 100644 www/community/platinum-weather-card/a-rain-and-sleet-mix.svg create mode 100644 www/community/platinum-weather-card/a-rain-and-snow-mix.svg create mode 100644 www/community/platinum-weather-card/a-rainy-1-day.svg create mode 100644 www/community/platinum-weather-card/a-rainy-1-night.svg create mode 100644 www/community/platinum-weather-card/a-rainy-1.svg create mode 100644 www/community/platinum-weather-card/a-rainy-2-day.svg create mode 100644 www/community/platinum-weather-card/a-rainy-2-night.svg create mode 100644 www/community/platinum-weather-card/a-rainy-2.svg create mode 100644 www/community/platinum-weather-card/a-rainy-3-day.svg create mode 100644 www/community/platinum-weather-card/a-rainy-3-night.svg create mode 100644 www/community/platinum-weather-card/a-rainy-3.svg create mode 100644 www/community/platinum-weather-card/a-scattered-thunderstorms-day.svg create mode 100644 www/community/platinum-weather-card/a-scattered-thunderstorms-night.svg create mode 100644 www/community/platinum-weather-card/a-scattered-thunderstorms.svg create mode 100644 www/community/platinum-weather-card/a-severe-thunderstorm.svg create mode 100644 www/community/platinum-weather-card/a-snow-and-sleet-mix.svg create mode 100644 www/community/platinum-weather-card/a-snowy-1-day.svg create mode 100644 www/community/platinum-weather-card/a-snowy-1-night.svg create mode 100644 www/community/platinum-weather-card/a-snowy-1.svg create mode 100644 www/community/platinum-weather-card/a-snowy-2-day.svg create mode 100644 www/community/platinum-weather-card/a-snowy-2-night.svg create mode 100644 www/community/platinum-weather-card/a-snowy-2.svg create mode 100644 www/community/platinum-weather-card/a-snowy-3-day.svg create mode 100644 www/community/platinum-weather-card/a-snowy-3-night.svg create mode 100644 www/community/platinum-weather-card/a-snowy-3.svg create mode 100644 www/community/platinum-weather-card/a-thunderstorms.svg create mode 100644 www/community/platinum-weather-card/a-tornado.svg create mode 100644 www/community/platinum-weather-card/a-tropical-storm.svg create mode 100644 www/community/platinum-weather-card/a-unknown.svg create mode 100644 www/community/platinum-weather-card/a-wind.svg create mode 100644 www/community/platinum-weather-card/editor-92788164.js create mode 100644 www/community/platinum-weather-card/editor-92788164.js.gz create mode 100644 www/community/platinum-weather-card/fire-ban-black.svg create mode 100644 www/community/platinum-weather-card/fire-ban-white.svg create mode 100644 www/community/platinum-weather-card/platinum-weather-card-df6d26e2.js create mode 100644 www/community/platinum-weather-card/platinum-weather-card-df6d26e2.js.gz create mode 100644 www/community/platinum-weather-card/platinum-weather-card.js create mode 100644 www/community/platinum-weather-card/platinum-weather-card.js.gz create mode 100644 www/community/platinum-weather-card/s-clear-day.svg create mode 100644 www/community/platinum-weather-card/s-clear-night.svg create mode 100644 www/community/platinum-weather-card/s-cloudy-1-day.svg create mode 100644 www/community/platinum-weather-card/s-cloudy-1-night.svg create mode 100644 www/community/platinum-weather-card/s-cloudy-2-day.svg create mode 100644 www/community/platinum-weather-card/s-cloudy-2-night.svg create mode 100644 www/community/platinum-weather-card/s-cloudy-3-day.svg create mode 100644 www/community/platinum-weather-card/s-cloudy-3-night.svg create mode 100644 www/community/platinum-weather-card/s-cloudy.svg create mode 100644 www/community/platinum-weather-card/s-dust.svg create mode 100644 www/community/platinum-weather-card/s-fog-day.svg create mode 100644 www/community/platinum-weather-card/s-fog-night.svg create mode 100644 www/community/platinum-weather-card/s-fog.svg create mode 100644 www/community/platinum-weather-card/s-frost-day.svg create mode 100644 www/community/platinum-weather-card/s-frost-night.svg create mode 100644 www/community/platinum-weather-card/s-frost.svg create mode 100644 www/community/platinum-weather-card/s-hail.svg create mode 100644 www/community/platinum-weather-card/s-haze-day.svg create mode 100644 www/community/platinum-weather-card/s-haze-night.svg create mode 100644 www/community/platinum-weather-card/s-haze.svg create mode 100644 www/community/platinum-weather-card/s-hurricane.svg create mode 100644 www/community/platinum-weather-card/s-isolated-thunderstorms-day.svg create mode 100644 www/community/platinum-weather-card/s-isolated-thunderstorms-night.svg create mode 100644 www/community/platinum-weather-card/s-isolated-thunderstorms.svg create mode 100644 www/community/platinum-weather-card/s-rain-and-sleet-mix.svg create mode 100644 www/community/platinum-weather-card/s-rain-and-snow-mix.svg create mode 100644 www/community/platinum-weather-card/s-rainy-1-day.svg create mode 100644 www/community/platinum-weather-card/s-rainy-1-night.svg create mode 100644 www/community/platinum-weather-card/s-rainy-1.svg create mode 100644 www/community/platinum-weather-card/s-rainy-2-day.svg create mode 100644 www/community/platinum-weather-card/s-rainy-2-night.svg create mode 100644 www/community/platinum-weather-card/s-rainy-2.svg create mode 100644 www/community/platinum-weather-card/s-rainy-3-day.svg create mode 100644 www/community/platinum-weather-card/s-rainy-3-night.svg create mode 100644 www/community/platinum-weather-card/s-rainy-3.svg create mode 100644 www/community/platinum-weather-card/s-scattered-thunderstorms-day.svg create mode 100644 www/community/platinum-weather-card/s-scattered-thunderstorms-night.svg create mode 100644 www/community/platinum-weather-card/s-scattered-thunderstorms.svg create mode 100644 www/community/platinum-weather-card/s-severe-thunderstorm.svg create mode 100644 www/community/platinum-weather-card/s-snow-and-sleet-mix.svg create mode 100644 www/community/platinum-weather-card/s-snowy-1-day.svg create mode 100644 www/community/platinum-weather-card/s-snowy-1-night.svg create mode 100644 www/community/platinum-weather-card/s-snowy-1.svg create mode 100644 www/community/platinum-weather-card/s-snowy-2-day.svg create mode 100644 www/community/platinum-weather-card/s-snowy-2-night.svg create mode 100644 www/community/platinum-weather-card/s-snowy-2.svg create mode 100644 www/community/platinum-weather-card/s-snowy-3-day.svg create mode 100644 www/community/platinum-weather-card/s-snowy-3-night.svg create mode 100644 www/community/platinum-weather-card/s-snowy-3.svg create mode 100644 www/community/platinum-weather-card/s-thunderstorms.svg create mode 100644 www/community/platinum-weather-card/s-tornado.svg create mode 100644 www/community/platinum-weather-card/s-tropical-storm.svg create mode 100644 www/community/platinum-weather-card/s-unknown.svg create mode 100644 www/community/platinum-weather-card/s-wind.svg create mode 100644 www/community/scheduler-card/scheduler-card.js create mode 100644 www/community/scheduler-card/scheduler-card.js.gz create mode 100644 www/community/sensor-bar-card-plus/sensor-bar-card-plus.js create mode 100644 www/community/sensor-bar-card-plus/sensor-bar-card-plus.js.gz create mode 100644 www/commute_snapshot.jpg create mode 100644 www/nest.jpg create mode 100644 www/temp/nest_snapshot.jpg create mode 100644 www/thermostat-pro-timeline.js diff --git a/.HA_VERSION b/.HA_VERSION index 6784e1e..3f77693 100644 --- a/.HA_VERSION +++ b/.HA_VERSION @@ -1 +1 @@ -2026.6.3 \ No newline at end of file +2026.7.1 \ No newline at end of file diff --git a/.cache/brands/integrations/asusrouter/dark_logo.png b/.cache/brands/integrations/asusrouter/dark_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5ba9daed54097626e967476ba35b8f3bd9031224 GIT binary patch literal 7911 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 Z(`dy5o(7BO!M`B}Kom6<>f|iK{txDIE$;vT literal 0 HcmV?d00001 diff --git a/.cache/brands/integrations/blink/logo.png b/.cache/brands/integrations/blink/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..be8fdedc2c8a5b0886934568e7c304045bd1c6df GIT binary patch literal 15466 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 Z(`dy5o(7BO!M`B}Kom6<>f|iK{txDIE$;vT literal 0 HcmV?d00001 diff --git a/.cache/brands/integrations/climate_scheduler/logo.png b/.cache/brands/integrations/climate_scheduler/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4451c38e717bb33973b6f39be665b15b9aa574ae GIT binary patch literal 13114 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 literal 0 HcmV?d00001 diff --git a/.cache/brands/integrations/generic/logo.png b/.cache/brands/integrations/generic/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..69a2a575a6b53fb809b4045a6493b3d9cc26abef GIT binary patch literal 924 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fm9dz$e5NNHeVd!La%#h+Opx z%my+bB0m|He}#&y0V?lO&}1+PX8=tA8Vl62>L60eKeTn(@bHD7I_|V7NpdiHp zCL}$+JpKIUs7?9(^KEP|80F;qt`e62X1Y(>`fC5n%HG_%y}LdIKE8Et`zCxpBG{q)1%Pe1(q^uynxA9a>B`(^gU^WE2{mT-7^eNn^TXMYc0 z?Yhjl#?+mGNuYs&MS+2l!vQbzz$&&I#T5GcaJENT#qJHVx%&?>&+5O>5w z)-@lP!YUZG_fSW8>oZo1t)^dC|Ae;Jtv=WvZ`Z58A)sE3d#i=rtjrx}U!7MrwW?lr z?bDNIhYkJnUKM?PcI~ZU|GHPF^1h~41wYuc`AfGu`Jcu70&MTz{*` t`ultS@A~uk_xXAIEd;PcHc03|{XfPzHGM_yfvsmid{0+Dmvv4FO#oobs^I_t literal 0 HcmV?d00001 diff --git a/.cache/brands/integrations/google_generative_ai_conversation/logo.png b/.cache/brands/integrations/google_generative_ai_conversation/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ceac1e84aed6a4d2f75ad4d2888428f9e90ece0d GIT binary patch literal 37470 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

DP-$HprOOp2B52I`G>w#y`4A6<~+O z49Mw5l&XQ@WTaHTE>jT>9;M0Knrw~M zPRhOX=6psJ#QTkJRYl9sIuDzNyPwvH77@7iuC?fXk5s}MK?N(;kv0b@9{L})th{62 zA)!L!oaPA-goP75yQI8y&-?PKq!I_4#QW}j;f-%%+%iFeBUxYvNdSrkG9(WbdODyh z0)hbr#iAt0pglrtl2!q4rlh9Tc5Nizcx!E~DE#(K;oq*ulJGrFL}153pw|5iD3&$Y z>0Z9<5YTsRItOHH7Crxg;}w4BwPi+RR%MJ}KXjqI@K=kpi;d?xVjrOTd|bL*_R}wGsG0bBIM>|Y&<{VwKqpqtCqmAGN}-6aGR+ApLlf4)So)~ zyGc5`dV|4*ZUX1$xYS`ifn+pm&Lznbjj6!1L6Dn!>`INVF66`AP3q^&PLRjbibZNp zQ`Kw5BpL-Oi4w7KcssrbS>EqM_Qa@WhIT9DaTk!3)rGEOBf;<^2I^6m3ch3wVUoji zDAEaUClZvb21MO48^qhc0HQZIPM2p5)}UBo(2hg8FUc>WR!@t}`=)N$}6pkdti z1{KqdGPH1@3Vi*hpwZbRXm=K!UG9Kq{unRSM{6s#_Z#)^Q-UI+HCqpGhT{34vEk@ zy_Im>KJ`_D;M3Nomqg?m(}7HUa=vuJ+JQARAK8u~EEI$@T^Xq^h1gmu94h~JNARk3*maZpXPb zLf1R@w1AKgplfXu3s)&&xpK+C1gx?DAKg;6r`!9-^h1(y;!Hu zA}f)h3&+ao+d;a*eZm;J-iB$wJMZA^Vsv82N5{W!;75J{_L?! zbOQRd8>$c;{GVI~Sp*x8;W``5*ugeXiUqIJt=tA(%z{L$({sguK(W372)?r^m{KzG zHEo^-J<$7(Q@8wfZMeQ5#wI0XkoE0;WJDl+&u))WzNVI6-Ss6iKssO;y2(~ zU~|$0WJgWvmvi7H$#=V)86sLeYXbz;;8#J~zYm4(OoRJ80v=ukIt5D?#&Ukz>xb0C zX%V0JXJAUKY*M0cbEZL<*#jIr?{cm`@Ex1s89yC__NRs7NqCebu?->j4f_uTSzmM) zy6tG)52y%vMQ$^99W3$DM;2`cq-EQn_p8J=Z$U~?flO4iYlTed7Y?;SOD!Ltk(^ji z`n5>Jw7VnIzB(#4b`hg1E(>xstY4RYs;+eXL48C-dyVRv?GNgGO0%;XiRNWvQ~xqx zJ;)U4{3Axic4CUVLC*jD6yUO@9Ip(dG)s~EoDQV%umh+hqF#apK&J9vUHsXD%c+Jz zvXT653MxAYyE@R$u&@XWdPqr2=ejn`=Lv|~H>C!^1^mocEYGM5pT-i=)v1&1zB z9)6cCH3}(sWx*i-2NSM_F6InrcGEg|)=vb5@AkH%l2w0SU0v%W_uviPRjBm6n$X%q zi*m!&B%rUI;LZ<}8bt=vs&Z-4 zbP)x~zt=22^^aF0(<1&VtGpYSslMtLXI}7`pzp)?gi6cJzMq`lt_ew0>6qM%ugts5 zPe!b~v(lh%He5#2K8#ak^hVgrzbNNxd1x;c4I#9_wh}J^fsmkn;{z`br2UG zRKM=Bw2!RjH=SVhOCcF$8N3`509)Tz2vKJt&~d&LA7_xs=m2Dzssj2mWay(sRmh6C z>90TbOQ18^PVWEhk)&veVmZRlTUbsCuB7tarhxAmAX-Ze1d>;jI300(OMkN9r4ZG= zXfDMZ=@vQq9{a{dlNmEiLy-@}A$@Wa!yVPSTth7z>KqoDZ-g zmsGU-`w^lCa!J!rPD6b@1ShN%fbfl}gB2mhUm7CLRmBOXhYRrUV zXeESSYw`-4^&3U8k!298>t{MD{qVSAEj*bvrK)emTk$+G^P(q33^`V>3S zPOR!EW?>?>P$?$*ab%Z_{&nw^yS#ufkKEI{=o1TJe!8cm1ARm4?HRzJm&-8OpxV#D zMKvp&bqhvP6$fTM{@5jN8CZ~wQ~}>0D;!=7-n|ola(`7V%J#kiuG%K`|1Q_p1q^Va z{xfN+yCBufgKjMIMnEhl&`6ECO-?h=KB$V^r+0Lio3wxkzYsiYRu|Rms zZ5P{|*iQ*#?VSOeg9@iY2By_o%x)>%_|1?`_WKV4(bU9gB1?Vr{nx@m(cB6b3X);G z8ZC14`^v_$usCtY4r|*bFY@qFEKQDMThM%@3a|W|n&EO&3a9UsmhveZHWQ+?{3D^p zest;LQ0#ltlE`O8jYGn}BxE_?<`@%r+trI1-^uAAII;on&SO-u=P@u$l#ST}!9wVs z6Sf!SkJDQr7ds7DW0zi_&ScWQiDh|vG4|{-4k~8UfE;E<`=qYU`_rPbn=7ayD!*NQ zTF)C&UQiB`R}llzY!H!&yl&*uD?q<*$Olgn+LTU+uQdcT>XmLmPm>}rdCMiw?>T@t zzs05}-BXF@b&YBhSeGN0ywCmnI0cgw0Wi8=!kxa}Yzk&3@iG7>)H$1SMl5-Y2H#$> zZadxlQmFYS4cK^S1frjEN>Vhfraiv(N88-*?}l=$eXq}le1>cHEaixR_Q=`QGgk5$ zLx+N*pRV`8)PaGP_cKq!$4`ow^p+KW4D92vi`U?5CWz{(f4*p(KR^3A$z$v*#e*xPH$qUH(8Rr9*W;`qxEcAkE`mNr_SUQEpCjAUj4r4n< z=X{=DlFRk^ddKvmu61e9t?nz?4B(6>L$L#Jck6nw`8zpJ;CpitvE`Xc^UG6C)vfo@ zFvlJfMV};M<*u2``ky`=&ZTRAVXB^TAH3J=3q)*F>;`F0l*0*l zXDk^q1RZ}L+M1wc?lNkkZdR&+Dq5B!&Khy!JQl)YoPk#4wkGp~K=_DM4Fg-o} z5fN1eUD*KpwRU|_O5V4xa{Wg%fXE*vF6-0OPWYHQH zV|ROK5S4{{R0zqP7~|m7MnhiaUyo9dYvc|Tf*&khF~}4~q+F~|Q6s;L;LpV;+Kvb| zk-wGqZ?wY;dU7_Wi*CGfAQslwwLNg-l$?FV9nkkHJ6z4_s5!(Y7fX^cte$679NCO$ zE>lC;Dvl&2Wcid2@b!-L+$Og$3iOK=ieLJ=-5_~^rClux5Z~N;K%cda1DV97dmHZ3 zMty4nk`Lu1iC97Woo|S}{M4mpQ;_?^0GLp>fp&&fblJ{Pn5Z1B7ZU;no+Svp+^W(< z8(Lhe8vyYJxwnU?EaOKmqaEgg+-cI z5%H58aX3Jh{z27A=mBo4fC0@K(ns}NQUD;kKvd95WRgvKRXV?|?J^Bi7@K;i4lw)4 zQna69mJY+2`WEZy@OIOhrTxhyl3i&=hQap9UFr8lCY53>>$beYRXyqlCz-2z7FKxi1Cr+c zUXw=hcHL1}Wfd+lEq$Y{*r#XAX~(9&5O1W<+GtcRRdiZqZ3{TxSz1xkf5q(VqTROd zvdx@Sxt0AKuW(^j=cd&=P?3a9u=uWK$AI$5sq{a;nJ)zUN4!;QS<)tT?!LaCtTyNw zuQIt0dBFDyoE@?s!OL}P?I$M?c5-sO`Eo=Hoxr8l%@(_)qu(K!b-EGz4Nq_VcD{Sd z-Dv%44|n$xyqyWCtVV?OC1~x-pt4$U4R>NHCi-r!j`^B#1$xG)hUaCiX5ZfCX#VMU zGr8rZbtlIP_+^ac=$=>shs|TV^Wo#O^ZQhr*@-9P?pCKgE@yqAtP)J{yp-cq*M|ET z*3mHW$u`^5b3^&Pw6Kie zZhFIW4;P)X%zJ%>#R(d^9-0dgaZwdEst28^{fxfneV&MOPNp@PH&QzqP-N0mMC`c! zsG?@Yxx`IvWug^nF57tV+1r%5f~uEiXHZJ>ckS4kjYfufk(ldL4$U}xA9qEG`2qX? zIrK-6TQlXb2I}n11M2OaEFh6v2QG(A|+g5|; zLuDCKj^n|EADOrpcWNisH8d!_eP#+VzaD!WeIp~Q^!Yuvr#GkM^jVPT*Q4fbpxTYP zTb_D(c*sN|fgze&^H2xA=&+vnv1BLF_qJnT8*M|>t&1V zX?!Yv^A`5EI)1w+a?)5Wk363~RX*m}=R>@BVTg>f#n)^Sy!$97NBU(=R^YNw+ijh< ztxylownef+vU?|_&-m?jBG^r)&0+qJk6(VVsgguWZoe36U!@sIB10a=HK=Tms2+N4 ze8=8c+RESWbh`-r6Mi{f0i^)Y!ahWnwmS_EHvh6zTa9=L`${5K8mW6yAA?Ud`;Md6 zQ;CT4af89uot#VJrAGvHAPDal1Cq!687^}e#a-%Q> z#-4409Sl_VVQVr+Z&lZV=SMK=R3Y}tEiiFFTZ`>Fa@PcY?8k6H3Yz8NtSw4ae+lHv9 zMTMr-bRuAPX16LVF_Ta>uW5ufB7ZZR@~*U{%%jYF1u?r(MU@1e)N&hf!weI`#P@0H z_D>3W7nEBB5@vD-Ob%~q$XQ~N4M|2Dg*<(d`w#$+GlN^JMtdum*2ozV4*;mNEVW5?>h;zgQ zJ7PM(64)Ij?7 zi414}4R~I=^-vB+B?}t2%0{EFxG=VB@Ca2oa6MATc1-tCj#TtkAUm4Zp&Lnh5M}Oa4h!tWzE+72Mx-B%(pE zLkYW#L~cWzZ^JG>ATK*mG$(rE1c*R+&{vTzRYeDYS{rXeot_3c%RpY580Q8Nr1Xxv z$pdt1Vsdh_@@+WqeYbg}Ua{&!1wnRhAX=l5^VWX#ZRG8|8=H<08OR(OHBaaqA~6q79u%3b975|2eeumLR`%pD;bgNB8t8e7A-t3! z^%ei#JXLUo33AO|iwE1gDjSFr=@L1t;eW`&#HFC}{|vWM&~kojMlbV$@*1(a0^d7wR)N@+ zqF2|~Pxb06-dR88|CrgsewmWez3#jp{H!mIx-{Ov4V!`*;tA}c& z99IFU=~Ahf=sFCm4VmZ2 zSqJo&_&*m`r;M~207IK@M14y@9zQuif8AQlM6{#2Q2y=!-*z&vhksbAByl`N@35U@ ztHfZ#2npYWvnA&v54Y?M?9yuojS!#a(Fwf=KdJ+y42v4qXV(;e01eYJSX&0OtBszW zCkc*ZCd_lX94pUUP&v7oIv^|FyvyLRllH(qjqYfkuKqOl8^6VN!uzy^ryY|!l`Dd6 z=e_nN>uTg?XKoCmYMn>-bM6LFv0e77zciuL$Sy72dUtQHV!#DQGIjFYkz`u=JynVOY7Ya}|p^y;8BQMNKz`G#Ni3*fy|81>r>W2^`1v z@f>Z8O3wHXYV-nF72?x?8w~eX-5R}FlJ+lb#$w& zBJ%ikvvmHR<$9@U`R)07E4l0(o+&zL!eAA1>P-I{c3^8|?kwc{9&^Of? ziK-ai)23*M7)^WY}CCC4}iwEZ{UPCN%2*L>}XC< z1()LWg*cSIOfWC<_!6)FYoOb{C&H*7m33u@1mUP#o&oIj0bqz8y~hfGQ%|lOwZZtO ze;-2v2^N$fpHXM3h*$@V@3({P#+&WsNFV5ZpT;|E`H3fR9UUE|z+#!3K-b#Dn=cDf zBI-UPX$LIr8&va$2*3_dBU%%Y6Zl6Utx~oJb<%{cV|wdR z5v%!n>j~B4fSA{{3y{KjF#)2zMS3>d9HsduwLyf`LiP)eY^NRu2VR&`15ip*o!-Sb zw7~2%;3@3W!T!Dj=^M9yv}8Mzc(?czD;z~a44dHBk- z=EjGI@b#vqCJKtB1TfrH<5}A_-9cQ4!`f_sYHQ(Z#oZa$jk1=5DV&#_`dIXJ#SgPnUpW327vYncXFIf8%;3! z5v#WWT8CQ#_2D1Mgti7?btj+(W6d!M4#z0s#Y~&!V&GK$dMZ$yqz;}+;0#i|${xYd ztzakK{MIU<0Cu4W+xYXxH(m2j!@>E%hA!K90LG!gx4~fUc6)hYRWmv_3)%JzZcOAn zdga2X8@Ft!{{z2tWW?Z=A!3s{z*U=IG;8z>>ALCgVY+4H`hznd2Z0lRyG-S^7l*Syl`@Z^w(lYyHk=%8%ony@r%R1FZ{0~kH50YZE&3)LJpp907HA6p zEOx@dC|*c8OlIm|3B}d{2W$UCiSFoMly=X53$v_cf(yv}%rPw5#bZ`xq<4uSKTl%L zCu*L&DZYk@ixPatO{<9f3ZJWk)eBGa$&umnvWr&T*EGwEsI-^jtP-z@*VJDmwj8fn zZ?ZY{9!I`=l6)Es$vqA=&qZm=DmF5ok z(dKmia!w1@6q`kCdzB(4ws#?&o1I!IX4&7Q?zzk*x-f8gVsXkJ)Wx9Qz^-}sl;aZH z8Y#h`_pI@5;m_kae01m;1p#283*VbvyGe(-514wu|JwupUygZ6u$WlmUedvH#g-Wa zBje37U|G8WIXYv60=bk8#C80r`!sEozSr8vY*GXL!75Cz(5M8k52FW)ZGZ@*+s(gsLX^UiMsUotT=p3MNGJ2NpI7jCmgM1+wV+$mMSMU)&#ebM55Jb5zIq+a*pzr zeOmcVO~88IO41(h?2w}@I%!g?IEWx&N?B=0iueW0Hn!b|fi$LI>yLcC&57=8jMJ4 z4{`uf0ztCE1T{JUaXe!4y?o{;Aqf)X-v0iq7$!#4JCa*d7Bu;}EgCl?iW*zyEOew5iHOdkKl z`_}8_C>Pkapml?@W6Rih7nM4`&6y-1^f=7m>2Gfe7A%nqL;6fbl0(qoa9r=T!7u9V zYHaS+XW~9E;!vAPm5_)LC?_9jiqA-n>F>UzO~oRLx?gFLUn+k0ZNE@t(k#_Bt^;S5 zCFU>PXdbS~(~0L;-v$+wedBA#e5juuF_j?z+tY?8?38hqu!^?LOw|i>eIh6s0^lTAay+Xz^ChaE!bz1d++!t+JWV0FdSj?cXsi{F~&`luYk zY2vqb!pWZ0iWka(eXav0?eDZbkKPHJCDBjc%U>oo-$&q$ zfhOsK-@yLngpr>E&|FzbOG(Azt7mq17+hIsDcvBUd!pf#5XgO-0)=8Jn^=!iiMQCa z=-=snY;0^CgiFd#hJk~>O<~@7laftO58zk#Vsg3l)ZqNC3{8}JuzfcTPvifI7V zMlq50-3A=0Xef6zs6x!8iH)Bag1`XLfMXFtcM-$DseC^;Ieb^aRRhFF^;w!|#?QG= zAUz~1`I}y39w&^SeC1Hkn|k9S&!JFh2}FGv0Ieufxyj~h%h;1J_&O8pDt!ZFifF}n z3(iXU+ql5L6}*&kT(>wy%kxyj-I6~28qNwtIcx;l8SZ@TgS|cF2{e~^JcyH8!BYzp z#Dy3vtHuY5c>}taw&+cT!P!i|X-w+9z&Gp=LSERprn|Af-p@W2hr%K|W5=hY2VXhQ zP%1h0DO8iK8Ojxwf9BdWU+syojiOIb`3M2<3B(p@fT;X47NoUTo8~CxqH%!=-K{*F zF!mc#f3Dy2HVgi7!S5Qr%hF=YwOr;aVQgtm{fKw(uD9|N8)Gq@5F5&+I0w;-zYZ|iMzV9 zcvS|uc~J{sog}`EI7gXDaSG5U-ab#R?ZeJyK`ko+qH>QLaI{Gw1h$hsxyK1BMa&Vo8qE^8QTQ? zbr7=%iX4iz3%a8a&l1*g`~U>NgLv@C>-ppeOg~6-LGn=Se4-}T;D-)n5LQwO@g%9k zMjaifauRblJcJP#8wBmg1nW5;$8!XI8nacqx`TCgczYifyQE-cMmAe zg1n|v)u)J@L}i54hxZKElzz5sSh`3lU#cX0>n&rP<26<@4Wl~FU-w|EiTUy5(m)J! zxUV<7&esoF_&y&NF06#wv41y`-liwxy5Hpt1jG?OlxB^KuSR8sx9(hXyD|Ny;)Svf zA{lu07>T-jr)=Vv4!QGV)#CGP=AWr1*4Ry4Sc2Nbh4)hYWZqY0MFyZJneJhm);OmY zWL$hLypm~`sdPU9D1`zk1%T2!AV5xSbk7uY3LP*-= z`}YB0G(8s9X=Q$^Hh{TLITcPkdleKWHOBihQZVv4q?Q|Cc$4120ErH0mteoRv19XBI$RfSM>_!L1bc_v^;`6?u=u{u`>}Be$OOUFD;@~X&_C!D8&1;AY3(&Wc zoSd9o+$d8K$BBL(gDh7F!(Q#GK|F@{*v~fB6v-*mT@S6J9{vlRv;T*T$TXFgJ3T!$ z2QeQ=)lLF;SB()-5rkCu;habQAUmK9nmiqfIhM!)P^6TtE)fN`eb}k|+607)-W2>= z4Cr~zkr4&YZ~igCeCAJcyj~1A8~amY@i!hIWn`4SeZv4UjTKO}*#ZCaXU#thkXGoR z=0P49!u(8~#hmxYbS8jYOtSXy==mgQyBvCYpH+olI&`yUYFFY`%>h9{DCB}3m%^p| zE8DKH=NN)>s;;QAN=G=_k)&hfBM0L$ojH&XC?7xBRftaqNl32BwCg~h)KSMZ(|rVd zoZ0g;<S2xx2K0Q(DGp(xbkOl&n+DqU?YwnV( z)(Sy7aptTjTNPowKWP{1;Cb&$vaj00ks5+?p4HMZjtw~Y4;l*A8#4xFzC zsQpLj=eT^ zv$nQ&pf~0&F>E@JU*fZzuLm>wCGnf8NL&KXf~5Xs)>6#4(T!jSr5+^BsIDV>i-A;8 z3ZO;mL|iOGPm$1nh^QDCPo9}*7wpJ>wT{EnOwgt(<(TXPu0Tm z&+$UGL9i|FR}l?Axty^&LEf^Kvx!2{Z*S4!3oz=hD3yVi zB&L{o3No^cxxR;@TG?PnRe|pOP%I(U<`j^`od7Hj&=uIo)wWJ=m?=gObT}AYsr#>r z$85!6b-uov3E-dIIu|Ugot)(8z%sh#UjH(%9U_b1B#*ZIh!&>xnQJT@) z-?;AFO_D2+M$;h63}*M&OS<+}>`@2qGP1)&t4SjF&n_tDJx3m1th5eIZg)>)H7EVa zQhbPJdhaZX$FNo@c5W^*vu?9>giQ8~82(g`A&mJ9L;WP}MOX{XYX8`G3SYK?ND&G~V7N16< z|A(pfj;H#6%b-;v-t%qN3?3VgTi_lEmg+66|dK?H94K? zfiU0>BqD5=ZI2(q-?k9N-F;yf#J0M+`bL%HRzLQMewA4OB~(h}TKeg0?B?dCbL+8M z7+7`zhzx3gCow}?WlY?tR7yxX(*&dr4i4-qymSKpJVUH_zkUs(c6(HT3h6eq6&9nJ z7;Wb;)>-}$+-g9R=Ih#O7Ia|!xx+RJyr_q^mo^|E6zeZXlOgZVrJQ(0^3EfSBT8S$ zqlWOgl+!gL5%%@r&*V)5nC~l^49yr-CHhz2a<0Ucy5fr%2kONz{ofB*ZLLll2PSKU zD4%+>C+V5<`3;?M51zS75Ia)+yD${RNGYC6iGgGlO6#}GKYuoC$D6M*K*nym_L?iIlEh#I z4&t~goHg3s0D$g1?5V{iJV8yP;!rixzDQsWAw4`XaaH_4IpXM=8vfT7rl5?b9D{4eb#p= zxIK%%z+t-2%ynKZ(b|+IllcFNh@V_b?#?0pH#_#b|Ec>@yjZ4AR$+BmgFgdbdcb_! zpiUUjKPk5vC~=O7^?E#bvCPk=Jd*4%v!69}8U1CLF;6qJI#S4(l^qT?etYWC%fGn+izucTRa z(RX1#k}*y@(`*|C>1vRDfEnn8RqCMc1topY06}1%Y7<~K;li2 z+*w=eJ-Via%$#i8USvT{WUHIWF1CGQ%+4{YndrHtnGC$Cmp-2sL6}6Sv)B|Zi1+=r zweNlF4L;kr;l5#0&Cq{FY{a8kN$g?EJJ0Zw@Z)#uz5PB?5{1IbG-s}6%Is`#v-W^}0}wr~>N%JaCv&GqL|ZU#l}u8MG+}BH5Ou!L8Dqg$BeoFbik`1&v?$?(Wa2 zDO#vD_jyrZJGjjl?+kih!|{Z_!v*GeHYX(iMYx-oyy|6b#s?Fg7>VtrGgya!q{*qc zt})}~oc$!hAHv_V2~x@r_X)NoWbzdQw3$0@afmv)3ji;Cu=LbK4HyzJntb`=+kYUC z=gx~}+?$?WVt0DYllkvJa*Oxd;eW30m$_PBgkwndBS83C7~4P7(@6mVRVCnXnD{*I zfRj&TF^Bx!|DU!38+nIu#7NBM1p`zHSm2Yso%maLJ`lcreGvuSaX%MHS8m1*{L9a( zm+Je|KVagAUuivQDwe)4fFIr zK0if&LU6V?G}y0lSDj87+C2nPl%q}JfoDf;mm+?f-isUaZFkQ|yt>#m58cKh1$}b{ z3ExUn09l83@X}sL$ zam>)o5n)r@_q`8;!epX`f9#n>KG0+%vC0+*t3ue?w#}mF@(;I&g%73>FF&!{r}lr z;u7YQ;NYI|?$da`Y?w)h!Ad0+AJKy$3o27ByC%}J<9nr7b)vmO)5^KR7h%@^IiSj| zZ8z*$LQ}oMKR)$I?is7UYKN%jZ-gr|e*E~gjq$1Ibn>fm^6vaFu^jj&yU`BlwzC}m z+h?}r3zoj@Ad!)9V)N5rM~q_I>7Qjo2}5sw{nGb#M0krpV0&I)gTVH3E8uwc7*H(+ zz#YjA_|PX{CQFke-0JU?tpPO!00F6o%q}3h6Hw{A51jMsvPYd15WIc^X^lgd5YZx~ zL~9aw)!juoQfM7ZbP~E@(t;bO(r6NBN#A4ORB@$&*l|he$b$Is8`nF3OY#g`eu~&3 z{0k&^9EjR0;MxCToJx5+Yz!EqXmSXykSZ0#@4o%BD%BZB`<$W}lI^m+loOqa-+80} zn8ZztI|HtcgLh6oKL98nsccQ#JwFp*i3|k5Z~wQj#=%gQl|P#8=L{mI|I%z%)db>T zqFJNZqH^O4N>X+tgtOi-rye_F8-%fDDW%Bwp<;uyyA?^%yXG}y0r`&M(Z>V0S02Scv;ZA5 zs|5{1#Y`p{n%qnO=#aI8U!7eiPJ^b*q}BZ+kkkpIhX0B$?V!4kWBuaOp1FJe+Wwxi zr(vQc;K9whg0nvP8DedM2K6iv)U#ydmB+PO$ikm*_e1-?B|2Nzv5tZw^*rmWw9_t& zPoO_G<c(anYr^}5zqQ(R4(amBA5q8NWZMyirB6;?is)0MxNvVqGMlisxbCcrtmDVt08>v6CJqS&3_?#FN<5mCQz@xG%8ahcbX zv$k?s#9m~MTE#VTri%@ns}#_(p`cTT9JCHP_l$#Fbjt`*!Ww`Smg@1(@&M;MvZ@DSWsSBx0cOsT0Njm+ejj z=34OId@lX?e-@#&6Z{PY9FxF)5Af5TTFev~UP%k=KYSqOD%q4Wzw-ZTUXlZ+zG8R4 zl)w!h!}Yqj!{cL_ICMN?-ZPZFCZrj@FO8_pS8IsCb0+*t7JLqIX33MmIQXtuW14}Q zFsBPU0UBPQLjv$klHe$-P~PGXI}s8S!2&)&7tof448~ zbEpN$1Ox00E4DV4RM@!I$#e+;}Sitdv~zw?tXU> z07?pgFrl41vnG?9owLh_JRvs!H&v!Mx5_!{?-J8DpLBc8 zeK(iI+WpYI{!2*cuAwvUOlW@iPwDA?3W?!Mn4A4nEg@;cuT%AIf~KkvB-4REU@(MVI?2qJvxL1VrOQWpGaoCpyIO9G;L|J#NGGzY)- zmin)Xlbc98NNlTrTSMmY|0bRT!O?Fp;$@$5JzgOPy})-YNU2no#riMN$h>*)vLiS> zjW|^)ySXBF&5Q%#>e1skz(Jnl`FGdXJ&$Sc~ zUN(SY3^a7MAg#V7=IoY}BE8SZPW@5-VietQkV%d@+oXlu@{a0j(bEq=BZLj@3va6wN=v>S~hD^_Gez>`8ZPHS4pETge4SZnAgC2rh)Rc zIXqIba3ed4Ob(Q18p#P)ag$tLHmH(WK3{2H{h49beibREXSDlaDUJT>R4@AYa(shy z{-T@2?0DtYsg7~}S;H;Vys?455WE4#Z9mPJ7w$aJtMzkzTB?Dbah@fu{e2L5clwY0 z>+aN;2GIPru8{!LX^*JZwvY73``ZcXcTm{Z#yfB^(>u?zKV4%7K=Ero<|MkE2rTca z(nXP|wPH)GB0^z z7f;kdL{~Xc;8IwBF*(J8$XKxlvfaNUF{gQ`g(N=dlu@#FWb`YBco8RGoLJ; z!7s}PQ~<`b7XXP5-HrSP^sF|3L46?8i*3Jq9i8>)#*0Ga`i4r$0QlGIfp74BkNZmD z2eT%{FF~S$oV~=53Htp~hS8Dw_@Y2jRev=xym*5|Hjsc#A1cffwA$sSHGSdpFA%89DkKvlTn~<R|)v|T*Z{~l%XGhOa40nYVQ;KE%-pQ zXRmj&$!S-BdEnAj;G3c{&41hN-jkkdws}qw$S#pCN``)x*H#OA6e21gS<8CvOMDBL z4gM_n%{RS#(NJW`eQfx073sqB_Iap!6fDpMN7ApQ%s=%WpQ(as?7!mme}AGC2^Rj0 z|4G%^78ez>ZnEQ0F1-5=WfVrX$obJ34NDV`Y|6W6+i|P&CV@V5o?c*l`t;>A4IN!m zDmuxY?|mY2+|#lhm@2YR_6E2|w~&39C$@4Z#3KsmmR^}amPV1eBBBB#Mfl}8kC`pq zgA`ARETw@F1-8g!G26i0hKAS3*in;-tg`Qs&3H3+ekT#Pc3Zu@-mJ$CC1v~*c8~3&?`-qVnAzOEHNIL8KD78O<}0E zM+9G=#VwlrY1(ea3R1|c(B^Cl5T`YA;zmUpOQYzam9oBhs|WN&LV1SAFv3tV>F?5u>A4UZb|cVWuS6XKmtc2Y>9{ z^*TQqvV7tX{YOBuO~L!s?G~=>k!(`;7f9AR*Y@#y(repHwB`ecjRtZs2EXV!0GKHzfT ziD}*H>ch&j8TThTL&ZPW3$cyDz zk7aD*K&_a>ssH76fy8Toq@-Un3^vo23v;028RgYI2mv(EE%YfAC*3*a_+fQd4$heOwag&?bOc+ zCS5?tsLnjdmNj)p(82fNfO+Su_U!(2{pCLFF|3eCegdh7_l!P3u%PVmf*(BWX*5(h zb@jl$DajXlr2cx)Agnz-9Q*_o9xH8;gCVg<)AN>=L|jW zfg4TR;0FD`Ao^%n)RpaUA!sb)QK0pWfj#^J!n_b$!_7Nx%&$@@2ybzH)gBwS7W%Gs zKexOmwtqSod&5dhNWBXy~OM9)d-$mB*fN_#*b-n06-ChO_cN z1F>2nBPI;_0N$<{&xw-CEpvJmk8B-Ji}!qHNBI3t^|Es;$gT%I&6iFQPY5sFq9D|uy))!D?WuStc8_BI@lPTDF|zwX_sIqsGQOH;DEW+JG7d^nKSM_MQg z2{)tTH!^XQ3_Z=4_K4$8(@5^63*wBL2m1nyvIc{+8aZy2;z_%bJAt;9CGUO_h{KY4 z)tYdbtkR&~e>NpwTgSGH;je?y-{r3y5vGcLgTSp|bl9B95z z0(Wf+LSIoQalrPM>BPk#;MU(E4#s@r&K{A#B!ClMOcE2Z(K)!x?mR)7FW7psv0d=H6? z_(t*b*tlM%I?EcK;PBq4ki}vOopRr&bm%8mtArG+*OI1KR(|i1CCQM8(!F?C?Y6Gr z^TfB^Q=%nKftMu3V!GK#lcaGy(2y|}OTGQ_OQ7fC`s+4=qBMbsn_CFZ>6DR{dqqol znTZjyKN=YC(_>+i_wfCo$Yv$fL!}p@#4uzY^HYe?>e!Xaa4fxo5Pq(bGDK&xcV^Q> z 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 literal 0 HcmV?d00001 diff --git a/.cache/brands/integrations/intelligent_heating_pilot/logo.png b/.cache/brands/integrations/intelligent_heating_pilot/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..098cccfc7f716c0d7243acd534ba6f6a81970f9c GIT binary patch literal 77624 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

VX*{5ZyIcGYOIjx?Htx7gem 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 Hc6|7M@RS3m literal 0 HcmV?d00001 diff --git a/.cache/brands/integrations/midea_ac_lan/dark_logo.png b/.cache/brands/integrations/midea_ac_lan/dark_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..33adb36ff46b19d386e0f5315623687ab8c528f7 GIT binary patch literal 28356 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 W45|;wfFf$ny-<_|%altQ2mTL^e&0s` literal 0 HcmV?d00001 diff --git a/.cache/brands/integrations/nest_protect/dark_logo.png b/.cache/brands/integrations/nest_protect/dark_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..20507c6e57cb9e1f296fd2fbbc473740fa845ed4 GIT binary patch literal 15439 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 literal 0 HcmV?d00001 diff --git a/.ha_run.lock b/.ha_run.lock index 54d43ec..ce2b266 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 71, "version": 1, "ha_version": "2026.6.3", "start_ts": 1781553089.670326} \ No newline at end of file +{"pid": 71, "version": 1, "ha_version": "2026.7.1", "start_ts": 1783446559.6159952} \ No newline at end of file diff --git a/automations.yaml b/automations.yaml index df79d91..58f9590 100644 --- a/automations.yaml +++ b/automations.yaml @@ -189,7 +189,7 @@ hours: 0 minutes: 45 seconds: 0 - below: 1 + below: 0.2 id: freezer_down - trigger: numeric_state entity_id: @@ -198,7 +198,7 @@ hours: 0 minutes: 20 seconds: 0 - below: 1 + below: 0.2 id: fridge_down conditions: [] actions: @@ -303,14 +303,12 @@ mode: single - id: '1781034724187' alias: 'Office: Turn Off Lights on Exit' - description: Turns off office lights 2 minutes after the door closes, but only if living - room motion was detected after the door shut. + description: Turns off office lights 2 minutes after the door closes, but only if + living room motion was detected after the door shut. triggers: - trigger: state entity_id: binary_sensor.office_door_contact to: 'off' - for: - minutes: 2 conditions: - condition: state entity_id: light.playroom_light @@ -319,6 +317,8 @@ value_template: '{{ states.binary_sensor.basement.last_changed > states.binary_sensor.office_door_contact.last_changed }}' actions: + - delay: + minutes: 2 - action: light.turn_off target: entity_id: light.playroom_light @@ -708,44 +708,471 @@ entity_id: sensor.coffee_maker_power above: 1000 id: coffee_started + conditions: + - condition: state + entity_id: input_boolean.coffee_maker_running + state: 'off' + actions: + - action: input_boolean.turn_on + target: + entity_id: input_boolean.coffee_maker_running + - action: notify.notify + data: + title: ☕ Coffee Time + message: Someone is making coffee. + - action: notify.persistent_notification + data: + title: ☕ Coffee Time + message: Someone is making coffee. + - delay: + hours: 0 + minutes: 4 + seconds: 0 + - wait_template: '{{ states(''sensor.coffee_maker_power'') | float(0) < 350 }}' + timeout: + hours: 0 + minutes: 5 + seconds: 0 + - action: input_boolean.turn_off + target: + entity_id: input_boolean.coffee_maker_running + - action: notify.notify + data: + title: ☕ Coffee Ready + message: Your coffee is ready to enjoy! + mode: single +- id: '1781793117883' + alias: Basement Temperature Change + description: '' + triggers: + - type: temperature + device_id: 3e67afd88a4b4d17552c8f370aaf40b5 + entity_id: 64689573a8b11a83829efcae0022d7c1 + domain: sensor + trigger: device + above: 15 + below: 10 conditions: [] actions: - - choose: - - conditions: - - condition: trigger - id: coffee_started - - condition: state - entity_id: input_boolean.coffee_maker_running - state: 'off' - sequence: - - action: input_boolean.turn_on - target: - entity_id: input_boolean.coffee_maker_running - - action: notify.notify - data: - title: ☕ Coffee Time - message: Someone is making coffee. - - action: notify.persistent_notification - data: - title: ☕ Coffee Time - message: Someone is making coffee. - - delay: - hours: 0 - minutes: 4 - seconds: 0 - - wait_for_trigger: - - trigger: numeric_state - entity_id: sensor.coffee_maker_power - below: 350 - timeout: - hours: 0 - minutes: 3 - seconds: 0 - - action: input_boolean.turn_off - target: - entity_id: input_boolean.coffee_maker_running - - action: notify.notify - data: - title: ☕ Coffee Ready - message: Your coffee is ready to enjoy! + - action: notify.notify + metadata: {} + data: + title: Basement Temperature Change + message: Basement Temperature Changed! mode: single +- id: '1782161891986' + alias: 'Notification: Pool Sensor Low Battery' + description: Alert when the pool temperature sensor battery drops below 15% + triggers: + - entity_id: sensor.pool_temperature_battery + below: 15 + trigger: numeric_state + conditions: [] + actions: + - data: + title: Pool Sensor Battery Low + message: The pool temperature sensor battery is currently at {{ states('sensor.pool_temperature_battery') + }}%. Please change it soon. + action: notify.persistent_notification + - action: notify.notify + metadata: {} + data: + title: Pool Sensor Battery Low + message: The pool temperature sensor battery is currently at {{ states('sensor.pool_temperature_battery') + }}%. Please change it soon. + mode: single +- &id001 + id: commute_morning_traffic_alert_map + alias: 'Commute: Morning Traffic Alert with Map' + description: Checks commute times at 7:15 AM and attaches a map if traffic is bad. + triggers: + - at: 07:15:00 + trigger: time + conditions: + - condition: or + conditions: + - condition: template + value_template: '{{ states(''sensor.google_travel_time_work_work'') | float(0) + > 35 }}' + - condition: template + value_template: '{{ states(''sensor.waze_travel_time'') | float(0) > 35 }}' + actions: + - action: camera.snapshot + target: + entity_id: camera.camera_commute_map + data: + filename: /config/www/commute_snapshot.jpg + - action: notify.notify + data: + title: "\U0001F6A8 Commute Delay Alert!" + message: "Traffic is heavier than usual this morning. \nGoogle Maps: {{ states('sensor.google_travel_time_work_work') + }} mins. Waze: {{ states('sensor.waze_travel_time') }} mins." + data: + image: /local/commute_snapshot.jpg + - action: notify.persistent_notification + data: + title: "\U0001F6A8 Commute Delay Alert!" + message: "Traffic is heavier than usual this morning. \nGoogle Maps: {{ states('sensor.google_travel_time_work_work') + }} mins. Waze: {{ states('sensor.waze_travel_time') }} mins.\n![Commute Map](/local/commute_snapshot.jpg)" + mode: single +- *id001 +- *id001 +- id: '1782999027626' + alias: 'Commute: Morning Traffic Alert' + description: Pushes an interactive, live traffic map to my phone before the commute. + triggers: + - at: 07:15:00 + trigger: time + actions: + - action: notify.notify + data: + title: Morning Commute Traffic + message: Swipe down to view live traffic along your route. + data: + push: + category: map + action_data: + latitude: '45.474668' + longitude: '-73.737158' + shows_traffic: true + shows_scale: true +- id: '1783000087983' + alias: Morning Commute Alert + description: Notify when commute is longer than 35 minutes. + triggers: + - trigger: time + at: 07:15:00 + conditions: + - condition: time + weekday: + - mon + - tue + - wed + - thu + - fri + - condition: or + conditions: + - condition: numeric_state + entity_id: sensor.google_travel_time_work_work + above: 35 + - condition: numeric_state + entity_id: sensor.waze_travel_time + above: 35 + actions: + - action: notify.notify + data: + title: "\U0001F697 Morning Commute" + message: "Google: {{ states('sensor.google_travel_time_work_work') }} min\nWaze: + {{ states('sensor.waze_travel_time') }} min\n{% set g = states('sensor.google_travel_time_work_work') + | float(0) %} {% if g > 50 %} \U0001F6A8 Major traffic delays. {% elif g > + 40 %} ⚠️ Heavy traffic. {% else %} \U0001F7E1 Moderate traffic. {% endif %}" + mode: single +- id: '1783049020714' + alias: Aqara-Magic-Cube-T1-Pro-(CTP-R01) - Franco + description: '' + use_blueprint: + path: SirGoodenough/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml + input: + topic: zigbee2mqtt/Ziggy + action_slide_face_1: + - action: light.toggle + metadata: {} + target: + device_id: 2f8c5ab2ca0d4be31919fba6df365813 + data: + brightness_pct: 25 + action_doubletap_face_1: [] + action_rotate_cw_face_1: + - action: script.cube_dimmer_control + metadata: {} + data: + light: light.playroom_light + angle: -20 + action_rotate_ccw_face_1: [] + action_flipped90_face_1: [] +- id: '1783162325964' + alias: Backyard Shed Camera Motion AI Description + description: Takes a snapshot when the Blink camera detects motion, analyzes it + with AI, and sends a mobile and persistent notification. + triggers: + - trigger: state + entity_id: binary_sensor.backyard_shed_motion + to: 'on' + actions: + - action: blink.trigger_camera + target: + entity_id: camera.backyard_shed + - delay: 00:00:02 + - action: camera.snapshot + target: + entity_id: camera.backyard_shed + data: + filename: /media/blink.jpg + - action: camera.snapshot + target: + entity_id: camera.backyard_shed + data: + filename: /config/www/blink.jpg + - delay: 00:00:02 + - action: ai_task.generate_data + continue_on_error: true + data: + task_name: Backyard Shed Camera Analysis + entity_id: ai_task.google_ai_task + instructions: Describe what you see in this image in brief. Focus on any people, + objects, animals or activities. Text needs to be max 240 characters. + attachments: + - media_content_id: media-source://media_source/local/blink.jpg + media_content_type: image/jpeg + response_variable: ai_profile + - action: notify.notify + continue_on_error: true + data: + title: Motion Detected - Backyard Shed + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }}' + - action: notify.persistent_notification + data: + title: Motion Detected - Backyard Shed + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }} + + ![Snapshot](/local/blink.jpg)' + data: + notification_id: backyard_shed_motion + mode: queued + max: 10 +- id: '1783293479398' + alias: Blink Camera Fast Update Cycle + description: Polls the Blink cloud every 30 seconds to bypass the default 5-minute + delay and force instant motion detection state updates. + triggers: + - trigger: time_pattern + seconds: /30 + actions: + - action: homeassistant.update_entity + target: + entity_id: + - binary_sensor.backyard_shed_motion + - binary_sensor.front_door_motion + mode: single +- id: '1783304677475' + alias: Motion Detection AI Tree + description: Takes a snapshot when the Blink camera detects motion, analyzes it + with AI, and sends a mobile and persistent notification. + triggers: + - trigger: state + entity_id: binary_sensor.backyard_tree_motion + to: 'on' + conditions: [] + actions: + - action: blink.trigger_camera + target: + entity_id: camera.backyard_tree + - delay: 00:00:02 + - action: camera.snapshot + target: + entity_id: camera.backyard_tree + data: + filename: /media/blinkTree.jpg + - action: camera.snapshot + target: + entity_id: camera.backyard_tree + data: + filename: /config/www/blinkTree.jpg + - delay: 00:00:02 + - action: ai_task.generate_data + continue_on_error: true + data: + task_name: Backyard Shed Camera Analysis + entity_id: ai_task.google_ai_task + instructions: Describe what you see in this image in brief. Focus on any people, + objects, animals or activities. Text needs to be max 240 characters. + attachments: + - media_content_id: media-source://media_source/local/blinkTree.jpg + media_content_type: image/jpeg + response_variable: ai_profile + - action: notify.notify + data: + title: Motion Detected - Backyard Tree + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }}' + - action: persistent_notification.create + data: + title: Motion Detected - Backyard Tree + notification_id: backyard_tree_motion + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }} + + ![Snapshot](/local/blinkTree.jpg)' + mode: queued + max: 10 +- id: '1783387792679' + description: Takes a snapshot when the Blink camera detects motion, analyzes it + with AI, and sends a mobile and persistent notification. + triggers: + - trigger: state + entity_id: binary_sensor.backyard_shed_motion + to: 'on' + conditions: [] + actions: + - action: blink.trigger_camera + target: + entity_id: camera.living_room_living_room_camera + - delay: 00:00:02 + - action: camera.snapshot + target: + entity_id: camera.living_room_living_room_camera + data: + filename: /media/blink.jpg + - action: camera.snapshot + target: + entity_id: camera.living_room_living_room_camera + data: + filename: /config/www/nest.jpg + - delay: 00:00:02 + - action: ai_task.generate_data + continue_on_error: true + data: + task_name: Backyard Shed Camera Analysis + entity_id: ai_task.google_ai_task + instructions: Describe what you see in this image in brief. Focus on any people, + objects, or activities. Text needs to be max 225 characters. + attachments: + - media_content_id: media-source://media_source/local/nest.jpg + media_content_type: image/jpeg + response_variable: ai_profile + - action: notify.notify + data: + title: Motion Detected - Backyard Shed + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }}' + - action: persistent_notification.create + data: + title: Motion Detected - Backyard Shed + notification_id: backyard_shed_motion2 + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }} + + ![Snapshot](/local/nest.jpg)' + mode: queued + max: 10 +- id: '1783452320698' + alias: Nest Test Motion + description: Takes a snapshot when the Blink camera detects motion, analyzes it + with AI, and sends a mobile and persistent notification. + triggers: + - trigger: state + entity_id: binary_sensor.backyard_tree_motion + to: 'on' + conditions: [] + actions: + - delay: 00:00:02 + - action: camera.snapshot + target: + entity_id: + - camera.front + - camera.backyard_shed + data: + filename: /media/nest_Living2.jpg + - action: camera.snapshot + target: + entity_id: camera.backyard_tree + data: + filename: /config/www/blinkTree.jpg + - delay: 00:00:02 + - action: ai_task.generate_data + continue_on_error: true + data: + task_name: Backyard Shed Camera Analysis + entity_id: ai_task.google_ai_task + instructions: Describe what you see in this image in brief. Focus on any people, + objects, or activities. Text needs to be max 225 characters. + attachments: + - media_content_id: media-source://media_source/local/blinkTree.jpg + media_content_type: image/jpeg + response_variable: ai_profile + - action: notify.notify + data: + title: Motion Detected - Backyard Tree + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }}' + - action: persistent_notification.create + data: + title: Motion Detected - Backyard Tree + notification_id: backyard_tree_motion + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }} + + ![Snapshot](/local/blinkTree.jpg)' + mode: queued + max: 10 +- id: '1783469453396' + alias: Motion Front Door - AI Description + description: Takes a snapshot when the Nest camera detects motion, a person, an + animal, or a doorbell press, analyzes it with AI, and sends a mobile and persistent + notification. + triggers: + - trigger: event + event_type: state_changed + event_data: + entity_id: event.front_door_front_door_motion + conditions: + - condition: template + value_template: '{{ trigger.event.data.new_state.attributes.event_type in [''motion'', + ''person'', ''animal'', ''chime''] }}' + actions: + - action: blink.trigger_camera + target: + entity_id: camera.front + data: {} + - delay: 00:00:02 + - action: camera.snapshot + target: + entity_id: camera.front + data: + filename: /media/blink_Front.jpg + - action: camera.snapshot + target: + entity_id: camera.front + data: + filename: /config/www/blink_Front.jpg + - delay: 00:00:02 + - action: ai_task.generate_data + continue_on_error: true + data: + task_name: Nest Front Camera Analysis + entity_id: ai_task.google_ai_task + instructions: Describe what you see in this image in brief. Focus on any people, + objects, animals or activities. Text needs to be max 240 characters. + attachments: + - media_content_id: media-source://media_source/local/blink_Front.jpg + media_content_type: image/jpeg + response_variable: ai_profile + - action: notify.notify + continue_on_error: true + data: + title: Motion Detected - Backyard Shed + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }}' + - action: notify.persistent_notification + data: + title: Motion Detected - Backyard Shed + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }} + + ![Snapshot](/local/blink_Front.jpg)' + data: + notification_id: nest_front_motion + mode: queued + max: 10 diff --git a/blueprints/automation/SirGoodenough/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml b/blueprints/automation/SirGoodenough/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml new file mode 100644 index 0000000..e534c45 --- /dev/null +++ b/blueprints/automation/SirGoodenough/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml @@ -0,0 +1,1882 @@ +blueprint: + name: Aqara-Magic-Cube-T1-Pro-(CTP-R01) - 2024-06-04 + author: SirGoodenough + description: "This Blueprint uses a Zigbee2MQTT built sensor to sort out the 58+ + unique commands (118+ total) available from the Xiaomi Magic Cube Remote. (Some + unique commands listed as + are not counted & available thru templating only. + See the related document.)\n**NOTE:** This blueprint is for the PRO cube version + ONLY and is not fully compatible with the original version of the cube. Links + for those are in my [GIT repository](https://github.com/SirGoodenough/HA_Blueprints).\nThis + cube has an 'Action' mode (Shown below with this graphic: \U0001FAF3 ACTION MODE + ONLY \U0001FAF3 )\nand a 'Scene' mode (Shown below with this graphic: \U0001F440 + SCENE MODE ONLY \U0001F440 ).\nMake sure the cube is in the mode you think it + is in when triggering it. The functions are separate. IE 'Action - slide' will + do something different from 'Scene - slide'.\nThe split out of functions gives + you the ability to assign local scripts or functions to do the things you want + the remote to do.\nFunctions that are left empty will simply do nothing.\n#### + \U0001F34E There is a set of 36 + 18 actions that will trigger on specific actions + on specific sides that are listed as **Group 1 \U0001F34E**\n#### \U0001F352 There + is a set of 6 + 3 actions that will trigger on specific actions on *ANY* side + that are listed as **Group 2 \U0001F352**\n#### \U0001F350 There is a set of 30 + + 30 event functions that will trigger on cube flips to & from specific sides + that are listed as **Group 3 \U0001F350**\n#### \U0001F355 There are 5 actions + (shake & throw for each mode & hold in scene mode) that only occur once in each + mode & are OK to be combined with any other group in that mode. Listed as **Group + 4 sections \U0001F355**.\nPlease be aware that ALL actions except the 4 listed + above, \U0001F355 will trigger an action in **ALL 3 groups in the matching mode + at the same time** every time. Therefore I suggest if you just have a couple of + things you want this remote to do that you choose the *ANY / Group 2 / \U0001F352* + events. Action events and Scene events are separate from each other.\nIf you + want more than a few events, you should select actions in **Group 1 / \U0001F34E + OR Group 3 / \U0001F350**.\nWith careful selection you can use mixed groups, but + you run the risk of a single cube action triggering more than 1 Home Assistant + action and making a mess of things \U0001F371.\n\n#### NOTICE: This cube *can* + be triggered 124 ways, but only 38(+54) of them are unique (with templating).\n##### + This blueprint has been known to freak out when there are spaces or odd characters + in the MQTT Topic. Make sure there are no spaces, ONLY a single word, and ONLY + A thru Z, a thru z, and 0 thru 9 in the topic. (``` / ``` is ok between device + and topic) If there are, you will need to change the name of the cube to remove + those characters.\n### IF YOU SEE --> TemplateError: Must provide a device or + entity ID\nIf you get an error like that, The friendly_name in Z2M likely does + not match the friendly_name on HA. To fix go into the Z2M ```Open web UI``` and + set the friendly_name there. Setting this in just HA or in Z2M without ticking + the update HA box will cause this. See the blueprint documentation page for details.\n\nThere + is sample code to make the template sensor in the help file on GitHib named [Zigbee2MQTT + - Xiaomi Cube Controller MQTT Triggered.md](https://github.com/SirGoodenough/HA_Blueprints/blob/master/Automations/Zigbee2MQTT%20-%20Xiaomi%20Cube%20Controller%20MQTT%20Triggered.md) + & in the [community page related to this](https://community.home-assistant.io/t/zigbee2mqtt-xiaomi-cube-controller/393203).\n\nWithin + this blueprint there is an event handler that will latch the last command that + the blueprint finds and sends that to the event buss. From there a simple Template + sensor can grab it and show you the last action sent. Thie will help when setting + up new functions and to troubleshoot strange behaviours.\n\n> This was forked + from https://community.home-assistant.io/t/z2m-xiaomi-cube-controller/263006 1.2 + project authored by luckypoppy and the friends he pulled together to create the + base. I sincerely thank Him (Them) for their work. I felt there needed to be more + documentation for rookie users to properly set this up. I had quite a few questions + and when I saw a few questions in that chat from people struggling, I wanted to + help. I also had a better idea for troubleshooting info.\n\n[Community link for + this blueprint](https://community.home-assistant.io/t/zigbee2mqtt-aqara-magic-cube-t1-pro-ctp-r01-xiaomi-lumi-cagl02/525111)\n\n## + \U0001F5FFNotice\U0001F46E\U0001F3FF‍♂️:\n* Copies of the original Blueprint that + were converted via the 'Take Control' feature or other means are officially not + supported by me.\n\n* I may or may not be able to support you when you have a + problem after you make changes to my code, as some of the code is no longer mine.\n\n* + I & my license also require attribution as a link back to the original should + you use this code in your own creation.\n\n* [Here is a link to my license & the + original github post](https://github.com/SirGoodenough/HA_Blueprints?tab=License-1-ov-file) + expected to be followed & referenced as attribution should you use this code elsewhere.\n" + source_url: https://github.com/SirGoodenough/HA_Blueprints/blob/master/Automations/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml + domain: automation + homeassistant: + min_version: 2024.6.0 + input: + required_input: + name: "Required input \U0001F440" + icon: mdi:alert-box + collapsed: false + description: "This blueprint has been known to freak out when there are spaces + or odd characters\n in the MQTT Topic. Make sure there are no spaces, ONLY + a single word,\n and ONLY A thru Z, a thru z, and 0 thru 9 in the topic.\n + \ (``` / ``` is ok between device and topic)\n\n If there are other characters, + you will need to change the name of the cube to\n remove those characters.\n\n + \ ### IF YOU SEE --> TemplateError: Must provide a device or entity ID\n\n + \ If you get an error like that, The friendly_name in Z2M likely does not + match the \n friendly_name on HA. To fix go into the Z2M ```Open web UI``` + and set the \n friendly_name there. Setting this in just HA or in Z2M without + ticking the update HA\n box will cause this. See the blueprint documentation + page for details.\n" + input: + topic: + name: Topic + description: "The main MQTT Topic for your cube. \nDetails on finding the + correct topic can be found here: \n[Find my Cube Topic](https://github.com/SirGoodenough/HA_Blueprints/blob/master/Automations/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.md#to-make-the-blueprint-work-it-will-need)\n" + default: zigbee2mqtt/? + selector: + text: + multiline: false + multiple: false + optional_conditions: + name: "Optional conditions\U0001F913" + icon: mdi:alert-octagon-outline + collapsed: true + description: "This input was added by user request. With it you can add your + own condition.\n If you only want the cube to work like this in the morning,\n + \ then after 6pm you want it to do different actions, add a condition that\n + \ will only let this run from 6am until 6pm. Then have another copy of\n the + blueprint that will only run from 6pm until 6am. (for example)\n\n Any valid + conditions you can dream up, you should be able to add here.\n\n Leave it + as the default of you don't want to add a condition of your own.\n" + input: + additional_conditions: + name: Additional conditions + description: "Extra conditions you may want to add to this automation \n(Example: + Home occupied, TV on, etc)\n" + default: [] + selector: + condition: {} + action_side_1_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 1 Group 1 actions \U0001F34E" + icon: mdi:numeric-1-box + collapsed: true + description: "These are Group 1 actions that end with Face 1\n (the one with + the 'Aqara' LOGO) of the cube up to start.\n" + input: + action_slide_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_doubletap_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_flipped90_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 1" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_flipped180_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 1" + description: '**NOTE:** This action is trick to master, and should be used + sparingly... Face 1 is the one with the ''Aqara'' LOGO' + default: [] + selector: + action: {} + action_flip_from_any_to_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 1" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_rotate_cw_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_rotate_ccw_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_side_2_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 2 Group 1 actions \U0001F34E" + icon: mdi:numeric-2-box + collapsed: true + description: 'These are Group 1 actions that end with Face 2 + + ' + input: + action_slide_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 2 up" + default: [] + selector: + action: {} + action_doubletap_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 2 up" + default: [] + selector: + action: {} + action_flipped90_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 2" + default: [] + selector: + action: {} + action_flipped180_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 2" + description: '**NOTE:** This action is trick to master, and should be used + sparingly...' + default: [] + selector: + action: {} + action_flip_from_any_to_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 2" + default: [] + selector: + action: {} + action_rotate_cw_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 2 up" + default: [] + selector: + action: {} + action_rotate_ccw_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 2 up" + default: [] + selector: + action: {} + action_side_3_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 3 Group 1 actions \U0001F34E" + icon: mdi:numeric-3-box + collapsed: true + description: 'These are Group 1 actions that end with Face 3 + + ' + input: + action_slide_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 3 up" + default: [] + selector: + action: {} + action_doubletap_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 3 up" + default: [] + selector: + action: {} + action_flipped90_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 3" + default: [] + selector: + action: {} + action_flipped180_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 3" + description: '**NOTE:** This action is trick to master, and should be used + sparingly...' + default: [] + selector: + action: {} + action_flip_from_any_to_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 3" + default: [] + selector: + action: {} + action_rotate_cw_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 3 up" + default: [] + selector: + action: {} + action_rotate_ccw_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 3 up" + default: [] + selector: + action: {} + action_side_4_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 4 Group 1 actions \U0001F34E" + icon: mdi:numeric-4-box + collapsed: true + description: 'These are Group 1 actions that end with Face 4 + + ' + input: + action_slide_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 4 up" + default: [] + selector: + action: {} + action_doubletap_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 4 up" + default: [] + selector: + action: {} + action_flipped90_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 4" + default: [] + selector: + action: {} + action_flipped180_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 4" + description: '**NOTE:** This action is trick to master, and should be used + sparingly...' + default: [] + selector: + action: {} + action_flip_from_any_to_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 4" + default: [] + selector: + action: {} + action_rotate_cw_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 4 up" + default: [] + selector: + action: {} + action_rotate_ccw_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 4 up" + default: [] + selector: + action: {} + action_side_5_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 5 Group 1 actions \U0001F34E" + icon: mdi:numeric-5-box + collapsed: true + description: 'These are Group 1 actions that end with Face 5 + + ' + input: + action_slide_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 5 up" + default: [] + selector: + action: {} + action_doubletap_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 5 up" + default: [] + selector: + action: {} + action_flipped90_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 5" + default: [] + selector: + action: {} + action_flipped180_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 5" + description: '**NOTE:** This action is trick to master, and should be used + sparingly...' + default: [] + selector: + action: {} + action_flip_from_any_to_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 5" + default: [] + selector: + action: {} + action_rotate_cw_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 5 up" + default: [] + selector: + action: {} + action_rotate_ccw_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 5 up" + default: [] + selector: + action: {} + action_side_6_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 6 Group 1 actions \U0001F34E" + icon: mdi:numeric-6-box + collapsed: true + description: 'These are Group 1 actions that end with Face 6 + + ' + input: + action_slide_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 6 up" + default: [] + selector: + action: {} + action_doubletap_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 6 up" + default: [] + selector: + action: {} + action_flipped90_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 6" + default: [] + selector: + action: {} + action_flipped180_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 6" + description: '**NOTE:** This action is trick to master, and should be used + sparingly...' + default: [] + selector: + action: {} + action_flip_from_any_to_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 6" + default: [] + selector: + action: {} + action_rotate_cw_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 6 up" + default: [] + selector: + action: {} + action_rotate_ccw_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 6 up" + default: [] + selector: + action: {} + action_any_side_group_2_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Any side Group 2 actions \U0001F352" + icon: mdi:fruit-cherries + collapsed: true + description: "\U0001F352 These are a set of 6 event functions that will trigger + on specific actions\n on *ANY* side. **Group 2 actions \U0001F352**\n" + input: + action_slide_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Slide on + any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_doubletap_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Double + tap on any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_flipped90_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Flip 90 + degrees to any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_flipped180_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Flip 180 + degrees to any side" + description: "NOTE: This action is trick to master, and should be used sparingly... + Side doesn't matter on this one.\n**!!Warning!!** The automations for + the specific side will also trigger. \U0001F34E\U0001F350\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_rotate_cw_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Rotate + CW on any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_rotate_ccw_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Rotate + CCW on any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_1_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 1 Group 3 actions \U0001F350" + icon: mdi:numeric-1-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 1 as the ending + point.\n All are flips from another side to side 1.\n" + input: + action_1_from_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 1 from side 6" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_1_from_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 1 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_1_from_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 1 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_1_from_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 1 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_1_from_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 1 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_2_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 2 Group 3 actions \U0001F350" + icon: mdi:numeric-2-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 2 as the ending + point.\n All are flips from another side to side 2.\n" + input: + action_2_from_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 2 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_2_from_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 2 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_2_from_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 2 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_2_from_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 2 from side 5" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_2_from_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 2 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_3_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 3 Group 3 actions \U0001F350" + icon: mdi:numeric-3-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 3 as the ending + point.\n All are flips from another side to side 3.\n" + input: + action_3_from_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 3 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_3_from_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 3 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_3_from_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 3 from side 4" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_3_from_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 3 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_3_from_6: + name: "G\U0001FAF3 ACTION MODE ONLY \U0001FAF3 roup 3 \U0001F350 Flip to + side 3 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_4_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 4 Group 3 actions \U0001F350" + icon: mdi:numeric-4-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 4 as the ending + point.\n All are flips from another side to side 4.\n" + input: + action_4_from_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 4 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_4_from_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 4 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_4_from_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 4 from side 3" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_4_from_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 4 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_4_from_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 4 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_5_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 5 Group 3 actions \U0001F350" + icon: mdi:numeric-5-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 5 as the ending + point.\n All are flips from another side to side 5.\n" + input: + action_5_from_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 5 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger. \U0001F34E\U0001F350\nI + suggest using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_5_from_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 5 from side 2" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_5_from_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 5 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_5_from_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 5 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_5_from_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 5 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_6_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 2 Group 3 actions \U0001F350" + icon: mdi:numeric-6-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 6 as the ending + point.\n All are flips from another side to side 6.\n" + input: + action_6_from_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 6 from side 1" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_6_from_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 6 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_6_from_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 6 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_6_from_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 6 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_6_from_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 6 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + group_4_actions: + name: "Group 4 actions only used once (no side) \U0001F355" + icon: mdi:pizza + collapsed: true + description: "These are Group 4 actions only used once (no side) \U0001F355\n + \ they can be safely compined with any other action mode command.\n" + input: + action_shake: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Shake the cube \U0001F355 + Group 4" + description: 'This trigger only occurs once in the set-up. + + It can be combined in any group.' + default: [] + selector: + action: {} + action_throw: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Throw the cube \U0001F355 + Group 4" + description: ' You don''t actually ''Throw'' the cube. The action is as + follows: + + > Pick up the cube firmly. + + > Make a throwing motion with it but do not motion back towards yourself. + + > Hold it there for a second. + + > The throw trigger should be sent to Z2M. + + + This trigger only occurs once in the set-up. + + It can be combined in any group.' + default: [] + selector: + action: {} + scene_side_1_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 1 Group 1 actions \U0001F34E" + icon: mdi:numeric-1-box + collapsed: true + description: "These are Group 1 actions that end with Face 1\n (the one with + the 'Aqara' LOGO) of the cube up to start.\n" + input: + scene_flip_to_face_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 1 from any other face" + description: Face 1 is the one with the ''Aqara'' LOGO' + default: [] + selector: + action: {} + scene_rotate_cw_face_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + scene_rotate_ccw_face_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + scene_side_2_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 2 Group 1 actions \U0001F34E" + icon: mdi:numeric-2-box + collapsed: true + description: 'These are Group 1 actions that end with Face 2 + + ' + input: + scene_flip_to_face_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 2 from any other face" + default: [] + selector: + action: {} + scene_rotate_cw_face_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 2 up" + default: [] + selector: + action: {} + scene_rotate_ccw_face_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 2 up" + default: [] + selector: + action: {} + scene_side_3_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 3 Group 1 actions \U0001F34E" + icon: mdi:numeric-3-box + collapsed: true + description: 'These are Group 1 actions that end with Face 3 + + ' + input: + scene_flip_to_face_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 3 from any other face" + default: [] + selector: + action: {} + scene_rotate_cw_face_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 3 up" + default: [] + selector: + action: {} + scene_rotate_ccw_face_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 3 up" + default: [] + selector: + action: {} + scene_side_4_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 4 Group 1 actions \U0001F34E" + icon: mdi:numeric-4-box + collapsed: true + description: 'These are Group 1 actions that end with Face 4 + + ' + input: + scene_flip_to_face_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 4 from any other face" + default: [] + selector: + action: {} + scene_rotate_cw_face_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 4 up" + default: [] + selector: + action: {} + scene_rotate_ccw_face_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 4 up" + default: [] + selector: + action: {} + scene_side_5_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 5 Group 1 actions \U0001F34E" + icon: mdi:numeric-5-box + collapsed: true + description: 'These are Group 1 actions that end with Face 5 + + ' + input: + scene_flip_to_face_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 5 from any other face" + default: [] + selector: + action: {} + scene_rotate_cw_face_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 5 up" + default: [] + selector: + action: {} + scene_rotate_ccw_face_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 5 up" + default: [] + selector: + action: {} + scene_side_6_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 6 Group 1 actions \U0001F34E" + icon: mdi:numeric-6-box + collapsed: true + description: 'These are Group 1 actions that end with Face 6 + + ' + input: + scene_flip_to_face_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 6 from any other face" + default: [] + selector: + action: {} + scene_rotate_cw_face_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 6 up" + default: [] + selector: + action: {} + scene_rotate_ccw_face_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 6 up" + default: [] + selector: + action: {} + scene_any_side_group_2_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Any side Group 2 actions \U0001F352" + icon: mdi:fruit-cherries + collapsed: true + description: "\U0001F352 These are a set of 6 event functions that will trigger + on specific actions\n on *ANY* side. **Group 2 actions \U0001F352**\n" + input: + scene_flip_to_side_any: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 2 \U0001F352 Flip to + any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_rotate_cw_any: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 2 \U0001F352 Rotate cube + CW with any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_rotate_ccw_any: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 2 \U0001F352 Rotate cube + CCW with any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_1_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 1 Group 3 actions \U0001F350" + icon: mdi:numeric-1-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 1 as the ending + point.\n All are flips from another side to side 1.\n" + input: + scene_1_from_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 1 from side 6" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_1_from_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 1 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_1_from_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 1 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_1_from_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 1 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_1_from_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 1 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_2_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 2 Group 3 actions \U0001F350" + icon: mdi:numeric-2-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 2 as the ending + point.\n All are flips from another side to side 2.\n" + input: + scene_2_from_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 2 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_2_from_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 2 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_2_from_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 2 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_2_from_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 2 from side 5" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_2_from_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 2 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_3_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 3 Group 3 actions \U0001F350" + icon: mdi:numeric-3-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 3 as the ending + point.\n All are flips from another side to side 3.\n" + input: + scene_3_from_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 3 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_3_from_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 3 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_3_from_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 3 from side 4" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_3_from_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 3 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_3_from_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 3 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_4_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 4 Group 3 actions \U0001F350" + icon: mdi:numeric-4-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 4 as the ending + point.\n All are flips from another side to side 4.\n" + input: + scene_4_from_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 4 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_4_from_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 4 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_4_from_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 4 from side 3" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_4_from_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 4 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_4_from_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 4 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_5_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 5 Group 3 actions \U0001F350" + icon: mdi:numeric-5-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 5 as the ending + point.\n All are flips from another side to side 5.\n" + input: + scene_5_from_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 5 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger. \U0001F34E\U0001F350\nI + suggest using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_5_from_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 5 from side 2" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_5_from_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 5 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_5_from_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 5 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_5_from_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 5 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_6_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 6 Group 3 actions \U0001F350" + icon: mdi:numeric-6-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 6 as the ending + point.\n All are flips from another side to side 6.\n" + input: + scene_6_from_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 6 from side 1" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_6_from_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 6 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_6_from_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 6 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_6_from_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 6 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_6_from_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 6 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_group_4_actions: + name: "Group 4 actions only used once (no side) \U0001F355" + icon: mdi:pizza + collapsed: true + description: "These are Group 4 actions only used once (no side) \U0001F355\n + \ they can be safely compined with any other scene mode command.\n" + input: + scene_hold: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Lift up the cube and hold it + still \U0001F355" + description: 'This trigger only occurs once in the set-up. + + It can be combined in any group.' + default: [] + selector: + action: {} + scene_shake: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Shake the cube \U0001F355" + description: 'This trigger only occurs once in the set-up. + + It can be combined in any group.' + default: [] + selector: + action: {} + scene_throw: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Throw the cube \U0001F355" + description: ' You don''t actually ''Throw'' the cube. The action is as + follows: + + > Pick up the cube firmly. + + > Make a throwing motion with it but do not motion back towards yourself. + + > Hold it there for a second. + + > The throw trigger should be sent to Z2M. + + + This trigger only occurs once in the set-up. + + It can be combined in any group.' + default: [] + selector: + action: {} +triggers: +- trigger: mqtt + topic: !input topic +variables: + action: "{% if trigger.payload_json.action is defined %}\n {{ trigger.payload_json.action + }}\n{% else %}\n none\n{% endif %}\n" + angle: "{% if trigger.payload_json.action_angle is number %}\n {{ trigger.payload_json.action_angle + | float(0.0) }}\n{% else %}\n 0.0\n{% endif %}\n" + side: "{% if trigger.payload_json.side is number %}\n {{ trigger.payload_json.side + | int(0) }}\n{% else %}\n 0\n{% endif %}\n" + op_mode: "{% if trigger.payload_json.operation_mode is defined %}\n {{ trigger.payload_json.operation_mode + }}\n{% else %}\n none\n{% endif %}\n" + topic_var: !input topic + friendly_name: '{{ topic_var.split(''/'')[1] | regex_replace("[^A-Za-z0-9_/./-]", + "") }}' + device_id: '{{ device_id( friendly_name ) }}' + ieee_id_long: '{{(( device_attr( device_id, ''identifiers'') |list)[0][1]) }}' + ieee_id: '{{ieee_id_long.split(''_'')[1] }}' + num_name: last side + num_ha_name: '{{ ''number.'' + friendly_name + ''_last_side'' }}' + num_ha_val: '{{ states(num_ha_name) | default(0) | int(0) }}' + last_side: "{# The first time it will not be there, so make sure its 0 instead of + undefined. #} {% if not num_ha_val in [\"undefined\", \"unknown\", \"unavailable\", + \"none\", \"null\", \"\"] %}\n {{ num_ha_val }}\n{% else %}\n 0\n{% endif %}\n" + mdl: Number Helper for Z2M Cube T1-Pro Integration + mf: SirGoodenough + sa: house + sw: '2023-12-09' + hw: https://github.com/SirGoodenough/HA_Blueprints/blob/master/Automations/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml + cu: https://community.home-assistant.io/t/zigbee2mqtt-aqara-magic-cube-t1-pro-ctp-r01-xiaomi-lumi-cagl02/525111 + config_topic_2: '{{ ''homeassistant/number/'' + ieee_id + ''/last_side/config'' + }}' + topic_2: '{{ ''homeassistant/number/'' + ieee_id + ''/last_side'' }}' +conditions: +- condition: or + conditions: + - condition: template + alias: Check for valid Action Mode triggers + value_template: "{{ trigger.payload_json is defined and\n trigger.payload_json.operation_mode + is defined and\n trigger.payload_json.action is defined and\n trigger.payload_json.operation_mode + == \"action_mode\" and\n trigger.payload_json.action in [\"rotate_right\", + \"rotate_left\",\n \"flip90\", \"flip180\", \"slide\", \"tap\", \"shake\", + \"throw\"] }}\n" + - condition: template + alias: Check for valid Scene Mode triggers + value_template: "{{ trigger.payload_json is defined and\n trigger.payload_json.operation_mode + is defined and\n trigger.payload_json.action is defined and\n trigger.payload_json.operation_mode + == \"scene_mode\" and\n trigger.payload_json.action in [\"rotate_right\", \"rotate_left\",\n + \ \"flip_to_side\", \"shake\", \"throw\", \"hold\"] }}\n" +- alias: User pick + condition: !input additional_conditions +actions: +- alias: Make number helper to store last side + action: mqtt.publish + data: + topic: '{{ config_topic_2 }}' + retain: true + payload: "{ \"name\": \"{{ num_name }}\",\n \"avty_t\": \"homeassistant/status\",\n + \ \"uniq_id\": \"{{ friendly_name + '-' + device_id }}\",\n \"cmd_t\": \"{{ + topic_2 }}\",\n \"sta_t\": \"{{ topic_2 }}\",\n \"min\": \"0\",\n \"max\": + \"6\",\n \"step\": \"1\",\n \"mode\": \"box\",\n \"ret\": true,\n \"dev\": + {\n \"name\": \"{{ friendly_name }}\",\n \"mdl\": \"{{ mdl }}\",\n \"mf\": + \"{{ mf }}\",\n \"sa\": \"{{ sa }}\",\n \"sw\": \"{{ sw }}\",\n \"hw\": + \"{{ hw }}\",\n \"cu\": \"{{ cu }}\",\n \"ids\": [\n \"{{ ieee_id + + '-' + num_name }}\"\n ]\n }\n}\n" +- alias: Fire Last Action event. This is for optional troubleshooting data. + event: cube_last_action + event_data: + action: '{{ action }}' + side: '{{ side }}' + last_side: '{{ last_side }}' + friendly_name: '{{ friendly_name }}' + device_id: '{{ device_id }}' + ieee_id: '{{ ieee_id }}' + angle: '{{ angle }}' + mode: '{{ op_mode }}' +- alias: Store the current_side as the last_side for the next trigger + action: number.set_value + target: + entity_id: '{{ num_ha_name }}' + data: + value: '{{ side }}' +- alias: Choose Action Mode or Scene Mode + choose: + - conditions: '{{ op_mode == "action_mode" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 2 \U0001F352 and No Group \U0001F355 + These are action events for ANY Side" + choose: + - conditions: '{{ action == "slide" }}' + sequence: !input action_slide_any + - conditions: '{{ action == "tap" }}' + sequence: !input action_doubletap_any + - conditions: '{{ action == "rotate_right" }}' + sequence: !input action_rotate_cw_any + - conditions: '{{ action == "rotate_left" }}' + sequence: !input action_rotate_ccw_any + - conditions: '{{ action == "shake" }}' + sequence: !input action_shake + - conditions: '{{ action == "throw" }}' + sequence: !input action_throw + - conditions: '{{ action == "flip90" }}' + sequence: + - parallel: + - !input action_flipped90_any + - alias: These are flip to specific side from any side events + choose: &id001 + - conditions: '{{ side == 1 }}' + sequence: !input action_flip_from_any_to_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_flip_from_any_to_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_flip_from_any_to_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_flip_from_any_to_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_flip_from_any_to_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_flip_from_any_to_face_6 + - conditions: '{{ action == "flip180" }}' + sequence: + - parallel: + - !input action_flipped180_any + - alias: These are flip to specific side from any side events + choose: *id001 + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are action + events based on the action broken down by side" + choose: + - conditions: '{{ action == "slide" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + slide events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_slide_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_slide_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_slide_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_slide_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_slide_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_slide_face_6 + - conditions: '{{ action == "tap" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + doubletap events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_doubletap_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_doubletap_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_doubletap_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_doubletap_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_doubletap_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_doubletap_face_6 + - conditions: '{{ action == "flip90" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + flipped90 events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_flipped90_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_flipped90_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_flipped90_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_flipped90_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_flipped90_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_flipped90_face_6 + - conditions: '{{ action == "flip180" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + flipped180 events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_flipped180_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_flipped180_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_flipped180_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_flipped180_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_flipped180_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_flipped180_face_6 + - conditions: '{{ action == "rotate_right" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + rotate CW events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_rotate_cw_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_rotate_cw_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_rotate_cw_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_rotate_cw_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_rotate_cw_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_rotate_cw_face_6 + - conditions: '{{ action == "rotate_left" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + rotate CCW events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_rotate_ccw_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_rotate_ccw_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_rotate_ccw_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_rotate_ccw_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_rotate_ccw_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_rotate_ccw_face_6 + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 Side to side jump + events" + choose: + - conditions: '{{ side == 1 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 1 from + any events" + choose: + - conditions: '{{ last_side == 2 }}' + sequence: !input action_1_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input action_1_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input action_1_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input action_1_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input action_1_from_6 + - conditions: '{{ side == 2 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 2 from + any events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input action_2_from_1 + - conditions: '{{ last_side == 3 }}' + sequence: !input action_2_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input action_2_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input action_2_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input action_2_from_6 + - conditions: '{{ side == 3 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 3 from + any events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input action_3_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input action_3_from_2 + - conditions: '{{ last_side == 4 }}' + sequence: !input action_3_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input action_3_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input action_3_from_6 + - conditions: '{{ side == 4 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 4 from + any events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input action_4_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input action_4_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input action_4_from_3 + - conditions: '{{ last_side == 5 }}' + sequence: !input action_4_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input action_4_from_6 + - conditions: '{{ side == 5 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 5 from + any events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input action_5_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input action_5_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input action_5_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input action_5_from_4 + - conditions: '{{ last_side == 6 }}' + sequence: !input action_5_from_6 + - conditions: '{{ side == 6 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 6 from + any events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input action_6_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input action_6_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input action_6_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input action_6_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input action_6_from_5 + - conditions: '{{ op_mode == "scene_mode" }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 2 \U0001F352 and No Group \U0001F355 + These are action events for ANY Side" + choose: + - conditions: '{{ action == "flip_to_side" }}' + sequence: !input scene_flip_to_side_any + - conditions: '{{ action == "rotate_right" }}' + sequence: !input scene_rotate_cw_any + - conditions: '{{ action == "rotate_left" }}' + sequence: !input scene_rotate_ccw_any + - conditions: '{{ action == "hold" }}' + sequence: !input scene_hold + - conditions: '{{ action == "shake" }}' + sequence: !input scene_shake + - conditions: '{{ action == "throw" }}' + sequence: !input scene_throw + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 1 \U0001F34E These are action + events based on the action broken down by side" + choose: + - conditions: '{{ action == "flip_to_side" }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 1 \U0001F34E These are flip_to_side + events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input scene_flip_to_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input scene_flip_to_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input scene_flip_to_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input scene_flip_to_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input scene_flip_to_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input scene_flip_to_face_6 + - conditions: '{{ action == "rotate_right" }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 1 \U0001F34E These are rotate + CW events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input scene_rotate_cw_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input scene_rotate_cw_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input scene_rotate_cw_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input scene_rotate_cw_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input scene_rotate_cw_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input scene_rotate_cw_face_6 + - conditions: '{{ action == "rotate_left" }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 1 \U0001F34E These are rotate + CCW events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input scene_rotate_ccw_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input scene_rotate_ccw_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input scene_rotate_ccw_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input scene_rotate_ccw_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input scene_rotate_ccw_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input scene_rotate_ccw_face_6 + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 Side to side jump + events" + choose: + - conditions: '{{ side == 1 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 1 from + specific events" + choose: + - conditions: '{{ last_side == 2 }}' + sequence: !input scene_1_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input scene_1_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input scene_1_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input scene_1_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input scene_1_from_6 + - conditions: '{{ side == 2 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 2 from + specific events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input scene_2_from_1 + - conditions: '{{ last_side == 3 }}' + sequence: !input scene_2_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input scene_2_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input scene_2_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input scene_2_from_6 + - conditions: '{{ side == 3 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 3 from + specific events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input scene_3_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input scene_3_from_2 + - conditions: '{{ last_side == 4 }}' + sequence: !input scene_3_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input scene_3_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input scene_3_from_6 + - conditions: '{{ side == 4 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 4 from + specific events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input scene_4_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input scene_4_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input scene_4_from_3 + - conditions: '{{ last_side == 5 }}' + sequence: !input scene_4_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input scene_4_from_6 + - conditions: '{{ side == 5 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 5 from + specific events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input scene_5_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input scene_5_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input scene_5_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input scene_5_from_4 + - conditions: '{{ last_side == 6 }}' + sequence: !input scene_5_from_6 + - conditions: '{{ side == 6 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 6 from + specific events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input scene_6_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input scene_6_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input scene_6_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input scene_6_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input scene_6_from_5 +- alias: Delay ⏱ for debounce so toggle functions work. Also blueprint is in single + mode. + delay: 00:00:01 +mode: single +max_exceeded: silent diff --git a/blueprints/script/SirGoodenough/dimmer_control_4_magic_cube.yaml b/blueprints/script/SirGoodenough/dimmer_control_4_magic_cube.yaml new file mode 100644 index 0000000..4531b8d --- /dev/null +++ b/blueprints/script/SirGoodenough/dimmer_control_4_magic_cube.yaml @@ -0,0 +1,153 @@ +blueprint: + name: dimmer_control_4_magic_cube_and_other_things - 2023-12-07 + author: SirGoodenough + domain: script + homeassistant: + min_version: 2023.8.0 + source_url: https://github.com/SirGoodenough/HA_Blueprints/blob/master/Scripts/dimmer_control_4_magic_cube.yaml + description: "This script was invented to be used with my Magic Cube Blueprints. + I released it as a standalone Script Blueprint because I saw others struggling + with how so convert cube rotation to light dimming in other places.\n\nTo use + this blueprint, install it from the source in the normal way. After that add your + unique name for the Script, and add an icon and change the entity_id if desired. + You should only need to run the blueprint once, as you will be calling the unique + name/entity you added above to use the code. This script has 2 inputs in the Blueprint + screen to set the sensitivity of the rotation action and the minimum bbrightness + level for your lights. If the rotation is too sensitive the lights will change + from dim to bright in too short of a rotation so make it less sensitive. If the + lights flicker at low brightness, raise the minimum brightness level to stop it. + The data is sent to the generated script to control the action live at each use. + You only need to ececute this blueprint generator script if you wish to change + in input values or create another dimmer script.\n\nThis requires you call this + script wth 2 data values. One is a positive or negative number between 360 and + -360 that in the original, represents the input angle from the cube movement. + The other data point is the entity of the light or light group that you are trying + to control.\n***NOTE:*** the trigger variable below will be different for Z2M + and ZHA and others. Look at my documentation for that cube integration help if + you need it.\n\n## \U0001F5FFNotice\U0001F46E\U0001F3FF‍♂️:\n* Copies of the original + Blueprint that were converted via the 'Take Control' feature or other means are + officially not supported by me.\n\n* I may or may not be able to support you when + you have a problem after you make changes to my code, as some of the code is no + longer mine.\n\n* I & my license also require attribution as a link back to the + original should you use this code in your own creation.\n\n* [Here is a link to + my license & the original github post](https://github.com/SirGoodenough/HA_Blueprints?tab=License-1-ov-file) + expected to be followed & referenced as attribution should you use this code elsewhere.\n\n + \ Sample call / use of this script for ZHA:\n ```yaml\n - action: script.cube_dimmer_control_bp\n + \ data:\n angle: '{{ trigger.event.data.args.relative_degrees | default(0.1) + | float(0.2) }}'\n light: light.grp_studio\n ```\n\n Sample call / + use of this script for Z2M:\n ```yaml\n - action: script.cube_dimmer_control_bp\n + \ data:\n angle: '{{ trigger.payload_json.action_angle | default(0.1) + | float(0.2) }}'\n light: light.grp_kitch\n ```\n" + input: + sensi: + name: Sensitivity of the cube rotational motion. + description: 'This sets how ''fast'' the changes happen compared to how far + you move the cube. Higher percentage is faster, lower is slower. + + I have found that 40% is a good number, but you can adjust as desired. + + ' + default: 40 + selector: + number: + min: 1.0 + max: 200.0 + step: 1.0 + mode: box + min_b: + name: Minimum Brighness + description: 'This sets the lowest brightness it will set before it turns off. + Some bulbs react poorly below a certain percent, you can adjust that here. + + I have found that 10% is a good number, but you can adjust as desired. + + ' + default: 10 + selector: + number: + min: 5.0 + max: 90.0 + step: 1.0 + mode: slider + transition_i: + name: Transition Input + description: Seconds to take to change light one level to another + default: [] + selector: + number: + min: 0.0 + max: 1800.0 + step: 0.5 + mode: slider +fields: + angle: + name: Angle to represent new brightness + description: ' This is the new angle to be used in the calculation for Brightness' + required: true + example: '-120.7' + default: -120.7 + selector: + number: + min: -360 + max: 360 + step: 0.1 + mode: box + light: + name: Light entity to control + description: Add the light entity you want to control here + required: true + example: light.grp_studio + default: light.grp_studio + selector: + entity: + multiple: false + filter: + domain: light + transition: + name: Transition Field + description: Seconds to take to change light one level to another + required: false + example: '4' + selector: + number: + min: 0 + max: 1800 + step: 0.5 +variables: + sensi_var: !input sensi + sens_d: '{{ sensi_var / 100 | default( 0.4 ) }}' + min_b_var: !input min_b + bright_p: "{% set step_size = angle * sens_d %}\n {# Get brightness as a percent. + #}\n{% set cb = (state_attr( light, 'brightness') | float(10) / 255.0) * 100.0 + %} {% set new_brightness = cb | int(10) + step_size %} {% if new_brightness < + 5 %}\n {# If it gets really low set to 0. Adjust if needed for your lights. #}\n + \ 0\n{% elif new_brightness <= min_b_var %}\n {# If it's not quite 0, set to + a minimum working brightness. #}\n {{ min_b_var }}\n{% elif 91 <= new_brightness + < (90 + step_size) %}\n {# If it's almost full brightness, set to 100%. #}\n + \ 100\n{% else %}\n {# Send actual calculated value. #}\n{{ new_brightness }} + {% endif %}" + trans_i: !input transition_i + transition_var: "{% if is_number(transition | default('none')) %}\n {{ transition + }}\n{% elif is_number(trans_i | default('none')) %}\n {{ trans_i }}\n{% else + %}\n none\n{% endif %}" +sequence: +- if: + - alias: turn the light & set brightness + condition: template + value_template: '{{ is_number(transition_var) }}' + then: + - alias: Transition number IS available + action: light.turn_on + data: + entity_id: '{{ light }}' + brightness_pct: '{{ bright_p }}' + transition: '{{ transition_var }}' + else: + - alias: NO transition number available + action: light.turn_on + data: + entity_id: '{{ light }}' + brightness_pct: '{{ bright_p }}' +icon: mdi:lightbulb-on-50 +mode: queued +max: 10 diff --git a/configuration.yaml b/configuration.yaml index 7a3894e..43fd883 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -60,13 +60,13 @@ template: device_class: power state_class: measurement state: > - {% set hvac = states.climate | selectattr('entity_id', 'search', 'living_room') | map(attribute='entity_id') | first %} - {% if hvac %} + {% set hvac = 'climate.living_room_living_room' %} + {% if states(hvac) not in ['unknown', 'unavailable'] %} {% set action = state_attr(hvac, 'hvac_action') | string | lower %} {% if action == 'cooling' %} 2.12 {% elif action == 'heating' %} - {% if is_state_attr(hvac, 'is_aux_heat', true) %} + {% if is_state_attr(hvac, 'preset_mode', 'eco') or is_state_attr(hvac, 'aux_heat', 'on') %} 20.5 {% else %} 2.5 @@ -116,9 +116,7 @@ http: use_x_forwarded_for: true trusted_proxies: - 192.168.122.1 - - 192.168.1.130 - - 192.168.1.125 - - 192.168.1.135 + - 192.168.1.0/24 # Legacy and Integration Sensor Section sensor: diff --git a/custom_components/album_slideshow/__init__.py b/custom_components/album_slideshow/__init__.py new file mode 100644 index 0000000..5647de8 --- /dev/null +++ b/custom_components/album_slideshow/__init__.py @@ -0,0 +1,431 @@ +from __future__ import annotations + +import asyncio +import json +import logging +import os + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .const import DOMAIN, SERVICE_NEXT_SLIDE, SERVICE_REFRESH_ALBUM, ATTR_ENTRY_ID +from .store import SlideshowStore + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[str] = ["camera", "sensor", "button", "number", "select", "text", "switch"] + +CARD_STATIC_PATH = "/album_slideshow_static" +CARD_FILE = "album-slideshow-card.js" + + +async def _async_register_card(hass: HomeAssistant) -> None: + """Serve the Lovelace card JS and register it as a frontend module. + + Idempotent: only the first config entry to load triggers registration + for the HA session. The card lets dashboards cross-fade between slides + in the browser (GPU compositor) instead of forcing the camera entity + to render a JPEG burst on the event loop. + """ + if hass.data.get(DOMAIN, {}).get("card_registered"): + return + + integration_dir = os.path.dirname(__file__) + www_dir = os.path.join(integration_dir, "www") + card_path = os.path.join(www_dir, CARD_FILE) + + if not os.path.isfile(card_path): + # Some HACS upgrade paths (and broken zip extractors) drop the + # ``www/`` subdirectory. Try to recover by checking whether the + # integration root has the file under a literal-backslash name + # (a symptom of zips written with Windows path separators) or + # directly at the root, and salvage it into ``www/`` so the + # rest of the registration can proceed. + recovered = await hass.async_add_executor_job( + _recover_card_from_root, integration_dir, www_dir, card_path + ) + if not recovered: + _LOGGER.warning( + "Album Slideshow card missing on disk (%s). Re-install" + " the integration via HACS (3-dot menu -> Redownload)" + " or copy %s/%s into the album_slideshow folder." + " The custom:album-slideshow-card type will not be" + " available until this is fixed.", + card_path, + "www", + CARD_FILE, + ) + return + + try: + from homeassistant.components.http import StaticPathConfig + + await hass.http.async_register_static_paths( + [StaticPathConfig(CARD_STATIC_PATH, www_dir, False)] + ) + except Exception: # noqa: BLE001 - many possible failure modes here + _LOGGER.exception( + "Failed to register static path for Album Slideshow card" + ) + return + + # Cache-bust the card URL with the integration version so dashboards + # always pick up the script that matches the running integration + # rather than a stale copy from a previous release. + version = await hass.async_add_executor_job( + _read_manifest_version, integration_dir + ) + card_url = f"{CARD_STATIC_PATH}/{CARD_FILE}" + if version: + card_url = f"{card_url}?v={version}" + + # Prefer registering the card as a Lovelace resource for storage-mode + # dashboards. Resources are loaded as part of the Lovelace bootstrap, + # before any dashboard renders custom cards, which removes the race + # where the dashboard can hit "Custom element doesn't exist" if the + # browser hasn't finished loading the module yet (a real risk after + # HA restarts and integration upgrades that bust the cache). + # + # The storage-mode resources collection is only consumed by + # storage-mode dashboards; YAML-mode dashboards (whether the user + # has ``lovelace.mode: yaml`` globally or per-dashboard + # ``mode: yaml`` entries) read only the resources declared in their + # own YAML and would otherwise never load the card. That's why we + # *always* also call ``add_extra_js_url``: the frontend injects a + # `` + +`; + + const win = window.open("", "_blank", "width=900,height=1100"); + if (!win) { + // Popup blocker — fall back to the legacy in-place print so the user + // still gets *something* (with the HA shell drawback they reported). + window.print(); + return; + } + win.document.open(); + win.document.write(html); + win.document.close(); + } + + private _escapeHtml(s: string): string { + return s.replace(/[&<>"']/g, (c) => ({ + "&": "&", "<": "<", ">": ">", '"': """, "'": "'", + }[c] as string)); + } + + private _renderImportExport(L: string) { + return html` +

+ `; + } + + // --- Export / Import actions --- + + private async _exportJson(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/export", + format: "json", + include_history: this._includeHistory, + }) as { data: string }; + const ts = new Date().toISOString().slice(0, 10); + this._downloadFile(result.data, `maintenance_export_${ts}.json`, "application/json"); + this._showToast(t("settings_export_success", this._lang)); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private async _exportYaml(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/export", + format: "yaml", + include_history: this._includeHistory, + }) as { data: string }; + const ts = new Date().toISOString().slice(0, 10); + this._downloadFile(result.data, `maintenance_export_${ts}.yaml`, "application/yaml"); + this._showToast(t("settings_export_success", this._lang)); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private async _exportCsv(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/csv/export", + }) as { csv: string }; + const ts = new Date().toISOString().slice(0, 10); + this._downloadFile(result.csv, `maintenance_export_${ts}.csv`, "text/csv"); + this._showToast(t("settings_export_success", this._lang)); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private async _importCsvAction(): Promise { + const content = this._importCsv.trim(); + if (!content) return; + this._importLoading = true; + try { + // CSV exports start with the "object_name" header; anything else + // (JSON `{`/`[` or YAML `version:`) goes to the structured importer, + // which parses JSON and YAML alike. + const isCsv = content.startsWith("object_name"); + const result = await this.hass.connection.sendMessagePromise( + isCsv + ? { type: "maintenance_supporter/csv/import", csv_content: content } + : { type: "maintenance_supporter/json/import", json_content: content } + ) as { created: number }; + const count = result.created ?? 0; + this._showToast(t("settings_import_success", this._lang).replace("{count}", String(count))); + this._importCsv = ""; + this.dispatchEvent(new CustomEvent("settings-changed")); + } catch { + this._showToast(t("action_error", this._lang)); + } + this._importLoading = false; + } + + // --- Styles --- + + static styles = css` + :host { display: block; } + + .settings-loading { + text-align: center; + padding: 32px; + color: var(--secondary-text-color); + } + + .settings-section { + margin-bottom: 24px; + padding: 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + .settings-section h3 { + margin: 0 0 4px 0; + font-size: 16px; + } + .section-desc { + font-size: 13px; + color: var(--secondary-text-color); + margin: 0 0 16px 0; + } + + .setting-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 0; + border-bottom: 1px solid var(--divider-color, #e0e0e0); + cursor: pointer; + gap: 12px; + } + .setting-row:last-child { border-bottom: none; } + .setting-row.sub-row { + padding-left: 16px; + } + + .setting-label { font-size: 14px; display: block; } + .setting-desc { font-size: 12px; color: var(--secondary-text-color); display: block; } + + .setting-row input[type="checkbox"] { + width: 18px; height: 18px; flex-shrink: 0; + } + .setting-row input[type="number"], + .setting-row input[type="text"], + .setting-row input[type="time"] { + width: 120px; + padding: 6px 8px; + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 14px; + flex-shrink: 0; + } + .setting-row input[type="number"] { + text-align: right; + } + .setting-row select { + padding: 6px 8px; + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 14px; + flex-shrink: 0; + } + + .settings-actions { + display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; + } + .settings-actions button { + padding: 8px 16px; + border-radius: 8px; + border: 1px solid var(--divider-color, #e0e0e0); + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + cursor: pointer; + font-size: 14px; + } + .settings-actions button:hover { + background: var(--secondary-background-color, #f5f5f5); + } + .settings-actions button[disabled] { + opacity: 0.5; + cursor: not-allowed; + } + + .export-history-toggle { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + cursor: pointer; + } + .export-history-toggle input { width: 16px; height: 16px; } + + .import-section { margin-top: 16px; } + + .import-area { + width: 100%; + min-height: 120px; + padding: 8px; + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 8px; + font-family: monospace; + font-size: 12px; + resize: vertical; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + box-sizing: border-box; + } + + .settings-toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: var(--primary-color, #03a9f4); + color: #fff; + padding: 10px 24px; + border-radius: 8px; + font-size: 14px; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0,0,0,.3); + animation: toast-in .3s ease; + } + @keyframes toast-in { + from { opacity: 0; transform: translateX(-50%) translateY(16px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } + } + + /* ─── Vacation mode section (v1.2.0) ─── */ + + .vacation-section h3 { + display: flex; + align-items: center; + gap: 8px; + } + .vac-badge { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.4px; + text-transform: uppercase; + padding: 2px 8px; + border-radius: 10px; + } + .vac-badge.active { + background: var(--success-color, #4caf50); + color: #fff; + } + .vac-badge.stale { + background: var(--warning-color, #ff9800); + color: #fff; + } + .vac-toggle { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + margin: 8px 0 12px; + } + .vac-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 12px; + margin-bottom: 12px; + } + .vac-field { + display: flex; + flex-direction: column; + gap: 4px; + } + .vac-field input { + padding: 6px 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + } + .vac-exempt-panel { + border: 1px solid var(--divider-color); + border-radius: 6px; + padding: 10px; + margin: 12px 0; + } + .vac-exempt-panel summary { + cursor: pointer; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; + } + .section-badge { + background: var(--primary-color, #03a9f4); + color: #fff; + font-size: 11px; + font-weight: 600; + padding: 1px 8px; + border-radius: 10px; + } + .vac-task-list { + max-height: 280px; + overflow-y: auto; + margin-top: 8px; + } + .vac-task-group { + margin: 8px 0; + } + .vac-task-group-name { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--secondary-text-color); + padding: 4px 0; + } + .vac-task-row { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + cursor: pointer; + border-radius: 4px; + } + .vac-task-row:hover { background: var(--secondary-background-color, rgba(127,127,127,0.1)); } + .vac-preview-toolbar { + display: flex; + align-items: center; + gap: 12px; + margin: 12px 0 8px; + } + .vac-preview-count { + color: var(--secondary-text-color); + font-size: 13px; + } + .vac-preview-list { + display: flex; + flex-direction: column; + gap: 6px; + } + .vac-preview-row { + display: flex; + gap: 12px; + padding: 10px 12px; + background: var(--secondary-background-color, rgba(127,127,127,0.08)); + border-radius: 6px; + border-left: 3px solid var(--warning-color, #ff9800); + } + .vac-preview-row.exempt { + border-left-color: var(--success-color, #4caf50); + } + .vac-preview-info { flex: 1; } + .vac-preview-name { font-size: 14px; } + .vac-preview-kind { + font-size: 11px; + color: var(--secondary-text-color); + margin-left: 6px; + } + .vac-preview-events { + font-size: 12px; + color: var(--secondary-text-color); + margin-top: 2px; + } + .vac-preview-actions { + display: flex; + gap: 6px; + align-items: center; + flex-wrap: wrap; + } + .vac-preview-actions button { + font-size: 12px; + padding: 4px 10px; + } + .vac-notify-on { background: var(--success-color, #4caf50) !important; color: #fff; } + .vac-end-now { + margin-top: 12px; + background: var(--error-color, #f44336); + color: #fff; + } + + /* ─── Print QR codes section (v1.1.0) ─── */ + + .qr-filter-panel { + border: 1px solid var(--divider-color); + border-radius: 6px; + padding: 12px; + margin-top: 8px; + } + .qr-filter-panel > summary { + cursor: pointer; + font-weight: 500; + } + .qr-filter-group { + margin-top: 12px; + } + .qr-filter-label { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--secondary-text-color); + margin-bottom: 4px; + } + .qr-object-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 4px 12px; + max-height: 240px; + overflow-y: auto; + } + .qr-object-row { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 6px; + cursor: pointer; + border-radius: 4px; + } + .qr-object-row:hover { background: var(--secondary-background-color, rgba(127,127,127,0.1)); } + .qr-object-row > span:nth-of-type(1) { flex: 1; } + .qr-task-count { + color: var(--secondary-text-color); + font-size: 12px; + font-variant-numeric: tabular-nums; + } + + .qr-action-chips { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + .qr-action-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 14px; + border: 1px solid var(--divider-color); + cursor: pointer; + user-select: none; + } + .qr-action-chip.active { + background: var(--primary-color, #03a9f4); + color: #fff; + border-color: transparent; + } + .qr-action-chip input { accent-color: currentColor; } + + .qr-filter-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + .qr-estimate { font-size: 13px; } + .qr-estimate.error { color: var(--error-color, #f44336); } + + .qr-results-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 16px; + padding: 8px 12px; + background: var(--secondary-background-color, rgba(127,127,127,0.1)); + border-radius: 6px; + } + + .qr-print-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 16px; + margin-top: 16px; + } + .qr-print-cell { + border: 1px solid var(--divider-color); + border-radius: 6px; + padding: 8px; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + background: #fff; + color: #000; + } + .qr-print-cell .qr-svg { + width: 100%; + max-width: 160px; + } + .qr-print-cell .qr-svg svg { width: 100%; height: auto; display: block; } + .qr-label { + margin-top: 6px; + font-size: 11px; + line-height: 1.3; + } + .qr-label-obj { font-weight: 600; } + .qr-label-task { color: #444; } + .qr-label-action { + margin-top: 2px; + text-transform: uppercase; + letter-spacing: 0.5px; + font-size: 10px; + color: #777; + } + + /* ─── Print stylesheet ─── */ + @media print { + /* Strip everything except the QR grid itself */ + :host { color: #000; background: #fff; } + .qr-print-section h3, + .qr-print-section .section-desc, + .qr-filter-panel, + .qr-results-toolbar, + .settings-section:not(.qr-print-section), + .settings-toast { + display: none !important; + } + .qr-print-section { padding: 0; margin: 0; } + .qr-print-grid { + grid-template-columns: repeat(3, 1fr); + gap: 12mm 8mm; + margin: 0; + } + .qr-print-cell { + border: none; + padding: 0; + page-break-inside: avoid; + } + .qr-print-cell .qr-svg { max-width: 48mm; } + .qr-label { font-size: 9pt; } + } + `; +} + +customElements.define("maintenance-settings-view", MaintenanceSettingsView); diff --git a/custom_components/maintenance_supporter/frontend-src/components/storage-section-card.ts b/custom_components/maintenance_supporter/frontend-src/components/storage-section-card.ts new file mode 100644 index 0000000..f0828a3 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/components/storage-section-card.ts @@ -0,0 +1,361 @@ +/** Document-storage overview card (panel overview). + * + * Shows the physical footprint (real backup cost), the dedup saving, and a + * per-object drill-down sorted by size. Object ids from the WS summary are + * mapped to names via the panel's already-loaded objects. Self-hides when no + * documents exist, so it never clutters the overview for non-users. + */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { t, ensureLocale } from "../styles"; +import { describeWsError } from "../ws-errors"; +import { formatBytes } from "../helpers/format-bytes"; +import type { HomeAssistant } from "../types"; + +interface StorageSummary { + total_bytes: number; + dedup_savings_bytes: number; + file_count: number; + link_count: number; + document_count: number; + by_object: Record; +} + +interface PanelObject { + entry_id: string; + object?: { id?: string; name?: string }; +} + +interface SearchResult { + id: string; + entry_id: string; + object_name: string; + kind: "file" | "weblink"; + title: string; + filename?: string; + url?: string; + size?: number; + tags?: string[]; +} + +export class MaintenanceStorageSectionCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public objects: PanelObject[] = []; + + @state() private _summary: StorageSummary | null = null; + @state() private _loaded = false; + @state() private _busy = false; + @state() private _error = ""; + @state() private _query = ""; + @state() private _results: SearchResult[] = []; + @state() private _expanded = false; + + private _initiallyLoaded = false; + private _searchTimer = 0; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + updated(changed: Map): void { + super.updated(changed); + if (changed.has("hass") && this.hass && !this._initiallyLoaded) { + this._initiallyLoaded = true; + void this._load(); + // Re-render once the runtime locale JSON arrives (t() falls back to + // English until then; this card can paint before the panel's fetch lands). + void ensureLocale(this._lang).then(() => this.requestUpdate()); + } + } + + private async _load(): Promise { + this._busy = true; + try { + this._summary = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/documents/storage", + }); + this._error = ""; + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._loaded = true; + this._busy = false; + } + } + + private _nameFor(objectId: string): string { + const o = this.objects.find((x) => x.object?.id === objectId); + return o?.object?.name || objectId.slice(0, 8); + } + + private _entryFor(objectId: string): string | undefined { + return this.objects.find((x) => x.object?.id === objectId)?.entry_id; + } + + private _toggle(): void { + this._expanded = !this._expanded; + } + + /** Ask the panel (which owns navigation) to open an object's detail view. */ + private _openObject(entryId: string): void { + this.dispatchEvent( + new CustomEvent("open-object", { + detail: { entry_id: entryId }, + bubbles: true, + composed: true, + }), + ); + } + + private _onSearch(e: Event): void { + this._query = (e.target as HTMLInputElement).value; + clearTimeout(this._searchTimer); + this._searchTimer = window.setTimeout(() => void this._doSearch(), 250); + } + + private async _doSearch(): Promise { + const q = this._query.trim(); + if (!q) { + this._results = []; + return; + } + try { + const r = await this.hass.connection.sendMessagePromise<{ results: SearchResult[] }>({ + type: "maintenance_supporter/documents/search", + query: q, + }); + this._results = r.results || []; + } catch (e) { + this._error = describeWsError(e, this._lang); + this._results = []; + } + } + + private async _openResult(doc: SearchResult): Promise { + if (doc.kind === "weblink") { + window.open(doc.url, "_blank", "noopener"); + return; + } + const win = window.open("about:blank", "_blank"); + try { + const s = await this.hass.connection.sendMessagePromise<{ path: string }>({ + type: "auth/sign_path", + path: `/api/maintenance_supporter/document/${doc.id}`, + expires: 300, + }); + // Absolute URL so it navigates the blank popup reliably (about:blank base). + if (win) win.location.href = new URL(s.path, window.location.origin).href; + } catch (e) { + if (win) win.close(); + this._error = describeWsError(e, this._lang); + } + } + + private _renderResult(doc: SearchResult, L: string) { + return html` +
this._openResult(doc)}> + +
+
${doc.title || doc.filename || doc.url}
+
${doc.object_name}
+
+ +
+ `; + } + + render() { + if (!this._loaded || !this._summary) return nothing; + const s = this._summary; + if (s.document_count === 0) return nothing; // self-hide when unused + const L = this._lang; + + const rows = Object.entries(s.by_object) + .filter(([, v]) => v.files > 0 || v.links > 0) + .map(([id, v]) => ({ id, name: this._nameFor(id), entry: this._entryFor(id), ...v })) + .sort((a, b) => b.bytes - a.bytes); + + return html` + +
+
+ + +
+ + ${this._expanded + ? html` +
+
+
+
${formatBytes(s.total_bytes)}
+
+ ${s.file_count} + ${s.link_count} +
+
+ ${s.dedup_savings_bytes > 0 + ? html`
+
−${formatBytes(s.dedup_savings_bytes)}
+
${t("doc_storage_saved", L)}
+
` + : nothing} +
+ + + + ${this._error ? html`
${this._error}
` : nothing} + + ${this._query.trim() + ? this._results.length + ? html`
${this._results.map((d) => this._renderResult(d, L))}
` + : html`
${t("doc_search_none", L)}
` + : rows.length + ? html`
${rows.map((r) => this._renderObjRow(r, L))}
` + : nothing} +
+ ` + : nothing} +
+
+ `; + } + + private _renderObjRow( + r: { id: string; name: string; entry?: string; bytes: number; files: number; links: number }, + L: string, + ) { + const eid = r.entry; + return html` +
this._openObject(eid) : undefined} + @keydown=${eid + ? (e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + this._openObject(eid); + } + } + : undefined} + > + ${r.name} + + ${r.files > 0 ? html`${r.files}` : nothing} + ${r.links > 0 ? html`${r.links}` : nothing} + + ${formatBytes(r.bytes)} + ${eid ? html`` : nothing} +
+ `; + } + + static styles = css` + ha-card { margin-top: 16px; } + .card-content { padding: 16px; } + .doc-search { + display: flex; align-items: center; gap: 6px; margin: 10px 0 4px; + padding: 2px 10px; border-radius: 8px; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); + border: 1px solid var(--divider-color); + } + .doc-search ha-icon { --mdc-icon-size: 18px; color: var(--secondary-text-color, #888); } + .doc-search input { + flex: 1; border: none; background: transparent; font: inherit; outline: none; + color: var(--primary-text-color); padding: 6px 0; + } + .result-row { cursor: pointer; } + .result-row > ha-icon { color: var(--primary-color); --mdc-icon-size: 20px; flex: none; } + .result-info { flex: 1; min-width: 0; } + .result-title { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .result-obj { font-size: 12px; color: var(--secondary-text-color, #888); } + .result-open { color: var(--secondary-text-color, #888); --mdc-icon-size: 18px; flex: none; } + .search-empty { color: var(--secondary-text-color, #888); font-size: 13px; padding: 8px 2px; } + .header { display: flex; align-items: center; justify-content: space-between; gap: 8px; } + .toggle { + display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; + background: none; border: none; padding: 4px 0; margin: 0; cursor: pointer; + font: inherit; color: var(--primary-text-color); text-align: left; + } + .toggle:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; border-radius: 6px; } + .chevron { --mdc-icon-size: 22px; color: var(--secondary-text-color, #888); flex: none; } + .title-text { font-size: 16px; font-weight: 500; } + .header-summary { + margin-left: auto; display: flex; align-items: center; gap: 8px; + font-size: 14px; font-weight: 600; white-space: nowrap; + } + .header-summary .saved { color: var(--success-color, #4caf50); font-weight: 500; } + .emoji { font-size: 18px; } + .body { margin-top: 4px; } + .totals { display: flex; gap: 24px; margin: 12px 0 8px; flex-wrap: wrap; } + .stat-value { font-size: 22px; font-weight: 600; } + .stat-value.saved { color: var(--success-color, #4caf50); } + .stat-label { + font-size: 12px; color: var(--secondary-text-color, #888); + display: flex; align-items: center; gap: 4px; + } + .stat-label ha-icon { --mdc-icon-size: 15px; } + .obj-list { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; } + .obj-row { + display: flex; align-items: center; gap: 10px; + padding: 6px 8px; border-radius: 6px; + } + .obj-row:nth-child(odd) { background: var(--secondary-background-color, rgba(0,0,0,0.04)); } + .obj-row.clickable { cursor: pointer; } + .obj-row.clickable:hover { background: var(--secondary-background-color, rgba(0,0,0,0.10)); } + .obj-row.clickable:focus-visible { outline: 2px solid var(--primary-color); outline-offset: -2px; } + .obj-go { --mdc-icon-size: 18px; color: var(--secondary-text-color, #888); flex: none; } + .obj-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; } + .obj-meta { + display: flex; align-items: center; gap: 4px; + color: var(--secondary-text-color, #888); font-size: 13px; + } + .obj-meta ha-icon { --mdc-icon-size: 15px; } + .obj-size { font-variant-numeric: tabular-nums; font-size: 13px; min-width: 64px; text-align: right; } + .icon-btn { + display: inline-flex; align-items: center; justify-content: center; + width: 32px; height: 32px; border-radius: 8px; cursor: pointer; + background: transparent; border: none; color: var(--primary-text-color); + } + .icon-btn:hover { background: var(--secondary-background-color, rgba(0,0,0,0.06)); } + .icon-btn[disabled] { opacity: 0.4; pointer-events: none; } + .error { color: var(--error-color, #f44336); font-size: 13px; margin-top: 6px; } + `; +} + +if (!customElements.get("maintenance-storage-section-card")) { + customElements.define("maintenance-storage-section-card", MaintenanceStorageSectionCard); +} diff --git a/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts b/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts new file mode 100644 index 0000000..5ec6d72 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts @@ -0,0 +1,1791 @@ +/** Dialog for creating/editing a maintenance task. */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, TriggerConfig, HAUser } from "../types"; +import { t, weekdayName } from "../styles"; +import { UserService } from "../user-service"; + +import { describeWsError } from "../ws-errors"; +import "./ms-textfield"; + +const MAINTENANCE_TYPE_KEYS = ["cleaning", "inspection", "replacement", "calibration", "service", "reading", "custom"]; +const PRIORITY_KEYS = ["low", "normal", "high"]; +const SCHEDULE_TYPE_KEYS = ["time_based", "weekdays", "nth_weekday", "day_of_month", "sensor_based", "one_time", "manual"]; +const CALENDAR_KINDS = ["weekdays", "nth_weekday", "day_of_month"]; +const TRIGGER_TYPE_KEYS = ["threshold", "counter", "state_change", "runtime"]; +// The type selector additionally offers "compound" (a group of conditions +// joined by AND/OR); its per-condition sub-type is limited to the flat kinds. +const TRIGGER_TYPE_KEYS_WITH_COMPOUND = [...TRIGGER_TYPE_KEYS, "compound"]; + +/** One condition of a compound trigger — a flat trigger the user edits inline. + * String-typed like the top-level fields (form inputs); coerced on save. */ +interface CompoundConditionDraft { + entityIds: string; // comma-separated raw input + type: string; // threshold | counter | state_change | runtime + above: string; + below: string; + forMinutes: string; + targetValue: string; + deltaMode: boolean; + fromState: string; + toState: string; + targetChanges: string; + runtimeHours: string; +} + +function emptyCondition(): CompoundConditionDraft { + return { + entityIds: "", type: "threshold", above: "", below: "", forMinutes: "0", + targetValue: "", deltaMode: false, fromState: "", toState: "", + targetChanges: "", runtimeHours: "", + }; +} + +/** Map a persisted compound condition (storage shape) to an editable draft. */ +function conditionToDraft(c: TriggerConfig): CompoundConditionDraft { + const ids = c.entity_ids || (c.entity_id ? [c.entity_id] : []); + return { + entityIds: ids.join(", "), + type: c.type || "threshold", + above: c.trigger_above?.toString() ?? "", + below: c.trigger_below?.toString() ?? "", + forMinutes: c.trigger_for_minutes?.toString() ?? "0", + targetValue: c.trigger_target_value?.toString() ?? "", + deltaMode: c.trigger_delta_mode || false, + fromState: c.trigger_from_state || "", + toState: c.trigger_to_state || "", + targetChanges: c.trigger_target_changes?.toString() ?? "", + runtimeHours: c.trigger_runtime_hours?.toString() ?? "", + }; +} + +/** Build the persisted condition (storage shape) from an editable draft. + * Returns null when the condition has no entity (skip it). */ +function draftToCondition(d: CompoundConditionDraft): TriggerConfig | null { + const ids = d.entityIds.split(",").map((s) => s.trim()).filter(Boolean); + if (ids.length === 0) return null; + const c: TriggerConfig = { entity_id: ids[0], entity_ids: ids, type: d.type }; + if (d.type === "threshold") { + const a = parseFloat(d.above); if (!isNaN(a)) c.trigger_above = a; + const b = parseFloat(d.below); if (!isNaN(b)) c.trigger_below = b; + const f = parseInt(d.forMinutes, 10); if (!isNaN(f)) c.trigger_for_minutes = f; + } else if (d.type === "counter") { + const v = parseFloat(d.targetValue); if (!isNaN(v)) c.trigger_target_value = v; + c.trigger_delta_mode = d.deltaMode; + } else if (d.type === "state_change") { + if (d.fromState) c.trigger_from_state = d.fromState; + if (d.toState) c.trigger_to_state = d.toState; + const n = parseInt(d.targetChanges, 10); if (!isNaN(n)) c.trigger_target_changes = n; + } else if (d.type === "runtime") { + const h = parseFloat(d.runtimeHours); if (!isNaN(h)) c.trigger_runtime_hours = h; + } + return c; +} + +/** Short localized weekday names (0=Mon … 6=Sun) — uses the shared weekdayName (DRY). */ +function weekdayNames(lang?: string): string[] { + return Array.from({ length: 7 }, (_, i) => weekdayName(i, lang, "short")); +} + +export class MaintenanceTaskDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean, attribute: "checklists-enabled" }) public checklistsEnabled = false; + @property({ type: Boolean, attribute: "schedule-time-enabled" }) public scheduleTimeEnabled = false; + @property({ type: Boolean, attribute: "completion-actions-enabled" }) public completionActionsEnabled = false; + @property({ type: Number, attribute: "default-warning-days" }) public defaultWarningDays = 7; + @state() private _open = false; + @state() private _loading = false; + @state() private _error = ""; + @state() private _entryId = ""; + @state() private _taskId: string | null = null; // null = create + // When openCreate is called without an entry_id and a list of objects is supplied, + // the dialog renders an Object selector dropdown so the user can pick the parent. + @state() private _objectChoices: Array<{ entry_id: string; name: string }> = []; + + // Task fields + @state() private _name = ""; + @state() private _type = "custom"; + @state() private _scheduleType = "time_based"; + @state() private _intervalDays = "30"; + @state() private _intervalUnit = "days"; + @state() private _dueDate = ""; + @state() private _warningDays = "7"; + @state() private _earliestCompletionDays = ""; + @state() private _intervalAnchor: "completion" | "planned" = "completion"; + // Calendar kinds (Phase 4): weekdays (0=Mon…6=Sun), nth_weekday, day_of_month + @state() private _weekdays: number[] = []; + @state() private _nth = "1"; // "1".."5" or "-1" (last) + @state() private _nthWeekday = "5"; // Saturday + @state() private _domDay = "1"; + // (#83) end-of-month options: last day / business-day rollback / ±N offset. + @state() private _domLastDay = false; + @state() private _domBusiness = false; + @state() private _calOffset = "0"; + @state() private _notes = ""; + @state() private _documentationUrl = ""; + @state() private _customIcon = ""; + @state() private _priority = "normal"; + @state() private _labels = ""; + @state() private _enabled = true; + + // Trigger fields + @state() private _triggerEntityId = ""; + @state() private _triggerEntityIds: string[] = []; + @state() private _triggerEntityLogic: "any" | "all" = "any"; + @state() private _triggerAttribute = ""; + @state() private _triggerType = "threshold"; + @state() private _triggerAbove = ""; + @state() private _triggerBelow = ""; + @state() private _triggerForMinutes = "0"; + @state() private _triggerTargetValue = ""; + @state() private _triggerDeltaMode = false; + @state() private _autoCompleteOnRecovery = false; + @state() private _triggerFromState = ""; + @state() private _triggerToState = ""; + @state() private _triggerTargetChanges = ""; + @state() private _triggerRuntimeHours = ""; + // Compound trigger (type === "compound"): a list of conditions + AND/OR logic + @state() private _compoundLogic: "AND" | "OR" = "AND"; + @state() private _compoundConditions: CompoundConditionDraft[] = []; + + // Entity attribute introspection + @state() private _suggestedAttributes: string[] = []; + @state() private _availableAttributes: Array<{ name: string; value: unknown; numeric: boolean }> = []; + @state() private _entityDomain = ""; + + // NFC + @state() private _lastPerformed = ""; + @state() private _nfcTagId = ""; + // v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...) + @state() private _readingUnit = ""; + @state() private _availableTags: Array<{id: string; name: string}> = []; + + // User assignment + @state() private _responsibleUserId: string | null = null; + @state() private _assigneePool: string[] = []; + @state() private _rotationStrategy = ""; + @state() private _availableUsers: HAUser[] = []; + + // Checklist (newline-separated steps, one per line) + @state() private _checklistText = ""; + + // Schedule time (HH:MM, advanced feature) + @state() private _scheduleTime = ""; + + // v1.3.0: on_complete_action (gated by completionActionsEnabled) + // v1.3.1: _actionData is the parsed object (was: _actionDataJson string) + // so ha-form can drive the data fields when the service schema is known. + @state() private _actionService = ""; + @state() private _actionTargetEntity = ""; + @state() private _actionData: Record = {}; + @state() private _actionDataJsonFallback = ""; + @state() private _actionTesting = false; + @state() private _actionTestResult: "" | "ok" | "error" = ""; + @state() private _actionTestError = ""; + + // v1.3.0: quick_complete_defaults (gated by completionActionsEnabled) + @state() private _qcNotes = ""; + @state() private _qcCost = ""; + @state() private _qcDuration = ""; + @state() private _qcFeedback: "" | "needed" | "not_needed" = ""; + + // Environmental entity (adaptive_config) + @state() private _environmentalEntity = ""; + @state() private _environmentalAttribute = ""; + private _environmentalInitial = ""; // for change detection on save + private _environmentalAttributeInitial = ""; + private _userService: UserService | null = null; + + private get _lang(): string { + return this.hass?.language ?? navigator.language.split("-")[0] ?? "en"; + } + + public async openCreate(entryId: string, objects?: Array<{ entry_id: string; object: { name: string } }>): Promise { + this._entryId = entryId; + this._taskId = null; + this._error = ""; + // If no entryId is preset but caller passed objects, expose them as a dropdown. + // Sort alphabetically by name so the user doesn't have to scan for a target + // object in creation order (#40). First object after sort becomes the + // default selection so save can work without forced UI. + if (!entryId && objects && objects.length > 0) { + this._objectChoices = objects + .map(o => ({ entry_id: o.entry_id, name: o.object.name })) + .sort((a, b) => a.name.localeCompare(b.name)); + this._entryId = this._objectChoices[0].entry_id; + } else { + this._objectChoices = []; + } + this._resetFields(); + await Promise.all([this._loadUsers(), this._loadTags()]); + this._open = true; + } + + public async openEdit(entryId: string, task: MaintenanceTask): Promise { + this._entryId = entryId; + this._taskId = task.id; + this._error = ""; + this._name = task.name; + this._type = task.type; + this._scheduleType = task.schedule_type; + // Preserve null (= no safety interval set) instead of forcing "30". + // Bug #42: the old `?.toString() || "30"` fallback reverted a cleared + // safety_interval back to 30 on next edit, silently overwriting the + // user's intent the moment they touched any other field and saved. + this._intervalDays = task.interval_days != null ? String(task.interval_days) : ""; + this._intervalUnit = task.interval_unit || "days"; + this._dueDate = task.due_date || ""; + // Calendar kinds read the nested schedule (the only place they live). + const sched = task.schedule; + this._weekdays = sched?.kind === "weekdays" ? [...(sched.weekdays ?? [])] : []; + this._nth = sched?.kind === "nth_weekday" ? String(sched.nth ?? 1) : "1"; + this._nthWeekday = sched?.kind === "nth_weekday" ? String(sched.weekday ?? 5) : "5"; + this._domDay = sched?.kind === "day_of_month" && (sched.day ?? 1) >= 1 ? String(sched.day ?? 1) : "1"; + this._domLastDay = sched?.kind === "day_of_month" && sched.day === -1; + this._domBusiness = sched?.kind === "day_of_month" && sched.business === true; + this._calOffset = sched?.offset ? String(sched.offset) : "0"; + this._warningDays = task.warning_days.toString(); + this._earliestCompletionDays = + task.earliest_completion_days != null ? String(task.earliest_completion_days) : ""; + this._intervalAnchor = task.interval_anchor || "completion"; + this._notes = task.notes || ""; + this._documentationUrl = task.documentation_url || ""; + this._customIcon = task.custom_icon || ""; + this._priority = task.priority || "normal"; + this._labels = (task.labels || []).join(", "); + this._enabled = task.enabled !== false; + this._lastPerformed = task.last_performed || ""; + this._nfcTagId = task.nfc_tag_id || ""; + this._readingUnit = task.reading_unit || ""; + this._responsibleUserId = task.responsible_user_id || null; + this._assigneePool = [...(task.assignee_pool || [])]; + this._rotationStrategy = task.rotation_strategy || ""; + + this._checklistText = (task.checklist || []).join("\n"); + this._scheduleTime = task.schedule_time || ""; + + // v1.3.0: hydrate on_complete_action + quick_complete_defaults + const oca = task.on_complete_action; + if (oca && oca.service) { + this._actionService = oca.service; + const tgt = oca.target?.entity_id; + this._actionTargetEntity = Array.isArray(tgt) ? (tgt[0] || "") : (tgt || ""); + this._actionData = (oca.data && typeof oca.data === "object") ? { ...oca.data } : {}; + this._actionDataJsonFallback = ""; + } else { + this._actionService = ""; + this._actionTargetEntity = ""; + this._actionData = {}; + this._actionDataJsonFallback = ""; + } + const qcd = task.quick_complete_defaults; + this._qcNotes = qcd?.notes || ""; + this._qcCost = qcd?.cost != null ? String(qcd.cost) : ""; + this._qcDuration = qcd?.duration != null ? String(qcd.duration) : ""; + this._qcFeedback = (qcd?.feedback as "needed" | "not_needed" | undefined) || ""; + + const ac: Partial = task.adaptive_config || {}; + this._environmentalEntity = ac.environmental_entity || ""; + this._environmentalAttribute = ac.environmental_attribute || ""; + this._environmentalInitial = this._environmentalEntity; + this._environmentalAttributeInitial = this._environmentalAttribute; + + if (task.trigger_config) { + const tc = task.trigger_config; + this._triggerEntityId = tc.entity_id || ""; + this._triggerEntityIds = tc.entity_ids || (tc.entity_id ? [tc.entity_id] : []); + this._triggerEntityLogic = tc.entity_logic || "any"; + this._triggerAttribute = tc.attribute || ""; + this._triggerType = tc.type || "threshold"; + this._triggerAbove = tc.trigger_above?.toString() || ""; + this._triggerBelow = tc.trigger_below?.toString() || ""; + this._triggerForMinutes = tc.trigger_for_minutes?.toString() || "0"; + this._triggerTargetValue = tc.trigger_target_value?.toString() || ""; + this._triggerDeltaMode = tc.trigger_delta_mode || false; + this._autoCompleteOnRecovery = tc.auto_complete_on_recovery || false; + this._triggerFromState = tc.trigger_from_state || ""; + this._triggerToState = tc.trigger_to_state || ""; + this._triggerTargetChanges = tc.trigger_target_changes?.toString() || ""; + this._triggerRuntimeHours = tc.trigger_runtime_hours?.toString() || ""; + if (tc.type === "compound") { + this._compoundLogic = tc.compound_logic === "OR" ? "OR" : "AND"; + this._compoundConditions = (tc.conditions || []).map(conditionToDraft); + } else { + this._compoundLogic = "AND"; + this._compoundConditions = []; + } + } else { + this._resetTriggerFields(); + } + + // Fetch entity attributes if trigger entity is set + if (this._triggerEntityId) { + this._fetchEntityAttributes(this._triggerEntityId); + } + + await Promise.all([this._loadUsers(), this._loadTags()]); + this._open = true; + } + + private _resetFields(): void { + this._name = ""; + this._type = "custom"; + this._scheduleType = "time_based"; + this._intervalDays = "30"; + this._intervalUnit = "days"; + this._dueDate = ""; + this._warningDays = String(this.defaultWarningDays); + this._earliestCompletionDays = ""; + this._intervalAnchor = "completion"; + this._weekdays = []; + this._nth = "1"; + this._nthWeekday = "5"; + this._domDay = "1"; + this._domLastDay = false; + this._domBusiness = false; + this._calOffset = "0"; + this._notes = ""; + this._documentationUrl = ""; + this._customIcon = ""; + this._priority = "normal"; + this._labels = ""; + this._enabled = true; + this._lastPerformed = ""; + this._nfcTagId = ""; + this._readingUnit = ""; + this._responsibleUserId = null; + this._assigneePool = []; + this._rotationStrategy = ""; + this._checklistText = ""; + this._scheduleTime = ""; + this._environmentalEntity = ""; + this._environmentalAttribute = ""; + this._environmentalInitial = ""; + this._environmentalAttributeInitial = ""; + // v1.3.0 + this._actionService = ""; + this._actionTargetEntity = ""; + this._actionData = {}; + this._actionDataJsonFallback = ""; + this._actionTesting = false; + this._actionTestResult = ""; + this._qcNotes = ""; + this._qcCost = ""; + this._qcDuration = ""; + this._qcFeedback = ""; + this._resetTriggerFields(); + } + + private _resetTriggerFields(): void { + this._triggerEntityId = ""; + this._triggerEntityIds = []; + this._triggerEntityLogic = "any"; + this._triggerAttribute = ""; + this._suggestedAttributes = []; + this._availableAttributes = []; + this._entityDomain = ""; + this._triggerType = "threshold"; + this._triggerAbove = ""; + this._triggerBelow = ""; + this._triggerForMinutes = "0"; + this._triggerTargetValue = ""; + this._triggerDeltaMode = false; + this._autoCompleteOnRecovery = false; + this._triggerFromState = ""; + this._triggerToState = ""; + this._triggerTargetChanges = ""; + this._triggerRuntimeHours = ""; + this._compoundLogic = "AND"; + this._compoundConditions = []; + } + + private async _loadUsers(): Promise { + if (!this._userService) { + this._userService = new UserService(this.hass); + } + try { + this._availableUsers = await this._userService.getUsers(); + } catch (error) { + console.error("Failed to load users:", error); + this._availableUsers = []; + } + } + + private _toggleAssignee(userId: string): void { + this._assigneePool = this._assigneePool.includes(userId) + ? this._assigneePool.filter((u) => u !== userId) + : [...this._assigneePool, userId]; + } + + // v1.3.0: fire the configured action immediately so the user can verify + // it works before saving the task. Doesn't persist anything. + private async _testAction(): Promise { + // VALIDATE-ONLY — does NOT actually fire the service-call. + // + // Earlier this method called hass.callService(...) for real, which + // had real side effects: counter.increment incremented the counter, + // input_button.press fired the button (which on the user's vacuum + // robot would actually reset the dirty-time sensor as if maintenance + // had been performed). User pushback (correct): + // *"setzt Test auch den zustand? dann würde beim testen bereits so + // getan als ob eine Wartung ausgeführt wurde — das ist nicht gut"* + // + // The Test button is for verifying the *configuration* is correct + // before saving — not for triggering the action. The user can mark + // the task complete to actually fire it. + // + // Checks performed (all without side effects): + // 1. Service format matches `domain.service` regex + // 2. Service exists in hass.services registry + // 3. Domain whitelist for cross-domain services + // 4. Service-entity domain match (otherwise HA silently drops the + // call at fire-time with "Referenced entities ... missing or + // not currently available") + // 5. Target entity exists in hass.states (when target set) + const svc = this._actionService.trim(); + if (!svc || !/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(svc)) { + this._actionTestResult = "error"; + this._actionTestError = "Invalid service format (expected 'domain.service')"; + setTimeout(() => { this._actionTestResult = ""; this._actionTestError = ""; }, 5000); + return; + } + const [domain, name] = svc.split("."); + + // 2. Service exists? + if (!this.hass?.services?.[domain]?.[name]) { + this._actionTestResult = "error"; + this._actionTestError = `Service "${svc}" is not registered in Home Assistant. Check spelling and that the integration providing it is loaded.`; + setTimeout(() => { this._actionTestResult = ""; this._actionTestError = ""; }, 8000); + return; + } + + const tgt = this._actionTargetEntity.trim(); + + // 3 + 4. Cross-domain whitelist + domain match. + // homeassistant.* / scene.* / notify.* / persistent_notification.* + // legitimately accept cross-domain targets; everything else needs the + // entity domain to match the service domain. + if (tgt) { + const entityDomain = tgt.split(".")[0]; + const crossDomainServices = new Set([ + "homeassistant", "scene", "notify", "persistent_notification", + ]); + if (entityDomain !== domain && !crossDomainServices.has(domain)) { + this._actionTestResult = "error"; + this._actionTestError = `Service "${svc}" only works on ${domain}.* entities; entity "${tgt}" is in ${entityDomain}.* — pick a service that matches the entity domain (e.g. ${entityDomain}.${name})`; + setTimeout(() => { this._actionTestResult = ""; this._actionTestError = ""; }, 8000); + return; + } + // 5. Entity exists? + if (!this.hass.states?.[tgt]) { + this._actionTestResult = "error"; + this._actionTestError = `Target entity "${tgt}" not found in Home Assistant — the entity may have been renamed or its integration removed.`; + setTimeout(() => { this._actionTestResult = ""; this._actionTestError = ""; }, 8000); + return; + } + } + + // All checks passed — configuration is valid. The action will fire + // when the task is marked complete (not now). + this._actionTestResult = "ok"; + setTimeout(() => { this._actionTestResult = ""; this._actionTestError = ""; }, 5000); + } + + // v1.3.1: derive the data dict from either the schema-driven _actionData + // (preferred) or the JSON fallback textfield. Returns {} on any parse + // problem so the caller still gets a usable empty object. + private _buildActionData(): Record { + if (this._actionDataJsonFallback.trim()) { + try { + const parsed = JSON.parse(this._actionDataJsonFallback); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed as Record; + } + } catch { /* fallthrough to ha-form data */ } + } + return { ...this._actionData }; + } + + // v1.3.1: look up the selected service in hass.services and convert its + // `fields` map into the schema shape ha-form expects. Returns null when + // the service is unknown or has no fields metadata — caller falls back + // to a free-form JSON textfield. + private _serviceSchema(): Array<{ + name: string; + required: boolean; + selector: Record; + }> | null { + const svc = this._actionService.trim(); + if (!svc || !/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(svc)) return null; + const [domain, name] = svc.split("."); + const fields = this.hass?.services?.[domain]?.[name]?.fields; + if (!fields || Object.keys(fields).length === 0) return null; + return Object.entries(fields).map(([fname, def]) => ({ + name: fname, + required: !!def.required, + selector: (def.selector as Record) || { text: {} }, + })); + } + + private _renderCompletionActionsSection(L: string) { + if (!this.completionActionsEnabled) return nothing; + const schema = this._serviceSchema(); + return html` +
+ ${t("on_complete_action_title", L)} +

${t("on_complete_action_desc", L)}

+ { + this._actionService = e.detail.value || ""; + // Schema changed → drop fields the new service doesn't accept. + const newSchema = this._serviceSchema(); + if (newSchema) { + const allowed = new Set(newSchema.map(f => f.name)); + this._actionData = Object.fromEntries( + Object.entries(this._actionData).filter(([k]) => allowed.has(k)), + ); + } + }} + > + t("on_complete_action_target", L)} + @value-changed=${(e: CustomEvent) => { + const v = e.detail.value as { target_entity?: string }; + this._actionTargetEntity = v.target_entity || ""; + }} + > +

+ ${t("on_complete_action_target_hint", L)} +

+ ${schema + ? html` + { + this._actionData = { ...(e.detail.value as Record) }; + }} + > + ` + : html` + { this._actionDataJsonFallback = (e.target as HTMLInputElement).value; }} + > + `} +
+ + ${this._actionTestResult === "ok" + ? html`${t("on_complete_action_test_success", L)}` + : nothing} + ${this._actionTestResult === "error" + ? html`
+ ${t("on_complete_action_test_failed", L)} + ${this._actionTestError + ? html`
${this._actionTestError}
` + : nothing} +
` + : nothing} +
+
+ +
+ ${t("quick_complete_defaults_title", L)} +

${t("quick_complete_defaults_desc", L)}

+ { this._qcNotes = (e.target as HTMLInputElement).value; }} + > + { this._qcCost = (e.target as HTMLInputElement).value; }} + > + { this._qcDuration = (e.target as HTMLInputElement).value; }} + > + +
+ `; + } + + private async _loadTags(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/tags/list", + }) as { tags: Array<{id: string; name: string}> }; + this._availableTags = result.tags || []; + } catch { + this._availableTags = []; + } + } + + private async _fetchEntityAttributes(entityId: string): Promise { + if (!entityId || !this.hass) { + this._suggestedAttributes = []; + this._availableAttributes = []; + this._entityDomain = ""; + return; + } + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/entity/attributes", + entity_id: entityId, + }) as { + domain: string; + suggested_attributes: string[]; + available_attributes: Array<{ name: string; value: unknown; numeric: boolean }>; + }; + this._entityDomain = result.domain || ""; + this._suggestedAttributes = result.suggested_attributes || []; + this._availableAttributes = result.available_attributes || []; + } catch { + this._suggestedAttributes = []; + this._availableAttributes = []; + this._entityDomain = ""; + } + } + + private async _save(): Promise { + if (this._loading) return; // synchronous re-entry guard (double-click) + if (!this._name.trim()) return; + this._loading = true; + this._error = ""; + try { + const data: Record = { + type: this._taskId + ? "maintenance_supporter/task/update" + : "maintenance_supporter/task/create", + entry_id: this._entryId, + name: this._name, + task_type: this._type, + schedule_type: this._scheduleType, + warning_days: parseInt(this._warningDays, 10) || 7, + }; + const ecd = this._earliestCompletionDays.trim(); + data.earliest_completion_days = ecd === "" ? null : Math.max(0, parseInt(ecd, 10) || 0); + + if (this._taskId) data.task_id = this._taskId; + + if (this._scheduleType === "one_time") { + data.due_date = this._dueDate || null; + data.interval_days = null; + } else if (CALENDAR_KINDS.includes(this._scheduleType)) { + // Calendar kinds are sent as the nested schedule; the backend prefers + // it over the flat fields and clears any stale interval/due_date. + data.schedule = this._buildSchedule(); + data.interval_days = null; + if (this._taskId) data.due_date = null; + } else { + // Switching away from one-time clears the stale due_date on edit. + if (this._taskId) data.due_date = null; + if (this._scheduleType !== "manual" && this._intervalDays) { + data.interval_days = parseInt(this._intervalDays, 10); + data.interval_unit = this._intervalUnit; + data.interval_anchor = this._intervalAnchor; + } else if (this._taskId) { + data.interval_days = null; + data.interval_anchor = "completion"; + } + } + + data.notes = this._notes || null; + data.documentation_url = this._documentationUrl || null; + data.custom_icon = this._customIcon || null; + data.priority = this._priority; + data.labels = this._labels + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + data.enabled = this._enabled; + data.last_performed = this._lastPerformed || null; + data.nfc_tag_id = this._nfcTagId || null; + data.reading_unit = this._readingUnit.trim() || null; + data.responsible_user_id = this._responsibleUserId; + data.assignee_pool = this._assigneePool; + data.rotation_strategy = + this._assigneePool.length >= 2 && this._rotationStrategy + ? this._rotationStrategy + : null; + + if (this._scheduleType === "sensor_based" && this._triggerType === "compound") { + // Compound: a group of conditions joined by AND/OR. Each condition + // carries its own entity + type + params; the top-level entity picker + // does not apply here. + const conditions = this._compoundConditions + .map(draftToCondition) + .filter((c): c is TriggerConfig => c !== null); + if (conditions.length > 0) { + const triggerConfig: TriggerConfig = { + type: "compound", + compound_logic: this._compoundLogic, + conditions, + }; + if (this._autoCompleteOnRecovery) triggerConfig.auto_complete_on_recovery = true; + data.trigger_config = triggerConfig; + } else if (this._taskId) { + data.trigger_config = null; + } + } else if (this._scheduleType === "sensor_based" && this._triggerEntityId) { + const entityIds = this._triggerEntityIds.length > 0 + ? this._triggerEntityIds + : [this._triggerEntityId]; + const triggerConfig: TriggerConfig = { + entity_id: entityIds[0], + entity_ids: entityIds, + type: this._triggerType, + }; + if (this._triggerAttribute) triggerConfig.attribute = this._triggerAttribute; + if (this._autoCompleteOnRecovery) triggerConfig.auto_complete_on_recovery = true; + + // Multi-entity: store entity_logic for all trigger types + if (entityIds.length > 1) { + triggerConfig.entity_logic = this._triggerEntityLogic; + } + + if (this._triggerType === "threshold") { + if (this._triggerAbove) { const v = parseFloat(this._triggerAbove); if (!isNaN(v)) triggerConfig.trigger_above = v; } + if (this._triggerBelow) { const v = parseFloat(this._triggerBelow); if (!isNaN(v)) triggerConfig.trigger_below = v; } + if (this._triggerForMinutes) { const v = parseInt(this._triggerForMinutes, 10); if (!isNaN(v)) triggerConfig.trigger_for_minutes = v; } + } else if (this._triggerType === "counter") { + if (this._triggerTargetValue) { const v = parseFloat(this._triggerTargetValue); if (!isNaN(v)) triggerConfig.trigger_target_value = v; } + triggerConfig.trigger_delta_mode = this._triggerDeltaMode; + } else if (this._triggerType === "state_change") { + if (this._triggerFromState) triggerConfig.trigger_from_state = this._triggerFromState; + if (this._triggerToState) triggerConfig.trigger_to_state = this._triggerToState; + if (this._triggerTargetChanges) { const v = parseInt(this._triggerTargetChanges, 10); if (!isNaN(v)) triggerConfig.trigger_target_changes = v; } + } else if (this._triggerType === "runtime") { + if (this._triggerRuntimeHours) { const v = parseFloat(this._triggerRuntimeHours); if (!isNaN(v)) triggerConfig.trigger_runtime_hours = v; } + } + + data.trigger_config = triggerConfig; + } else if (this._taskId) { + data.trigger_config = null; + } + + // Schedule time only sent when feature is enabled — empty string clears. + if (this.scheduleTimeEnabled && this._scheduleType === "time_based") { + const t = this._scheduleTime.trim(); + data.schedule_time = /^([01]\d|2[0-3]):[0-5]\d$/.test(t) ? t : null; + } + + if (this.checklistsEnabled) { + const items = this._checklistText + .split("\n") + .map((l) => l.trim()) + .filter(Boolean) + .slice(0, 100); + data.checklist = items.length ? items : null; + } + + // v1.3.0: on_complete_action + quick_complete_defaults (gated) + if (this.completionActionsEnabled) { + const svc = this._actionService.trim(); + if (svc && /^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(svc)) { + const action: Record = { service: svc }; + const tgt = this._actionTargetEntity.trim(); + if (tgt) action.target = { entity_id: tgt }; + const dataDict = this._buildActionData(); + if (Object.keys(dataDict).length > 0) { + action.data = dataDict; + } + data.on_complete_action = action; + } else { + data.on_complete_action = null; + } + + const qcd: Record = {}; + if (this._qcNotes.trim()) qcd.notes = this._qcNotes.trim(); + const cost = parseFloat(this._qcCost); + if (!isNaN(cost) && cost >= 0) qcd.cost = cost; + const dur = parseInt(this._qcDuration, 10); + if (!isNaN(dur) && dur >= 0) qcd.duration = dur; + if (this._qcFeedback) qcd.feedback = this._qcFeedback; + data.quick_complete_defaults = Object.keys(qcd).length ? qcd : null; + } + + const result = await this.hass.connection.sendMessagePromise(data) as { task_id?: string }; + const savedTaskId = this._taskId || result?.task_id; + + // Environmental entity lives in adaptive_config (Store-managed), + // so it has a dedicated endpoint. Only call it when something + // actually changed, and only for sensor_based tasks. + const envChanged = + this._environmentalEntity !== this._environmentalInitial + || this._environmentalAttribute !== this._environmentalAttributeInitial; + if ( + savedTaskId + && this._scheduleType === "sensor_based" + && envChanged + ) { + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/set_environmental_entity", + entry_id: this._entryId, + task_id: savedTaskId, + environmental_entity: this._environmentalEntity || null, + environmental_attribute: this._environmentalAttribute || null, + }); + this._environmentalInitial = this._environmentalEntity; + this._environmentalAttributeInitial = this._environmentalAttribute; + } catch { + /* non-fatal — task itself saved */ + } + } + + this._open = false; + this.dispatchEvent(new CustomEvent("task-saved")); + } catch (e) { + this._error = describeWsError(e, this._lang, t("save_error", this._lang)); + } finally { + this._loading = false; + } + } + + private _close(): void { + this._open = false; + } + + private _renderTriggerFields() { + if (this._scheduleType !== "sensor_based") return nothing; + const L = this._lang; + + const isCompound = this._triggerType === "compound"; + return html` +

${t("trigger_configuration", L)}

+
+ + +
+ ${isCompound ? this._renderCompoundEditor() : html` + 0 ? this._triggerEntityIds.join(", ") : this._triggerEntityId} + @input=${(e: Event) => { + const raw = (e.target as HTMLInputElement).value; + const ids = raw.split(",").map((s: string) => s.trim()).filter(Boolean); + this._triggerEntityId = ids[0] || ""; + this._triggerEntityIds = ids; + if (ids[0]) this._fetchEntityAttributes(ids[0]); + }} + > + ${this._triggerEntityIds.length > 1 ? html` +
+ + +
+ ` : nothing} + ${this._availableAttributes.length > 0 + ? html` +
+ + +
+ ` + : html` + (this._triggerAttribute = (e.target as HTMLInputElement).value)} + > + ` + } + ${this._renderTriggerTypeFields()} + `} + +
${t("auto_complete_on_recovery_help", L)}
+ (this._intervalDays = (e.target as HTMLInputElement).value)} + > + ${this._intervalDays ? this._renderUnitSelect() : nothing} + `; + } + + /** Immutably patch one compound condition and trigger a re-render. */ + private _patchCondition(index: number, patch: Partial): void { + this._compoundConditions = this._compoundConditions.map((c, i) => + i === index ? { ...c, ...patch } : c + ); + } + + private _addCondition(): void { + this._compoundConditions = [...this._compoundConditions, emptyCondition()]; + } + + private _removeCondition(index: number): void { + this._compoundConditions = this._compoundConditions.filter((_, i) => i !== index); + } + + /** Compound trigger editor: AND/OR logic + a list of inline conditions. */ + private _renderCompoundEditor() { + const L = this._lang; + return html` +
+ + +
+
${t("compound_help", L)}
+ ${this._compoundConditions.length === 0 + ? html`
${t("compound_no_conditions", L)}
` + : this._compoundConditions.map((c, i) => this._renderCondition(c, i))} + + `; + } + + /** One compound condition row: entity + sub-type + type-specific params. */ + private _renderCondition(c: CompoundConditionDraft, i: number) { + const L = this._lang; + const num = i + 1; + return html` +
+
+ ${t("compound_condition", L)} ${num} + +
+ this._patchCondition(i, { entityIds: (e.target as HTMLInputElement).value })} + > +
+ + +
+ ${this._renderConditionTypeFields(c, i)} +
+ `; + } + + /** Type-specific inputs for a single compound condition (mirrors the flat + * per-type fields, bound to the condition draft). */ + private _renderConditionTypeFields(c: CompoundConditionDraft, i: number) { + const L = this._lang; + if (c.type === "threshold") { + return html` + this._patchCondition(i, { above: (e.target as HTMLInputElement).value })}> + this._patchCondition(i, { below: (e.target as HTMLInputElement).value })}> + this._patchCondition(i, { forMinutes: (e.target as HTMLInputElement).value })}> + `; + } + if (c.type === "counter") { + return html` + this._patchCondition(i, { targetValue: (e.target as HTMLInputElement).value })}> + + `; + } + if (c.type === "state_change") { + return html` + this._patchCondition(i, { fromState: (e.target as HTMLInputElement).value })}> + this._patchCondition(i, { toState: (e.target as HTMLInputElement).value })}> + this._patchCondition(i, { targetChanges: (e.target as HTMLInputElement).value })}> + `; + } + if (c.type === "runtime") { + return html` + this._patchCondition(i, { runtimeHours: (e.target as HTMLInputElement).value })}> + `; + } + return nothing; + } + + /** Shared interval-unit dropdown (DRY: time-based interval + sensor safety + * interval). Bound to _intervalUnit; options localized via unit_* keys. */ + private _renderUnitSelect() { + const L = this._lang; + return html` +
+ + +
`; + } + + private _toggleWeekday(i: number): void { + this._weekdays = this._weekdays.includes(i) + ? this._weekdays.filter((d) => d !== i) + : [...this._weekdays, i]; + } + + /** Build the nested `schedule` object for the selected calendar kind. */ + private _buildSchedule(): Record { + const withOffset = (schedule: Record) => { + const off = parseInt(this._calOffset, 10) || 0; + if (off) schedule.offset = Math.max(-15, Math.min(off, 15)); + return schedule; + }; + if (this._scheduleType === "weekdays") { + return withOffset({ kind: "weekdays", weekdays: [...this._weekdays].sort((a, b) => a - b) }); + } + if (this._scheduleType === "nth_weekday") { + return withOffset({ + kind: "nth_weekday", + nth: parseInt(this._nth, 10), + weekday: parseInt(this._nthWeekday, 10), + }); + } + // (#83) "last day" wins over the day number; business rolls weekends back. + const schedule: Record = { + kind: "day_of_month", + day: this._domLastDay ? -1 : (parseInt(this._domDay, 10) || 1), + }; + if (this._domBusiness) schedule.business = true; + return withOffset(schedule); + } + + /** Per-kind field groups for the calendar recurrence kinds. */ + private _renderCalendarFields() { + const L = this._lang; + const days = weekdayNames(L); + if (this._scheduleType === "weekdays") { + return html` + +
+ ${days.map((name, i) => html` + `)} +
+ ${this._renderCalOffsetField()}`; + } + if (this._scheduleType === "nth_weekday") { + const nths: Array<[string, string]> = [ + ["1", t("ord_1", L)], ["2", t("ord_2", L)], ["3", t("ord_3", L)], + ["4", t("ord_4", L)], ["5", t("ord_5", L)], ["-1", t("ord_last", L)], + ]; + return html` +
+ + +
+
+ + +
+ ${this._renderCalOffsetField()}`; + } + if (this._scheduleType === "day_of_month") { + return html` + ${this._domLastDay ? nothing : html` + (this._domDay = (e.target as HTMLInputElement).value)} + >`} + + + ${this._renderCalOffsetField()}`; + } + return nothing; + } + + /** (#83) ±N-day shift shared by all calendar kinds. */ + private _renderCalOffsetField() { + const L = this._lang; + return html` + (this._calOffset = (e.target as HTMLInputElement).value)} + >`; + } + + private _renderTriggerTypeFields() { + const L = this._lang; + if (this._triggerType === "threshold") { + return html` + (this._triggerAbove = (e.target as HTMLInputElement).value)} + > + (this._triggerBelow = (e.target as HTMLInputElement).value)} + > + (this._triggerForMinutes = (e.target as HTMLInputElement).value)} + > + `; + } + if (this._triggerType === "counter") { + return html` + (this._triggerTargetValue = (e.target as HTMLInputElement).value)} + > + + `; + } + if (this._triggerType === "state_change") { + return html` + (this._triggerFromState = (e.target as HTMLInputElement).value)} + > +
${t("state_value_help", L)}
+ (this._triggerToState = (e.target as HTMLInputElement).value)} + > + (this._triggerTargetChanges = (e.target as HTMLInputElement).value)} + > +
${t("target_changes_help", L)}
+ `; + } + if (this._triggerType === "runtime") { + return html` + (this._triggerRuntimeHours = (e.target as HTMLInputElement).value)} + > + `; + } + return nothing; + } + + render() { + if (!this._open) return html``; + const L = this._lang; + const title = this._taskId ? t("edit_task", L) : t("new_task", L); + return html` + +
${title}
+
+ ${this._error ? html`
${this._error}
` : nothing} + ${this._objectChoices.length > 0 ? html` +
+ + +
+ ` : nothing} + (this._name = (e.target as HTMLInputElement).value)} + > +
+ + +
+ ${this._type === "reading" + ? html` + (this._readingUnit = (e.target as HTMLInputElement).value)} + > +
${t("reading_unit_help", L)}
+ ` + : nothing} +
+ + +
+
+ + (this._labels = (e.target as HTMLInputElement).value)} + /> +
${t("labels_help", L)}
+
+
+ + +
+ ${this._scheduleType === "time_based" + ? html` + (this._intervalDays = (e.target as HTMLInputElement).value)} + > + ${this._renderUnitSelect()} +
+ + +
+ ${this.scheduleTimeEnabled ? html` + (this._scheduleTime = (e.target as HTMLInputElement).value)} + > + ` : nothing} + ` + : nothing} + ${this._renderCalendarFields()} + ${this._scheduleType === "one_time" + ? html` + (this._dueDate = (e.target as HTMLInputElement).value)} + > + ` + : nothing} + (this._warningDays = (e.target as HTMLInputElement).value)} + > + (this._earliestCompletionDays = (e.target as HTMLInputElement).value)} + > + ${this.checklistsEnabled ? html` +

${t("checklist_steps_optional", L)}

+ +
${t("checklist_help", L)}
+ ` : nothing} + (this._lastPerformed = (e.target as HTMLInputElement).value)} + > +
+ + +
+ ${this._availableUsers.length >= 2 ? html` +
+ +
${t("shared_with_help", L)}
+
+ ${this._availableUsers.map((user) => html` + `)} +
+
+ ${this._assigneePool.length >= 2 ? html` +
+ + +
` : nothing} + ` : nothing} + ${this._renderTriggerFields()} + ${this._scheduleType === "sensor_based" ? html` + (this._environmentalEntity = (e.target as HTMLInputElement).value.trim())} + > + ${this._environmentalEntity ? html` + (this._environmentalAttribute = (e.target as HTMLInputElement).value.trim())} + > + ` : nothing} + ` : nothing} + (this._notes = (e.target as HTMLInputElement).value)} + > + (this._documentationUrl = (e.target as HTMLInputElement).value)} + > + + (this._customIcon = (e.detail.value as string) || "")} + > + ${this._availableTags.length > 0 + ? html` +
+ + + +
+ ` + : html` + (this._nfcTagId = (e.target as HTMLInputElement).value)} + > +
+ ` + } + + ${this._renderCompletionActionsSection(L)} +
+
+ ${t("cancel", L)} + + ${this._loading ? t("saving", L) : t("save", L)} + +
+ + `; + } + + static styles = css` + .dialog-title { + font-size: 18px; + font-weight: 500; + padding-bottom: 12px; + } + /* v1.3.0: completion-action sections */ + .ca-section { + border: 1px solid var(--divider-color); + border-radius: 6px; + padding: 8px 12px; + margin-top: 8px; + } + .ca-section > summary { + cursor: pointer; + font-weight: 500; + } + .ca-section ms-textfield, + .ca-section ha-entity-picker, + .ca-section ha-service-picker, + .ca-section ha-form, + .ca-section .qc-feedback { + width: 100%; + margin-top: 8px; + display: block; + } + .ca-section .qc-feedback { + padding: 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + } + .ca-test-row { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; + } + .ca-test-ok { color: var(--success-color, #4caf50); font-size: 13px; } + .ca-test-error { color: var(--error-color, #f44336); font-size: 13px; font-weight: 500; } + .ca-test-error-block { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; } + .ca-test-error-detail { + font-size: 12px; + color: var(--secondary-text-color); + background: rgba(244, 67, 54, 0.08); + padding: 6px 8px; border-radius: 4px; + line-height: 1.4; + word-break: break-word; + } + .content { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 350px; + max-height: 70vh; + overflow-y: auto; + } + @media (max-width: 600px) { + .content { + min-width: 0; + max-height: none; + } + } + .dialog-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 16px; + } + ms-textfield { + display: block; + } + .field-label { + font-size: 12px; + color: var(--secondary-text-color); + } + .checklist-textarea { + width: 100%; + min-height: 88px; + padding: 8px; + font-family: inherit; + font-size: 14px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color); + color: var(--primary-text-color); + resize: vertical; + box-sizing: border-box; + } + .field-help { + font-size: 12px; + color: var(--secondary-text-color); + } + .field-help a, + .link-button { + background: none; + border: 0; + padding: 0; + color: var(--primary-color); + cursor: pointer; + font: inherit; + text-decoration: underline; + } + .field-help a:hover, + .link-button:hover { + text-decoration: none; + } + /* Smaller refresh icon-button when shown next to the dropdown. */ + .select-row .link-button { + margin-left: 8px; + text-decoration: none; + font-size: 16px; + } + .select-row .link-button:hover { + color: var(--primary-color); + opacity: 0.7; + } + h3 { + margin: 8px 0 0; + font-size: 14px; + color: var(--primary-color); + } + .select-row { + display: flex; + flex-direction: column; + gap: 4px; + } + .assignee-pool { + display: flex; + flex-wrap: wrap; + gap: 6px 14px; + margin-top: 4px; + } + .checkbox-row { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + cursor: pointer; + margin: 2px 0; + } + .checkbox-row input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + } + .pool-item { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + cursor: pointer; + } + .pool-item input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + } + .select-row label { + font-size: 12px; + color: var(--secondary-text-color); + } + .select-row select { + padding: 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 14px; + } + .field-label { + font-size: 12px; + color: var(--secondary-text-color); + } + .weekday-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + .weekday-chip { + padding: 6px 12px; + border: 1px solid var(--divider-color); + border-radius: 16px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + cursor: pointer; + } + .weekday-chip.selected { + background: var(--primary-color, #03a9f4); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color, #03a9f4); + } + .error { + color: var(--error-color, #f44336); + font-size: 13px; + } + .toggle-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + cursor: pointer; + } + `; +} + +if (!customElements.get("maintenance-task-dialog")) { + customElements.define("maintenance-task-dialog", MaintenanceTaskDialog); +} diff --git a/custom_components/maintenance_supporter/frontend-src/components/task-documents.ts b/custom_components/maintenance_supporter/frontend-src/components/task-documents.ts new file mode 100644 index 0000000..c2e7009 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/components/task-documents.ts @@ -0,0 +1,339 @@ +/** Documents linked to a maintenance task. + * + * A filtered view over the object's document pool via each doc's `task_ids`: + * link/unlink existing object documents to the task and open/download them. Full + * upload + management lives at the object level (documents-section); this keeps + * the manual / spare-parts list right where the maintenance work happens. Hides + * itself entirely when the object has no documents at all. + */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { t, ensureLocale } from "../styles"; +import { describeWsError } from "../ws-errors"; +import { downloadUrl } from "../helpers/download"; +import { formatBytes } from "../helpers/format-bytes"; +import type { HomeAssistant } from "../types"; + +interface Doc { + id: string; + kind: "file" | "weblink"; + title: string; + filename?: string; + url?: string; + mime?: string; + size?: number; + tags?: string[]; + task_ids?: string[]; + task_pages?: Record; +} + +const CATEGORIES = ["manual", "warranty", "invoice", "spare_parts", "photo", "other"] as const; +const CATEGORY_ICONS: Record = { + manual: "mdi:book-open-variant", + warranty: "mdi:shield-check", + invoice: "mdi:receipt-text-outline", + spare_parts: "mdi:cog-outline", + photo: "mdi:image-outline", + other: "mdi:file-document-outline", +}; + +export class MaintenanceTaskDocuments extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public entryId!: string; + @property({ attribute: false }) public taskId!: string; + @property({ type: Boolean }) public canWrite = false; + + @state() private _docs: Doc[] = []; + @state() private _loaded = false; + @state() private _busy = false; + @state() private _error = ""; + @state() private _attachId = ""; + + private _loadedKey = ""; + private _localeReady = false; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + updated(changed: Map): void { + super.updated(changed); + if (this.hass && !this._localeReady) { + this._localeReady = true; + void ensureLocale(this._lang).then(() => this.requestUpdate()); + } + const key = `${this.entryId}|${this.taskId}`; + if (this.hass && this.entryId && this.taskId && this._loadedKey !== key) { + this._loadedKey = key; + void this._load(); + } + } + + private async _load(): Promise { + try { + const r = await this.hass.connection.sendMessagePromise<{ documents: Doc[] }>({ + type: "maintenance_supporter/documents/list", + entry_id: this.entryId, + }); + this._docs = r.documents || []; + this._loaded = true; + this._error = ""; + } catch (e) { + this._error = describeWsError(e, this._lang); + this._loaded = true; + } + } + + private _linked(): Doc[] { + return this._docs.filter((d) => (d.task_ids || []).includes(this.taskId)); + } + + private _available(): Doc[] { + return this._docs.filter((d) => !(d.task_ids || []).includes(this.taskId)); + } + + private async _setTaskIds(doc: Doc, taskIds: string[]): Promise { + this._busy = true; + this._error = ""; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/documents/update", + doc_id: doc.id, + task_ids: taskIds, + }); + await this._load(); + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private _link(): void { + const doc = this._docs.find((d) => d.id === this._attachId); + if (!doc) return; + this._attachId = ""; + void this._setTaskIds(doc, [...(doc.task_ids || []), this.taskId]); + } + + private _unlink(doc: Doc): void { + void this._setTaskIds(doc, (doc.task_ids || []).filter((x) => x !== this.taskId)); + } + + private _isPdf(doc: Doc): boolean { + return doc.mime === "application/pdf" || (doc.filename || "").toLowerCase().endsWith(".pdf"); + } + + /** The page this doc should open at for the current task, if set (PDFs only). */ + private _pageFor(doc: Doc): number | undefined { + return this._isPdf(doc) ? doc.task_pages?.[this.taskId] : undefined; + } + + private async _open(doc: Doc): Promise { + if (doc.kind === "weblink") { + window.open(doc.url, "_blank", "noopener"); + return; + } + // A per-task page hint jumps straight to the relevant page via the PDF + // viewer's #page=N fragment (client-side, so it never breaks the signature). + const page = this._pageFor(doc); + const frag = page ? `#page=${page}` : ""; + const win = window.open("about:blank", "_blank"); + try { + const s = await this.hass.connection.sendMessagePromise<{ path: string }>({ + type: "auth/sign_path", + path: `/api/maintenance_supporter/document/${doc.id}`, + expires: 300, + }); + // Absolute URL: a fragment on a *root-relative* path won't resolve against + // the blank popup's about:blank base, so it would silently stay blank. + if (win) win.location.href = new URL(s.path + frag, window.location.origin).href; + } catch (e) { + if (win) win.close(); + this._error = describeWsError(e, this._lang); + } + } + + /** Set (page >= 1) or clear (0) the jump-to page for this doc's task link. */ + private async _setPage(doc: Doc, page: number): Promise { + this._busy = true; + this._error = ""; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/documents/update", + doc_id: doc.id, + task_pages: { [this.taskId]: page }, + }); + await this._load(); + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private async _download(doc: Doc): Promise { + try { + const s = await this.hass.connection.sendMessagePromise<{ path: string }>({ + type: "auth/sign_path", + path: `/api/maintenance_supporter/document/${doc.id}`, + expires: 30, + }); + downloadUrl(s.path, doc.filename || doc.title || "document"); + } catch (e) { + this._error = describeWsError(e, this._lang); + } + } + + render() { + // Hide entirely for objects with no documents — no clutter on every task. + if (!this._loaded || this._docs.length === 0) return nothing; + const L = this._lang; + const linked = this._linked(); + const available = this._available(); + return html` +
+

${t("documents", L)} (${linked.length})

+ ${this._error ? html`
${this._error}
` : nothing} + ${linked.length === 0 + ? html`
${t("doc_task_none", L)}
` + : html`
${linked.map((d) => this._renderRow(d, L))}
`} + ${this.canWrite && available.length + ? html`
+ + +
` + : nothing} +
+ `; + } + + private _renderRow(doc: Doc, L: string) { + const isFile = doc.kind === "file"; + const isPdf = this._isPdf(doc); + const page = doc.task_pages?.[this.taskId]; + const cat = (doc.tags || []).find((x) => (CATEGORIES as readonly string[]).includes(x)) || "other"; + const meta = isFile ? formatBytes(doc.size) : t("doc_link_badge", L); + return html` +
+ +
this._open(doc)} + @keydown=${(e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + void this._open(doc); + } + }} + > +
${doc.title || doc.filename || doc.url}
+
+ ${meta}${page ? html` · ${t("doc_page", L)} ${page}` : nothing} +
+
+ ${this.canWrite && isPdf + ? html` { + const v = parseInt((e.target as HTMLInputElement).value, 10); + void this._setPage(doc, Number.isFinite(v) && v >= 1 ? v : 0); + }} + />` + : nothing} + + ${isFile + ? html`` + : nothing} + ${this.canWrite + ? html`` + : nothing} +
+ `; + } + + static styles = css` + :host { display: block; } + .task-docs { margin-top: 20px; } + h3 { + display: flex; align-items: center; gap: 6px; margin: 0 0 8px; + font-size: 15px; color: var(--primary-text-color); + } + h3 ha-icon { --mdc-icon-size: 18px; color: var(--secondary-text-color, #888); } + .tdoc-empty { color: var(--secondary-text-color, #888); font-size: 13px; padding: 2px 0 8px; } + .tdoc-error { color: var(--error-color, #f44336); font-size: 13px; margin: 4px 0; } + .tdoc-list { display: flex; flex-direction: column; gap: 4px; } + .tdoc-row { + display: flex; align-items: center; gap: 10px; padding: 6px 10px; + border: 1px solid var(--divider-color); border-radius: 8px; + } + .tdoc-icon { color: var(--primary-color); --mdc-icon-size: 22px; flex: none; } + .tdoc-info { flex: 1; min-width: 0; cursor: pointer; border-radius: 6px; } + .tdoc-info:hover .tdoc-title { text-decoration: underline; } + .tdoc-info:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; } + .tdoc-title { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .tdoc-meta { font-size: 12px; color: var(--secondary-text-color, #888); } + .tdoc-pagetag { color: var(--primary-color); font-weight: 500; } + .tdoc-page { + flex: none; width: 76px; padding: 5px 8px; border-radius: 6px; font: inherit; font-size: 13px; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); + color: var(--primary-text-color); border: 1px solid var(--divider-color); + } + .tdoc-page:disabled { opacity: 0.5; } + .icon-btn { + display: inline-flex; align-items: center; justify-content: center; + width: 34px; height: 34px; border-radius: 8px; cursor: pointer; + background: transparent; border: none; color: var(--primary-text-color); + } + .icon-btn:hover { background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); } + .icon-btn[disabled] { opacity: 0.4; pointer-events: none; } + .icon-btn ha-icon { --mdc-icon-size: 20px; } + .tdoc-attach { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; } + .tdoc-select { + flex: 1; min-width: 160px; padding: 6px 10px; border-radius: 6px; font: inherit; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); + color: var(--primary-text-color); border: 1px solid var(--divider-color); + } + .tdoc-btn { + display: inline-flex; align-items: center; gap: 6px; cursor: pointer; + padding: 6px 12px; border-radius: 6px; font: inherit; font-size: 13px; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); + color: var(--primary-text-color); border: 1px solid var(--divider-color); + } + .tdoc-btn ha-icon { --mdc-icon-size: 18px; } + .tdoc-btn[disabled] { opacity: 0.5; pointer-events: none; } + `; +} + +if (!customElements.get("maintenance-task-documents")) { + customElements.define("maintenance-task-documents", MaintenanceTaskDocuments); +} diff --git a/custom_components/maintenance_supporter/frontend-src/components/task-quick-actions-dialog.ts b/custom_components/maintenance_supporter/frontend-src/components/task-quick-actions-dialog.ts new file mode 100644 index 0000000..8c87cc5 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/components/task-quick-actions-dialog.ts @@ -0,0 +1,815 @@ +/** v2.3.0 — Task Quick-Actions Dialog. + * + * Opens from the Lovelace card / strategy when the user clicks a task row. + * Surfaces every action the panel's Task-Detail header offers, in-place, + * without forcing a panel-roundtrip: + * + * • Quick info — name + status + next due + last performed + interval + * • Primary actions — Complete (existing dialog), Skip (inline), Reset (inline) + * • Secondary (admin) — Edit settings (existing dialog), QR Code, Delete + * • Footer — "Open in Maintenance Panel" deep-link for History/Statistics + * + * Mounted via dialog-mount.openTaskQuickActions(entryId, taskId). + */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatRecurrence } from "../styles"; +import { describeWsError } from "../ws-errors"; +import { renderWeibullSection } from "../renderers/weibull"; +import { renderPredictionSection } from "../renderers/prediction"; +import { renderRecommendationBars } from "../renderers/recommendation"; +import { + renderSeasonalCardCompact, + renderSeasonalCardExpanded, +} from "../renderers/seasonal"; +import type { + HomeAssistant, + HistoryEntry, + MaintenanceObjectResponse, + MaintenanceTask, + AdvancedFeatures, +} from "../types"; + +interface MaintenanceObjectFull { + entry_id: string; + object: { id: string; name: string }; + tasks: MaintenanceTask[]; +} + +export class MaintenanceTaskQuickActionsDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _open = false; + @state() private _entryId: string | null = null; + @state() private _taskId: string | null = null; + @state() private _task: MaintenanceTask | null = null; + @state() private _objectName = ""; + @state() private _busy = false; + @state() private _error = ""; + @state() private _showSkip = false; + @state() private _showReset = false; + @state() private _showDetails = false; + @state() private _showAdaptive = false; + @state() private _skipReason = ""; + @state() private _resetDate = ""; + @state() private _features: AdvancedFeatures = { + adaptive: false, predictions: false, seasonal: false, environmental: false, + budget: false, groups: false, checklists: false, schedule_time: false, + completion_actions: false, + }; + @state() private _toast = ""; + private _featuresLoaded = false; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + /** Open the dialog. Loads fresh data from /object via WS so dialog stays in + * sync even if the underlying card has stale data. */ + public async openFor(entryId: string, taskId: string): Promise { + this._entryId = entryId; + this._taskId = taskId; + this._error = ""; + this._showSkip = false; + this._showReset = false; + this._showAdaptive = false; + this._skipReason = ""; + this._resetDate = new Date().toISOString().slice(0, 10); + this._open = true; + await Promise.all([this._loadTask(), this._loadFeatures()]); + } + + /** Pull the active feature flags so adaptive sections only render when + * Adaptive / Seasonal / Environmental are actually enabled (matches the + * panel's behaviour). Cached after first load. */ + private async _loadFeatures(): Promise { + if (this._featuresLoaded) return; + try { + const r = await this.hass.connection.sendMessagePromise<{ + features?: Partial; + }>({ type: "maintenance_supporter/settings" }); + if (r?.features) { + this._features = { ...this._features, ...r.features }; + } + this._featuresLoaded = true; + } catch { + // Settings endpoint unavailable — leave defaults (all false), the + // adaptive panel will then stay hidden. + } + } + + public close(): void { + this._open = false; + this._task = null; + this._error = ""; + } + + private async _loadTask(): Promise { + if (!this._entryId || !this._taskId) return; + try { + const r = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/object", + entry_id: this._entryId, + }); + this._objectName = r.object?.name || ""; + const found = (r.tasks || []).find((t) => t.id === this._taskId); + this._task = found ?? null; + } catch (e) { + this._error = describeWsError(e, this._lang); + } + } + + private async _runWs(payload: Record): Promise { + this._busy = true; + this._error = ""; + try { + await this.hass.connection.sendMessagePromise(payload); + this._busy = false; + return true; + } catch (e) { + this._error = describeWsError(e, this._lang); + this._busy = false; + return false; + } + } + + private _notifyChanged(action: string): void { + this.dispatchEvent( + new CustomEvent("task-action-fired", { + detail: { entry_id: this._entryId, task_id: this._taskId, action }, + bubbles: true, + composed: true, + }), + ); + } + + private _onComplete(): void { + if (!this._entryId || !this._taskId || !this._task) return; + // Reuse the existing rich complete-dialog by mounting it on body + import("../dialog-mount").then(({ openCompleteDialog }) => { + const ok = openCompleteDialog({ + entry_id: this._entryId!, + task_id: this._taskId!, + task_name: this._task!.name, + checklist: this._task!.checklist || [], + adaptive_enabled: !!this._task!.adaptive_config?.enabled, + }); + if (ok) { + this._notifyChanged("complete"); + this.close(); + } + }); + } + + private async _onSkipConfirm(): Promise { + if (!this._entryId || !this._taskId) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/skip", + entry_id: this._entryId, + task_id: this._taskId, + reason: this._skipReason.trim() || null, + }); + if (ok) { + this._notifyChanged("skip"); + this.close(); + } + } + + private async _onResetConfirm(): Promise { + if (!this._entryId || !this._taskId) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/reset", + entry_id: this._entryId, + task_id: this._taskId, + date: this._resetDate || undefined, + }); + if (ok) { + this._notifyChanged("reset"); + this.close(); + } + } + + private _onEdit(): void { + if (!this._entryId || !this._taskId) return; + import("../dialog-mount").then(({ openEditTaskDialog }) => { + openEditTaskDialog(this._entryId!, this._taskId!); + this.close(); + }); + } + + private _onQr(): void { + if (!this._entryId || !this._taskId || !this._task) return; + import("../dialog-mount").then(({ openQrDialog }) => { + openQrDialog({ + entry_id: this._entryId!, + task_id: this._taskId!, + task_name: this._task!.name, + object_name: this._objectName, + }); + this.close(); + }); + } + + private async _onDelete(): Promise { + if (!this._entryId || !this._taskId) return; + const confirmText = t("delete_task_confirm", this._lang) + || `Delete "${this._task?.name}"?`; + if (!window.confirm(confirmText)) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/delete", + entry_id: this._entryId, + task_id: this._taskId, + }); + if (ok) { + this._notifyChanged("delete"); + this.close(); + } + } + + private async _onArchive(): Promise { + if (!this._entryId || !this._taskId) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/archive", + entry_id: this._entryId, + task_id: this._taskId, + }); + if (ok) { + this._notifyChanged("archive"); + this.close(); + } + } + + private async _onUnarchive(): Promise { + if (!this._entryId || !this._taskId) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/unarchive", + entry_id: this._entryId, + task_id: this._taskId, + }); + if (ok) { + this._notifyChanged("unarchive"); + this.close(); + } + } + + private _onOpenInPanel(): void { + if (!this._entryId || !this._taskId) return; + const path = `/maintenance-supporter?entry_id=${encodeURIComponent(this._entryId)}` + + `&task_id=${encodeURIComponent(this._taskId)}`; + history.pushState(null, "", path); + window.dispatchEvent(new CustomEvent("location-changed")); + this.close(); + } + + private async _applySuggestion(): Promise { + if (!this._entryId || !this._taskId || !this._task?.suggested_interval) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/apply_suggestion", + entry_id: this._entryId, + task_id: this._taskId, + interval: this._task.suggested_interval, + }); + if (ok) { + this._toast = t("suggestion_applied", this._lang) || "Applied"; + this._notifyChanged("apply_suggestion"); + // Refresh local task so the recommendation card hides + await this._loadTask(); + setTimeout(() => { this._toast = ""; }, 2500); + } + } + + private async _reanalyzeInterval(): Promise { + if (!this._entryId || !this._taskId) return; + this._busy = true; + this._error = ""; + try { + const r = await this.hass.connection.sendMessagePromise<{ + recommended_interval: number | null; + confidence: string; + data_points: number; + }>({ + type: "maintenance_supporter/task/analyze_interval", + entry_id: this._entryId, + task_id: this._taskId, + }); + this._toast = r.recommended_interval + ? `${t("reanalyze_result", this._lang) || "Recomputed"}: ${r.recommended_interval}d (${r.data_points} pts)` + : (t("reanalyze_insufficient_data", this._lang) || "Not enough data"); + await this._loadTask(); + setTimeout(() => { this._toast = ""; }, 3500); + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private _onEditHistoryEntry(entry: HistoryEntry): void { + if (!this._entryId || !this._taskId) return; + import("../dialog-mount").then(({ openHistoryEditDialog }) => { + openHistoryEditDialog({ + entry_id: this._entryId!, + task_id: this._taskId!, + original_timestamp: entry.timestamp, + type: entry.type, + timestamp: entry.timestamp, + notes: entry.notes ?? null, + cost: entry.cost ?? null, + duration: entry.duration ?? null, + completed_by: entry.completed_by ?? null, + }); + }); + } + + /** Inline recommendation card (Current vs Suggested with apply/reanalyze). + * Bars + confidence badge come from the shared renderer; the action row + * uses native + + + + `; + } + + /** Adaptive section: prediction + recommendation + Weibull + seasonal, + * reusing the panel's renderers. Only renders blocks that have data. */ + private _renderAdaptive(task: MaintenanceTask) { + const L = this._lang; + const hasRecommendation = this._features.adaptive + && task.suggested_interval + && task.suggested_interval !== task.interval_days; + const hasPrediction = (task.degradation_trend != null + && task.degradation_trend !== "insufficient_data") + || task.days_until_threshold != null + || (task.environmental_factor != null && task.environmental_factor !== 1.0); + const hasWeibull = this._features.adaptive + && task.interval_analysis?.weibull_beta != null + && task.interval_analysis?.weibull_eta != null; + const hasSeasonal = this._features.seasonal + && task.seasonal_factor + && task.seasonal_factor !== 1.0; + + if (!hasRecommendation && !hasPrediction && !hasWeibull && !hasSeasonal) { + return html`
+ ${t("adaptive_no_data", L) || "Not enough completion history yet for adaptive analysis."} +
`; + } + return html` +
+ ${this._toast + ? html`
${this._toast}
` + : nothing} + ${hasRecommendation ? this._renderRecommendation(task) : nothing} + ${hasPrediction ? renderPredictionSection(task, L, this._features) : nothing} + ${hasWeibull ? renderWeibullSection(task, L) : nothing} + ${hasSeasonal ? html` + ${renderSeasonalCardCompact(task, L, this._features)} + ${task.seasonal_factors?.length === 12 + || task.interval_analysis?.seasonal_factors?.length === 12 + ? renderSeasonalCardExpanded(task, L) + : nothing} + ` : nothing} +
+ `; + } + + /** Read-only details panel: stats + history. Shown when the user clicks + * "Show details" in the dialog. Edit-buttons on history entries open the + * existing history-edit dialog (which lives in the same dialog-mount). */ + private _renderDetails(task: MaintenanceTask) { + const L = this._lang; + const history = (task.history || []) as HistoryEntry[]; + const completed = history.filter((h) => h.type === "completed"); + const totalCost = completed.reduce( + (s, h) => s + (typeof h.cost === "number" ? h.cost : 0), + 0, + ); + const avgDuration = (() => { + const durs = completed + .map((h) => (typeof h.duration === "number" ? h.duration : null)) + .filter((d): d is number => d != null); + if (!durs.length) return null; + return Math.round(durs.reduce((s, d) => s + d, 0) / durs.length); + })(); + + return html` +
+
+
+ ${t("times_performed", L) || "Performed"} + ${completed.length} +
+
+ ${t("total_cost", L) || "Total cost"} + ${totalCost.toFixed(2)} +
+
+ ${t("avg_duration", L) || "Avg duration"} + ${avgDuration != null ? `${avgDuration}m` : "—"} +
+
+
+ ${t("history", L) || "History"} + ${history.length} +
+ ${history.length === 0 + ? html`
${t("history_empty", L) || "No history yet."}
` + : html` +
+ ${[...history].reverse().slice(0, 20).map((entry) => { + const editable = ["completed", "reset", "skipped"].includes(entry.type); + return html` +
+
+ ${t(entry.type, L)} + ${formatDateTime(entry.timestamp, L)} + ${editable + ? html`` + : nothing} +
+ ${entry.notes + ? html`
${entry.notes}
` + : nothing} + ${entry.cost != null || entry.duration != null + ? html`
+ ${entry.cost != null ? html`💰 ${entry.cost.toFixed(2)}` : nothing} + ${entry.duration != null ? html`⏱️ ${entry.duration}m` : nothing} +
` + : nothing} +
+ `; + })} + ${history.length > 20 + ? html`
… +${history.length - 20} ${t("older_entries", L) || "older"}
` + : nothing} +
+ `} +
+ `; + } + + render() { + if (!this._open) return nothing; + const L = this._lang; + const task = this._task; + const isAdmin = (this.hass?.user?.is_admin ?? true) as boolean; + + return html` +
+ + `; + } + + static styles = [sharedStyles, css` + :host { display: contents; } + .backdrop { + position: fixed; inset: 0; z-index: 100; + background: rgba(0,0,0,0.5); + } + .dialog { + position: fixed; left: 50%; top: 50%; + transform: translate(-50%, -50%); + width: 95vw; max-width: 460px; + max-height: 92vh; overflow: auto; + background: var(--card-background-color, var(--ha-card-background, #1c1c1c)); + color: var(--primary-text-color); + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0,0,0,0.4); + padding: 20px; + display: flex; flex-direction: column; gap: 14px; + z-index: 101; + } + .header { display: flex; flex-direction: column; gap: 6px; } + .title { display: flex; align-items: center; gap: 10px; } + .status-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; } + .task-name { font-size: 18px; font-weight: 600; } + .object { font-size: 13px; color: var(--secondary-text-color); } + .link-inline { + background: transparent; border: none; padding: 0; cursor: pointer; + color: var(--primary-color); font-size: inherit; font-family: inherit; + } + .link-inline:hover { text-decoration: underline; } + .quick-info { + display: flex; flex-wrap: wrap; gap: 12px; + font-size: 12px; color: var(--secondary-text-color); + padding-top: 4px; border-top: 1px solid var(--divider-color); + } + .quick-info strong { color: var(--primary-text-color); font-weight: 500; } + .actions { display: flex; gap: 8px; } + .actions.primary-row { gap: 6px; } + .actions.primary-row .btn { flex: 1; } + /* Edit + QR are admin-tools — left-align as a group; Delete is destructive + so it gets pushed to the far right with margin-left:auto for visual + separation. Earlier this row was flex-end which left a strange empty + gap on the left (user feedback). */ + .actions.secondary-row { + padding-top: 8px; border-top: 1px solid var(--divider-color); + justify-content: flex-start; + } + .actions.secondary-row .btn.danger { + margin-left: auto; + } + .btn { + padding: 8px 12px; font-size: 14px; + border-radius: 6px; cursor: pointer; + border: 1px solid var(--divider-color); + background: var(--secondary-background-color, transparent); + color: var(--primary-text-color); + font-weight: 500; + display: inline-flex; align-items: center; gap: 6px; + transition: background 0.12s; + } + .btn:hover { background: var(--state-icon-color, rgba(255,255,255,0.06)); } + .btn[disabled] { opacity: 0.5; cursor: wait; } + .btn.primary { + background: var(--primary-color); + color: var(--text-primary-color, white); + border-color: var(--primary-color); + } + .btn.cancel { background: transparent; } + .btn.ghost { padding: 6px 10px; font-size: 13px; } + .btn.danger { color: var(--error-color); } + .btn ha-icon { --mdc-icon-size: 18px; } + .inline-form { display: flex; flex-direction: column; gap: 8px; } + .inline-form label { font-size: 13px; color: var(--secondary-text-color); } + .inline-form input { + padding: 8px; font-size: 14px; + background: var(--secondary-background-color, #2c2c2c); + color: var(--primary-text-color); + border: 1px solid var(--divider-color, #444); + border-radius: 6px; + } + .inline-actions { display: flex; gap: 8px; justify-content: flex-end; } + .footer { display: flex; justify-content: center; padding-top: 4px; } + .link { + background: transparent; border: none; cursor: pointer; + color: var(--primary-color); font-size: 13px; + display: inline-flex; align-items: center; gap: 4px; + } + .link:hover { text-decoration: underline; } + .link ha-icon { --mdc-icon-size: 14px; } + .loading { padding: 24px; text-align: center; color: var(--secondary-text-color); } + .error { + padding: 8px; border-radius: 6px; + background: rgba(211,47,47,0.1); + color: var(--error-color, #d32f2f); font-size: 13px; + } + + /* Details (expandable Show details section) */ + .details-toggle { display: flex; justify-content: center; margin-top: 4px; } + .details { + display: flex; flex-direction: column; gap: 12px; + border-top: 1px solid var(--divider-color); + padding-top: 12px; + } + .stats-grid { + display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; + } + .stat { + display: flex; flex-direction: column; gap: 2px; + background: var(--secondary-background-color, rgba(255,255,255,0.04)); + padding: 8px; border-radius: 6px; + align-items: center; + } + .stat-label { font-size: 11px; color: var(--secondary-text-color); text-transform: uppercase; letter-spacing: 0.5px; } + .stat-value { font-size: 16px; font-weight: 600; } + .history-header { + display: flex; align-items: baseline; gap: 8px; + font-size: 14px; + } + .history-count { + font-size: 12px; color: var(--secondary-text-color); + background: var(--secondary-background-color); padding: 2px 8px; border-radius: 999px; + } + .history-empty { color: var(--secondary-text-color); font-style: italic; font-size: 13px; } + .history-list { display: flex; flex-direction: column; gap: 8px; max-height: 280px; overflow: auto; } + .history-entry { + padding: 6px 8px; border-radius: 6px; + background: var(--secondary-background-color, rgba(255,255,255,0.03)); + font-size: 13px; + } + .history-line { + display: flex; align-items: center; gap: 8px; + justify-content: space-between; + } + .history-type { + font-weight: 600; font-size: 11px; + padding: 2px 6px; border-radius: 4px; + text-transform: uppercase; letter-spacing: 0.5px; + } + .type-completed { background: rgba(46,125,50,0.2); color: #66bb6a; } + .type-skipped { background: rgba(158,158,158,0.2); color: var(--secondary-text-color); } + .type-reset { background: rgba(33,150,243,0.2); color: #64b5f6; } + .type-triggered { background: rgba(255,87,34,0.2); color: #ff8a65; } + .history-date { font-size: 11px; color: var(--secondary-text-color); flex: 1; text-align: right; } + .history-edit { + background: transparent; border: none; cursor: pointer; + padding: 4px; border-radius: 4px; + color: var(--secondary-text-color); + } + .history-edit:hover { background: var(--state-icon-color, rgba(255,255,255,0.06)); color: var(--primary-color); } + .history-edit ha-icon { --mdc-icon-size: 14px; } + .history-notes { margin-top: 4px; color: var(--primary-text-color); } + .history-meta { display: flex; gap: 12px; margin-top: 4px; color: var(--secondary-text-color); font-size: 11px; } + .history-more { padding: 8px; text-align: center; font-size: 12px; color: var(--secondary-text-color); font-style: italic; } + + /* Adaptive section — wraps the panel renderers (which assume sharedStyles + are present) and adds dialog-specific layout. */ + .adaptive-stack { + display: flex; flex-direction: column; gap: 12px; + border-top: 1px solid var(--divider-color); + padding-top: 12px; + } + .adaptive-empty { + padding: 16px; text-align: center; + color: var(--secondary-text-color); + font-style: italic; font-size: 13px; + border-top: 1px solid var(--divider-color); + } + .toast { + padding: 8px 12px; border-radius: 6px; + background: rgba(76, 175, 80, 0.15); + color: #4caf50; font-size: 13px; font-weight: 500; + } + /* The panel's recommendation-card uses ha-button. We use plain ` + : nothing} + ${RANGES.map( + (r) => html``, + )} + ` + : nothing} + ${pts.length < 2 + ? html`
+ ${t("loading_chart", L)} +
` + : this._renderSvg(W, pts)} + + `; + } + + private _renderSvg(W: number, pts: ChartPoint[]) { + const L = this.lang; + const plotW = W - PAD_L - PAD_R; + const plotB = H - PAD_B; + const plotH = plotB - PAD_T; + + // Domain: data ∪ min/max band ∪ reference lines ∪ optional zero floor. + let lo = Infinity; + let hi = -Infinity; + for (const p of pts) { + lo = Math.min(lo, p.min ?? p.val); + hi = Math.max(hi, p.max ?? p.val); + } + if (this.thresholdAbove != null) { lo = Math.min(lo, this.thresholdAbove); hi = Math.max(hi, this.thresholdAbove); } + if (this.thresholdBelow != null) { lo = Math.min(lo, this.thresholdBelow); hi = Math.max(hi, this.thresholdBelow); } + if (this.targetValue != null) { lo = Math.min(lo, this.targetValue); hi = Math.max(hi, this.targetValue); } + if (this.forceZero) lo = Math.min(lo, 0); + const pad = (hi - lo || 1) * 0.06; + // A progress axis never dips below zero — padding must not create a -50 tick. + const loPadded = this.forceZero && lo >= 0 ? 0 : lo - pad; + let { ticks, niceMin, niceMax } = niceTicks(loPadded, hi + pad, 4); + if (this.forceZero && lo >= 0 && niceMin < 0) { + niceMin = 0; + ticks = ticks.filter((v) => v >= 0); + } + + const tsMin = pts[0].ts; + const tsMax = pts[pts.length - 1].ts; + const tsSpan = tsMax - tsMin || 1; + const withYear = needsYear(tsMin, tsMax); + + const toX = (ts: number) => PAD_L + ((ts - tsMin) / tsSpan) * plotW; + const toY = (v: number) => PAD_T + (1 - (v - niceMin) / (niceMax - niceMin || 1)) * plotH; + + const linePts = pts.map((p) => `${toX(p.ts).toFixed(1)},${toY(p.val).toFixed(1)}`).join(" "); + const areaPath = + `M${toX(pts[0].ts).toFixed(1)},${plotB} ` + + pts.map((p) => `L${toX(p.ts).toFixed(1)},${toY(p.val).toFixed(1)}`).join(" ") + + ` L${toX(pts[pts.length - 1].ts).toFixed(1)},${plotB} Z`; + + // Optional min/max band behind the line. + let bandPath = ""; + const band = pts.filter((p) => p.min != null && p.max != null); + if (band.length >= 2) { + const up = band.map((p) => `${toX(p.ts).toFixed(1)},${toY(p.max!).toFixed(1)}`); + const dn = [...band].reverse().map((p) => `${toX(p.ts).toFixed(1)},${toY(p.min!).toFixed(1)}`); + bandPath = `M${up[0]} ` + up.slice(1).map((x) => `L${x}`).join(" ") + ` L${dn.join(" L")} Z`; + } + + // Danger zones: rect (shading + line-color clip), the threshold line y, + // and where its label sits (just inside the zone). + const zones: { y: number; h: number; lineY: number; label: string; labelY: number }[] = []; + if (this.thresholdBelow != null) { + const zy = toY(this.thresholdBelow); + zones.push({ y: zy, h: Math.max(0, plotB - zy), lineY: zy, label: `▼ ${fmtNum(this.thresholdBelow)}`, labelY: Math.min(plotB - 4, zy + 13) }); + } + if (this.thresholdAbove != null) { + const zy = toY(this.thresholdAbove); + zones.push({ y: PAD_T, h: Math.max(0, zy - PAD_T), lineY: zy, label: `▲ ${fmtNum(this.thresholdAbove)}`, labelY: Math.max(PAD_T + 11, zy - 5) }); + } + + const lastP = pts[pts.length - 1]; + const events = (this.events || []).filter((e) => e.ts >= tsMin && e.ts <= tsMax); + const xTicks = timeTicks(tsMin, tsMax, Math.max(2, Math.min(5, Math.floor(plotW / 110) + 1))); + const hover = this._hover; + + return html` +
+ this._onPointer(e, pts, toX, toY, W)} + @pointerdown=${(e: PointerEvent) => this._onPointer(e, pts, toX, toY, W)} + @pointerleave=${() => (this._hover = null)} + > + + + ${zones.length + ? svg`${zones.map((z) => svg``)}` + : nothing} + + + + + + + + ${ticks.map((v) => { + const y = toY(v); + if (y < PAD_T - 1 || y > plotB + 1) return nothing; + return svg` + + ${fmtNum(v)}`; + })} + + ${zones.map( + (z) => svg``, + )} + + ${bandPath ? svg`` : nothing} + + + ${zones.length + ? svg`` + : nothing} + + ${zones.map( + (z) => svg` + + ${z.label}`, + )} + + ${this.targetValue != null + ? svg` + ◆ ${fmtNum(this.targetValue)} ${this.unit}` + : nothing} + + ${this.projection && this.projection.length === 2 + ? svg`` + : nothing} + + ${xTicks.map((ts, i) => { + const x = toX(ts); + const anchor = i === 0 ? "start" : i === xTicks.length - 1 ? "end" : "middle"; + return svg`${fmtDateTick(ts, L, withYear)}`; + })} + + + + ${events.map((e) => { + const x = toX(e.ts); + const color = + e.type === "completed" + ? "var(--success-color, #4caf50)" + : e.type === "skipped" + ? "var(--warning-color, #ff9800)" + : "var(--info-color, #2196f3)"; + return svg` + + + ${fmtDateTime(e.ts, L)} + `; + })} + + ${hover + ? svg` + + ` + : svg``} + + ${hover + ? html`
+
${fmtDateTime(hover.p.ts, L)}
+
+ ${fmtVal(hover.p.val, this.unit, L)} + ${hover.p.min != null && hover.p.max != null + ? html`(${fmtNum(hover.p.min)}–${fmtNum(hover.p.max)})` + : nothing} +
+
` + : nothing} +
+ `; + } + + private _onPointer( + e: PointerEvent, + pts: ChartPoint[], + toX: (ts: number) => number, + toY: (v: number) => number, + W: number, + ): void { + const svgEl = e.currentTarget as SVGSVGElement; + const rect = svgEl.getBoundingClientRect(); + const px = ((e.clientX - rect.left) / rect.width) * W; + if (px < PAD_L - 8 || px > W - PAD_R + 8) { + this._hover = null; + return; + } + let best = pts[0]; + let bestD = Infinity; + for (const p of pts) { + const d = Math.abs(toX(p.ts) - px); + if (d < bestD) { + bestD = d; + best = p; + } + } + this._hover = { x: toX(best.ts), y: toY(best.val), p: best }; + } + + static styles = css` + :host { display: block; width: 100%; } + .chart-wrap { position: relative; } + .range-chips { display: flex; gap: 4px; justify-content: flex-end; margin-bottom: 2px; } + .range-chip { + font: inherit; font-size: 11.5px; padding: 2px 9px; border-radius: 12px; cursor: pointer; + border: 1px solid var(--divider-color); background: transparent; + color: var(--secondary-text-color); + } + /* Outlier toggle sits left of the range chips as an icon button. */ + .outlier-chip { margin-right: auto; padding: 2px 7px; display: inline-flex; align-items: center; } + .outlier-chip ha-icon { --mdc-icon-size: 15px; } + .range-chip.active { + background: var(--primary-color); border-color: var(--primary-color); + color: var(--text-primary-color, #fff); + } + .range-chip[disabled] { opacity: 0.5; pointer-events: none; } + .svg-holder { position: relative; } + .chart-svg { display: block; touch-action: pan-y; } + .tick-label { fill: var(--secondary-text-color); font-size: 10.5px; } + .zone-label { fill: var(--error-color, #f44336); font-size: 11px; font-weight: 600; } + .chart-empty { + display: flex; align-items: center; justify-content: center; gap: 8px; height: 120px; + color: var(--secondary-text-color); font-size: 12.5px; + } + .chart-empty ha-icon { --mdc-icon-size: 17px; } + .hover-chip { + position: absolute; top: 0; transform: translateX(-50%); + background: var(--card-background-color, #fff); border: 1px solid var(--divider-color); + border-radius: 8px; padding: 4px 9px; pointer-events: none; white-space: nowrap; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); z-index: 3; + } + .hover-date { font-size: 10.5px; color: var(--secondary-text-color); } + .hover-val { font-size: 12.5px; font-weight: 600; color: var(--primary-text-color); } + .hover-range { font-weight: 400; color: var(--secondary-text-color); font-size: 11px; } + `; +} + +if (!customElements.get("maintenance-trigger-chart")) { + customElements.define("maintenance-trigger-chart", MaintenanceTriggerChart); +} diff --git a/custom_components/maintenance_supporter/frontend-src/components/vacation-section-card.ts b/custom_components/maintenance_supporter/frontend-src/components/vacation-section-card.ts new file mode 100644 index 0000000..363fbca --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/components/vacation-section-card.ts @@ -0,0 +1,327 @@ +/** v2.4.0 — Interactive Vacation Section Card. + * + * Replaces the read-only markdown card from Phase 5. Lets the user toggle + * vacation mode + edit start/end/buffer inline, without leaving Lovelace. + * + * Used as a section-strategy card (HA 2026.5+ sections) AND as a stand-alone + * card the user can drop onto any dashboard. + * + * Permissions: only admins see edit affordances; non-admins get a read-only + * view with a deep-link to the panel (vacation config is admin-only at the + * WS layer too). + */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { t, ensureLocale } from "../styles"; +import { describeWsError } from "../ws-errors"; +import { sectionCardSharedStyles } from "./section-card-shared-styles"; +import type { HomeAssistant } from "../types"; + +interface VacationState { + enabled?: boolean; + is_active?: boolean; + start?: string | null; + end?: string | null; + buffer_days?: number; + window_end?: string | null; + exempt_task_ids?: string[]; +} + +interface CardConfig { + type: string; + title?: string; +} + +export class MaintenanceVacationSectionCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _config: CardConfig = { type: "" }; + @state() private _state: VacationState | null = null; + @state() private _busy = false; + @state() private _error = ""; + @state() private _localStart = ""; + @state() private _localEnd = ""; + @state() private _localBuffer = 7; + @state() private _dirty = false; + + private _loaded = false; + + setConfig(config: CardConfig): void { + this._config = config; + } + + getCardSize(): number { + return 2; + } + + private get _lang(): string { + return this.hass?.language || "en"; + } + + private get _isAdmin(): boolean { + return (this.hass?.user?.is_admin ?? true) as boolean; + } + + updated(changedProps: Map): void { + super.updated(changedProps); + if (changedProps.has("hass") && this.hass && !this._loaded) { + this._loaded = true; + void this._load(); + void ensureLocale(this._lang).then(() => this.requestUpdate()); + } + } + + private async _load(): Promise { + try { + const r = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/vacation/state", + }); + this._state = r; + this._localStart = r.start || ""; + this._localEnd = r.end || ""; + this._localBuffer = r.buffer_days ?? 7; + this._dirty = false; + } catch (e) { + this._error = describeWsError(e, this._lang); + } + } + + private async _toggleEnabled(on: boolean): Promise { + this._busy = true; + this._error = ""; + try { + const r = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/vacation/update", + enabled: on, + }); + this._state = r; + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private async _save(): Promise { + if (!this._isAdmin) return; + this._busy = true; + this._error = ""; + try { + const r = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/vacation/update", + start: this._localStart || null, + end: this._localEnd || null, + buffer_days: this._localBuffer, + }); + this._state = r; + this._dirty = false; + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private async _endNow(): Promise { + if (!this._isAdmin) return; + if (!window.confirm(t("vacation_end_now_confirm", this._lang) + || "End vacation immediately?")) return; + this._busy = true; + try { + const r = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/vacation/end_now", + }); + this._state = r; + this._localStart = r.start || ""; + this._localEnd = r.end || ""; + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private _onDeepLink(): void { + const path = "/maintenance-supporter?ms_action=open_vacation"; + history.pushState(null, "", path); + window.dispatchEvent(new CustomEvent("location-changed")); + } + + render() { + const L = this._lang; + const s = this._state; + if (!s) { + return html`
${t("loading", L) || "Loading…"}
`; + } + const active = s.is_active === true; + const enabled = s.enabled === true; + const exemptCount = s.exempt_task_ids?.length ?? 0; + const statusLabel = active + ? (t("vacation_status_active", L) || "Active now") + : enabled + ? (t("vacation_status_scheduled", L) || "Scheduled") + : (t("vacation_status_inactive", L) || "Inactive"); + const statusClass = active ? "active" : enabled ? "scheduled" : "inactive"; + + return html` + +
+
+
+ 🏖️ + ${this._config.title || (t("vacation_mode", L) || "Vacation mode")} +
+ ${statusLabel} +
+ + ${this._error ? html`
${this._error}
` : nothing} + + ${this._isAdmin + ? html` +
+ + + this._toggleEnabled((e.target as HTMLInputElement).checked)} + > +
+ +
+
+ + { + this._localStart = (e.target as HTMLInputElement).value; + this._dirty = true; + }} /> +
+
+ + { + this._localEnd = (e.target as HTMLInputElement).value; + this._dirty = true; + }} /> +
+
+ + { + this._localBuffer = parseInt( + (e.target as HTMLInputElement).value, 10) || 0; + this._dirty = true; + }} /> +
+
+ +
+ + ${active + ? html`` + : nothing} + ${exemptCount > 0 + ? html`` + : html``} +
+ ` + : html` +
+ ${enabled && s.start && s.end + ? html`
${s.start} → ${s.end}
` + : nothing} + +
+ `} +
+
+ `; + } + + static styles = [sectionCardSharedStyles, css` + .status-pill { + font-size: 11px; font-weight: 600; + padding: 3px 8px; border-radius: 999px; + text-transform: uppercase; letter-spacing: 0.5px; + } + .status-pill.active { + background: rgba(76, 175, 80, 0.15); + color: #4caf50; + } + .status-pill.scheduled { + background: rgba(255, 152, 0, 0.15); + color: #ff9800; + } + .status-pill.inactive { + background: rgba(158, 158, 158, 0.15); + color: var(--secondary-text-color); + } + .row.toggle-row { + display: flex; align-items: center; justify-content: space-between; + } + .row.toggle-row label { + font-size: 14px; color: var(--primary-text-color); + } + .dates-row { + display: grid; grid-template-columns: 1fr 1fr 100px; gap: 10px; + } + .date-field.buffer label { white-space: nowrap; } + .date-field { display: flex; flex-direction: column; gap: 4px; } + .date-field label { + font-size: 11px; color: var(--secondary-text-color); + text-transform: uppercase; letter-spacing: 0.3px; + } + .date-field input { + padding: 6px 8px; font-size: 13px; + background: var(--secondary-background-color, #2c2c2c); + color: var(--primary-text-color); + border: 1px solid var(--divider-color); border-radius: 6px; + font-family: inherit; + } + .date-field input:disabled { opacity: 0.5; cursor: not-allowed; } + .actions { + display: flex; gap: 8px; align-items: center; flex-wrap: wrap; + } + .readonly { display: flex; flex-direction: column; gap: 8px; } + `]; +} + +if (!customElements.get("maintenance-vacation-section-card")) { + customElements.define( + "maintenance-vacation-section-card", + MaintenanceVacationSectionCard, + ); +} + +(window as { customCards?: unknown[] }).customCards = + (window as { customCards?: unknown[] }).customCards || []; +((window as { customCards?: unknown[] }).customCards!).push({ + type: "maintenance-vacation-section-card", + name: "Maintenance Supporter — Vacation", + description: "Inline vacation mode toggle + dates", + preview: false, +}); diff --git a/custom_components/maintenance_supporter/frontend-src/deeplink_test.mjs b/custom_components/maintenance_supporter/frontend-src/deeplink_test.mjs new file mode 100644 index 0000000..8981603 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/deeplink_test.mjs @@ -0,0 +1,50 @@ +import { chromium } from "playwright"; + +const HA = "http://homeassistant-dev:8123"; +const REFRESH = process.env.HA_REFRESH_TOKEN; +if (!REFRESH) { console.error("Set HA_REFRESH_TOKEN env var"); process.exit(1); } + +const browser = await chromium.connect("ws://localhost:3000"); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 }, locale: "de" }); +const page = await ctx.newPage(); + +await page.goto(HA); +await page.waitForTimeout(1000); +await page.evaluate((refresh) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: "http://homeassistant-dev:8123", + clientId: "http://homeassistant-dev:8123/", + refresh_token: refresh, access_token: "", token_type: "Bearer", + expires_in: 1800, expires: 0, + })); +}, REFRESH); + +// Test 1: Deep-link with valid entry_id + task_id +console.log("=== Test 1: Valid deep-link ==="); +await page.goto(HA + "/maintenance-supporter?entry_id=01KJTX1VHBT6TN5JMF9W6WADTJ&task_id=49c49ecf36204031be94c8f132aa6479"); +await page.waitForTimeout(6000); +var url1 = page.url(); +console.log("URL after deep-link:", url1); +console.log("Query params cleaned:", !url1.includes("entry_id")); +await page.screenshot({ path: "screenshots/deeplink_valid.png" }); + +// Test 2: Deep-link with invalid entry_id +console.log("\n=== Test 2: Invalid entry_id ==="); +await page.goto(HA + "/maintenance-supporter?entry_id=NONEXISTENT"); +await page.waitForTimeout(6000); +var url2 = page.url(); +console.log("URL after invalid deep-link:", url2); +console.log("Query params cleaned:", !url2.includes("entry_id")); +await page.screenshot({ path: "screenshots/deeplink_invalid.png" }); + +// Test 3: Deep-link with valid entry but invalid task +console.log("\n=== Test 3: Invalid task_id ==="); +await page.goto(HA + "/maintenance-supporter?entry_id=01KJTX1VHBT6TN5JMF9W6WADTJ&task_id=NONEXISTENT"); +await page.waitForTimeout(6000); +var url3 = page.url(); +console.log("URL after invalid task deep-link:", url3); +await page.screenshot({ path: "screenshots/deeplink_invalid_task.png" }); + +await ctx.close(); +await browser.close(); +console.log("\nDone!"); diff --git a/custom_components/maintenance_supporter/frontend-src/dialog-mount.ts b/custom_components/maintenance_supporter/frontend-src/dialog-mount.ts new file mode 100644 index 0000000..059b9bc --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/dialog-mount.ts @@ -0,0 +1,300 @@ +/** Standalone dialog mounting helper. + * + * Mounts the existing MaintenanceObjectDialog / MaintenanceTaskDialog onto + * document.body so they can be opened from any Lovelace context — without + * the user navigating to the panel first. + * + * Usage from a strategy or card click handler: + * + * import { openCreateObjectDialog, openTaskDialog } from "./dialog-mount"; + * openCreateObjectDialog(); + * + * The hass instance is pulled from .hass at open time and + * re-injected on every Lovelace re-render so the dialog stays connected to + * a fresh WS connection across HA reconnects. + * + * Each dialog is created once and re-used. Closing the dialog (via its own + * Cancel/Save buttons) unsets ``_open`` internally; we never destroy it. + */ + +import "./components/object-dialog"; +import "./components/task-dialog"; +import "./components/complete-dialog"; +import "./components/history-edit-dialog"; +import "./components/qr-dialog"; +import "./components/task-quick-actions-dialog"; +import "./components/object-quick-actions-dialog"; +import type { MaintenanceObjectDialog } from "./components/object-dialog"; +import type { MaintenanceTaskDialog } from "./components/task-dialog"; +import type { + MaintenanceHistoryEditDialog, + HistoryEntryDraft, +} from "./components/history-edit-dialog"; +import type { MaintenanceCompleteDialog } from "./components/complete-dialog"; +import type { MaintenanceQrDialog } from "./components/qr-dialog"; +import type { MaintenanceTaskQuickActionsDialog } from "./components/task-quick-actions-dialog"; +import type { MaintenanceObjectQuickActionsDialog } from "./components/object-quick-actions-dialog"; +import type { HomeAssistant, MaintenanceObject } from "./types"; +import { ensureLocale, isLocaleLoaded } from "./styles"; + +const OBJECT_DIALOG_TAG = "maintenance-object-dialog"; +const TASK_DIALOG_TAG = "maintenance-task-dialog"; +const HISTORY_EDIT_DIALOG_TAG = "maintenance-history-edit-dialog"; +const COMPLETE_DIALOG_TAG = "maintenance-complete-dialog"; +const QR_DIALOG_TAG = "maintenance-qr-dialog"; +const QUICK_ACTIONS_DIALOG_TAG = "maintenance-task-quick-actions-dialog"; +const OBJECT_QUICK_ACTIONS_DIALOG_TAG = "maintenance-object-quick-actions-dialog"; + +interface HassRoot extends HTMLElement { + hass?: HomeAssistant; +} + +function getHass(): HomeAssistant | undefined { + const root = document.querySelector("home-assistant"); + return root?.hass; +} + +function getOrCreate(tag: string): T { + let el = document.body.querySelector(tag); + if (!el) { + el = document.createElement(tag) as T; + document.body.appendChild(el); + } + return el; +} + +function syncHass(el: HTMLElement & { hass?: HomeAssistant }): boolean { + const hass = getHass(); + if (!hass) return false; + el.hass = hass; + // Dialogs opened from the dashboard strategy live in a lazily-loaded chunk + // that never ran the panel's ensureLocale — so without this the popup renders + // in English even when the rest of HA is localized. Fetch the user's locale + // (idempotent + cached) and force a re-render once it's in, so the first paint + // (which may be English) is corrected to the user's language. + const lang = hass.language || "en"; + if (!isLocaleLoaded(lang)) { + void ensureLocale(lang).then(() => { + (el as unknown as { requestUpdate?: () => void }).requestUpdate?.(); + }); + } + return true; +} + +/** Settings cache used to populate task-dialog's feature-gated sections + * (checklists / schedule_time / completion_actions) AND its + * defaultWarningDays prop when the dialog is mounted from Lovelace. + * + * Without this, every section that's gated on a feature flag stays + * hidden (default false) — that's the "ich habe nicht alles wie im + * Panel gefunden" symptom. The panel reads the same settings object + * via maintenance-panel.ts and passes it through as element properties. + * + * Cache lifetime: page session. Invalidated on full HA reload (which + * also drops our custom-element registry). The settings WS itself is + * cheap (~10ms) so even uncached this isn't a hot path concern, but + * caching means re-opening the dialog feels instant. + */ +interface SettingsCache { + features: { + adaptive: boolean; predictions: boolean; seasonal: boolean; + environmental: boolean; budget: boolean; groups: boolean; + checklists: boolean; schedule_time: boolean; completion_actions: boolean; + }; + defaultWarningDays: number; +} + +const FALLBACK_SETTINGS: SettingsCache = { + features: { + adaptive: false, predictions: false, seasonal: false, + environmental: false, budget: false, groups: false, + checklists: false, schedule_time: false, completion_actions: false, + }, + defaultWarningDays: 7, +}; + +let _cachedSettings: Promise | null = null; + +function fetchSettingsOnce(hass: HomeAssistant): Promise { + if (_cachedSettings) return _cachedSettings; + _cachedSettings = hass.connection + .sendMessagePromise<{ + features?: SettingsCache["features"]; + general?: { default_warning_days?: number }; + }>({ type: "maintenance_supporter/settings" }) + .then((r) => ({ + features: r.features ?? FALLBACK_SETTINGS.features, + defaultWarningDays: r.general?.default_warning_days ?? 7, + })) + .catch(() => FALLBACK_SETTINGS); + return _cachedSettings; +} + +export function openCreateObjectDialog(): boolean { + const dlg = getOrCreate(OBJECT_DIALOG_TAG); + if (!syncHass(dlg)) return false; + // openCreate is defined on MaintenanceObjectDialog + dlg.openCreate(); + return true; +} + +export function openEditObjectDialog( + entryId: string, + obj: MaintenanceObject, +): boolean { + const dlg = getOrCreate(OBJECT_DIALOG_TAG); + if (!syncHass(dlg)) return false; + dlg.openEdit(entryId, obj); + return true; +} + +export function openCreateTaskDialog(): boolean { + const dlg = getOrCreate(TASK_DIALOG_TAG); + if (!syncHass(dlg)) return false; + const hass = getHass(); + if (!hass) return false; + void (async () => { + const settings = await fetchSettingsOnce(hass); + const dlgFull = dlg as MaintenanceTaskDialog & { + checklistsEnabled: boolean; + scheduleTimeEnabled: boolean; + completionActionsEnabled: boolean; + defaultWarningDays: number; + openCreate: (entryId?: string) => void; + }; + dlgFull.checklistsEnabled = settings.features.checklists; + dlgFull.scheduleTimeEnabled = settings.features.schedule_time; + dlgFull.completionActionsEnabled = settings.features.completion_actions; + dlgFull.defaultWarningDays = settings.defaultWarningDays; + dlgFull.openCreate(); + })(); + return true; +} + +export function openEditTaskDialog( + entryId: string, + taskId: string, +): boolean { + const dlg = getOrCreate(TASK_DIALOG_TAG); + if (!syncHass(dlg)) return false; + + // openEdit on the panel-side expects a FULL MaintenanceTask (it accesses + // task.warning_days.toString(), task.checklist, task.adaptive_config etc. + // unconditionally on hydrate). For the Lovelace-mount path we only have + // (entry_id, task_id) at the click site, so we MUST fetch the full task + // via WS before calling openEdit. + // + // Earlier the comment claimed "the dialog re-loads task data via WS in + // its own openEdit handler when given just an id" — that was aspirational + // but never implemented. Calling openEdit with a stub `{id: taskId}` + // crashed silently with TypeError: Cannot read properties of undefined + // (reading 'toString'). Reported as #50-followup ("strategy dashboard + // edit button geht nicht mehr"). + // + // The fetch is fire-and-forget — boolean return is kept for back-compat + // (true = mount succeeded, false = no hass yet). Errors during fetch are + // logged + the dialog stays closed. + const hass = getHass(); + if (!hass) return false; + void (async () => { + try { + // Fetch features + task in parallel — both are needed before openEdit. + // Without features, the completion-action / checklist / schedule-time + // sections stay hidden (their @property defaults to false), which is + // the "im Dashboard nicht alles wie im Panel" symptom. + const [r, settings] = await Promise.all([ + hass.connection.sendMessagePromise<{ + tasks?: Array<{ id: string } & Record>; + }>({ type: "maintenance_supporter/object", entry_id: entryId }), + fetchSettingsOnce(hass), + ]); + const fullTask = (r.tasks || []).find((t) => t.id === taskId); + if (!fullTask) { + // eslint-disable-next-line no-console + console.warn(`openEditTaskDialog: task ${taskId} not found in entry ${entryId}`); + return; + } + const dlgFull = dlg as MaintenanceTaskDialog & { + checklistsEnabled: boolean; + scheduleTimeEnabled: boolean; + completionActionsEnabled: boolean; + defaultWarningDays: number; + openEdit: (entryId: string, task: unknown) => Promise; + }; + dlgFull.checklistsEnabled = settings.features.checklists; + dlgFull.scheduleTimeEnabled = settings.features.schedule_time; + dlgFull.completionActionsEnabled = settings.features.completion_actions; + dlgFull.defaultWarningDays = settings.defaultWarningDays; + await dlgFull.openEdit(entryId, fullTask); + } catch (e) { + // eslint-disable-next-line no-console + console.warn("openEditTaskDialog: failed to load task/features", e); + } + })(); + return true; +} + +/** v2.2.0: open the history-entry editor in place, e.g. from a calendar + * card past-event click. The caller fetches the existing entry data via + * the maintenance_supporter/object WS first (or uses what the calendar + * event already carries). */ +export function openHistoryEditDialog(draft: HistoryEntryDraft): boolean { + const dlg = getOrCreate(HISTORY_EDIT_DIALOG_TAG); + if (!syncHass(dlg)) return false; + dlg.openEdit(draft); + return true; +} + +/** v2.3.0: open the rich complete-dialog from any Lovelace context. */ +export function openCompleteDialog(args: { + entry_id: string; + task_id: string; + task_name: string; + checklist?: string[]; + adaptive_enabled?: boolean; +}): boolean { + const dlg = getOrCreate(COMPLETE_DIALOG_TAG); + if (!syncHass(dlg)) return false; + dlg.entryId = args.entry_id; + dlg.taskId = args.task_id; + dlg.taskName = args.task_name; + dlg.checklist = args.checklist ?? []; + dlg.adaptiveEnabled = !!args.adaptive_enabled; + dlg.lang = (getHass()?.language) || "en"; + dlg.open(); + return true; +} + +/** v2.3.0: open the QR dialog for a single task. */ +export function openQrDialog(args: { + entry_id: string; + task_id: string; + task_name: string; + object_name: string; +}): boolean { + const dlg = getOrCreate(QR_DIALOG_TAG); + if (!syncHass(dlg)) return false; + dlg.openForTask(args.entry_id, args.task_id, args.object_name, args.task_name); + return true; +} + +/** v2.3.0: open the task quick-actions dialog (Complete/Skip/Reset/Edit/QR/Delete). + * This is the primary entry point from a card row click. */ +export function openTaskQuickActions(entryId: string, taskId: string): boolean { + const dlg = getOrCreate( + QUICK_ACTIONS_DIALOG_TAG, + ); + if (!syncHass(dlg)) return false; + void dlg.openFor(entryId, taskId); + return true; +} + +/** v2.3.0 Phase 3: open the object quick-actions dialog (Edit/Add-task/Delete + read-only meta + task list). */ +export function openObjectQuickActions(entryId: string): boolean { + const dlg = getOrCreate( + OBJECT_QUICK_ACTIONS_DIALOG_TAG, + ); + if (!syncHass(dlg)) return false; + void dlg.openFor(entryId); + return true; +} diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-checklist-preview.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-checklist-preview.mjs new file mode 100644 index 0000000..aae59db --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-checklist-preview.mjs @@ -0,0 +1,116 @@ +/** + * Verifies the read-only checklist preview in the task detail (overview tab). + * Adds a task with a checklist via WS, navigates to its detail, asserts the + * preview card with all 3 steps is in the DOM. + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; + +const RUN = Date.now().toString(36); +const OBJ_NAME = `Checklist Preview ${RUN}`; +const STEPS = ["Drain water", "Replace filter cartridge", "Run flush cycle"]; + +const log = (...a) => console.log(...a); +let pass = 0, fail = 0; +const check = (label, ok, detail) => { + if (ok) { pass++; log(` ✓ ${label}`); } + else { fail++; log(` ✗ ${label}${detail ? " — " + detail : ""}`); } +}; + +const PANEL_EVAL = ` +window._ha=document.querySelector('home-assistant'); +window._main=_ha.shadowRoot.querySelector('home-assistant-main'); +window._drawer=_main.shadowRoot.querySelector('ha-drawer'); +window._resolver=_drawer.querySelector('partial-panel-resolver'); +window._custom=_resolver.querySelector('ha-panel-custom'); +window._panel=_custom.querySelector('maintenance-supporter-panel'); +window._sr=_panel.shadowRoot; +`; + +async function main() { + const { browser, ctx, page } = await setup({ mobile: false }); + + log("\n== Step 1: enable Checklists feature"); + await ws(page, { + type: "maintenance_supporter/global/update", + settings: { advanced_checklists_visible: true }, + }); + + log("\n== Step 2: create object + task with checklist"); + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: OBJ_NAME }); + const taskRes = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "Filter Maintenance", interval_days: 30, + checklist: STEPS, + }); + log(" created task_id =", taskRes.task_id); + + log("\n== Step 3: wait for panel to refresh + select the task"); + await page.waitForTimeout(2500); + await page.evaluate(({ eid, tid, fn }) => { + eval(fn); + window._panel._selectedEntryId = eid; + window._panel._selectedTaskId = tid; + window._panel._view = "task"; + window._panel.requestUpdate(); + }, { eid: obj.entry_id, tid: taskRes.task_id, fn: PANEL_EVAL }); + await page.waitForTimeout(1500); + + log("\n== Step 4: inspect overview tab DOM for the checklist preview card"); + const result = await page.evaluate(({ fn }) => { + eval(fn); + const sr = window._sr; + const card = sr.querySelector(".checklist-preview-card"); + const header = card?.querySelector(".checklist-preview-header")?.textContent?.trim(); + const items = [...(card?.querySelectorAll("li") || [])].map((li) => li.textContent?.trim()); + return { + hasCard: !!card, + header, + items, + cardOffsetTop: card?.offsetTop, + }; + }, { fn: PANEL_EVAL }); + log(" hasCard:", result.hasCard); + log(" header:", JSON.stringify(result.header)); + log(" items:", JSON.stringify(result.items)); + log(" offsetTop:", result.cardOffsetTop); + + check("preview card rendered in task detail", result.hasCard === true, ""); + check("header shows count = 3", result.header?.includes("(3)"), `header="${result.header}"`); + check("all 3 items rendered in order", + JSON.stringify(result.items) === JSON.stringify(STEPS), + `items=${JSON.stringify(result.items)}`); + + await page.screenshot({ path: "checklist-preview.png", fullPage: false }); + log(" saved checklist-preview.png"); + + log("\n== Step 5: verify preview is HIDDEN when checklist is empty"); + const obj2 = await ws(page, { type: "maintenance_supporter/object/create", name: `Empty ${RUN}` }); + const t2 = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj2.entry_id, name: "Bare Task", interval_days: 7, + }); + await page.waitForTimeout(1500); + await page.evaluate(({ eid, tid, fn }) => { + eval(fn); + window._panel._selectedEntryId = eid; + window._panel._selectedTaskId = tid; + window._panel._view = "task"; + window._panel.requestUpdate(); + }, { eid: obj2.entry_id, tid: t2.task_id, fn: PANEL_EVAL }); + await page.waitForTimeout(1000); + const empty = await page.evaluate(({ fn }) => { + eval(fn); + return !!window._sr.querySelector(".checklist-preview-card"); + }, { fn: PANEL_EVAL }); + check("preview hidden for tasks without checklist", empty === false, ""); + + log("\n== Cleanup"); + await ws(page, { type: "maintenance_supporter/object/delete", entry_id: obj.entry_id }); + await ws(page, { type: "maintenance_supporter/object/delete", entry_id: obj2.entry_id }); + await cleanup(browser, ctx); + + log(`\n=== ${pass} passed, ${fail} failed ===`); + process.exit(fail === 0 ? 0 : 1); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-checklist-visibility.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-checklist-visibility.mjs new file mode 100644 index 0000000..6e7081c --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-checklist-visibility.mjs @@ -0,0 +1,178 @@ +/** + * Reproduces issue #32 item 1: enable Checklists feature, then verify + * the textarea is actually visible in the task dialog. + * + * Run via: + * docker exec playwright-server sh -c \ + * 'cd /tmp && node /work/frontend-src/e2e-checklist-visibility.mjs' + * or from host with the helper container. + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; + +const RUN = Date.now().toString(36); +const OBJ_NAME = `Checklist Probe ${RUN}`; + +const log = (...args) => console.log(...args); +let pass = 0, fail = 0; +const check = (label, ok, detail) => { + if (ok) { pass++; log(` ✓ ${label}`); } + else { fail++; log(` ✗ ${label}${detail ? " — " + detail : ""}`); } +}; + +const PANEL_EVAL = ` +window._ha=document.querySelector('home-assistant'); +window._main=_ha.shadowRoot.querySelector('home-assistant-main'); +window._drawer=_main.shadowRoot.querySelector('ha-drawer'); +window._resolver=_drawer.querySelector('partial-panel-resolver'); +window._custom=_resolver.querySelector('ha-panel-custom'); +window._panel=_custom.querySelector('maintenance-supporter-panel'); +window._sr=_panel.shadowRoot; +`; + +async function main() { + const { browser, ctx, page } = await setup({ mobile: false }); + + log("\n== Step 1: read /settings (initial)"); + let settings = await ws(page, { type: "maintenance_supporter/settings" }); + log(" features.checklists =", settings.features.checklists); + + log("\n== Step 2: enable Checklists via /global/update"); + settings = await ws(page, { + type: "maintenance_supporter/global/update", + settings: { advanced_checklists_visible: true }, + }); + check("backend reports checklists=true", settings.features.checklists === true, + `actual=${settings.features.checklists}`); + + log("\n== Step 3: wait for panel to react to settings event"); + await page.waitForTimeout(2500); + + log("\n== Step 4: read panel's _features"); + const panelFeatures = await page.evaluate((fn) => { + eval(fn); + return window._panel._features; + }, PANEL_EVAL); + log(" _features =", JSON.stringify(panelFeatures)); + check("panel._features.checklists is true", panelFeatures?.checklists === true, + `panel sees checklists=${panelFeatures?.checklists}`); + + log("\n== Step 5: create object + task to open dialog against"); + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: OBJ_NAME }); + const taskRes = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "Probe Task", interval_days: 7, + }); + await page.waitForTimeout(2000); + + log("\n== Step 6: open task-dialog in EDIT mode"); + const objWithTasks = await ws(page, { + type: "maintenance_supporter/object", entry_id: obj.entry_id, + }); + const task = objWithTasks.tasks.find(t => t.id === taskRes.task_id); + await page.evaluate(({ eid, task, fn }) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog").openEdit(eid, task); + }, { eid: obj.entry_id, task, fn: PANEL_EVAL }); + await page.waitForTimeout(1500); + + log("\n== Step 7: inspect the dialog DOM for the checklist textarea"); + const dlgState = await page.evaluate((fn) => { + eval(fn); + const dlg = window._sr.querySelector("maintenance-task-dialog"); + const dlgSr = dlg?.shadowRoot; + return { + dialogExists: !!dlg, + checklistsEnabled: dlg?.checklistsEnabled, + open: dlg?._open, + hasTextarea: !!dlgSr?.querySelector("textarea.checklist-textarea"), + hasFieldLabel: !!dlgSr?.querySelector('label[for="checklist-textarea"]'), + labelText: dlgSr?.querySelector('label[for="checklist-textarea"]')?.textContent?.trim(), + // grab the rendered HTML around the env field for reference + contentSnippet: dlgSr?.querySelector(".content")?.innerHTML?.slice(0, 1500), + }; + }, PANEL_EVAL); + + log(" dialogExists:", dlgState.dialogExists); + log(" checklistsEnabled prop:", dlgState.checklistsEnabled); + log(" dialog open:", dlgState.open); + log(" hasTextarea:", dlgState.hasTextarea); + log(" hasFieldLabel:", dlgState.hasFieldLabel); + log(" labelText:", dlgState.labelText); + check("checklistsEnabled prop is true on the dialog", + dlgState.checklistsEnabled === true, + `prop=${dlgState.checklistsEnabled}`); + check("checklist textarea is in the DOM", + dlgState.hasTextarea === true, + "the textarea.checklist-textarea was not found inside the dialog"); + check("label exists for checklist textarea", + dlgState.hasFieldLabel === true, + "the label for=\"checklist-textarea\" was not found"); + + if (!dlgState.hasTextarea) { + log("\n --- dialog content snippet (first 1500 chars) ---"); + log(dlgState.contentSnippet); + } + + log("\n== Step 8: take screenshot of edit dialog"); + await page.screenshot({ path: "/tmp/checklist-edit.png", fullPage: false }); + log(" saved /tmp/checklist-edit.png"); + + log("\n== Step 9: scroll inside dialog so the textarea is visible"); + const scrollInfo = await page.evaluate((fn) => { + eval(fn); + const dlgSr = window._sr.querySelector("maintenance-task-dialog").shadowRoot; + const content = dlgSr.querySelector(".content"); + const ta = dlgSr.querySelector("textarea.checklist-textarea"); + return { + contentScrollHeight: content?.scrollHeight, + contentClientHeight: content?.clientHeight, + contentOverflow: getComputedStyle(content).overflowY, + textareaOffsetTop: ta?.offsetTop, + textareaVisible: ta && ta.getBoundingClientRect().top < window.innerHeight, + }; + }, PANEL_EVAL); + log(" scroll info:", JSON.stringify(scrollInfo)); + check("dialog content is scrollable", + scrollInfo.contentOverflow === "auto" || scrollInfo.contentOverflow === "scroll", + `overflow-y=${scrollInfo.contentOverflow}`); + check("textarea is reachable by scrolling within dialog", + scrollInfo.textareaOffsetTop > 0, + `offsetTop=${scrollInfo.textareaOffsetTop}`); + + log("\n== Step 10: test CREATE mode (new task — does the field appear there?)"); + await page.evaluate((fn) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog")._open = false; + }, PANEL_EVAL); + await page.waitForTimeout(500); + await page.evaluate(({ eid, fn }) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog").openCreate(eid); + }, { eid: obj.entry_id, fn: PANEL_EVAL }); + await page.waitForTimeout(1500); + + const createState = await page.evaluate((fn) => { + eval(fn); + const dlgSr = window._sr.querySelector("maintenance-task-dialog").shadowRoot; + return { + hasTextarea: !!dlgSr?.querySelector("textarea.checklist-textarea"), + labelText: dlgSr?.querySelector('label[for="checklist-textarea"]')?.textContent?.trim(), + }; + }, PANEL_EVAL); + log(" CREATE mode:", JSON.stringify(createState)); + check("checklist textarea also visible in CREATE dialog", + createState.hasTextarea === true, ""); + await page.screenshot({ path: "/tmp/checklist-create.png", fullPage: false }); + log(" saved /tmp/checklist-create.png"); + + log("\n== Cleanup"); + await ws(page, { + type: "maintenance_supporter/object/delete", entry_id: obj.entry_id, + }); + await cleanup(browser, ctx); + + log(`\n=== ${pass} passed, ${fail} failed ===`); + process.exit(fail === 0 ? 0 : 1); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-error-messages.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-error-messages.mjs new file mode 100644 index 0000000..5e481f3 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-error-messages.mjs @@ -0,0 +1,98 @@ +/** + * Verifies that dialogs surface localized error messages when the WS schema + * rejects an input. Covers the two most common shapes: length cap and range + * cap. Asserts the error string contains the translated field label + the + * translated constraint (e.g. "max 200 characters"), not raw voluptuous text. + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; + +const RUN = Date.now().toString(36); +const log = (...a) => console.log(...a); +let pass = 0, fail = 0; +const check = (label, ok, detail) => { + if (ok) { pass++; log(` ✓ ${label}`); } + else { fail++; log(` ✗ ${label}${detail ? " — " + detail : ""}`); } +}; + +const PANEL_EVAL = ` +window._ha=document.querySelector('home-assistant'); +window._main=_ha.shadowRoot.querySelector('home-assistant-main'); +window._drawer=_main.shadowRoot.querySelector('ha-drawer'); +window._resolver=_drawer.querySelector('partial-panel-resolver'); +window._custom=_resolver.querySelector('ha-panel-custom'); +window._panel=_custom.querySelector('maintenance-supporter-panel'); +window._sr=_panel.shadowRoot; +`; + +async function main() { + const { browser, ctx, page } = await setup({ mobile: false }); + + log("\n== Direct WS call with oversize name (expect rejection)"); + let wsErr; + try { + await ws(page, { + type: "maintenance_supporter/object/create", + name: "X".repeat(500), + }); + } catch (e) { + wsErr = e; + } + check("WS rejected oversize name", !!wsErr, "no error raised"); + log(" raw error:", String(wsErr?.message || "").slice(0, 120)); + + log("\n== Call describeWsError from the panel with the raw error"); + const localized = await page.evaluate(({ rawMsg, fn }) => { + eval(fn); + // The ws-errors module is bundled into the panel; reach it via the panel instance. + // We can approximate the test by driving the object-dialog: open it, submit + // an oversize name, and read back the dialog's displayed error. + return rawMsg; // just echoed for now + }, { rawMsg: String(wsErr?.message || wsErr), fn: PANEL_EVAL }); + log(" echoed:", localized?.slice(0, 120)); + + log("\n== UI path: open object-dialog, enter oversize name, hit save"); + await page.evaluate((fn) => { + eval(fn); + window._sr.querySelector("maintenance-object-dialog").openCreate(); + }, PANEL_EVAL); + await page.waitForTimeout(800); + await page.evaluate(({ name, fn }) => { + eval(fn); + const dlg = window._sr.querySelector("maintenance-object-dialog"); + dlg._name = name; + dlg.requestUpdate(); + }, { name: "X".repeat(500), fn: PANEL_EVAL }); + await page.waitForTimeout(300); + await page.evaluate(({ fn }) => { + eval(fn); + const dlg = window._sr.querySelector("maintenance-object-dialog"); + dlg._save(); + }, { fn: PANEL_EVAL }); + await page.waitForTimeout(1500); + + const errText = await page.evaluate(({ fn }) => { + eval(fn); + const dlg = window._sr.querySelector("maintenance-object-dialog"); + return dlg._error; + }, { fn: PANEL_EVAL }); + + log(" dialog _error:", errText); + check( + "dialog error is localized (contains 'too long' in EN)", + typeof errText === "string" + && errText.toLowerCase().includes("too long") + && errText.includes("200"), + `got: "${errText}"`, + ); + check( + "dialog error is NOT the raw voluptuous string", + typeof errText === "string" && !errText.includes("dictionary value @"), + `got raw voluptuous: "${errText}"`, + ); + + await cleanup(browser, ctx); + log(`\n=== ${pass} passed, ${fail} failed ===`); + process.exit(fail === 0 ? 0 : 1); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-helpers.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-helpers.mjs new file mode 100644 index 0000000..dcc1267 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-helpers.mjs @@ -0,0 +1,127 @@ +/** + * Shared helpers for Playwright E2E tests. + * + * Usage: + * import { setup, ws, panelSR, cleanup } from "./e2e-helpers.mjs"; + * const { browser, page } = await setup({ mobile: false }); + */ +import { chromium } from "playwright"; + +const HA = "http://homeassistant-dev:8123"; + +export async function getRefreshToken() { + const http = await import("http"); + function post(path, body, contentType = "application/json") { + return new Promise((resolve, reject) => { + const data = typeof body === "string" ? body : JSON.stringify(body); + const opts = { + hostname: "localhost", port: 8125, path, method: "POST", + headers: { "Content-Type": contentType, "Content-Length": Buffer.byteLength(data) }, + }; + const req = http.request(opts, (res) => { + let d = ""; + res.on("data", (c) => (d += c)); + res.on("end", () => resolve(JSON.parse(d))); + }); + req.on("error", reject); + req.write(data); + req.end(); + }); + } + const flow = await post("/auth/login_flow", { client_id: `${HA}/`, handler: ["homeassistant", null], redirect_uri: `${HA}/` }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { client_id: `${HA}/`, username: "dev", password: "dev" }); + const tokens = await post("/auth/token", `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, "application/x-www-form-urlencoded"); + return tokens.refresh_token; +} + +export async function setup({ mobile = false, timezone = undefined } = {}) { + const refreshToken = process.env.HA_REFRESH_TOKEN || await getRefreshToken(); + const browser = await chromium.connect("ws://localhost:3000"); + const ctxOpts = { + viewport: mobile ? { width: 375, height: 812 } : { width: 1280, height: 900 }, + locale: "en-US", + colorScheme: "dark", + ...(mobile ? { isMobile: true } : {}), + ...(timezone ? { timezoneId: timezone } : {}), + }; + const ctx = await browser.newContext(ctxOpts); + const page = await ctx.newPage(); + + await page.goto(HA); + await page.waitForTimeout(1000); + await page.evaluate((args) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: args.ha, clientId: args.ha + "/", + refresh_token: args.r, access_token: "", + token_type: "Bearer", expires_in: 1800, expires: 0, + })); + }, { ha: HA, r: refreshToken }); + await page.goto(HA + "/maintenance-supporter"); + await page.waitForTimeout(7000); + + return { browser, ctx, page }; +} + +export async function ws(page, cmd) { + return page.evaluate(async (c) => { + const ha = document.querySelector("home-assistant"); + if (!ha?.hass?.connection) throw new Error("no hass connection"); + try { + return await ha.hass.connection.sendMessagePromise(c); + } catch (e) { + // Surface backend errors as readable JSON + throw new Error("WS error: " + JSON.stringify(e)); + } + }, cmd); +} + +export function panelSR(page) { + return page.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const main = ha?.shadowRoot?.querySelector("home-assistant-main"); + const drawer = main?.shadowRoot?.querySelector("ha-drawer"); + const resolver = drawer?.querySelector("partial-panel-resolver"); + const custom = resolver?.querySelector("ha-panel-custom"); + const panel = custom?.querySelector("maintenance-supporter-panel"); + return !!panel?.shadowRoot; + }); +} + +const PANEL_EVAL = "window._ha=document.querySelector('home-assistant');window._main=_ha.shadowRoot.querySelector('home-assistant-main');window._drawer=_main.shadowRoot.querySelector('ha-drawer');window._resolver=_drawer.querySelector('partial-panel-resolver');window._custom=_resolver.querySelector('ha-panel-custom');window._panel=_custom.querySelector('maintenance-supporter-panel');window._sr=_panel.shadowRoot;"; + +export async function openEditDialog(page, entryId, task) { + await page.evaluate(({ eid, task, fn }) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog").openEdit(eid, task); + }, { eid: entryId, task, fn: PANEL_EVAL }); + await page.waitForTimeout(1500); +} + +export async function readDialogSelects(page) { + return page.evaluate(({ fn }) => { + eval(fn); + const dlgSr = window._sr?.querySelector("maintenance-task-dialog")?.shadowRoot; + if (!dlgSr) return []; + return [...dlgSr.querySelectorAll("select")].map((sel) => { + const label = sel.closest(".select-row")?.querySelector("label")?.textContent?.trim() || "unknown"; + const selected = [...sel.options].find((o) => o.selected); + return { label, value: sel.value, selectedValue: selected?.value, match: sel.value === selected?.value }; + }); + }, { fn: PANEL_EVAL }); +} + +export async function readDialogFields(page) { + return page.evaluate(({ fn }) => { + eval(fn); + const dlgSr = window._sr?.querySelector("maintenance-task-dialog")?.shadowRoot; + if (!dlgSr) return []; + return [...dlgSr.querySelectorAll("ha-textfield")].map((f) => ({ + label: f.getAttribute("label"), type: f.getAttribute("type"), value: f.value, + })); + }, { fn: PANEL_EVAL }); +} + +export async function cleanup(browser, ctx) { + try { await ctx.close(); } catch { /* ignore */ } + try { await browser.close(); } catch { /* ignore */ } +} diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-issues-30-31.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-issues-30-31.mjs new file mode 100644 index 0000000..6c831e3 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-issues-30-31.mjs @@ -0,0 +1,292 @@ +/** + * E2E test scenarios for issues #30 and #31. + * + * #30: Task without last_performed should anchor next_due on created_at, + * not "today" every refresh. + * #31: Reset prompt strings should make it clear that the date is the + * "last performed" date, not the next due date. + * + * Usage: + * docker compose up -d homeassistant-dev playwright + * docker restart playwright-server + * cd custom_components/maintenance_supporter/frontend-src + * node e2e-issues-30-31.mjs + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; +import { readFile } from "fs/promises"; + +let passed = 0, failed = 0; +function check(name, ok, detail) { + if (ok) { passed++; console.log(" PASS " + name); } + else { failed++; console.log(" FAIL " + name + (detail ? " — " + detail : "")); } +} +async function runTest(name, fn) { + console.log("\n=== " + name + " ==="); + try { await fn(); } + catch (e) { failed++; console.log(" ERROR: " + e.message + (e.stack ? "\n" + e.stack : "")); } +} + +const { browser, ctx, page } = await setup(); +const RUN = `${Date.now().toString(36)}`; + +// Helper: create a temp object + task on a fresh entry, return ids. +async function wsLog(cmd) { + try { + return await ws(page, cmd); + } catch (e) { + const detail = e.message + " · cmd=" + JSON.stringify(cmd); + throw new Error(detail); + } +} +async function createScenarioObject(name, taskOptions) { + const created = await wsLog({ + type: "maintenance_supporter/object/create", name, + }); + const entryId = created.entry_id; + const taskRes = await wsLog({ + type: "maintenance_supporter/task/create", + entry_id: entryId, + ...taskOptions, + }); + return { entryId, taskId: taskRes.task_id }; +} +async function readTask(entryId, taskId) { + const obj = await ws(page, { type: "maintenance_supporter/object", entry_id: entryId }); + return obj.tasks.find(t => t.id === taskId); +} +async function deleteEntry(entryId) { + try { + await page.evaluate(async (eid) => { + const ha = document.querySelector("home-assistant"); + const conn = ha.__hass.connection; + await conn.sendMessagePromise({ + type: "config_entries/remove", + entry_id: eid, + }); + }, entryId); + } catch (e) { + console.log(" cleanup: ", e.message); + } +} + +// ===================================================================== +// SCENARIO #30: created_at fallback for tasks without last_performed +// ===================================================================== + +await runTest("#30 — Task ohne last_performed bekommt created_at=today", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Issue30 A ${RUN}`, { + name: "Daily Task", task_type: "cleaning", + schedule_type: "time_based", interval_days: 1, warning_days: 0, enabled: true, + }); + const task = await readTask(entryId, taskId); + console.log(` debug: next_due=${task.next_due} days_until_due=${task.days_until_due} last_performed=${task.last_performed}`); + // The frontend doesn't expose created_at, but we know that with last_performed=null + // and a fresh anchor, days_until_due must equal interval_days (1 here). + check("days_until_due equals interval_days", task.days_until_due === 1, + `got ${task.days_until_due} (expected 1)`); + check("status is OK with warning_days=0", task.status === "ok", + `got ${task.status}`); + + await deleteEntry(entryId); +}); + +await runTest("#30 — Interval=30 ergibt days_until_due=30", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Issue30 B ${RUN}`, { + name: "Monthly Task", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, warning_days: 7, enabled: true, + }); + const task = await readTask(entryId, taskId); + console.log(` debug: next_due=${task.next_due} days_until_due=${task.days_until_due}`); + check("days_until_due equals interval_days (30)", task.days_until_due === 30, + `got ${task.days_until_due}`); + check("status is OK (not DUE_SOON)", task.status === "ok", + `got ${task.status}`); + + await deleteEntry(entryId); +}); + +await runTest("#30 — Refresh ändert next_due nicht (Anker stabil)", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Issue30 C ${RUN}`, { + name: "Stable Date", task_type: "cleaning", + schedule_type: "time_based", interval_days: 7, enabled: true, + }); + const t1 = await readTask(entryId, taskId); + // Force a refresh by sleeping briefly + await page.waitForTimeout(1500); + const t2 = await readTask(entryId, taskId); + check("next_due stable across refreshes", t1.next_due === t2.next_due, + `t1=${t1.next_due}, t2=${t2.next_due}`); + + await deleteEntry(entryId); +}); + +await runTest("#30 — Task mit last_performed nutzt last_performed (nicht created_at)", async () => { + const past = new Date(Date.now() - 60 * 86400000).toISOString().slice(0, 10); + const { entryId, taskId } = await createScenarioObject(`E2E Issue30 D ${RUN}`, { + name: "Past LP Task", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, + last_performed: past, enabled: true, + }); + const task = await readTask(entryId, taskId); + // last_performed=60 days ago + interval=30 → next_due was 30 days ago → OVERDUE + check("status is OVERDUE", task.status === "overdue", `got ${task.status}`); + check("days_until_due negative", task.days_until_due < 0, `got ${task.days_until_due}`); + + await deleteEntry(entryId); +}); + +// ===================================================================== +// SCENARIO #31: Reset prompt strings clearer +// ===================================================================== + +await runTest("#31 — Reset-Prompt Label sagt 'Last performed date'", async () => { + // Create a task to reset + const { entryId, taskId } = await createScenarioObject(`E2E Issue31 ${RUN}`, { + name: "Reset Demo", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, enabled: true, + }); + await page.goto("http://homeassistant-dev:8123/maintenance-supporter"); + await page.waitForTimeout(3000); + + // Verify shipped strings via the built JS file (runs on host fs, not browser). + const builtJs = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN reset_date_optional shipped", + builtJs.includes("Last performed date (optional, defaults to today)")); + check("EN reset_date_prompt shipped", + builtJs.includes("Mark task as performed?")); + check("DE reset_date_optional shipped", + builtJs.includes("Letztes Erledigungs-Datum")); + check("Old confusing string removed", + !builtJs.includes("Reset this task?"), + "old 'Reset this task?' still present"); + + // Reset via WS using the new label semantics: setting the date sets last_performed. + const resetDate = "2025-01-15"; + await ws(page, { + type: "maintenance_supporter/task/reset", entry_id: entryId, task_id: taskId, + date: resetDate, + }); + const task = await readTask(entryId, taskId); + check("reset sets last_performed", task.last_performed === resetDate, + `got ${task.last_performed}`); + // next_due = last_performed + interval = 2025-01-15 + 30 = 2025-02-14 + check("next_due = last_performed + interval", task.next_due === "2025-02-14", + `got ${task.next_due}`); + + await deleteEntry(entryId); +}); + +// ===================================================================== +// EDGE CASES: TZ + lifecycle interaction +// ===================================================================== + +await runTest("EDGE — Lifecycle: create → complete → next_due aktualisiert", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Edge LC ${RUN}`, { + name: "Lifecycle Task", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, warning_days: 7, enabled: true, + }); + // Initial state: no last_performed, days_until_due == interval_days + const t1 = await readTask(entryId, taskId); + check("initial days_until_due == interval_days", t1.days_until_due === 30, + `got ${t1.days_until_due}`); + check("initial last_performed is null", t1.last_performed === null, + `got ${t1.last_performed}`); + + // Complete via WS + await ws(page, { + type: "maintenance_supporter/task/complete", + entry_id: entryId, task_id: taskId, + }); + const t2 = await readTask(entryId, taskId); + check("after complete: last_performed set", t2.last_performed !== null, + `got ${t2.last_performed}`); + check("after complete: days_until_due == interval", t2.days_until_due === 30, + `got ${t2.days_until_due}`); + // History entry written + check("after complete: history has entry", t2.history.length === 1, + `len=${t2.history.length}`); + // Timestamp must be TZ-aware + const ts = t2.history[0].timestamp; + check("history timestamp has TZ suffix", + /[+-]\d{2}:\d{2}$|Z$/.test(ts), + `ts=${ts}`); + + await deleteEntry(entryId); +}); + +await runTest("EDGE — Reset zu vergangenem Datum: last_performed + history", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Edge RST ${RUN}`, { + name: "Reset Edge", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, enabled: true, + }); + const past = "2025-01-15"; + await ws(page, { + type: "maintenance_supporter/task/reset", + entry_id: entryId, task_id: taskId, date: past, + }); + const t = await readTask(entryId, taskId); + check("reset: last_performed = past date", t.last_performed === past, + `got ${t.last_performed}`); + check("reset: next_due = past + interval (overdue)", + t.next_due === "2025-02-14", + `got ${t.next_due}`); + check("reset: status is overdue", t.status === "overdue", + `got ${t.status}`); + check("reset: history has reset entry", + t.history.some(h => h.type === "reset"), + `types=${t.history.map(h => h.type).join(",")}`); + + await deleteEntry(entryId); +}); + +await runTest("EDGE — Skip via WS: history entry + last_performed bumped", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Edge SKP ${RUN}`, { + name: "Skip Edge", task_type: "cleaning", + schedule_type: "time_based", interval_days: 14, enabled: true, + }); + await ws(page, { + type: "maintenance_supporter/task/skip", + entry_id: entryId, task_id: taskId, reason: "user busy", + }); + const t = await readTask(entryId, taskId); + check("skip: last_performed set (today)", t.last_performed !== null); + check("skip: history has skip entry", + t.history.some(h => h.type === "skipped"), + `types=${t.history.map(h => h.type).join(",")}`); + check("skip: next_due == today + interval (14)", + t.days_until_due === 14, + `got ${t.days_until_due}`); + + await deleteEntry(entryId); +}); + +await runTest("EDGE — Update last_performed setzt next_due neu", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Edge UPD ${RUN}`, { + name: "Update Edge", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, enabled: true, + }); + const past = "2024-06-01"; + await ws(page, { + type: "maintenance_supporter/task/update", + entry_id: entryId, task_id: taskId, + last_performed: past, + }); + const t = await readTask(entryId, taskId); + check("update: last_performed reflected", t.last_performed === past, + `got ${t.last_performed}`); + check("update: next_due = last + interval", + t.next_due === "2024-07-01", + `got ${t.next_due}`); + + await deleteEntry(entryId); +}); + +// ===================================================================== +await cleanup(browser, ctx); +console.log("\n" + "═".repeat(50)); +console.log(`RESULTS: ${passed} passed, ${failed} failed`); +if (failed > 0) { console.log("SOME TESTS FAILED!"); process.exit(1); } +else console.log("ALL TESTS PASSED!"); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-new-features.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-new-features.mjs new file mode 100644 index 0000000..e195e47 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-new-features.mjs @@ -0,0 +1,292 @@ +/** + * E2E tests for features that previously had WS endpoints without UI. + * + * Usage: + * docker compose up -d homeassistant-dev playwright + * docker restart playwright-server + * cd custom_components/maintenance_supporter/frontend-src + * node e2e-new-features.mjs + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; + +let passed = 0, failed = 0; +function check(name, ok, detail) { + if (ok) { passed++; console.log(" PASS " + name); } + else { failed++; console.log(" FAIL " + name + (detail ? " — " + detail : "")); } +} +async function runTest(name, fn) { + console.log("\n=== " + name + " ==="); + try { await fn(); } + catch (e) { failed++; console.log(" ERROR: " + e.message); } +} + +const { browser, ctx, page } = await setup(); + +// ===================================================================== +// Batch A: Test-Notification Button in Settings +// ===================================================================== + +await runTest("Test-Notification: WS endpoint reachable", async () => { + const res = await ws(page, { type: "maintenance_supporter/global/test_notification" }); + check("WS response has success field", typeof res.success === "boolean", + `got ${JSON.stringify(res)}`); + // When no notify_service is configured the backend returns success=false + // with a "no_service" message. Either outcome is a valid response — + // what matters is that the endpoint is reachable. + check("WS response has message field", typeof res.message === "string" || res.success, + `got ${JSON.stringify(res)}`); +}); + +await runTest("Test-Notification: Button renders in Settings view", async () => { + // Navigate to maintenance panel and open settings. In the base panel the + // settings tab is not always pre-rendered; we verify the string keys exist + // in the bundled JS so the button can be rendered when the tab is opened. + const { readFile } = await import("fs/promises"); + const js = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN send_test string shipped", js.includes("Send test")); + check("EN test_notification string shipped", js.includes("Test notification")); + check("DE test_notification string shipped", js.includes("Test-Benachrichtigung")); + check("testing state string shipped", + js.includes("Sending\\u2026") || js.includes("Sending…")); +}); + +// ===================================================================== +// Batch B: Re-analyze Button in Task Detail view +// ===================================================================== + +await runTest("Re-analyze: WS endpoint returns analysis", async () => { + const objs = await ws(page, { type: "maintenance_supporter/objects" }); + const obj = objs.objects[0]; + const t1 = obj.tasks[0]; + if (!t1) { console.log(" SKIP: no task in first object"); return; } + + const res = await ws(page, { + type: "maintenance_supporter/task/analyze_interval", + entry_id: obj.entry_id, task_id: t1.id, + }); + check("analysis has current_interval field", + "current_interval" in res, `got keys=${Object.keys(res).join(",")}`); + check("analysis has confidence field", typeof res.confidence === "string"); + check("analysis has data_points number", typeof res.data_points === "number"); +}); + +await runTest("Re-analyze: Button strings shipped", async () => { + const { readFile } = await import("fs/promises"); + const js = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN reanalyze label shipped", js.includes("Re-analyze")); + check("DE reanalyze label shipped", js.includes("Neu analysieren")); + check("reanalyze_result string shipped", js.includes("New analysis")); + check("reanalyze_insufficient_data shipped", + js.includes("Not enough data to produce")); +}); + +// ===================================================================== +// Batch B.2: Environmental Entity Selector (Task Dialog, sensor_based) +// ===================================================================== + +await runTest("Environmental entity: WS endpoint persists adaptive_config", async () => { + // Create a sensor-based task, then set and clear the env entity. + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: `env ${Date.now().toString(36)}` }); + const task = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "env-task", + schedule_type: "sensor_based", interval_days: 30, + trigger_config: { type: "threshold", entity_id: "sensor.dummy", trigger_above: 10 }, + enabled: true, + }); + + // Set + const r1 = await ws(page, { + type: "maintenance_supporter/task/set_environmental_entity", + entry_id: obj.entry_id, task_id: task.task_id, + environmental_entity: "sensor.outdoor_temperature", + environmental_attribute: null, + }); + check("set succeeds", r1.success === true, JSON.stringify(r1)); + + const detail = await ws(page, { + type: "maintenance_supporter/object", entry_id: obj.entry_id, + }); + const t = detail.tasks.find(x => x.id === task.task_id); + check("adaptive_config reflects env entity", + t?.adaptive_config?.environmental_entity === "sensor.outdoor_temperature", + `got ${JSON.stringify(t?.adaptive_config)}`); + + // Clear + await ws(page, { + type: "maintenance_supporter/task/set_environmental_entity", + entry_id: obj.entry_id, task_id: task.task_id, + environmental_entity: null, + }); + + // Cleanup (as admin via config_entries) + try { + await page.evaluate(async (eid) => { + const ha = document.querySelector("home-assistant"); + await ha.hass.connection.sendMessagePromise({ + type: "config_entries/remove", entry_id: eid, + }); + }, obj.entry_id); + } catch { /* best-effort */ } +}); + +await runTest("Environmental entity: dialog strings shipped", async () => { + const { readFile } = await import("fs/promises"); + const js = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN environmental_entity label", js.includes("Environmental sensor")); + check("DE environmental_entity label", js.includes("Umgebungs-Sensor")); + check("helper text shipped", js.includes("adjusts the interval")); +}); + +// ===================================================================== +// Batch C: Seasonal Overrides Dialog +// ===================================================================== + +await runTest("Seasonal overrides: WS roundtrip set + clear", async () => { + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: `seas ${Date.now().toString(36)}` }); + const task = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "seas-task", + schedule_type: "time_based", interval_days: 30, enabled: true, + }); + + // Set overrides for Jan + Jul + const r1 = await ws(page, { + type: "maintenance_supporter/task/seasonal_overrides", + entry_id: obj.entry_id, task_id: task.task_id, + overrides: { 1: 2.0, 7: 0.5 }, + }); + check("set succeeds", r1.success === true, JSON.stringify(r1)); + + const detail = await ws(page, { + type: "maintenance_supporter/object", entry_id: obj.entry_id, + }); + const t = detail.tasks.find(x => x.id === task.task_id); + const ov = t?.adaptive_config?.seasonal_overrides; + check("adaptive_config has overrides", + ov && (ov["1"] === 2.0 || ov[1] === 2.0) && (ov["7"] === 0.5 || ov[7] === 0.5), + `got ${JSON.stringify(ov)}`); + + // Clear + const r2 = await ws(page, { + type: "maintenance_supporter/task/seasonal_overrides", + entry_id: obj.entry_id, task_id: task.task_id, + overrides: {}, + }); + check("clear succeeds", r2.success === true); + + try { + await page.evaluate(async (eid) => { + const ha = document.querySelector("home-assistant"); + await ha.hass.connection.sendMessagePromise({ + type: "config_entries/remove", entry_id: eid, + }); + }, obj.entry_id); + } catch { /* best-effort */ } +}); + +await runTest("Seasonal overrides: dialog + button strings shipped", async () => { + const { readFile } = await import("fs/promises"); + const js = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN edit button", js.includes("Edit seasonal factors")); + check("EN title", js.includes("Seasonal factors (override)")); + check("clear_all string", js.includes("Clear all")); + check("DE title", js.includes("Saisonale Faktoren")); +}); + +// ===================================================================== +// Batch D: Groups CRUD UI +// ===================================================================== + +await runTest("Groups: WS roundtrip create + update + delete", async () => { + // Need an object + task to reference + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: `grp-obj ${Date.now().toString(36)}` }); + const task = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "grp-task", + schedule_type: "time_based", interval_days: 30, enabled: true, + }); + + // Create group + const create = await ws(page, { + type: "maintenance_supporter/group/create", + name: "E2E Group", + description: "created by e2e", + task_refs: [{ entry_id: obj.entry_id, task_id: task.task_id }], + }); + check("group/create returns group_id", typeof create.group_id === "string", + JSON.stringify(create)); + const gid = create.group_id; + + // Read back + const list = await ws(page, { type: "maintenance_supporter/groups" }); + check("group appears in list", !!list.groups[gid], + `got ${Object.keys(list.groups).join(",")}`); + check("task_refs persisted", + list.groups[gid].task_refs.length === 1, + JSON.stringify(list.groups[gid])); + + // Update + const update = await ws(page, { + type: "maintenance_supporter/group/update", + group_id: gid, + name: "E2E Group renamed", + task_refs: [], + }); + check("group/update succeeds", update.success === true); + + const list2 = await ws(page, { type: "maintenance_supporter/groups" }); + check("group renamed", list2.groups[gid].name === "E2E Group renamed"); + check("task_refs cleared", list2.groups[gid].task_refs.length === 0); + + // Delete + const del = await ws(page, { + type: "maintenance_supporter/group/delete", group_id: gid, + }); + check("group/delete succeeds", del.success === true); + + const list3 = await ws(page, { type: "maintenance_supporter/groups" }); + check("group gone", !list3.groups[gid]); + + // Cleanup object + try { + await page.evaluate(async (eid) => { + const ha = document.querySelector("home-assistant"); + await ha.hass.connection.sendMessagePromise({ + type: "config_entries/remove", entry_id: eid, + }); + }, obj.entry_id); + } catch { /* best-effort */ } +}); + +await runTest("Groups: dialog + buttons strings shipped", async () => { + const { readFile } = await import("fs/promises"); + const js = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN new_group", js.includes("New group")); + check("EN edit_group", js.includes("Edit group")); + check("EN no_groups", js.includes("No groups yet")); + check("EN group_select_tasks", js.includes("Select tasks")); + check("DE new_group", js.includes("Neue Gruppe")); +}); + +// ===================================================================== +await cleanup(browser, ctx); +console.log("\n" + "═".repeat(50)); +console.log(`RESULTS: ${passed} passed, ${failed} failed`); +if (failed > 0) { console.log("SOME TESTS FAILED!"); process.exit(1); } +else console.log("ALL TESTS PASSED!"); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-schedule-time.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-schedule-time.mjs new file mode 100644 index 0000000..556a554 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-schedule-time.mjs @@ -0,0 +1,143 @@ +/** + * E2E for the Uhrzeit / time-of-day scheduling feature. + * + * Verifies: + * 1. Feature flag off by default → dialog has NO time input → WS summary has + * schedule_time=null. + * 2. Toggling the flag on → dialog shows the field → a value round-trips to + * WS (create + /object) and back to the dialog in edit mode. + * 3. Feature flag off again → WS-computed _status ignores the stored time + * (backend gating confirms "OFF means midnight semantic"). + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; + +const RUN = Date.now().toString(36); +const OBJ_NAME = `Schedule Time ${RUN}`; + +const log = (...a) => console.log(...a); +let pass = 0, fail = 0; +const check = (label, ok, detail) => { + if (ok) { pass++; log(` ✓ ${label}`); } + else { fail++; log(` ✗ ${label}${detail ? " — " + detail : ""}`); } +}; + +const PANEL_EVAL = ` +window._ha=document.querySelector('home-assistant'); +window._main=_ha.shadowRoot.querySelector('home-assistant-main'); +window._drawer=_main.shadowRoot.querySelector('ha-drawer'); +window._resolver=_drawer.querySelector('partial-panel-resolver'); +window._custom=_resolver.querySelector('ha-panel-custom'); +window._panel=_custom.querySelector('maintenance-supporter-panel'); +window._sr=_panel.shadowRoot; +`; + +async function main() { + const { browser, ctx, page } = await setup({ mobile: false }); + + log("\n== Step 1: feature flag starts OFF"); + let settings = await ws(page, { + type: "maintenance_supporter/global/update", + settings: { advanced_schedule_time_visible: false }, + }); + check("backend reports schedule_time=false", settings.features.schedule_time === false); + // Force panel to pick up the flag change (direct WS doesn't trigger listener). + await page.evaluate((fn) => { eval(fn); return window._panel._loadData(); }, PANEL_EVAL); + await page.waitForTimeout(1000); + + log("\n== Step 2: create obj + time-based task"); + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: OBJ_NAME }); + const taskRes = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "Daily chore", interval_days: 1, + }); + await page.waitForTimeout(2000); + + log("\n== Step 3: open edit dialog, field should NOT be visible"); + const task = (await ws(page, { type: "maintenance_supporter/object", entry_id: obj.entry_id })) + .tasks.find(t => t.id === taskRes.task_id); + await page.evaluate(({ eid, task, fn }) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog").openEdit(eid, task); + }, { eid: obj.entry_id, task, fn: PANEL_EVAL }); + await page.waitForTimeout(1200); + + const hiddenState = await page.evaluate((fn) => { + eval(fn); + const dlgSr = window._sr.querySelector("maintenance-task-dialog").shadowRoot; + return { + scheduleTimeEnabled: window._sr.querySelector("maintenance-task-dialog").scheduleTimeEnabled, + hasTimeInput: !!dlgSr.querySelector('ha-textfield[type="time"]'), + }; + }, PANEL_EVAL); + check("scheduleTimeEnabled prop is false on dialog", hiddenState.scheduleTimeEnabled === false); + check("time input is NOT in the DOM when feature is off", hiddenState.hasTimeInput === false); + + log("\n== Step 4: enable the feature flag, re-open edit"); + settings = await ws(page, { + type: "maintenance_supporter/global/update", + settings: { advanced_schedule_time_visible: true }, + }); + check("backend reports schedule_time=true after toggle", settings.features.schedule_time === true); + await page.waitForTimeout(1000); + // Direct WS update doesn't fire the panel's "settings-changed" listener, + // so force a reload so _features picks up the flip. + await page.evaluate((fn) => { + eval(fn); + return window._panel._loadData(); + }, PANEL_EVAL); + await page.waitForTimeout(1500); + // Close dialog, reopen (the prop is bound from _features on the parent panel) + await page.evaluate((fn) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog")._open = false; + }, PANEL_EVAL); + await page.waitForTimeout(500); + await page.evaluate(({ eid, task, fn }) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog").openEdit(eid, task); + }, { eid: obj.entry_id, task, fn: PANEL_EVAL }); + await page.waitForTimeout(1200); + const visibleState = await page.evaluate((fn) => { + eval(fn); + const dlgSr = window._sr.querySelector("maintenance-task-dialog").shadowRoot; + return { + scheduleTimeEnabled: window._sr.querySelector("maintenance-task-dialog").scheduleTimeEnabled, + hasTimeInput: !!dlgSr.querySelector('ha-textfield[type="time"]'), + }; + }, PANEL_EVAL); + check("scheduleTimeEnabled prop is true after feature on", visibleState.scheduleTimeEnabled === true); + check("time input IS in the DOM after feature on", visibleState.hasTimeInput === true); + + log("\n== Step 5: set time via WS (skip the DOM event roundtrip), re-read task"); + await ws(page, { + type: "maintenance_supporter/task/update", + entry_id: obj.entry_id, task_id: taskRes.task_id, schedule_time: "14:00", + }); + await page.waitForTimeout(1500); + const updated = (await ws(page, { type: "maintenance_supporter/object", entry_id: obj.entry_id })) + .tasks.find(t => t.id === taskRes.task_id); + check("summary contains schedule_time=14:00", updated.schedule_time === "14:00", + `got ${updated.schedule_time}`); + + log("\n== Step 6: reject malformed schedule_time at the WS layer"); + let rejected = false; + try { + await ws(page, { + type: "maintenance_supporter/task/update", + entry_id: obj.entry_id, task_id: taskRes.task_id, schedule_time: "25:99", + }); + } catch { + rejected = true; + } + check("malformed HH:MM rejected by schema", rejected); + + log("\n== Step 7: cleanup"); + await ws(page, { type: "maintenance_supporter/object/delete", entry_id: obj.entry_id }); + // Leave the flag on — test fixture state is not restored, next run will re-toggle. + await cleanup(browser, ctx); + + log(`\n=== ${pass} passed, ${fail} failed ===`); + process.exit(fail === 0 ? 0 : 1); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-test.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-test.mjs new file mode 100644 index 0000000..4052633 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-test.mjs @@ -0,0 +1,197 @@ +/** + * Comprehensive E2E test suite for the Maintenance Supporter panel. + * + * Prerequisites: + * docker compose up -d # ha-maint + playwright + * docker restart playwright-server # clean session + * + * Usage: + * cd custom_components/maintenance_supporter/frontend-src + * node e2e-test.mjs # auto-fetches refresh token + * HA_REFRESH_TOKEN=... node e2e-test.mjs # explicit token + */ +import { setup, ws, openEditDialog, readDialogSelects, readDialogFields, cleanup } from "./e2e-helpers.mjs"; + +let passed = 0, failed = 0, skipped = 0; +function check(name, ok) { + if (ok) { passed++; console.log(" PASS " + name); } + else { failed++; console.log(" FAIL " + name); } +} + +async function runTest(name, fn) { + console.log("\n=== " + name + " ==="); + try { await fn(); } + catch (e) { failed++; console.log(" ERROR: " + e.message); } +} + +// ═══════════════════════════════════════════════════════════════ +// DESKTOP TESTS +// ═══════════════════════════════════════════════════════════════ +const { browser, ctx, page } = await setup(); +const ENTRY = (await ws(page, { type: "maintenance_supporter/objects" })).objects[0].entry_id; + +await runTest("Sort dropdown exists with 4 modes", async () => { + const selects = await page.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const main = ha?.shadowRoot?.querySelector("home-assistant-main"); + const drawer = main?.shadowRoot?.querySelector("ha-drawer"); + const resolver = drawer?.querySelector("partial-panel-resolver"); + const custom = resolver?.querySelector("ha-panel-custom"); + const panel = custom?.querySelector("maintenance-supporter-panel"); + const sr = panel?.shadowRoot; + const filterBar = sr?.querySelector(".filter-bar"); + if (!filterBar) return []; + return [...filterBar.querySelectorAll("select")].map(s => ({ + options: [...s.options].map(o => o.value), + })); + }); + const sortSelect = selects.find(s => s.options.includes("due_date")); + check("sort dropdown found", !!sortSelect); + check("has 4 sort options", sortSelect?.options.length === 4); +}); + +await runTest("All Objects view via KPI click", async () => { + const clicked = await page.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const sr = ha?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot?.querySelector("ha-drawer")?.querySelector("partial-panel-resolver")?.querySelector("ha-panel-custom")?.querySelector("maintenance-supporter-panel")?.shadowRoot; + const item = sr?.querySelector(".stat-item.clickable"); + if (item) { item.click(); return true; } + return false; + }); + check("KPI clickable", clicked); + await page.waitForTimeout(1000); + const cards = await page.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const sr = ha?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot?.querySelector("ha-drawer")?.querySelector("partial-panel-resolver")?.querySelector("ha-panel-custom")?.querySelector("maintenance-supporter-panel")?.shadowRoot; + return sr?.querySelectorAll(".object-card")?.length ?? 0; + }); + check("object cards rendered", cards > 0); +}); + +// Reload panel to reset view state after All Objects test +await page.goto("http://homeassistant-dev:8123/maintenance-supporter"); +await page.waitForTimeout(5000); + +await runTest("Select dropdowns preserve values on edit", async () => { + const objs = await ws(page, { type: "maintenance_supporter/objects" }); + let triggerTask = null, triggerEntry = null; + for (const o of objs.objects) { + for (const t of o.tasks) { + if (t.trigger_config?.type) { triggerTask = t; triggerEntry = o.entry_id; break; } + } + if (triggerTask) break; + } + if (!triggerTask) { skipped++; console.log(" SKIP: no sensor-trigger task"); return; } + await openEditDialog(page, triggerEntry, triggerTask); + const selects = await readDialogSelects(page); + for (const s of selects) { + check(s.label + " selected matches value", s.match); + } + await page.keyboard.press("Escape"); +}); + +await runTest("Last performed field in task dialog", async () => { + const cr = await ws(page, { + type: "maintenance_supporter/task/create", entry_id: ENTRY, + name: "E2E LP Test", task_type: "service", schedule_type: "time_based", + interval_days: 30, last_performed: "2025-11-01", enabled: true, + }); + // Reload so panel picks up the new task + await page.goto("http://homeassistant-dev:8123/maintenance-supporter"); + await page.waitForTimeout(5000); + const obj = await ws(page, { type: "maintenance_supporter/object", entry_id: ENTRY }); + const task = obj.tasks.find(t => t.id === cr.task_id); + await openEditDialog(page, ENTRY, task); + const fields = await readDialogFields(page); + console.log(" Fields found:", fields.length, fields.map(f => f.type + ":" + f.label?.slice(0, 20)).join(", ")); + const dateField = fields.find(f => f.type === "date"); + check("date field exists", !!dateField); + check("date value correct", dateField?.value === "2025-11-01"); + await page.keyboard.press("Escape"); + await ws(page, { type: "maintenance_supporter/task/delete", entry_id: ENTRY, task_id: cr.task_id }); +}); + +await runTest("Task create with enabled field (Bug #14 regression)", async () => { + const r = await ws(page, { + type: "maintenance_supporter/task/create", entry_id: ENTRY, + name: "E2E Enabled Test", task_type: "cleaning", schedule_type: "time_based", + interval_days: 30, enabled: true, + }); + check("create with enabled succeeds", !!r.task_id); + if (r.task_id) await ws(page, { type: "maintenance_supporter/task/delete", entry_id: ENTRY, task_id: r.task_id }); +}); + +await runTest("Reset with date field (Bug #12 regression)", async () => { + const objs = await ws(page, { type: "maintenance_supporter/objects" }); + const t = objs.objects[0].tasks[0]; + try { + await ws(page, { type: "maintenance_supporter/task/reset", entry_id: ENTRY, task_id: t.id, date: "2026-04-01" }); + check("reset with date succeeds", true); + } catch (e) { + check("reset with date succeeds", false); + } +}); + +await runTest("Desktop header bar hidden on overview", async () => { + await page.goto("http://homeassistant-dev:8123/maintenance-supporter"); + await page.waitForTimeout(3000); + const headerVisible = await page.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const sr = ha?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot?.querySelector("ha-drawer")?.querySelector("partial-panel-resolver")?.querySelector("ha-panel-custom")?.querySelector("maintenance-supporter-panel")?.shadowRoot; + const header = sr?.querySelector(".header"); + if (!header) return false; + const style = window.getComputedStyle(header); + return style.display !== "none" && header.offsetHeight > 0; + }); + check("header NOT visible on desktop overview", !headerVisible); +}); + +await cleanup(browser, ctx); + +// Reconnect for timezone + mobile tests +import { chromium } from "playwright"; +const browser2 = await chromium.connect("ws://localhost:3000"); + +// ═══════════════════════════════════════════════════════════════ +// TIMEZONE + MOBILE TESTS (reuse same browser connection) +// ═══════════════════════════════════════════════════════════════ +await runTest("Date formatting in US timezone (Bug #21)", async () => { + const tzCtx = await browser2.newContext({ viewport: { width: 800, height: 600 }, locale: "en-US", timezoneId: "America/New_York" }); + const tzPage = await tzCtx.newPage(); + await tzPage.goto("about:blank"); + const r = await tzPage.evaluate(() => ({ + utc: new Date("2025-11-01").toLocaleDateString("en-US"), + local: new Date("2025-11-01T00:00:00").toLocaleDateString("en-US"), + })); + check("UTC parse shows wrong date (Oct 31)", r.utc.includes("10/31")); + check("Local parse shows correct date (Nov)", r.local.includes("11/")); + await tzCtx.close(); +}); + +await runTest("Mobile hamburger menu button", async () => { + const mobCtx = await browser2.newContext({ viewport: { width: 375, height: 812 }, locale: "en-US", colorScheme: "dark", isMobile: true }); + const mobPage = await mobCtx.newPage(); + await mobPage.goto("http://homeassistant-dev:8123"); + await mobPage.waitForTimeout(1000); + await mobPage.evaluate((r) => localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: "http://homeassistant-dev:8123", clientId: "http://homeassistant-dev:8123/", + refresh_token: r, access_token: "", token_type: "Bearer", expires_in: 1800, expires: 0, + })), process.env.HA_REFRESH_TOKEN); + await mobPage.goto("http://homeassistant-dev:8123/maintenance-supporter"); + await mobPage.waitForTimeout(7000); + const hasMenu = await mobPage.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const sr = ha?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot?.querySelector("ha-drawer")?.querySelector("partial-panel-resolver")?.querySelector("ha-panel-custom")?.querySelector("maintenance-supporter-panel")?.shadowRoot; + return !!sr?.querySelector("ha-menu-button"); + }); + check("ha-menu-button visible on mobile", hasMenu); + await mobCtx.close(); +}); + +await cleanup(browser2, { close() {} }); + +// ═══════════════════════════════════════════════════════════════ +console.log("\n" + "═".repeat(50)); +console.log(`RESULTS: ${passed} passed, ${failed} failed, ${skipped} skipped`); +if (failed > 0) { console.log("SOME TESTS FAILED!"); process.exit(1); } +else console.log("ALL TESTS PASSED!"); diff --git a/custom_components/maintenance_supporter/frontend-src/esbuild.mjs b/custom_components/maintenance_supporter/frontend-src/esbuild.mjs new file mode 100644 index 0000000..c922aa8 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/esbuild.mjs @@ -0,0 +1,93 @@ +import { build } from "esbuild"; +import { cpSync, mkdirSync, rmSync } from "fs"; + +// Content-hashed chunk names change every time their inputs change; esbuild +// writes the new files but never removes the old ones. Clear the chunks dir up +// front so stale, unreferenced chunks don't accumulate in git (they did before +// this — every frontend change left orphans behind). +rmSync("../frontend/strategy/chunks", { recursive: true, force: true }); + +const common = { + bundle: true, + format: "esm", + target: "es2021", + minify: true, + sourcemap: false, + external: [], +}; + +// Panel +await build({ + ...common, + entryPoints: ["maintenance-panel.ts"], + outfile: "../frontend/maintenance-panel.js", +}); + +// Lovelace Card +await build({ + ...common, + entryPoints: ["maintenance-card.ts"], + outfile: "../frontend/maintenance-card.js", +}); + +// Dashboard Strategy (HA 2026.5+ — silent no-op on older HA) +// +// v2.3.4 (issue #52): we MUST split dynamic imports out into separate +// chunks. HA's strategy loader has a 5 s timeout on +// customElements.whenDefined("ll-strategy-dashboard-..."); if the strategy +// bundle takes longer than that to download + parse + execute, the +// dashboard fails to open. Without splitting, esbuild inlines every +// dynamic import — strategy file ballooned to 510 KB pulling in 7 dialog +// components + 3 section cards. With splitting enabled, the strategy +// entry stays small (~13 KB) and registers customElements instantly; +// heavy components load lazily as separate chunks only when they're +// actually rendered. +// +// All output lands in ../frontend/strategy/ so HA can serve the directory +// as a single static-path mount and the entry's relative ./chunks/ imports +// resolve naturally without per-chunk URL registration. +await build({ + ...common, + entryPoints: ["maintenance-dashboard-strategy.ts"], + outdir: "../frontend/strategy", + splitting: true, + entryNames: "[name]", + chunkNames: "chunks/[name]-[hash]", +}); + +// Dashboard-strategy registration shim (v2.8.1 — strategy timeout fix). +// +// HA loads frontend_extra_module_url entries as fire-and-forget import() with +// no ordering/await, then only waits ~5 s via whenDefined for a custom: strategy +// element. Under heavy HACS plugin load the full strategy bundle could lose that +// race (element downloaded but not yet executed). This tiny zero-import shim is +// what we register as the extra_module_url: it defines the dashboard tag +// synchronously (a few hundred bytes, no import graph) and lazy-import()s the +// heavy bundle above only on first generate()/getConfigElement(). +await build({ + ...common, + entryPoints: ["maintenance-strategy-shim.ts"], + outfile: "../frontend/maintenance-strategy-shim.js", +}); + +// Calendar Card (v1.9.0+ — Lovelace card extracted from panel's Calendar tab) +await build({ + ...common, + entryPoints: ["maintenance-calendar-card.ts"], + outfile: "../frontend/maintenance-calendar-card.js", +}); + +// Copy the runtime-loaded locale tables into the served frontend dir. Only EN +// is bundled (imported by styles.ts); the other languages are fetched at +// runtime from frontend/locales/, so editing a translation needs no rebuild. +mkdirSync("../frontend/locales", { recursive: true }); +cpSync("locales", "../frontend/locales", { recursive: true }); + +// v2.21: vendor pdf.js for the work sheet's inline manual excerpt. Copied, +// not bundled — the work-sheet page loads it on demand from VENDOR_URL, so +// the panel/card bundles stay untouched. +mkdirSync("../frontend/vendor", { recursive: true }); +cpSync("node_modules/pdfjs-dist/legacy/build/pdf.min.mjs", "../frontend/vendor/pdf.min.mjs"); +cpSync("node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs", "../frontend/vendor/pdf.worker.min.mjs"); + +console.log("Build complete."); diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/calendar-bucket.ts b/custom_components/maintenance_supporter/frontend-src/helpers/calendar-bucket.ts new file mode 100644 index 0000000..452d34e --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/calendar-bucket.ts @@ -0,0 +1,387 @@ +/** + * Pure helper for the panel's Calendar tab (v1.5.0+). + * + * Given the loaded objects, a window in days, and an optional user filter, + * produce a flat day-bucketed event list ready for rendering. + * + * Recurring projection (time-based tasks): the first occurrence is the task's + * next_due (or "today" if overdue/triggered). Subsequent occurrences are + * projected by adding interval_days repeatedly until the window end, capped + * at MAX_OCCURRENCES_PER_TASK to prevent absurdly small intervals (e.g. a + * 1-day-interval task in a 30-day window would otherwise produce 30 entries). + * + * Sensor-triggered tasks: only next_due is shown — we have no honest way to + * predict when a sensor will next fire. + */ + +import type { MaintenanceObjectResponse } from "../types"; +import { intervalSpanDays } from "./interval"; + +export const MAX_OCCURRENCES_PER_TASK = 5; + +export interface CalendarEvent { + /** ISO date string (YYYY-MM-DD) — the day this event is bucketed under. */ + date: string; + entry_id: string; + task_id: string; + task_name: string; + object_name: string; + status: string; // "ok" | "due_soon" | "overdue" | "triggered" + days_until_due: number | null; + /** True if this is a projected recurrence (not the actual next_due). */ + projected: boolean; + schedule_type: string; + interval_days: number | null; + /** v2.6.3 (#59): interval unit so the "every N …" label + projection match. */ + interval_unit?: string | null; + responsible_user_id: string | null; + avg_cost: number | null; + /** v1.5.1: source indicator. */ + adaptive_enabled: boolean; + /** v1.5.1: sensor prediction confidence ("low" | "medium" | "high") or null + * for time-based / no prediction available. */ + prediction_confidence: string | null; + /** v2.2.0 — for past-window events, the original ISO timestamp of the + * underlying history entry. The history-edit WS uses this as the stable + * identifier (see ws_update_history_entry / original_timestamp). null + * for forward-projected / next_due events. */ + history_timestamp?: string | null; + /** v2.2.0 — history entry type for past events + * ("completed" | "reset" | "skipped" | "triggered"). null for future. */ + history_type?: string | null; + /** v2.2.0 — original cost / notes / duration from the history entry, + * so the past-events list shows what really happened (not avg_cost). */ + history_cost?: number | null; + history_notes?: string | null; + history_duration?: number | null; +} + +export interface CalendarDayBucket { + date: string; // ISO date YYYY-MM-DD + events: CalendarEvent[]; +} + +/** + * Format a Date as ISO YYYY-MM-DD using LOCAL time components — matches what + * the user sees on their wall clock, not UTC. The naive `.toISOString().slice(0,10)` + * approach silently shifts dates near midnight in non-UTC timezones. + */ +export function isoDateLocal(d: Date): string { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${y}-${m}-${day}`; +} + +/** Build the list of N consecutive ISO dates starting today (local). */ +export function buildWindowDates(today: Date, windowDays: number): string[] { + const out: string[] = []; + for (let i = 0; i < windowDays; i++) { + const d = new Date(today); + d.setDate(d.getDate() + i); + d.setHours(0, 0, 0, 0); + out.push(isoDateLocal(d)); + } + return out; +} + +/** Add `days` to an ISO date (local), return new ISO date. */ +function addDaysIso(iso: string, days: number): string { + const [y, m, d] = iso.split("-").map(Number); + const date = new Date(y, m - 1, d); + date.setDate(date.getDate() + days); + return isoDateLocal(date); +} + +/** Average recorded cost across history rows that HAVE a cost (mean of + * non-null `cost` values). Note this differs from the panel KPI, which divides + * total_cost by times_performed — completions without a recorded cost are + * counted differently. This mean is used only for the calendar tooltip. */ +function computeAvgCost(history: Array<{ cost?: number | null }> | undefined): number | null { + if (!history || history.length === 0) return null; + const costs = history.map((h) => h.cost).filter((c): c is number => typeof c === "number"); + if (costs.length === 0) return null; + return costs.reduce((a, b) => a + b, 0) / costs.length; +} + +interface ProjectionInput { + windowStart: string; + windowEnd: string; // inclusive + task: any; // panel's task shape from MaintenanceObjectResponse + entryId: string; + objectName: string; +} + +/** + * Project up to MAX_OCCURRENCES_PER_TASK occurrences for a single task. + * Returns events keyed by their bucketed date. + */ +function projectTask(input: ProjectionInput): CalendarEvent[] { + const { windowStart, windowEnd, task, entryId, objectName } = input; + const out: CalendarEvent[] = []; + + const baseEvent = (date: string, projected: boolean): CalendarEvent => ({ + date, + entry_id: entryId, + task_id: task.id, + task_name: task.name, + object_name: objectName, + // Projected recurrences are hypothetical future occurrences that assume + // the current cycle resolves on schedule. If the parent task is currently + // overdue/triggered, carrying that status forward to the projection (e.g. + // "OVERDUE 211d" on the May 7 projection of a 7-day-interval task) is + // misleading — the projection IS the assumption that the user completes + // it today, so the projected slot should read as a fresh "ok" event. + status: projected && (task.status === "overdue" || task.status === "triggered") + ? "ok" + : task.status, + days_until_due: projected ? null : (task.days_until_due ?? null), + projected, + schedule_type: task.schedule_type, + interval_days: task.interval_days ?? null, + interval_unit: task.interval_unit ?? null, + responsible_user_id: task.responsible_user_id ?? null, + avg_cost: computeAvgCost(task.history), + adaptive_enabled: !!task.adaptive_config?.enabled, + prediction_confidence: task.threshold_prediction_confidence ?? null, + }); + + // Step between projected occurrences, unit-aware (issue #59): a weeks/months/ + // years interval must advance by its real day-span, not the raw count (else a + // "1 year" task projects at 1-day steps and spams the window). + const stepDays = Math.max( + 1, Math.round(intervalSpanDays(task.interval_days, task.interval_unit)), + ); + + // Overdue / triggered → bucket on today (windowStart) regardless of next_due + if (task.status === "overdue" || task.status === "triggered") { + out.push(baseEvent(windowStart, false)); + // Continue projecting from "today" forward for time-based tasks + if (task.schedule_type === "time_based" && task.interval_days && task.interval_days > 0) { + let cursor = addDaysIso(windowStart, stepDays); + let count = 1; + while (cursor <= windowEnd && count < MAX_OCCURRENCES_PER_TASK) { + out.push(baseEvent(cursor, true)); + count++; + cursor = addDaysIso(cursor, stepDays); + } + } + return out; + } + + // Non-actionable status (ok / due_soon): need a next_due + const nextDue = task.next_due; + if (typeof nextDue !== "string" || !nextDue) return out; + const firstDate = nextDue.slice(0, 10); // strip time portion if any + + // First occurrence must be in window + if (firstDate >= windowStart && firstDate <= windowEnd) { + out.push(baseEvent(firstDate, false)); + } else if (firstDate > windowEnd) { + return out; // entire occurrence chain is past window + } + + // Subsequent projected occurrences (time-based only, with interval_days) + if (task.schedule_type === "time_based" && task.interval_days && task.interval_days > 0) { + let cursor = addDaysIso(firstDate, stepDays); + let count = out.length; + while (cursor <= windowEnd && count < MAX_OCCURRENCES_PER_TASK) { + // Skip occurrences before window start (when next_due itself was past) + if (cursor >= windowStart) { + out.push(baseEvent(cursor, true)); + count++; + } + cursor = addDaysIso(cursor, stepDays); + } + } + + return out; +} + +/** Status sort priority: lower = shown first. */ +const STATUS_RANK: Record = { + overdue: 0, + triggered: 1, + due_soon: 2, + ok: 3, +}; + +/** + * Main entry point: build the day-bucketed event list for the Calendar tab. + * + * @param objects The panel's loaded objects (with nested tasks). + * @param today "Today" as a Date (caller passes new Date() — easier for tests). + * @param windowDays Number of days to include (7 / 14 / 30). + * @param userFilter null/empty = all; otherwise filter tasks by responsible_user_id. + */ +export function buildCalendarBuckets( + objects: MaintenanceObjectResponse[], + today: Date, + windowDays: number, + userFilter: string | null = null, +): CalendarDayBucket[] { + const days = buildWindowDates(today, windowDays); + const windowStart = days[0]; + const windowEnd = days[days.length - 1]; + + const allEvents: CalendarEvent[] = []; + for (const obj of objects) { + const objectName = obj.object?.name || ""; + const entryId = obj.entry_id; + const tasks = obj.tasks || []; + for (const task of tasks) { + // User filter + if (userFilter && task.responsible_user_id !== userFilter) continue; + // Disabled tasks never produce calendar events + if (task.enabled === false) continue; + const projected = projectTask({ + windowStart, windowEnd, task, entryId, objectName, + }); + allEvents.push(...projected); + } + } + + // Bucket by date + const byDate = new Map(); + for (const day of days) byDate.set(day, []); + for (const ev of allEvents) { + const bucket = byDate.get(ev.date); + if (bucket) bucket.push(ev); + } + + // Sort within day: status priority first, then projected last, then by name + for (const [, evs] of byDate) { + evs.sort((a, b) => { + const rA = STATUS_RANK[a.status] ?? 99; + const rB = STATUS_RANK[b.status] ?? 99; + if (rA !== rB) return rA - rB; + if (a.projected !== b.projected) return a.projected ? 1 : -1; + const cmp = a.object_name.localeCompare(b.object_name); + if (cmp !== 0) return cmp; + return a.task_name.localeCompare(b.task_name); + }); + } + + return days.map((d) => ({ date: d, events: byDate.get(d) ?? [] })); +} + +// ── v2.2.0 — past-window bucketer ────────────────────────────────────────── +// +// Same shape as buildCalendarBuckets but walks the task HISTORY (not next_due) +// and buckets entries by their actual completion/reset/skip/trigger date. +// Used by the calendar card's past-mode chip ("← 30 d") to surface +// recently-performed maintenance for review or correction. +// +// Window: from (today − pastDays + 1) to today, inclusive — so passing 30 +// gives 30 day-buckets ending today. + +const HISTORY_TYPE_TO_STATUS: Record = { + completed: "ok", + reset: "ok", + skipped: "due_soon", + triggered: "triggered", + trigger_replaced: "triggered", + trigger_removed: "ok", // config change, not a due/triggered event +}; + +interface HistoryEntryShape { + timestamp?: string; + type?: string; + notes?: string; + cost?: number; + duration?: number; +} + +/** Build a list of N consecutive ISO dates ending today (local). */ +export function buildPastWindowDates(today: Date, pastDays: number): string[] { + const out: string[] = []; + for (let i = pastDays - 1; i >= 0; i--) { + const d = new Date(today); + d.setDate(d.getDate() - i); + d.setHours(0, 0, 0, 0); + out.push(isoDateLocal(d)); + } + return out; +} + +/** + * Build day-bucketed events for the *past* N days using task history. + * + * Each history entry whose timestamp falls inside the window becomes one + * CalendarEvent with `history_timestamp` set — the calendar card uses that + * timestamp as the stable key when dispatching an edit-history click event. + */ +export function buildPastBuckets( + objects: MaintenanceObjectResponse[], + today: Date, + pastDays: number, + userFilter: string | null = null, +): CalendarDayBucket[] { + const days = buildPastWindowDates(today, pastDays); + const windowStart = days[0]; + const windowEnd = days[days.length - 1]; + + const byDate = new Map(); + for (const day of days) byDate.set(day, []); + + for (const obj of objects) { + const objectName = obj.object?.name || ""; + const entryId = obj.entry_id; + const tasks = obj.tasks || []; + for (const task of tasks) { + if (userFilter && task.responsible_user_id !== userFilter) continue; + const history = (task.history || []) as HistoryEntryShape[]; + for (const h of history) { + if (typeof h?.timestamp !== "string") continue; + const dateKey = h.timestamp.slice(0, 10); // YYYY-MM-DD + if (dateKey < windowStart || dateKey > windowEnd) continue; + const bucket = byDate.get(dateKey); + if (!bucket) continue; + const evType = h.type ?? "completed"; + bucket.push({ + date: dateKey, + entry_id: entryId, + task_id: task.id, + task_name: task.name, + object_name: objectName, + status: HISTORY_TYPE_TO_STATUS[evType] ?? "ok", + days_until_due: null, + projected: false, + schedule_type: task.schedule_type, + interval_days: task.interval_days ?? null, + responsible_user_id: task.responsible_user_id ?? null, + avg_cost: typeof h.cost === "number" ? h.cost : null, + adaptive_enabled: !!task.adaptive_config?.enabled, + prediction_confidence: null, + history_timestamp: h.timestamp, + history_type: evType, + history_cost: typeof h.cost === "number" ? h.cost : null, + history_notes: typeof h.notes === "string" ? h.notes : null, + history_duration: typeof h.duration === "number" ? h.duration : null, + }); + } + } + } + + // Sort within day: type priority (completed first, triggered last) then by + // object/task name. Past events are never "projected" so no opacity dimming. + const PAST_TYPE_RANK: Record = { + completed: 0, + reset: 1, + skipped: 2, + triggered: 3, + trigger_replaced: 4, + }; + for (const [, evs] of byDate) { + evs.sort((a, b) => { + const rA = PAST_TYPE_RANK[a.history_type ?? ""] ?? 99; + const rB = PAST_TYPE_RANK[b.history_type ?? ""] ?? 99; + if (rA !== rB) return rA - rB; + const cmp = a.object_name.localeCompare(b.object_name); + if (cmp !== 0) return cmp; + return a.task_name.localeCompare(b.task_name); + }); + } + + return days.map((d) => ({ date: d, events: byDate.get(d) ?? [] })); +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/download.ts b/custom_components/maintenance_supporter/frontend-src/helpers/download.ts new file mode 100644 index 0000000..1d4d69f --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/download.ts @@ -0,0 +1,50 @@ +/** + * Download a text payload as a file — Companion-app safe. + * + * The naive pattern (a detached ``, `a.click()`, then an immediate + * `URL.revokeObjectURL`) silently fails inside the Home Assistant Companion + * app's WebView (especially iOS WKWebView): the anchor must be in the DOM, + * carry `target="_blank"` so the app's download handler engages, and the + * object URL must NOT be revoked before the (asynchronous) download has + * started. This mirrors home-assistant-frontend's own `fileDownload` helper. + */ +export function downloadTextFile( + content: string, + filename: string, + mime: string, +): void { + const blob = new Blob([content], { type: mime }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.target = "_blank"; + a.rel = "noopener"; + a.style.display = "none"; + document.body.appendChild(a); + a.dispatchEvent(new MouseEvent("click")); + document.body.removeChild(a); + // Revoke late: slower WebViews (the Companion app) kick off the download + // asynchronously, and revoking the blob URL immediately cancels it. + setTimeout(() => URL.revokeObjectURL(url), 60_000); +} + +/** + * Download a (same-origin) URL as a file — Companion-app safe. + * + * For binary blobs served by our authenticated view: fetch a signed path via + * `auth/sign_path` first, then hand the signed URL here. Same DOM-anchored, + * `target="_blank"` trick as {@link downloadTextFile} so the Companion WebView + * engages its download handler; no blob URL to revoke. + */ +export function downloadUrl(url: string, filename: string): void { + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.target = "_blank"; + a.rel = "noopener"; + a.style.display = "none"; + document.body.appendChild(a); + a.dispatchEvent(new MouseEvent("click")); + document.body.removeChild(a); +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/format-bytes.ts b/custom_components/maintenance_supporter/frontend-src/helpers/format-bytes.ts new file mode 100644 index 0000000..d841a10 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/format-bytes.ts @@ -0,0 +1,7 @@ +/** Human-readable byte size (B / KB / MB) for document storage figures. */ +export function formatBytes(bytes: number | undefined): string { + const b = bytes ?? 0; + if (b < 1024) return `${b} B`; + if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`; + return `${(b / (1024 * 1024)).toFixed(1)} MB`; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/interval.ts b/custom_components/maintenance_supporter/frontend-src/helpers/interval.ts new file mode 100644 index 0000000..70519c1 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/interval.ts @@ -0,0 +1,41 @@ +/** + * Unit-aware interval math (issue #59) for the frontend's progress bars and + * calendar projection stepping. Uses an AVERAGE day-span per unit (below), an + * intentional approximation — the backend `helpers/dates.add_interval` is + * calendar-exact (Jan+1mo = 31 days), so display-side spans can differ by a day + * or two. Cosmetic only. Pure functions — unit-tested in interval.test.ts. + */ + +/** Approximate (average) days per interval unit — mean Gregorian month / Julian + * year. NOT calendar-exact; see the module note above. */ +export const UNIT_DAYS: Record = { + days: 1, + weeks: 7, + months: 30.4368, + years: 365.25, +}; + +/** Approximate length of `intervalDays` of `unit` in days; 0 when no interval. */ +export function intervalSpanDays( + intervalDays: number | null | undefined, + unit?: string | null, +): number { + if (!intervalDays || intervalDays <= 0) return 0; + return intervalDays * (UNIT_DAYS[unit || "days"] ?? 1); +} + +/** + * Progress through the current cycle, unit-aware. + * Returns a clamped 0–100 % plus `overflow` (raw % > 100, i.e. past due). + * Falls back to {0, false} when there is no usable interval/countdown. + */ +export function daysProgress( + intervalDays: number | null | undefined, + daysUntilDue: number | null | undefined, + unit?: string | null, +): { pct: number; overflow: boolean } { + const span = intervalSpanDays(intervalDays, unit); + if (span <= 0 || daysUntilDue == null) return { pct: 0, overflow: false }; + const raw = ((span - daysUntilDue) / span) * 100; + return { pct: Math.max(0, Math.min(100, raw)), overflow: raw > 100 }; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/object-columns.ts b/custom_components/maintenance_supporter/frontend-src/helpers/object-columns.ts new file mode 100644 index 0000000..afec03b --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/object-columns.ts @@ -0,0 +1,65 @@ +/** (#67) Objects-table column catalog. + * + * Shared by the panel's table view and the Settings column-config UI. Keep + * KNOWN/DEFAULT in lockstep with const.py (KNOWN_OBJECT_TABLE_COLUMNS / + * DEFAULT_OBJECTS_TABLE_COLUMNS) — the backend sanitises to the same set. + * Parity is enforced by tests/test_frontend_const_parity.py (drift fails CI). + */ + +export interface ObjectColumnDef { + /** Stable column key (matches the persisted setting + object field). */ + key: string; + /** i18n key for the header cell + the Settings checkbox label. */ + labelKey: string; + /** Columns the user may not remove (always rendered). */ + required?: boolean; +} + +/** Canonical order + label mapping. Most labels reuse existing i18n keys. */ +export const OBJECT_COLUMNS: ObjectColumnDef[] = [ + { key: "name", labelKey: "name", required: true }, + { key: "manufacturer", labelKey: "manufacturer" }, + { key: "model", labelKey: "model" }, + { key: "serial_number", labelKey: "serial_number_label" }, + { key: "installation_date", labelKey: "installed" }, + { key: "warranty_expiry", labelKey: "warranty" }, + { key: "area_id", labelKey: "area" }, + { key: "documentation_url", labelKey: "documentation_url_label" }, + { key: "notes", labelKey: "object_notes_label" }, + { key: "task_count", labelKey: "tasks" }, + { key: "actions", labelKey: "actions" }, +]; + +export const KNOWN_OBJECT_COLUMNS: string[] = OBJECT_COLUMNS.map((c) => c.key); + +export const DEFAULT_OBJECTS_TABLE_COLUMNS: string[] = [ + "name", + "manufacturer", + "model", + "serial_number", + "installation_date", + "warranty_expiry", + "area_id", + "task_count", + "actions", +]; + +/** + * Sanitise a stored/configured column list to known keys (order preserved, + * deduped). Falls back to the defaults when empty/invalid, and always keeps + * the required `name` column so the table stays usable. Mirrors the backend. + */ +export function sanitizeColumns(cols: unknown): string[] { + if (!Array.isArray(cols)) return [...DEFAULT_OBJECTS_TABLE_COLUMNS]; + const seen = new Set(); + const out: string[] = []; + for (const c of cols) { + if (typeof c === "string" && KNOWN_OBJECT_COLUMNS.includes(c) && !seen.has(c)) { + seen.add(c); + out.push(c); + } + } + if (!out.length) return [...DEFAULT_OBJECTS_TABLE_COLUMNS]; + if (!out.includes("name")) out.unshift("name"); + return out; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/report.ts b/custom_components/maintenance_supporter/frontend-src/helpers/report.ts new file mode 100644 index 0000000..6e11588 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/report.ts @@ -0,0 +1,110 @@ +/** Self-contained printable maintenance report for one object. + * + * Produces a full HTML document (inline print CSS) that the panel opens in a new + * tab via a Blob URL — the user then prints or "Save as PDF" from the browser. + * No PDF dependency, works offline. All user content is HTML-escaped. + */ + +import type { MaintenanceObject, MaintenanceTask } from "../types"; + +export interface ReportLabels { + title: string; + generated: string; + manufacturer: string; + model: string; + serial: string; + installed: string; + warranty: string; + area: string; + notes: string; + tasksHeading: string; + colTask: string; + colType: string; + colStatus: string; + colSchedule: string; + colLastDone: string; + colNextDue: string; + colCost: string; + colTimes: string; + totalCost: string; + /** Human schedule label for a task — pass formatRecurrence so calendar + * kinds (weekdays/nth_weekday/…) render properly, not just intervals. */ + scheduleLabel: (t: MaintenanceTask) => string; + statusLabel: (s: string) => string; + typeLabel: (t: string) => string; + none: string; +} + +function esc(v: unknown): string { + return String(v ?? "").replace(/[&<>"']/g, (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c] as string)); +} + +export function buildObjectReportHtml( + obj: MaintenanceObject, + tasks: MaintenanceTask[], + labels: ReportLabels, + fmtDate: (iso: string | null | undefined) => string, + currencySymbol: string, + nowIso: string, +): string { + const meta = ([ + [labels.manufacturer, obj.manufacturer], + [labels.model, obj.model], + [labels.serial, obj.serial_number], + [labels.installed, obj.installation_date ? fmtDate(obj.installation_date) : null], + [labels.warranty, obj.warranty_expiry ? fmtDate(obj.warranty_expiry) : null], + ] as Array<[string, string | null | undefined]>).filter(([, v]) => !!v); + + const rows = tasks.map((t) => { + const schedule = labels.scheduleLabel(t); + return ` + ${esc(t.name)} + ${esc(labels.typeLabel(t.type))} + ${esc(labels.statusLabel(t.status))} + ${esc(schedule)} + ${esc(t.last_performed ? fmtDate(t.last_performed) : labels.none)} + ${esc(t.next_due ? fmtDate(t.next_due) : labels.none)} + ${t.times_performed ?? 0} + ${(t.total_cost ?? 0).toFixed(2)} ${esc(currencySymbol)} + `; + }).join(""); + + const totalCost = tasks.reduce((n, t) => n + (t.total_cost ?? 0), 0); + + return ` +${esc(labels.title)} — ${esc(obj.name)} + +

${esc(obj.name)}

+

${esc(labels.title)} · ${esc(labels.generated)}: ${esc(fmtDate(nowIso))}

+ ${meta.length ? `
${meta.map(([k, v]) => + `
${esc(k)}
${esc(v)}
`).join("")}
` : ""} +

${esc(labels.tasksHeading)} (${tasks.length})

+ + + + + + + ${rows || ``} + +
${esc(labels.colTask)}${esc(labels.colType)}${esc(labels.colStatus)}${esc(labels.colSchedule)}${esc(labels.colLastDone)}${esc(labels.colNextDue)}${esc(labels.colTimes)}${esc(labels.colCost)}
${esc(labels.none)}
${esc(labels.totalCost)}${totalCost.toFixed(2)} ${esc(currencySymbol)}
+ ${obj.notes ? `
${esc(labels.notes)}:\n${esc(obj.notes)}
` : ""} +`; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/virtual-window.ts b/custom_components/maintenance_supporter/frontend-src/helpers/virtual-window.ts new file mode 100644 index 0000000..1cf39b7 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/virtual-window.ts @@ -0,0 +1,69 @@ +/** Windowing math for the virtualized dashboard task table (500+ tasks). + * + * Pure functions only — the panel measures the scroll container / row height + * and renders `rows.slice(start, end)` between two grid-spanning spacers whose + * heights keep the scrollbar honest. The window start/end snap to a `step` + * grid so a 1-row scroll doesn't rebind the whole slice on every frame. + * + * Why windowing at all: the table's cross-row column alignment comes from CSS + * subgrid, which is incompatible with `content-visibility`/`size` containment + * (the cheap skip-offscreen-paint trick used elsewhere). So for large installs + * the initial O(n) render is paid on every keystroke/filter change unless the + * DOM itself is windowed. + */ + +/** Rows below this count render normally — windowing has bookkeeping cost and + * only pays off when the O(n) render actually hurts. */ +export const VIRTUAL_MIN_ROWS = 120; + +export interface VirtualWindowInput { + /** Scroll offset of the scrolling container. */ + scrollTop: number; + /** Visible (client) height of the scrolling container. */ + viewportHeight: number; + /** Top of the table relative to the container's content (scrollTop space). */ + listTop: number; + /** Measured uniform row height in px (clamped to >= 1). */ + rowHeight: number; + /** Total row count. */ + total: number; + /** Extra rows rendered above/below the viewport. Default 12. */ + overscan?: number; + /** Start/end snap to this grid to avoid re-render churn. Default 6. */ + step?: number; +} + +export interface VirtualWindow { + start: number; + end: number; + padTop: number; + padBottom: number; +} + +export function computeWindow(i: VirtualWindowInput): VirtualWindow { + if (i.total <= 0) return { start: 0, end: 0, padTop: 0, padBottom: 0 }; + const overscan = i.overscan ?? 12; + const step = Math.max(1, i.step ?? 6); + const rh = Math.max(1, i.rowHeight); + + const firstVisible = Math.floor((i.scrollTop - i.listTop) / rh); + const visibleCount = Math.ceil(i.viewportHeight / rh) + 1; + + let start = Math.max(0, firstVisible - overscan); + start = Math.floor(start / step) * step; + let end = Math.min(i.total, Math.max(firstVisible, 0) + visibleCount + overscan); + end = Math.min(i.total, Math.ceil(end / step) * step); + + // Degenerate inputs (table far above/below viewport): keep a sane window. + if (start >= end) { + start = Math.min(start, Math.max(0, i.total - 1)); + end = Math.min(i.total, start + Math.max(visibleCount, 1)); + } + + return { + start, + end, + padTop: start * rh, + padBottom: (i.total - end) * rh, + }; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/warranty.ts b/custom_components/maintenance_supporter/frontend-src/helpers/warranty.ts new file mode 100644 index 0000000..1df1017 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/warranty.ts @@ -0,0 +1,41 @@ +/** (#67) Warranty status computation for object asset tracking. + * + * Pure, side-effect-free, and `today`-injectable so it can be unit-tested + * deterministically without faking the system clock. + */ + +export type WarrantyStatusKind = "valid" | "expiring" | "expired" | "none"; + +export interface WarrantyStatus { + kind: WarrantyStatusKind; + /** Whole days until expiry (negative once expired); null when no date. */ + days: number | null; + /** The ISO date string echoed back, or null when absent/invalid. */ + date: string | null; +} + +/** Days before expiry at which the warranty is flagged as "expiring" (amber). */ +export const WARRANTY_WARN_DAYS = 60; + +/** + * Classify an object's warranty expiry date relative to `today`. + * + * @param iso ISO `YYYY-MM-DD` warranty expiry date (null/undefined/""/invalid → "none"). + * @param today Reference date; defaults to now. Injectable for deterministic tests. + */ +export function warrantyStatus( + iso: string | null | undefined, + today: Date = new Date(), +): WarrantyStatus { + if (!iso) return { kind: "none", days: null, date: null }; + const exp = new Date(`${iso}T00:00:00`); + if (isNaN(exp.getTime())) return { kind: "none", days: null, date: null }; + // Normalize both ends to local midnight so the difference is whole days + // regardless of the time-of-day component of `today`. + const t0 = Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()); + const t1 = Date.UTC(exp.getFullYear(), exp.getMonth(), exp.getDate()); + const days = Math.round((t1 - t0) / 86400000); + if (days < 0) return { kind: "expired", days, date: iso }; + if (days <= WARRANTY_WARN_DAYS) return { kind: "expiring", days, date: iso }; + return { kind: "valid", days, date: iso }; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/worksheet.ts b/custom_components/maintenance_supporter/frontend-src/helpers/worksheet.ts new file mode 100644 index 0000000..bad14bb --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/worksheet.ts @@ -0,0 +1,157 @@ +/** Task work sheet (v2.21) — a printable one-pager for a single task. + * + * Everything needed to actually DO the task, on one sheet of paper: object + * + task details, the checklist as real tick boxes, the notes, and a QR + * pair (open the task / complete it) so the paper links back to the panel. + * When the task has a linked PDF manual with a page hint, the manual + * excerpt pages are rendered INLINE (downscaled, two per row) via the + * vendored pdf.js legacy build — the whole work sheet prints as one + * document. The PDF link stays as the fallback when rendering fails. + * + * Mirrors helpers/report.ts: pure function building a self-contained HTML + * document; the panel opens it in a new tab where the user prints or saves + * as PDF. All user strings are escaped; labels arrive translated. + */ + +import type { MaintenanceTask } from "../types"; + +export interface WorksheetLabels { + title: string; // "Work sheet" + object: string; + type: string; + interval: string; + nextDue: string; + lastDone: string; + priority: string; + checklist: string; + notes: string; + scanView: string; // "Scan to open the task" + scanComplete: string; // "Scan to complete" + manualExcerpt: string; // "Manual excerpt" + pages: string; // "pages" + printedOn: string; // "Printed" + never: string; + typeLabel: (t: string) => string; + statusLabel: (s: string) => string; +} + +export interface WorksheetExcerpt { + title: string; + startPage: number; + endPage: number; + url: string; // signed excerpt-endpoint URL (absolute) + /** Absolute base URL of the vendored pdf.js assets; when set, the sheet + * renders the excerpt pages inline (downscaled 2-up) via pdf.js. */ + vendorBase?: string; +} + +const esc = (v: unknown): string => + String(v ?? "").replace(/[&<>"']/g, (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] as string); + +export function buildTaskWorksheetHtml( + task: MaintenanceTask, + objectName: string, + L: WorksheetLabels, + formatDate: (iso: string) => string, + formatRecurrence: (task: MaintenanceTask) => string, + qrViewDataUri: string | null, + qrCompleteDataUri: string | null, + excerpt: WorksheetExcerpt | null, + nowIso: string, +): string { + const meta: Array<[string, string]> = [ + [L.object, esc(objectName)], + [L.type, esc(L.typeLabel(task.type))], + [L.interval, esc(formatRecurrence(task))], + [L.nextDue, task.next_due ? esc(formatDate(task.next_due)) : "—"], + [L.lastDone, task.last_performed ? esc(formatDate(task.last_performed)) : esc(L.never)], + ]; + if (task.priority && task.priority !== "normal") { + meta.push([L.priority, esc(task.priority)]); + } + + const checklist = (task.checklist || []) + .map((item) => `
  • ${esc(item)}
  • `) + .join(""); + + const qr = (uri: string | null, caption: string) => + uri + ? `
    ${esc(caption)}
    ` + : ""; + + return ` +${esc(task.name)} — ${esc(L.title)} + + +
    +
    +

    ${esc(task.name)}

    +
    ${esc(objectName)}
    +
    +
    + ${qr(qrViewDataUri, L.scanView)} + ${qr(qrCompleteDataUri, L.scanComplete)} +
    +
    + + ${meta.map(([k, v]) => ``).join("")} +
    ${esc(k)}${v}
    + ${checklist ? `

    ${esc(L.checklist)}

      ${checklist}
    ` : ""} + ${task.notes ? `

    ${esc(L.notes)}

    ${esc(task.notes)}
    ` : ""} + ${excerpt ? `

    ${esc(L.manualExcerpt)}

    +
    +
    + ${excerpt.vendorBase ? `` : ""}` : ""} +
    ${esc(objectName)} · ${esc(task.name)} · ${esc(L.printedOn)} ${esc(nowIso.slice(0, 10))}
    +`; +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/cs.json b/custom_components/maintenance_supporter/frontend-src/locales/cs.json new file mode 100644 index 0000000..5225db4 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/cs.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Údržba", + "objects": "Objekty", + "tasks": "Úkoly", + "overdue": "Po termínu", + "due_soon": "Brzy", + "triggered": "Spuštěno", + "ok": "OK", + "all": "Vše", + "new_object": "+ Nový objekt", + "templates_from": "Ze šablony", + "templates_title": "Začít ze šablony", + "templates_task_count": "{n} úkolů", + "template_created": "Vytvořeno ze šablony", + "onboard_hint": "Přidejte první objekt a začněte sledovat údržbu.", + "edit": "Upravit", + "duplicate": "Duplikovat", + "task_duplicated": "Úkol duplikován", + "object_duplicated": "Objekt duplikován", + "delete": "Smazat", + "add_task": "+ Přidat úkol", + "complete": "Dokončit", + "completed": "Dokončeno", + "skip": "Přeskočit", + "skipped": "Přeskočeno", + "missed": "Missed", + "reset": "Reset", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Zrušit", + "bulk_select": "Vybrat", + "bulk_select_all": "Vybrat vše", + "bulk_n_selected": "{n} vybráno", + "bulk_completed": "Dokončeno {n} úkolů", + "bulk_archived": "Archivováno {n} úkolů", + "completing": "Dokončování…", + "interval": "Interval", + "warning": "Upozornění", + "last_performed": "Naposledy provedeno", + "next_due": "Další termín", + "days_until_due": "Dnů do termínu", + "avg_duration": "Prům. trvání", + "trigger": "Spouštěč", + "trigger_type": "Typ spouštěče", + "threshold_above": "Horní limit", + "threshold_below": "Dolní limit", + "threshold": "Práh", + "counter": "Čítač", + "state_change": "Změna stavu", + "runtime": "Doba běhu", + "runtime_hours": "Cílová doba běhu (hodiny)", + "target_value": "Cílová hodnota", + "baseline": "Základní hodnota", + "target_changes": "Cílový počet změn", + "for_minutes": "Po dobu (minut)", + "time_based": "Časový", + "sensor_based": "Založený na senzoru", + "manual": "Manuální", + "one_time": "Jednorázově", + "weekdays": "Dny v týdnu", + "nth_weekday": "N-tý den v týdnu v měsíci", + "day_of_month": "Den v měsíci", + "recurrence_on_days": "Opakovat v", + "recurrence_occurrence": "Výskyt", + "recurrence_weekday": "Den v týdnu", + "recurrence_day": "Den v měsíci (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Poslední", + "day_word": "Den", + "interval_value": "Interval", + "interval_unit": "Jednotka", + "unit_days": "Dny", + "unit_weeks": "Týdny", + "unit_months": "Měsíce", + "unit_years": "Roky", + "due_date": "Datum splnění", + "cleaning": "Čištění", + "inspection": "Inspekce", + "replacement": "Výměna", + "calibration": "Kalibrace", + "service": "Servis", + "reading": "Odečet", + "custom": "Vlastní", + "history": "Historie", + "cost": "Náklady", + "report_button": "Zpráva", + "report_title": "Zpráva o údržbě", + "report_generated": "Vytvořeno", + "report_times_done": "Provedeno", + "report_total_cost": "Celkové náklady", + "report_every": "každých {n} {unit}", + "report_notes": "Poznámky", + "report_col_type": "Typ", + "report_col_status": "Stav", + "report_col_schedule": "Plán", + "duration": "Doba trvání", + "both": "Obojí", + "trigger_val": "Hodnota spouštěče", + "complete_title": "Dokončit: ", + "checklist": "Kontrolní seznam", + "checklist_steps_optional": "Kroky kontrolního seznamu (volitelné)", + "checklist_placeholder": "Vyčistit filtr\nVyměnit těsnění\nOtestovat tlak", + "checklist_help": "Jeden krok na řádek. Max 100 položek.", + "err_too_long": "{field}: příliš dlouhé (max {n} znaků)", + "err_too_short": "{field}: příliš krátké (min {n} znaků)", + "err_value_too_high": "{field}: příliš velké (max {n})", + "err_value_too_low": "{field}: příliš malé (min {n})", + "err_required": "{field}: povinné", + "err_wrong_type": "{field}: špatný typ (očekáván: {type})", + "err_invalid_choice": "{field}: nepovolená hodnota", + "err_invalid_value": "{field}: neplatná hodnota", + "feat_schedule_time": "Plánování podle denní doby", + "feat_schedule_time_desc": "Úkoly se stanou po termínu v určenou denní dobu místo o půlnoci.", + "schedule_time_optional": "Termín v čase (volitelné, HH:MM)", + "schedule_time_help": "Prázdné = půlnoc (výchozí). Časové pásmo HA.", + "at_time": "v", + "notes_optional": "Poznámky (volitelné)", + "cost_optional": "Náklady (volitelné)", + "duration_minutes": "Doba trvání v minutách (volitelné)", + "days": "dní", + "day": "den", + "today": "Dnes", + "d_overdue": "d po termínu", + "no_tasks": "Zatím žádné úkoly údržby. Vytvořte objekt pro začátek.", + "no_tasks_short": "Žádné úkoly", + "no_history": "Zatím žádné záznamy historie.", + "show_all": "Zobrazit vše", + "cost_duration_chart": "Náklady a doba trvání", + "installed": "Nainstalováno", + "confirm_delete_object": "Smazat tento objekt a všechny jeho úkoly?", + "confirm_delete_task": "Smazat tento úkol?", + "min": "Min", + "max": "Max", + "save": "Uložit", + "saving": "Ukládání…", + "edit_task": "Upravit úkol", + "new_task": "Nový úkol údržby", + "task_name": "Název úkolu", + "maintenance_type": "Typ údržby", + "priority": "Priorita", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Nízká", + "priority_normal": "Normální", + "priority_high": "Vysoká", + "schedule_type": "Typ rozvrhu", + "interval_days": "Interval (dny)", + "warning_days": "Dny upozornění", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Naposledy provedeno (volitelné)", + "interval_anchor": "Ukotvení intervalu", + "anchor_completion": "Od data dokončení", + "anchor_planned": "Od plánovaného data (bez posunu)", + "edit_object": "Upravit objekt", + "name": "Název", + "manufacturer_optional": "Výrobce (volitelné)", + "model_optional": "Model (volitelné)", + "serial_number_optional": "Sériové číslo (volitelné)", + "serial_number_label": "S/N", + "documentation_url_label": "Návod", + "object_notes_label": "Poznámky", + "sort_due_date": "Termín", + "sort_object": "Název objektu", + "sort_type": "Typ", + "sort_task_name": "Název úkolu", + "all_objects": "Všechny objekty", + "tasks_lower": "úkolů", + "no_tasks_yet": "Zatím žádné úkoly", + "add_first_task": "Přidat první úkol", + "trigger_configuration": "Konfigurace spouštěče", + "entity_id": "ID entity", + "comma_separated": "oddělené čárkami", + "entity_logic": "Logika entit", + "entity_logic_any": "Spustí libovolná entita", + "entity_logic_all": "Všechny entity musí spustit", + "entities": "entity", + "attribute_optional": "Atribut (volitelný, prázdný = stav)", + "use_entity_state": "Použít stav entity (bez atributu)", + "trigger_above": "Spustit nad", + "trigger_below": "Spustit pod", + "for_at_least_minutes": "Po dobu alespoň (minut)", + "safety_interval_days": "Bezpečnostní interval (dny, volitelný)", + "safety_interval": "Bezpečnostní interval (volitelný)", + "delta_mode": "Režim delta", + "from_state_optional": "Ze stavu (volitelné)", + "to_state_optional": "Do stavu (volitelné)", + "documentation_url_optional": "URL dokumentace (volitelné)", + "object_notes_optional": "Poznámky (volitelné)", + "nfc_tag_id_optional": "ID NFC tagu (volitelné)", + "nfc_tags_empty_help": "V Home Assistant zatím nejsou registrovány žádné NFC tagy.", + "nfc_tags_open_settings": "Otevřít nastavení tagů", + "nfc_tags_refresh": "Obnovit", + "environmental_entity_optional": "Senzor prostředí (volitelný)", + "environmental_entity_helper": "např. sensor.outdoor_temperature — upravuje interval podle podmínek prostředí", + "environmental_attribute_optional": "Atribut prostředí (volitelný)", + "nfc_tag_id": "ID NFC tagu", + "nfc_linked": "NFC tag propojen", + "nfc_link_hint": "Klikněte pro propojení NFC tagu", + "responsible_user": "Zodpovědný uživatel", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Žádný uživatel přiřazen)", + "all_users": "Všichni uživatelé", + "my_tasks": "Moje úkoly", + "tab_calendar": "Kalendář", + "cal_no_events": "Bez údržby", + "cal_window_7": "7 dní", + "cal_window_14": "14 dní", + "cal_window_30": "30 dní", + "cal_window_365": "1 rok", + "cal_every_n_days": "každých {n} dní", + "cal_source_time": "Čas", + "cal_source_time_adaptive": "Čas (adaptivní)", + "cal_source_sensor": "Senzor", + "cal_predicted": "predikce", + "cal_confidence_high": "vysoká přesnost", + "cal_confidence_medium": "střední přesnost", + "cal_confidence_low": "nízká přesnost", + "budget_monthly": "Měsíční rozpočet", + "budget_yearly": "Roční rozpočet", + "groups": "Skupiny", + "new_group": "Nová skupina", + "edit_group": "Upravit skupinu", + "no_groups": "Zatím žádné skupiny", + "delete_group": "Smazat skupinu", + "delete_group_confirm": "Smazat skupinu '{name}'?", + "group_select_tasks": "Vybrat úkoly", + "group_name_required": "Název je povinný", + "description_optional": "Popis (volitelný)", + "selected": "Vybráno", + "loading_chart": "Načítání dat grafu...", + "hide_outliers": "Skrýt odlehlé hodnoty (chyby čidla)", + "was_maintenance_needed": "Byla tato údržba potřeba?", + "feedback_needed": "Potřebná", + "feedback_not_needed": "Nepotřebná", + "feedback_not_sure": "Nejsem si jistý", + "suggested_interval": "Navrhovaný interval", + "apply_suggestion": "Použít", + "reanalyze": "Znovu analyzovat", + "reanalyze_result": "Nová analýza", + "reanalyze_insufficient_data": "Nedostatek dat pro vytvoření doporučení", + "data_points": "datových bodů", + "dismiss_suggestion": "Zavřít", + "confidence_low": "Nízká", + "confidence_medium": "Střední", + "confidence_high": "Vysoká", + "recommended": "doporučeno", + "seasonal_awareness": "Sezónní povědomí", + "edit_seasonal_overrides": "Upravit sezónní faktory", + "seasonal_overrides_title": "Sezónní faktory (přepsání)", + "seasonal_overrides_hint": "Faktor na měsíc (0.1–5.0). Prázdné = naučeno automaticky.", + "seasonal_override_invalid": "Neplatná hodnota", + "seasonal_override_range": "Faktor musí být mezi 0.1 a 5.0", + "clear_all": "Vymazat vše", + "seasonal_chart_title": "Sezónní faktory", + "seasonal_learned": "Naučené", + "seasonal_manual": "Manuální", + "month_jan": "Led", + "month_feb": "Úno", + "month_mar": "Bře", + "month_apr": "Dub", + "month_may": "Kvě", + "month_jun": "Čer", + "month_jul": "Čvc", + "month_aug": "Srp", + "month_sep": "Zář", + "month_oct": "Říj", + "month_nov": "Lis", + "month_dec": "Pro", + "sensor_prediction": "Predikce senzoru", + "degradation_trend": "Trend", + "trend_rising": "Rostoucí", + "trend_falling": "Klesající", + "trend_stable": "Stabilní", + "trend_insufficient_data": "Nedostatek dat", + "days_until_threshold": "Dnů do prahu", + "threshold_exceeded": "Práh překročen", + "environmental_adjustment": "Faktor prostředí", + "sensor_prediction_urgency": "Senzor předpovídá práh za ~{days} dní", + "day_short": "d", + "weibull_reliability_curve": "Křivka spolehlivosti", + "weibull_failure_probability": "Pravděpodobnost selhání", + "weibull_r_squared": "Shoda R²", + "beta_early_failures": "Časná selhání", + "beta_random_failures": "Náhodná selhání", + "beta_wear_out": "Opotřebení", + "beta_highly_predictable": "Vysoce předvídatelné", + "confidence_interval": "Interval spolehlivosti", + "confidence_conservative": "Konzervativní", + "confidence_aggressive": "Optimistický", + "current_interval_marker": "Aktuální interval", + "recommended_marker": "Doporučený", + "characteristic_life": "Charakteristická životnost", + "chart_mini_sparkline": "Graf trendu", + "chart_history": "Historie nákladů a doby trvání", + "chart_seasonal": "Sezónní faktory, 12 měsíců", + "chart_weibull": "Weibullova křivka spolehlivosti", + "chart_sparkline": "Graf hodnoty spouštěče senzoru", + "days_progress": "Postup dnů", + "qr_code": "QR kód", + "qr_generating": "Generování QR kódu…", + "qr_error": "Nepodařilo se vygenerovat QR kód.", + "qr_error_no_url": "Není nakonfigurováno URL HA. Nastavte externí nebo interní URL v Nastavení → Systém → Síť.", + "save_error": "Nepodařilo se uložit. Zkuste to znovu.", + "qr_print": "Tisk", + "qr_download": "Stáhnout SVG", + "qr_action": "Akce při skenování", + "qr_action_view": "Zobrazit informace o údržbě", + "qr_action_complete": "Označit údržbu jako dokončenou", + "qr_url_mode": "Typ odkazu", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokální (mDNS)", + "qr_mode_server": "URL serveru", + "overview": "Přehled", + "analysis": "Analýza", + "recent_activities": "Nedávné aktivity", + "search_notes": "Hledat v poznámkách", + "avg_cost": "Prům. náklady", + "no_advanced_features": "Žádné pokročilé funkce nejsou povoleny", + "no_advanced_features_hint": "Povolte „Adaptivní intervaly” nebo „Sezónní vzory” v nastavení integrace pro zobrazení analytických dat.", + "analysis_not_enough_data": "Zatím nedostatek dat pro analýzu.", + "analysis_not_enough_data_hint": "Weibullova analýza vyžaduje alespoň 5 dokončených údržeb; sezónní vzory se stanou viditelné po 6+ datových bodech na měsíc.", + "analysis_manual_task_hint": "Manuální úkoly bez intervalu negenerují analytická data.", + "completions": "dokončení", + "current": "Aktuální", + "shorter": "Kratší", + "longer": "Delší", + "normal": "Normální", + "disabled": "Zakázáno", + "compound_logic": "Složená logika", + "compound": "Složené (více podmínek)", + "compound_logic_and": "A — musí platit všechny podmínky", + "compound_logic_or": "NEBO — stačí libovolná podmínka", + "compound_help": "Zkombinujte několik podmínek senzorů do jednoho spouštěče.", + "compound_no_conditions": "Zatím žádné podmínky — přidejte alespoň jednu.", + "compound_add_condition": "Přidat podmínku", + "compound_condition": "Podmínka", + "compound_remove_condition": "Odebrat podmínku", + "card_title": "Název", + "card_show_header": "Zobrazit záhlaví se statistikami", + "card_show_actions": "Zobrazit tlačítka akcí", + "card_compact": "Kompaktní režim", + "card_max_items": "Max položek (0 = vše)", + "card_filter_status": "Filtrovat podle stavu", + "card_filter_status_help": "Prázdné = zobrazit všechny stavy.", + "card_filter_objects": "Filtrovat podle objektů", + "card_filter_objects_help": "Prázdné = zobrazit všechny objekty.", + "card_filter_entities": "Filtrovat podle entit (entity_ids)", + "card_filter_entities_help": "Vyberte entity sensor / binary_sensor z této integrace. Prázdné = všechny.", + "card_loading_objects": "Načítání objektů…", + "card_load_error": "Nepodařilo se načíst objekty — zkontrolujte WebSocket spojení.", + "card_no_tasks_title": "Zatím žádné úkoly údržby", + "card_no_tasks_cta": "→ Vytvořte v panelu Maintenance", + "no_objects": "Zatím žádné objekty.", + "action_error": "Akce se nezdařila. Zkuste to znovu.", + "area_id_optional": "Oblast (volitelná)", + "installation_date_optional": "Datum instalace (volitelné)", + "warranty_expiry_optional": "Konec záruky (volitelné)", + "warranty": "Záruka", + "warranty_valid_until": "platná do {date}", + "warranty_expires_in": "vyprší za {days} dní", + "warranty_expired": "vypršela", + "cal_past_windows": "Minulá okna", + "cal_forward_windows": "Budoucí okna", + "history_edit_title": "Upravit záznam historie", + "history_edit_timestamp": "Časové razítko", + "manufacturer": "Výrobce", + "model": "Model", + "area": "Oblast", + "actions": "Akce", + "view_mode_label": "Zobrazení", + "view_cards": "Zobrazení karet", + "view_table": "Zobrazení tabulky", + "objects_table_columns_label": "Sloupce tabulky objektů", + "objects_table_columns_hint": "Vyberte, které sloupce se zobrazí v tabulkovém zobrazení objektů.", + "custom_icon_optional": "Ikona (volitelná, např. mdi:wrench)", + "task_enabled": "Úkol povolen", + "skip_reason_prompt": "Přeskočit tento úkol?", + "reason_optional": "Důvod (volitelný)", + "reset_date_prompt": "Označit úkol jako provedený?", + "reset_date_optional": "Datum posledního provedení (volitelné, výchozí dnes)", + "notes_label": "Poznámky", + "documentation_label": "Dokumentace", + "no_nfc_tag": "— Žádný tag —", + "dashboard": "Přehled", + "tab_today": "Dnes", + "palette_placeholder": "Hledat objekty a úkoly…", + "palette_no_results": "Žádné výsledky", + "palette_hint": "↑↓ pohyb · Enter otevřít · Esc zavřít", + "today_all_caught_up": "Vše hotovo! Tento týden nic nezbývá.", + "today_overdue": "Po termínu", + "today_due_today": "Dnes", + "today_this_week": "Tento týden", + "settings": "Nastavení", + "settings_features": "Pokročilé funkce", + "settings_features_desc": "Povolte nebo zakažte pokročilé funkce. Zakázání je skryje z UI, ale nesmaže data.", + "feat_adaptive": "Adaptivní plánování", + "feat_adaptive_desc": "Učte se optimální intervaly z historie údržby", + "feat_predictions": "Predikce senzorů", + "feat_predictions_desc": "Předpovídejte termíny spouštění z degradace senzoru", + "feat_seasonal": "Sezónní úpravy", + "feat_seasonal_desc": "Upravte intervaly podle sezónních vzorů", + "feat_environmental": "Korelace s prostředím", + "feat_environmental_desc": "Korelujte intervaly s teplotou/vlhkostí", + "feat_budget": "Sledování rozpočtu", + "feat_budget_desc": "Sledujte měsíční a roční výdaje na údržbu", + "feat_groups": "Skupiny úkolů", + "feat_groups_desc": "Organizujte úkoly do logických skupin", + "feat_checklists": "Kontrolní seznamy", + "feat_checklists_desc": "Vícestupňové procedury pro dokončení úkolu", + "settings_general": "Obecné", + "settings_default_warning": "Výchozí dny upozornění", + "settings_panel_enabled": "Boční panel", + "settings_panel_title": "Název panelu", + "settings_notifications": "Oznámení", + "settings_notify_service": "Služba oznámení", + "test_notification": "Testovací oznámení", + "send_test": "Odeslat test", + "testing": "Odesílání…", + "test_notification_success": "Testovací oznámení odesláno", + "test_notification_failed": "Testovací oznámení se nezdařilo", + "settings_notify_due_soon": "Oznámit když brzy", + "settings_notify_overdue": "Oznámit když po termínu", + "settings_notify_triggered": "Oznámit když spuštěno", + "settings_interval_hours": "Interval opakování (hodiny, 0 = jednou)", + "settings_quiet_hours": "Tiché hodiny", + "settings_quiet_start": "Začátek", + "settings_quiet_end": "Konec", + "settings_max_per_day": "Max oznámení denně (0 = bez limitu)", + "settings_bundling": "Seskupit oznámení", + "settings_bundle_threshold": "Práh seskupení", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobilní akční tlačítka", + "settings_action_complete": "Zobrazit tlačítko 'Dokončit'", + "settings_action_skip": "Zobrazit tlačítko 'Přeskočit'", + "settings_action_snooze": "Zobrazit tlačítko 'Odložit'", + "settings_weekly_digest": "Týdenní přehled", + "settings_weekly_digest_hint": "Jedno souhrnné oznámení v pondělí ráno, když jsou úkoly.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Doba odložení (hodiny)", + "settings_budget": "Rozpočet", + "settings_currency": "Měna", + "settings_budget_monthly": "Měsíční rozpočet", + "settings_budget_yearly": "Roční rozpočet", + "settings_budget_alerts": "Rozpočtová upozornění", + "settings_budget_threshold": "Práh upozornění (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Exportovat JSON", + "settings_export_yaml": "Exportovat YAML", + "settings_export_csv": "Exportovat CSV", + "settings_import_csv": "Importovat CSV", + "settings_import_placeholder": "Vložte sem obsah JSON nebo CSV…", + "settings_import_btn": "Importovat", + "settings_import_success": "{count} objektů úspěšně importováno.", + "settings_export_success": "Export stažen.", + "settings_saved": "Nastavení uloženo.", + "settings_include_history": "Zahrnout historii", + "sort_alphabetical": "Abecedně", + "sort_due_soonest": "Nejbližší termín", + "sort_task_count": "Počet úkolů", + "sort_area": "Oblast", + "sort_assigned_user": "Přiřazený uživatel", + "sort_group": "Skupina", + "groupby_none": "Bez seskupení", + "groupby_area": "Podle oblasti", + "groupby_group": "Podle skupiny", + "groupby_user": "Podle uživatele", + "filter_label": "Filtr", + "user_label": "Uživatel", + "sort_label": "Řazení", + "group_by_label": "Seskupit podle", + "state_value_help": "Použijte hodnotu stavu HA (obvykle malými písmeny, např. \"on\"/\"off\"). Velikost písmen se při uložení normalizuje.", + "target_changes_help": "Počet odpovídajících přechodů, po kterých se trigger spustí (výchozí: 1).", + "qr_print_title": "Tisk QR kódů", + "qr_print_desc": "Vygeneruj tiskovou stránku s QR kódy k vystřižení a nalepení na zařízení.", + "qr_print_load": "Načíst objekty", + "qr_print_filter": "Filtr", + "qr_print_objects": "Objekty", + "qr_print_actions": "Akce", + "qr_print_url_mode": "Typ odkazu", + "qr_print_estimate": "Odhad QR kódů", + "qr_print_over_limit": "limit 200, zužte filtr", + "qr_print_generate": "Vygenerovat QR kódy", + "qr_print_generating": "Generování…", + "qr_print_ready": "QR kódy připraveny", + "qr_print_print_button": "Tisk", + "qr_print_empty": "Nic k vygenerování", + "qr_action_skip": "Přeskočit", + "vacation_title": "Režim dovolené", + "vacation_active": "aktivní", + "vacation_ended": "ukončeno", + "vacation_desc": "Naplánuj dovolenou: oznámení jsou pozastavena během období plus dny rezervy. Lze definovat výjimky pro jednotlivé úkoly.", + "vacation_enable": "Zapnout režim dovolené", + "vacation_start": "Začátek", + "vacation_end": "Konec", + "vacation_buffer": "Rezerva (dnů)", + "vacation_exempt_title": "Upozorňovat i přes dovolenou", + "vacation_exempt_desc": "Vyber úkoly, které mají upozorňovat i během dovolené (např. kritická chemie bazénu).", + "vacation_load_tasks": "Načíst úkoly", + "vacation_preview_btn": "Zobrazit náhled", + "vacation_preview_affected": "úkolů ovlivněno", + "vacation_event_due_soon": "blíží se termín", + "vacation_event_overdue": "stane se po termínu", + "vacation_event_triggered_est": "možné spuštění senzoru", + "vacation_sensor_based": "(senzorové)", + "vacation_action_notify": "Přesto upozornit", + "vacation_action_unsilence": "Znovu ztlumit", + "vacation_marked_complete": "Označeno jako dokončené", + "vacation_marked_skip": "Přeskočeno", + "vacation_end_now": "Ukončit dovolenou nyní", + "unassigned": "Nepřiřazeno", + "no_area": "Bez oblasti", + "has_overdue": "Má úkoly po termínu", + "object": "Objekt", + "settings_panel_access": "Přístup k panelu", + "settings_panel_access_desc": "Administrátoři mají vždy plný přístup. Chcete-li delegovat vytváření, úpravy a mazání konkrétním uživatelům bez práv administrátora, zapněte tuto možnost a vyberte je níže — ostatní vidí pouze Dokončit a Přeskočit.", + "settings_operator_write": "Povolit vybraným uživatelům vytvářet, upravovat a mazat", + "settings_operator_write_desc": "Vypnuto: obsah mohou měnit pouze administrátoři. Zapnuto: plný přístup získají i vybraní uživatelé níže.", + "no_non_admin_users": "Nenalezeni žádní uživatelé bez admin práv. Přidejte je v Nastavení → Lidé.", + "owner_label": "Vlastník", + "feat_completion_actions": "Akce při dokončení", + "feat_completion_actions_desc": "Akce HA na úlohu při dokončení + QR rychlého dokončení s předvolenými hodnotami.", + "on_complete_action_title": "Při dokončení: spustit akci HA (volitelné)", + "on_complete_action_desc": "Volá službu HA, když je úloha dokončena — např. resetuje čítač na zařízení.", + "on_complete_action_service": "Služba", + "on_complete_action_target": "Cílová entita", + "on_complete_action_data": "Data (JSON, volitelné)", + "on_complete_action_test": "Testovat akci", + "on_complete_action_test_success": "Úspěch", + "on_complete_action_test_failed": "Selhalo", + "quick_complete_defaults_title": "Výchozí hodnoty rychlého dokončení (pro QR skenů, volitelné)", + "quick_complete_defaults_desc": "Předvolené hodnoty pro QR rychlého dokončení. Bez nich QR otevře dialog.", + "quick_complete_defaults_notes": "Poznámky", + "quick_complete_defaults_cost": "Cena", + "quick_complete_defaults_duration": "Trvání (minuty)", + "quick_complete_defaults_feedback_none": "Bez zpětné vazby", + "quick_complete_defaults_feedback_needed": "Bylo potřeba", + "quick_complete_defaults_feedback_not_needed": "Nebylo potřeba", + "quick_complete_success": "Rychle označeno jako hotové", + "trigger_replaced": "Spouštěč nahrazen", + "add": "Přidat", + "show_stats": "Zobrazit statistiky + grafy", + "hide_stats": "Skrýt statistiky", + "adaptive_no_data": "Zatím není dostatek historie dokončení pro adaptivní analýzu. Dokončete tento úkol ještě několikrát, abyste odemkli doporučení intervalu a grafy spolehlivosti.", + "suggestion_applied": "Navržený interval použit", + "vacation_mode": "Režim dovolené", + "vacation_status_active": "Nyní aktivní", + "vacation_status_scheduled": "Naplánováno", + "vacation_status_inactive": "Neaktivní", + "vacation_end_now_confirm": "Ukončit dovolenou okamžitě?", + "vacation_exempt_count": "vyňato", + "vacation_advanced": "Pokročilé…", + "vacation_open_panel": "Otevřít v panelu", + "enable": "Zapnout", + "saved": "Uloženo", + "budget_monthly_set": "Nastavit měsíční", + "budget_yearly_set": "Nastavit roční", + "budget_advanced": "Měna, výstrahy…", + "budget_open_panel": "Otevřít v panelu", + "groups_empty": "Zatím žádné skupiny.", + "group_new_placeholder": "Přidat skupinu…", + "group_delete_confirm": "Smazat skupinu „{name}\"?", + "groups_manage_tasks": "Spravovat přiřazení úkolů…", + "groups_open_panel": "Otevřít v panelu", + "on_complete_action_target_hint": "Poznámka: doména entity musí odpovídat službě — např. 'button.press' funguje jen na button.*, 'counter.increment' jen na counter.*, 'input_button.press' jen na input_button.* atd. Při neshodě akce tiše selže (HA zaznamená 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Zobrazit všechny objekty", + "show_all_tasks": "Vymazat filtr — zobrazit všechny úkoly", + "filter_to_overdue": "Filtrovat seznam jen na po termínu", + "filter_to_due_soon": "Filtrovat seznam jen na blížící se termín", + "filter_to_triggered": "Filtrovat seznam jen na spuštěné", + "open_task": "Otevřít úkol", + "show_details": "Zobrazit historii + statistiky", + "hide_details": "Skrýt podrobnosti", + "history_empty": "Zatím žádná historie.", + "history_edit_button": "Upravit záznam", + "total_cost": "Celkové náklady", + "times_performed": "Provedeno", + "older_entries": "starší", + "open_in_panel": "Otevřít v panelu Údržba", + "skip_reason": "Důvod přeskočení (volitelné)", + "reset_to_date": "Resetovat poslední provedení na", + "delete_task_confirm": "Smazat tento úkol a jeho historii?", + "delete_object_confirm": "Smazat tento objekt a všechny jeho úkoly?", + "loading": "Načítání…", + "archive": "Archivovat", + "undo": "Zpět", + "task_archived": "Úkol archivován", + "object_archived": "Objekt archivován", + "unarchive": "Obnovit z archivu", + "archived": "Archivováno", + "show_archived": "Zobrazit archivované", + "hide_archived": "Skrýt archivované", + "confirm_archive_object": "Archivovat tento objekt a jeho úkoly? Historie zůstane zachována a lze ji později obnovit.", + "settings_archive": "Archiv a uchovávání", + "settings_archive_desc": "Odkládejte dokončené jednorázové úkoly bez mazání. Archivované položky jsou skryté a neaktivní, ale zachovávají historii a náklady.", + "settings_archive_oneoff_days": "Automaticky archivovat dokončené jednorázové úkoly po (dny, 0 = vypnuto)", + "settings_delete_archived_oneoff_days": "Automaticky mazat archivované jednorázové úkoly po (dny, 0 = nikdy)", + "archive_object": "Archivovat objekt", + "unarchive_object": "Obnovit objekt z archivu", + "documents": "Dokumenty", + "documents_empty": "Zatím žádné dokumenty.", + "doc_upload": "Nahrát soubor", + "doc_uploading": "Nahrávání…", + "doc_add_link": "Přidat odkaz", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Název (nepovinné)", + "doc_open": "Otevřít", + "doc_delete_confirm": "Smazat „{name}“?", + "doc_too_large": "Soubor je příliš velký (max. 25 MB).", + "doc_upload_failed": "Nahrání selhalo.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Již uloženo jinde — sdíleno, bez dalšího místa.", + "doc_dup_in_object": "Tento soubor je již připojen k tomuto objektu.", + "doc_link_invalid": "Povoleny jsou pouze odkazy http/https.", + "doc_cat_manual": "Návod", + "doc_cat_warranty": "Záruka", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Náhradní díly", + "doc_cat_photo": "Fotografie", + "doc_cat_other": "Ostatní", + "doc_link_badge": "Odkaz", + "doc_storage_title": "Úložiště dokumentů", + "doc_storage_saved": "Ušetřeno deduplikací", + "doc_storage_refresh": "Obnovit", + "doc_download": "Stáhnout", + "doc_close": "Zavřít", + "doc_camera": "Vyfotit", + "doc_drop_hint": "Přetáhněte soubory sem", + "doc_task_none": "Žádné dokumenty spojené s tímto úkolem.", + "doc_link_existing": "Připojit dokument…", + "doc_attach": "Připojit", + "doc_unlink": "Odpojit", + "doc_page": "Strana", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1r", + "chart_since_service": "od poslední údržby", + "chart_no_stats": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazují se pouze hodnoty z údržbových záznamů", + "auto_complete_on_recovery": "Automaticky dokončit při zotavení senzoru", + "auto_complete_on_recovery_help": "Zaznamená dokončení (nastaví poslední provedení), jakmile se spouštěč sám vyřeší — např. doplněná sůl, vyměněný filtr.", + "doc_search": "Hledat dokumenty…", + "doc_search_none": "Žádné odpovídající dokumenty", + "link_device_optional": "Propojit s existujícím zařízením (volitelné)", + "parent_object_optional": "Nadřazený objekt (volitelné)", + "parent_none": "(Žádný nadřazený)", + "paused": "Pozastaveno", + "pause_object": "Pozastavit", + "resume_object": "Pokračovat", + "pause_until_prompt": "Zmrazit plány tohoto objektu — nic není splatné a nic neupozorňuje, dokud nebude obnoven. Volitelně nastavte datum automatického obnovení.", + "pause_until_label": "Pokračovat dne (volitelné)", + "object_paused": "Objekt pozastaven", + "object_resumed": "Objekt obnoven — plány restartovány", + "object_paused_badge": "Pozastaveno", + "paused_until_label": "do", + "replace_object": "Nahradit…", + "replace_object_prompt": "Vyřadit tento objekt a vytvořit nástupce. Historie a náklady zůstávají archivované u starého; úkoly a dokumenty přecházejí na nový, počítadla začínají od nuly.", + "replace_name_label": "Název nástupce", + "object_replaced": "Objekt nahrazen — nástupce vytvořen", + "reading_unit_label": "Jednotka odečtu (např. kWh, m³)", + "reading_unit_help": "Zobrazí se vedle zaznamenané hodnoty při dokončení tohoto úkolu.", + "reading_value_label": "Hodnota odečtu", + "reading_label": "Odečet", + "settings_templates_label": "Galerie šablon", + "settings_templates_hint": "Odškrtněte šablony, které nikdy nevyužijete — zmizí z výběru „Ze šablony\" (panel i config flow). Nic jiného se nemění; kdykoli je lze znovu zapnout.", + "worksheet": "Pracovní list", + "worksheet_scan_view": "Naskenujte pro otevření úkolu", + "worksheet_scan_complete": "Naskenujte pro dokončení", + "worksheet_manual_excerpt": "Výňatek z manuálu", + "worksheet_pages": "strany", + "worksheet_printed": "Vytištěno", + "worksheet_never": "Nikdy" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/da.json b/custom_components/maintenance_supporter/frontend-src/locales/da.json new file mode 100644 index 0000000..648bfc0 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/da.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Vedligeholdelse", + "objects": "Objekter", + "tasks": "Opgaver", + "overdue": "Forfalden", + "due_soon": "Snart forfalden", + "triggered": "Udløst", + "trigger_replaced": "Udløser erstattet", + "ok": "OK", + "all": "Alle", + "new_object": "+ Nyt objekt", + "templates_from": "Fra skabelon", + "templates_title": "Start fra en skabelon", + "templates_task_count": "{n} opgaver", + "template_created": "Oprettet fra skabelon", + "onboard_hint": "Tilføj dit første objekt for at spore vedligeholdelse.", + "edit": "Rediger", + "duplicate": "Dupliker", + "task_duplicated": "Opgave duplikeret", + "object_duplicated": "Objekt duplikeret", + "delete": "Slet", + "add_task": "+ Tilføj opgave", + "complete": "Fuldfør", + "completed": "Fuldført", + "skip": "Spring over", + "skipped": "Sprunget over", + "missed": "Missed", + "reset": "Nulstil", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annuller", + "bulk_select": "Vælg", + "bulk_select_all": "Vælg alle", + "bulk_n_selected": "{n} valgt", + "bulk_completed": "{n} opgaver fuldført", + "bulk_archived": "{n} opgaver arkiveret", + "completing": "Fuldfører…", + "interval": "Interval", + "warning": "Advarsel", + "last_performed": "Senest udført", + "next_due": "Næste forfald", + "days_until_due": "Dage til forfald", + "avg_duration": "Gns. varighed", + "trigger": "Udløser", + "trigger_type": "Udløsertype", + "threshold_above": "Øvre grænse", + "threshold_below": "Nedre grænse", + "threshold": "Tærskel", + "counter": "Tæller", + "state_change": "Tilstandsændring", + "runtime": "Driftstid", + "runtime_hours": "Måldriftstid (timer)", + "target_value": "Målværdi", + "baseline": "Referenceværdi", + "target_changes": "Antal målændringer", + "for_minutes": "I (minutter)", + "time_based": "Tidsbaseret", + "sensor_based": "Sensorbaseret", + "manual": "Manuel", + "one_time": "Engangs", + "weekdays": "Ugedage", + "nth_weekday": "N'te ugedag i måneden", + "day_of_month": "Dag i måneden", + "recurrence_on_days": "Gentag på", + "recurrence_occurrence": "Forekomst", + "recurrence_weekday": "Ugedag", + "recurrence_day": "Dag i måneden (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Sidste", + "day_word": "Dag", + "interval_value": "Interval", + "interval_unit": "Enhed", + "unit_days": "Dage", + "unit_weeks": "Uger", + "unit_months": "Måneder", + "unit_years": "År", + "due_date": "Forfaldsdato", + "cleaning": "Rengøring", + "inspection": "Inspektion", + "replacement": "Udskiftning", + "calibration": "Kalibrering", + "service": "Service", + "reading": "Aflæsning", + "custom": "Tilpasset", + "history": "Historik", + "cost": "Omkostning", + "report_button": "Rapport", + "report_title": "Vedligeholdelsesrapport", + "report_generated": "Genereret", + "report_times_done": "Udført", + "report_total_cost": "Samlede omkostninger", + "report_every": "hver {n}. {unit}", + "report_notes": "Noter", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Tidsplan", + "duration": "Varighed", + "both": "Begge", + "trigger_val": "Udløserværdi", + "complete_title": "Fuldfør: ", + "checklist": "Tjekliste", + "checklist_steps_optional": "Tjeklistetrin (valgfrit)", + "checklist_placeholder": "Rengør filter\nUdskift pakning\nTest tryk", + "checklist_help": "Ét trin pr. linje. Maks. 100 elementer.", + "err_too_long": "{field}: for lang (maks. {n} tegn)", + "err_too_short": "{field}: for kort (min. {n} tegn)", + "err_value_too_high": "{field}: for stor (maks. {n})", + "err_value_too_low": "{field}: for lille (min. {n})", + "err_required": "{field}: påkrævet", + "err_wrong_type": "{field}: forkert type (forventet: {type})", + "err_invalid_choice": "{field}: ikke en tilladt værdi", + "err_invalid_value": "{field}: ugyldig værdi", + "feat_schedule_time": "Planlægning efter tidspunkt på dagen", + "feat_schedule_time_desc": "Opgaver bliver forfaldne på et bestemt tidspunkt på dagen i stedet for midnat.", + "schedule_time_optional": "Forfalder kl. (valgfrit, TT:MM)", + "schedule_time_help": "Tom = midnat (standard). HA-tidszone.", + "at_time": "kl.", + "notes_optional": "Noter (valgfrit)", + "cost_optional": "Omkostning (valgfrit)", + "duration_minutes": "Varighed i minutter (valgfrit)", + "days": "dage", + "day": "dag", + "today": "I dag", + "d_overdue": "d forsinket", + "no_tasks": "Ingen vedligeholdelsesopgaver endnu. Opret et objekt for at komme i gang.", + "no_tasks_short": "Ingen opgaver", + "no_history": "Ingen historikposter endnu.", + "show_all": "Vis alle", + "cost_duration_chart": "Omkostning og varighed", + "installed": "Installeret", + "confirm_delete_object": "Slet dette objekt og alle dets opgaver?", + "confirm_delete_task": "Slet denne opgave?", + "min": "Min", + "max": "Maks", + "save": "Gem", + "saving": "Gemmer…", + "edit_task": "Rediger opgave", + "new_task": "Ny vedligeholdelsesopgave", + "task_name": "Opgavenavn", + "maintenance_type": "Vedligeholdelsestype", + "priority": "Prioritet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Lav", + "priority_normal": "Normal", + "priority_high": "Høj", + "schedule_type": "Planlægningstype", + "interval_days": "Interval (dage)", + "warning_days": "Advarselsdage", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Senest udført (valgfrit)", + "interval_anchor": "Intervalanker", + "anchor_completion": "Fra fuldførelsesdato", + "anchor_planned": "Fra planlagt dato (ingen drift)", + "edit_object": "Rediger objekt", + "name": "Navn", + "manufacturer_optional": "Producent (valgfrit)", + "model_optional": "Model (valgfrit)", + "serial_number_optional": "Serienummer (valgfrit)", + "serial_number_label": "S/N", + "documentation_url_label": "Manual", + "object_notes_label": "Noter", + "sort_due_date": "Forfaldsdato", + "sort_object": "Objektnavn", + "sort_type": "Type", + "sort_task_name": "Opgavenavn", + "all_objects": "Alle objekter", + "tasks_lower": "opgaver", + "no_tasks_yet": "Ingen opgaver endnu", + "add_first_task": "Tilføj første opgave", + "trigger_configuration": "Udløserkonfiguration", + "entity_id": "Enheds-ID", + "comma_separated": "kommasepareret", + "entity_logic": "Enhedslogik", + "entity_logic_any": "Enhver enhed udløser", + "entity_logic_all": "Alle enheder skal udløse", + "entities": "enheder", + "attribute_optional": "Attribut (valgfrit, tom = tilstand)", + "use_entity_state": "Brug enhedstilstand (ingen attribut)", + "trigger_above": "Udløs over", + "trigger_below": "Udløs under", + "for_at_least_minutes": "I mindst (minutter)", + "safety_interval_days": "Sikkerhedsinterval (dage, valgfrit)", + "safety_interval": "Sikkerhedsinterval (valgfrit)", + "delta_mode": "Delta-tilstand", + "from_state_optional": "Fra tilstand (valgfrit)", + "to_state_optional": "Til tilstand (valgfrit)", + "documentation_url_optional": "Dokumentations-URL (valgfrit)", + "object_notes_optional": "Noter (valgfrit)", + "nfc_tag_id_optional": "NFC-tag-ID (valgfrit)", + "nfc_tags_empty_help": "Ingen NFC-tags registreret i Home Assistant endnu.", + "nfc_tags_open_settings": "Åbn tag-indstillinger", + "nfc_tags_refresh": "Opdater", + "environmental_entity_optional": "Miljøsensor (valgfrit)", + "environmental_entity_helper": "f.eks. sensor.outdoor_temperature — justerer intervallet baseret på miljøforhold", + "environmental_attribute_optional": "Miljøattribut (valgfrit)", + "nfc_tag_id": "NFC-tag-ID", + "nfc_linked": "NFC-tag tilknyttet", + "nfc_link_hint": "Klik for at tilknytte NFC-tag", + "responsible_user": "Ansvarlig bruger", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ingen bruger tildelt)", + "all_users": "Alle brugere", + "my_tasks": "Mine opgaver", + "tab_calendar": "Kalender", + "cal_no_events": "Ingen vedligeholdelse", + "cal_window_7": "7 dage", + "cal_window_14": "14 dage", + "cal_window_30": "30 dage", + "cal_window_365": "1 år", + "cal_every_n_days": "hver {n}. dag", + "cal_source_time": "Tidsbaseret", + "cal_source_time_adaptive": "Tidsbaseret (adaptiv)", + "cal_source_sensor": "Sensorbaseret", + "cal_predicted": "forudsagt", + "cal_confidence_high": "høj sikkerhed", + "cal_confidence_medium": "mellem sikkerhed", + "cal_confidence_low": "lav sikkerhed", + "budget_monthly": "Månedligt budget", + "budget_yearly": "Årligt budget", + "groups": "Grupper", + "new_group": "Ny gruppe", + "edit_group": "Rediger gruppe", + "no_groups": "Ingen grupper endnu", + "delete_group": "Slet gruppe", + "delete_group_confirm": "Slet gruppen '{name}'?", + "group_select_tasks": "Vælg opgaver", + "group_name_required": "Navn er påkrævet", + "description_optional": "Beskrivelse (valgfrit)", + "selected": "Valgte", + "loading_chart": "Indlæser diagramdata...", + "hide_outliers": "Skjul outliers (sensorfejl)", + "was_maintenance_needed": "Var denne vedligeholdelse nødvendig?", + "feedback_needed": "Nødvendig", + "feedback_not_needed": "Ikke nødvendig", + "feedback_not_sure": "Usikker", + "suggested_interval": "Foreslået interval", + "apply_suggestion": "Anvend", + "reanalyze": "Analyser igen", + "reanalyze_result": "Ny analyse", + "reanalyze_insufficient_data": "Ikke nok data til at give en anbefaling", + "data_points": "datapunkter", + "dismiss_suggestion": "Afvis", + "confidence_low": "Lav", + "confidence_medium": "Mellem", + "confidence_high": "Høj", + "recommended": "anbefalet", + "seasonal_awareness": "Sæsonbevidsthed", + "edit_seasonal_overrides": "Rediger sæsonfaktorer", + "seasonal_overrides_title": "Sæsonfaktorer (tilsidesæt)", + "seasonal_overrides_hint": "Faktor pr. måned (0.1–5.0). Tom = lært automatisk.", + "seasonal_override_invalid": "Ugyldig værdi", + "seasonal_override_range": "Faktor skal være mellem 0.1 og 5.0", + "clear_all": "Ryd alle", + "seasonal_chart_title": "Sæsonfaktorer", + "seasonal_learned": "Lært", + "seasonal_manual": "Manuel", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Maj", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensorforudsigelse", + "degradation_trend": "Tendens", + "trend_rising": "Stigende", + "trend_falling": "Faldende", + "trend_stable": "Stabil", + "trend_insufficient_data": "Utilstrækkelige data", + "days_until_threshold": "Dage til tærskel", + "threshold_exceeded": "Tærskel overskredet", + "environmental_adjustment": "Miljøfaktor", + "sensor_prediction_urgency": "Sensor forudsiger tærskel om ~{days} dage", + "day_short": "d", + "weibull_reliability_curve": "Pålidelighedskurve", + "weibull_failure_probability": "Fejlsandsynlighed", + "weibull_r_squared": "Tilpasning R²", + "beta_early_failures": "Tidlige fejl", + "beta_random_failures": "Tilfældige fejl", + "beta_wear_out": "Slitage", + "beta_highly_predictable": "Meget forudsigelig", + "confidence_interval": "Konfidensinterval", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisk", + "current_interval_marker": "Nuværende interval", + "recommended_marker": "Anbefalet", + "characteristic_life": "Karakteristisk levetid", + "chart_mini_sparkline": "Tendensdiagram", + "chart_history": "Omkostnings- og varighedshistorik", + "chart_seasonal": "Sæsonfaktorer, 12 måneder", + "chart_weibull": "Weibull-pålidelighedskurve", + "chart_sparkline": "Diagram over sensorudløserværdi", + "days_progress": "Dagsforløb", + "qr_code": "QR-kode", + "qr_generating": "Genererer QR-kode…", + "qr_error": "Kunne ikke generere QR-kode.", + "qr_error_no_url": "Ingen HA-URL konfigureret. Angiv en ekstern eller intern URL under Indstillinger → System → Netværk.", + "save_error": "Kunne ikke gemme. Prøv igen.", + "qr_print": "Udskriv", + "qr_download": "Download SVG", + "qr_action": "Handling ved scanning", + "qr_action_view": "Vis vedligeholdelsesoplysninger", + "qr_action_complete": "Marker vedligeholdelse som fuldført", + "qr_url_mode": "Linktype", + "qr_mode_companion": "Companion-app", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Oversigt", + "analysis": "Analyse", + "recent_activities": "Seneste aktiviteter", + "search_notes": "Søg i noter", + "avg_cost": "Gns. omkostning", + "no_advanced_features": "Ingen avancerede funktioner aktiveret", + "no_advanced_features_hint": "Aktiver „Adaptive intervaller” eller „Sæsonmønstre” i integrationsindstillingerne for at se analysedata her.", + "analysis_not_enough_data": "Ikke nok data til analyse endnu.", + "analysis_not_enough_data_hint": "Weibull-analyse kræver mindst 5 fuldførte vedligeholdelser; sæsonmønstre bliver synlige efter 6+ datapunkter pr. måned.", + "analysis_manual_task_hint": "Manuelle opgaver uden et interval genererer ikke analysedata.", + "completions": "fuldførelser", + "current": "Nuværende", + "shorter": "Kortere", + "longer": "Længere", + "normal": "Normal", + "disabled": "Deaktiveret", + "compound_logic": "Sammensat logik", + "compound": "Sammensat (flere betingelser)", + "compound_logic_and": "OG — alle betingelser skal udløses", + "compound_logic_or": "ELLER — en betingelse er nok", + "compound_help": "Kombiner flere sensorbetingelser til én udløser.", + "compound_no_conditions": "Ingen betingelser endnu — tilføj mindst én.", + "compound_add_condition": "Tilføj betingelse", + "compound_condition": "Betingelse", + "compound_remove_condition": "Fjern betingelse", + "card_title": "Titel", + "card_show_header": "Vis overskrift med statistik", + "card_show_actions": "Vis handlingsknapper", + "card_compact": "Kompakt tilstand", + "card_max_items": "Maks. elementer (0 = alle)", + "card_filter_status": "Filtrer efter status", + "card_filter_status_help": "Tom = vis alle statusser.", + "card_filter_objects": "Filtrer efter objekter", + "card_filter_objects_help": "Tom = vis alle objekter.", + "card_filter_entities": "Filtrer efter enheder (entity_ids)", + "card_filter_entities_help": "Vælg sensor- / binary_sensor-enheder fra denne integration. Tom = alle.", + "card_loading_objects": "Indlæser objekter…", + "card_load_error": "Kunne ikke indlæse objekter — tjek WebSocket-forbindelsen.", + "card_no_tasks_title": "Ingen vedligeholdelsesopgaver endnu", + "card_no_tasks_cta": "→ Opret en i Vedligeholdelse-panelet", + "no_objects": "Ingen objekter endnu.", + "action_error": "Handlingen mislykkedes. Prøv igen.", + "area_id_optional": "Område (valgfrit)", + "installation_date_optional": "Installationsdato (valgfrit)", + "warranty_expiry_optional": "Garantiens udløb (valgfrit)", + "warranty": "Garanti", + "warranty_valid_until": "gyldig indtil {date}", + "warranty_expires_in": "udløber om {days} dage", + "warranty_expired": "udløbet", + "cal_past_windows": "Tidligere vinduer", + "cal_forward_windows": "Kommende vinduer", + "history_edit_title": "Rediger historikpost", + "history_edit_timestamp": "Tidsstempel", + "manufacturer": "Producent", + "model": "Model", + "area": "Område", + "actions": "Handlinger", + "view_mode_label": "Visning", + "view_cards": "Kortvisning", + "view_table": "Tabelvisning", + "objects_table_columns_label": "Kolonner i objekttabel", + "objects_table_columns_hint": "Vælg, hvilke kolonner der vises i objekttabelvisningen.", + "custom_icon_optional": "Ikon (valgfrit, f.eks. mdi:wrench)", + "task_enabled": "Opgave aktiveret", + "skip_reason_prompt": "Spring denne opgave over?", + "reason_optional": "Årsag (valgfrit)", + "reset_date_prompt": "Marker opgave som udført?", + "reset_date_optional": "Dato for seneste udførelse (valgfrit, standard er i dag)", + "notes_label": "Noter", + "documentation_label": "Dokumentation", + "no_nfc_tag": "— Intet tag —", + "dashboard": "Oversigt", + "tab_today": "I dag", + "palette_placeholder": "Søg objekter og opgaver…", + "palette_no_results": "Ingen match", + "palette_hint": "↑↓ naviger · Enter åbn · Esc luk", + "today_all_caught_up": "Alt er klaret! Intet forfalder i denne uge.", + "today_overdue": "Forfaldne", + "today_due_today": "Forfalder i dag", + "today_this_week": "Denne uge", + "settings": "Indstillinger", + "settings_features": "Avancerede funktioner", + "settings_features_desc": "Aktiver eller deaktiver avancerede funktioner. Deaktivering skjuler dem fra brugerfladen, men sletter ikke data.", + "feat_adaptive": "Adaptiv planlægning", + "feat_adaptive_desc": "Lær optimale intervaller fra vedligeholdelseshistorik", + "feat_predictions": "Sensorforudsigelser", + "feat_predictions_desc": "Forudsig udløsningsdatoer fra sensornedbrydning", + "feat_seasonal": "Sæsonjusteringer", + "feat_seasonal_desc": "Juster intervaller baseret på sæsonmønstre", + "feat_environmental": "Miljøkorrelation", + "feat_environmental_desc": "Korreler intervaller med temperatur/luftfugtighed", + "feat_budget": "Budgetopfølgning", + "feat_budget_desc": "Følg månedlige og årlige vedligeholdelsesudgifter", + "feat_groups": "Opgavegrupper", + "feat_groups_desc": "Organiser opgaver i logiske grupper", + "feat_checklists": "Tjeklister", + "feat_checklists_desc": "Procedurer i flere trin til fuldførelse af opgaver", + "settings_general": "Generelt", + "settings_default_warning": "Standard advarselsdage", + "settings_panel_enabled": "Sidepanel", + "settings_panel_title": "Sidepanelets titel", + "settings_notifications": "Notifikationer", + "settings_notify_service": "Notifikationstjeneste", + "test_notification": "Testnotifikation", + "send_test": "Send test", + "testing": "Sender…", + "test_notification_success": "Testnotifikation sendt", + "test_notification_failed": "Testnotifikation mislykkedes", + "settings_notify_due_soon": "Notificer ved snart forfalden", + "settings_notify_overdue": "Notificer ved forfalden", + "settings_notify_triggered": "Notificer ved udløst", + "settings_interval_hours": "Gentagelsesinterval (timer, 0 = én gang)", + "settings_quiet_hours": "Stilletimer", + "settings_quiet_start": "Start", + "settings_quiet_end": "Slut", + "settings_max_per_day": "Maks. notifikationer pr. dag (0 = ubegrænset)", + "settings_bundling": "Saml notifikationer", + "settings_bundle_threshold": "Samlingstærskel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobile handlingsknapper", + "settings_action_complete": "Vis 'Fuldfør'-knap", + "settings_action_skip": "Vis 'Spring over'-knap", + "settings_action_snooze": "Vis 'Udsæt'-knap", + "settings_weekly_digest": "Ugentlig oversigt", + "settings_weekly_digest_hint": "Én samlet notifikation mandag morgen, når opgaver forfalder.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Udsættelsesvarighed (timer)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Månedligt budget", + "settings_budget_yearly": "Årligt budget", + "settings_budget_alerts": "Budgetadvarsler", + "settings_budget_threshold": "Advarselstærskel (%)", + "settings_import_export": "Import / Eksport", + "settings_export_json": "Eksporter JSON", + "settings_export_yaml": "Eksporter YAML", + "settings_export_csv": "Eksporter CSV", + "settings_import_csv": "Importer CSV", + "settings_import_placeholder": "Indsæt JSON- eller CSV-indhold her…", + "settings_import_btn": "Importer", + "settings_import_success": "{count} objekter importeret.", + "settings_export_success": "Eksport downloadet.", + "settings_saved": "Indstilling gemt.", + "settings_include_history": "Inkluder historik", + "sort_alphabetical": "Alfabetisk", + "sort_due_soonest": "Forfalder først", + "sort_task_count": "Antal opgaver", + "sort_area": "Område", + "sort_assigned_user": "Tildelt bruger", + "sort_group": "Gruppe", + "groupby_none": "Ingen gruppering", + "groupby_area": "Efter område", + "groupby_group": "Efter gruppe", + "groupby_user": "Efter bruger", + "filter_label": "Filter", + "user_label": "Bruger", + "sort_label": "Sortering", + "group_by_label": "Grupper efter", + "state_value_help": "Brug HA-tilstandsværdien (normalt med små bogstaver, f.eks. \"on\"/\"off\"). Store/små bogstaver normaliseres ved lagring.", + "target_changes_help": "Antal matchende overgange, før udløseren aktiveres (standard: 1).", + "qr_print_title": "Udskriv QR-koder", + "qr_print_desc": "Generer en udskriftsklar side med QR-koder, der kan klippes ud og sættes på dit udstyr.", + "qr_print_load": "Indlæs objekter", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekter", + "qr_print_actions": "Handlinger", + "qr_print_url_mode": "Linktype", + "qr_print_estimate": "Anslåede QR-koder", + "qr_print_over_limit": "grænsen er 200, indsnævr filteret", + "qr_print_generate": "Generer QR-koder", + "qr_print_generating": "Genererer…", + "qr_print_ready": "QR-koder klar", + "qr_print_print_button": "Udskriv", + "qr_print_empty": "Intet at generere", + "qr_action_skip": "Spring over", + "vacation_title": "Ferietilstand", + "vacation_active": "aktiv", + "vacation_ended": "afsluttet", + "vacation_desc": "Planlæg en ferie: notifikationer sættes på pause i perioden plus et bufferantal dage. Du kan vælge bestemte opgaver til igen.", + "vacation_enable": "Aktiver ferietilstand", + "vacation_start": "Start", + "vacation_end": "Slut", + "vacation_buffer": "Buffer (dage)", + "vacation_exempt_title": "Notificer alligevel under ferie", + "vacation_exempt_desc": "Vælg opgaver, der stadig skal notificere under ferien (f.eks. kritisk poolkemi).", + "vacation_load_tasks": "Indlæs opgaver", + "vacation_preview_btn": "Vis forhåndsvisning", + "vacation_preview_affected": "opgaver berørt", + "vacation_event_due_soon": "bliver snart forfalden", + "vacation_event_overdue": "bliver forfalden", + "vacation_event_triggered_est": "sensorudløsning mulig", + "vacation_sensor_based": "(sensorbaseret)", + "vacation_action_notify": "Notificer alligevel", + "vacation_action_unsilence": "Dæmp igen", + "vacation_marked_complete": "Markeret som fuldført", + "vacation_marked_skip": "Sprunget over", + "vacation_end_now": "Afslut ferie nu", + "add": "Tilføj", + "show_stats": "Vis statistik + grafer", + "hide_stats": "Skjul statistik", + "adaptive_no_data": "Endnu ikke nok fuldførelseshistorik til adaptiv analyse. Fuldfør denne opgave et par gange mere for at låse op for intervalanbefalinger og pålidelighedsdiagrammer.", + "suggestion_applied": "Foreslået interval anvendt", + "vacation_mode": "Ferietilstand", + "vacation_status_active": "Aktiv nu", + "vacation_status_scheduled": "Planlagt", + "vacation_status_inactive": "Inaktiv", + "vacation_end_now_confirm": "Afslut ferie med det samme?", + "vacation_exempt_count": "undtaget", + "vacation_advanced": "Avanceret…", + "vacation_open_panel": "Åbn i panel", + "enable": "Aktiver", + "saved": "Gemt", + "budget_monthly_set": "Angiv måned", + "budget_yearly_set": "Angiv år", + "budget_advanced": "Valuta, advarsler…", + "budget_open_panel": "Åbn i panel", + "groups_empty": "Ingen grupper endnu.", + "group_new_placeholder": "Tilføj gruppe…", + "group_delete_confirm": "Slet gruppen \"{name}\"?", + "groups_manage_tasks": "Administrer opgavetildelinger…", + "groups_open_panel": "Åbn i panel", + "unassigned": "Ikke tildelt", + "no_area": "Intet område", + "has_overdue": "Har forfaldne opgaver", + "object": "Objekt", + "settings_panel_access": "Paneladgang", + "settings_panel_access_desc": "Administratorer har altid fuld adgang. For at delegere oprettelse, redigering og sletning til bestemte ikke-administratorer skal du slå dette til og vælge dem nedenfor — alle andre ser kun Fuldfør og Spring over.", + "settings_operator_write": "Tillad valgte brugere at oprette, redigere og slette", + "settings_operator_write_desc": "Fra: kun administratorer kan ændre indhold. Til: de valgte brugere nedenfor får også fuld adgang.", + "no_non_admin_users": "Ingen ikke-administratorbrugere fundet. Tilføj nogle under Indstillinger → Personer.", + "owner_label": "Ejer", + "feat_completion_actions": "Fuldførelseshandlinger", + "feat_completion_actions_desc": "HA-handling pr. opgave ved fuldførelse + hurtig-fuldfør-QR med forudindstillede værdier.", + "on_complete_action_title": "Ved fuldførelse: udløs HA-handling (valgfrit)", + "on_complete_action_desc": "Kalder en HA-tjeneste, når opgaven fuldføres — f.eks. nulstil en tæller på enheden.", + "on_complete_action_service": "Tjeneste", + "on_complete_action_target": "Målenhed", + "on_complete_action_target_hint": "Bemærk: enhedens domæne skal matche tjenesten — f.eks. virker 'button.press' kun på button.*, 'counter.increment' kun på counter.*, 'input_button.press' kun på input_button.* osv. Ved et misforhold mislykkes handlingen lydløst (HA logger 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Data (JSON, valgfrit)", + "on_complete_action_test": "Valider konfiguration", + "on_complete_action_test_success": "✓ Konfiguration gyldig (handlingen udløses kun ved fuldførelse af opgaven)", + "on_complete_action_test_failed": "Mislykkedes", + "quick_complete_defaults_title": "Standardværdier for hurtig-fuldfør (til QR-scanninger, valgfrit)", + "quick_complete_defaults_desc": "Forudindstillede værdier til hurtig-fuldfør-QR-scanninger. Uden disse åbner QR-koden fuldfør-dialogen.", + "quick_complete_defaults_notes": "Noter", + "quick_complete_defaults_cost": "Omkostning", + "quick_complete_defaults_duration": "Varighed (minutter)", + "quick_complete_defaults_feedback_none": "Ingen feedback", + "quick_complete_defaults_feedback_needed": "Var nødvendig", + "quick_complete_defaults_feedback_not_needed": "Ikke nødvendig", + "quick_complete_success": "Hurtigt markeret som fuldført", + "show_all_objects": "Vis alle objekter", + "show_all_tasks": "Ryd filter — vis alle opgaver", + "filter_to_overdue": "Filtrer opgavelisten til kun forfaldne", + "filter_to_due_soon": "Filtrer opgavelisten til kun snart forfaldne", + "filter_to_triggered": "Filtrer opgavelisten til kun udløste", + "open_task": "Åbn opgave", + "show_details": "Vis historik + statistik", + "hide_details": "Skjul detaljer", + "history_empty": "Ingen historik endnu.", + "history_edit_button": "Rediger post", + "total_cost": "Samlet omkostning", + "times_performed": "Udført", + "older_entries": "ældre", + "open_in_panel": "Åbn i Vedligeholdelse-panelet", + "skip_reason": "Årsag til at springe over (valgfrit)", + "reset_to_date": "Nulstil senest udført til", + "delete_task_confirm": "Slet denne opgave og dens historik?", + "delete_object_confirm": "Slet dette objekt og alle dets opgaver?", + "loading": "Indlæser…", + "archive": "Arkivér", + "undo": "Fortryd", + "task_archived": "Opgave arkiveret", + "object_archived": "Objekt arkiveret", + "unarchive": "Gendan", + "archived": "Arkiveret", + "show_archived": "Vis arkiverede", + "hide_archived": "Skjul arkiverede", + "confirm_archive_object": "Arkivér dette objekt og dets opgaver? Historikken bevares og kan gendannes senere.", + "settings_archive": "Arkiv og opbevaring", + "settings_archive_desc": "Læg færdige engangsopgaver væk uden at slette dem. Arkiverede elementer er skjult og inaktive, men bevarer historik og omkostninger.", + "settings_archive_oneoff_days": "Arkivér automatisk færdige engangsopgaver efter (dage, 0 = fra)", + "settings_delete_archived_oneoff_days": "Slet automatisk arkiverede engangsopgaver efter (dage, 0 = aldrig)", + "archive_object": "Arkivér objekt", + "unarchive_object": "Gendan objekt", + "documents": "Dokumenter", + "documents_empty": "Ingen dokumenter endnu.", + "doc_upload": "Upload fil", + "doc_uploading": "Uploader…", + "doc_add_link": "Tilføj link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (valgfri)", + "doc_open": "Åbn", + "doc_delete_confirm": "Slet \"{name}\"?", + "doc_too_large": "Filen er for stor (maks. 25 MB).", + "doc_upload_failed": "Upload mislykkedes.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Allerede gemt et andet sted — delt, ingen ekstra plads.", + "doc_dup_in_object": "Denne fil er allerede vedhæftet dette objekt.", + "doc_link_invalid": "Kun http/https-links er tilladt.", + "doc_cat_manual": "Vejledning", + "doc_cat_warranty": "Garanti", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Reservedele", + "doc_cat_photo": "Foto", + "doc_cat_other": "Andet", + "doc_link_badge": "Link", + "doc_storage_title": "Dokumentlager", + "doc_storage_saved": "Sparet ved deduplikering", + "doc_storage_refresh": "Opdater", + "doc_download": "Download", + "doc_close": "Luk", + "doc_camera": "Tag billede", + "doc_drop_hint": "Slip filer her", + "doc_task_none": "Ingen dokumenter knyttet til denne opgave.", + "doc_link_existing": "Knyt et dokument…", + "doc_attach": "Knyt", + "doc_unlink": "Fjern kobling", + "doc_page": "Side", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1å", + "chart_since_service": "siden sidste vedligeholdelse", + "chart_no_stats": "Ingen langtidsstatistik for denne enhed — viser kun værdier fra vedligeholdelsesposter", + "auto_complete_on_recovery": "Fuldfør automatisk når sensoren retter sig", + "auto_complete_on_recovery_help": "Registrerer en fuldførelse (sætter senest udført), når udløseren løser sig selv — f.eks. salt påfyldt, filter udskiftet.", + "doc_search": "Søg dokumenter…", + "doc_search_none": "Ingen matchende dokumenter", + "link_device_optional": "Tilknyt eksisterende enhed (valgfrit)", + "parent_object_optional": "Overordnet objekt (valgfrit)", + "parent_none": "(Intet overordnet)", + "paused": "Sat på pause", + "pause_object": "Sæt på pause", + "resume_object": "Genoptag", + "pause_until_prompt": "Frys objektets planer — intet forfalder og intet notificerer, før det genoptages. Angiv eventuelt en dato for automatisk genoptagelse.", + "pause_until_label": "Genoptag den (valgfrit)", + "object_paused": "Objekt sat på pause", + "object_resumed": "Objekt genoptaget — planer genstartet", + "object_paused_badge": "På pause", + "paused_until_label": "indtil", + "replace_object": "Udskift…", + "replace_object_prompt": "Pensioner dette objekt og opret en efterfølger. Historik og omkostninger forbliver arkiveret på det gamle; opgaver og dokumenter følger med til det nye, tællere starter forfra.", + "replace_name_label": "Efterfølgerens navn", + "object_replaced": "Objekt udskiftet — efterfølger oprettet", + "reading_unit_label": "Aflæsningsenhed (f.eks. kWh, m³)", + "reading_unit_help": "Vises ved siden af den registrerede værdi, når opgaven fuldføres.", + "reading_value_label": "Aflæst værdi", + "reading_label": "Aflæsning", + "settings_templates_label": "Skabelongalleri", + "settings_templates_hint": "Fravælg skabeloner, du aldrig får brug for — de forsvinder fra \"Fra skabelon\"-vælgerne (panel og config flow). Intet andet ændres; kan genaktiveres når som helst.", + "worksheet": "Arbejdsark", + "worksheet_scan_view": "Scan for at åbne opgaven", + "worksheet_scan_complete": "Scan for at fuldføre", + "worksheet_manual_excerpt": "Uddrag af manualen", + "worksheet_pages": "sider", + "worksheet_printed": "Udskrevet", + "worksheet_never": "Aldrig" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/de.json b/custom_components/maintenance_supporter/frontend-src/locales/de.json new file mode 100644 index 0000000..72ec8e6 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/de.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Wartung", + "objects": "Objekte", + "tasks": "Aufgaben", + "overdue": "Überfällig", + "due_soon": "Bald fällig", + "triggered": "Ausgelöst", + "trigger_replaced": "Auslöser ersetzt", + "ok": "OK", + "all": "Alle", + "new_object": "+ Neues Objekt", + "templates_from": "Aus Vorlage", + "templates_title": "Mit einer Vorlage starten", + "templates_task_count": "{n} Aufgaben", + "template_created": "Aus Vorlage erstellt", + "onboard_hint": "Füge dein erstes Objekt hinzu, um mit der Wartung zu starten.", + "edit": "Bearbeiten", + "duplicate": "Duplizieren", + "task_duplicated": "Aufgabe dupliziert", + "object_duplicated": "Objekt dupliziert", + "delete": "Löschen", + "add_task": "+ Aufgabe", + "complete": "Erledigt", + "completed": "Abgeschlossen", + "skip": "Überspringen", + "skipped": "Übersprungen", + "missed": "Verpasst", + "reset": "Zurücksetzen", + "snooze": "Schlummern", + "snoozed": "Schlummert", + "cancel": "Abbrechen", + "bulk_select": "Auswählen", + "bulk_select_all": "Alle auswählen", + "bulk_n_selected": "{n} ausgewählt", + "bulk_completed": "{n} Aufgaben erledigt", + "bulk_archived": "{n} Aufgaben archiviert", + "completing": "Wird erledigt…", + "interval": "Intervall", + "warning": "Vorwarnung", + "last_performed": "Zuletzt durchgeführt", + "next_due": "Nächste Fälligkeit", + "days_until_due": "Tage bis fällig", + "avg_duration": "Ø Dauer", + "trigger": "Trigger", + "trigger_type": "Trigger-Typ", + "threshold_above": "Obergrenze", + "threshold_below": "Untergrenze", + "threshold": "Schwellwert", + "counter": "Zähler", + "state_change": "Zustandsänderung", + "runtime": "Laufzeit", + "runtime_hours": "Ziel-Laufzeit (Stunden)", + "target_value": "Zielwert", + "baseline": "Nulllinie", + "target_changes": "Ziel-Änderungen", + "for_minutes": "Für (Minuten)", + "time_based": "Zeitbasiert", + "sensor_based": "Sensorbasiert", + "manual": "Manuell", + "one_time": "Einmalig", + "weekdays": "Wochentage", + "nth_weekday": "N-ter Wochentag im Monat", + "day_of_month": "Tag im Monat", + "recurrence_on_days": "Wiederholen an", + "recurrence_occurrence": "Vorkommen", + "recurrence_weekday": "Wochentag", + "recurrence_day": "Tag im Monat (1–31)", + "recurrence_last_day": "Letzter Tag des Monats", + "recurrence_business_day": "Nur Werktage (vom Wochenende vorziehen)", + "recurrence_offset": "Versatz (Tage, ±)", + "recurrence_offset_help": "Verschiebt das Datum um ±N Tage, z. B. -2 = zwei Tage davor.", + "last_day_month": "Letzter Tag des Monats", + "last_business_day_month": "Letzter Werktag", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Letzter", + "day_word": "Tag", + "interval_value": "Intervall", + "interval_unit": "Einheit", + "unit_days": "Tage", + "unit_weeks": "Wochen", + "unit_months": "Monate", + "unit_years": "Jahre", + "due_date": "Fälligkeitsdatum", + "cleaning": "Reinigung", + "inspection": "Inspektion", + "replacement": "Austausch", + "calibration": "Kalibrierung", + "service": "Service", + "reading": "Ablesung", + "custom": "Benutzerdefiniert", + "history": "Verlauf", + "cost": "Kosten", + "report_button": "Bericht", + "report_title": "Wartungsbericht", + "report_generated": "Erstellt", + "report_times_done": "Erledigt", + "report_total_cost": "Gesamtkosten", + "report_every": "alle {n} {unit}", + "report_notes": "Notizen", + "report_col_type": "Typ", + "report_col_status": "Status", + "report_col_schedule": "Zeitplan", + "duration": "Dauer", + "both": "Beides", + "trigger_val": "Trigger-Wert", + "complete_title": "Erledigt: ", + "checklist": "Checkliste", + "checklist_steps_optional": "Checkliste-Schritte (optional)", + "checklist_placeholder": "Filter reinigen\nDichtung ersetzen\nDruck testen", + "checklist_help": "Ein Schritt pro Zeile. Max. 100 Einträge.", + "err_too_long": "{field}: zu lang (max. {n} Zeichen)", + "err_too_short": "{field}: zu kurz (min. {n} Zeichen)", + "err_value_too_high": "{field}: zu groß (max. {n})", + "err_value_too_low": "{field}: zu klein (min. {n})", + "err_required": "{field}: Pflichtfeld", + "err_wrong_type": "{field}: falscher Typ (erwartet: {type})", + "err_invalid_choice": "{field}: nicht erlaubter Wert", + "err_invalid_value": "{field}: ungültiger Wert", + "feat_schedule_time": "Uhrzeit-Scheduling", + "feat_schedule_time_desc": "Tasks werden zu einer festen Uhrzeit fällig statt um Mitternacht.", + "schedule_time_optional": "Fällig um (optional, HH:MM)", + "schedule_time_help": "Leer = Mitternacht (Default). HA-Zeitzone.", + "at_time": "um", + "notes_optional": "Notizen (optional)", + "cost_optional": "Kosten (optional)", + "duration_minutes": "Dauer in Minuten (optional)", + "days": "Tage", + "day": "Tag", + "today": "Heute", + "d_overdue": "T überfällig", + "no_tasks": "Keine Wartungsaufgaben vorhanden. Erstellen Sie ein Objekt um zu beginnen.", + "no_tasks_short": "Keine Aufgaben", + "no_history": "Noch keine Verlaufseinträge.", + "show_all": "Alle anzeigen", + "cost_duration_chart": "Kosten & Dauer", + "installed": "Installiert", + "confirm_delete_object": "Dieses Objekt und alle zugehörigen Aufgaben löschen?", + "confirm_delete_task": "Diese Aufgabe wirklich löschen?", + "min": "Min", + "max": "Max", + "save": "Speichern", + "saving": "Speichern…", + "edit_task": "Aufgabe bearbeiten", + "new_task": "Neue Wartungsaufgabe", + "task_name": "Aufgabenname", + "maintenance_type": "Wartungstyp", + "priority": "Priorität", + "labels": "Labels", + "labels_placeholder": "z. B. Sicherheit, Saison, Mieter-sichtbar", + "labels_help": "Kommagetrennte Tags zum Filtern und für Berichte.", + "priority_low": "Niedrig", + "priority_normal": "Normal", + "priority_high": "Hoch", + "schedule_type": "Planungsart", + "interval_days": "Intervall (Tage)", + "warning_days": "Warntage", + "earliest_completion_days": "Frühester Abschluss (Tage vor Fälligkeit)", + "earliest_completion_days_help": "Leer lassen, um jederzeit abschließen zu können. 0 = erst am/nach dem Fälligkeitstag.", + "last_performed_optional": "Zuletzt durchgeführt (optional)", + "interval_anchor": "Intervall-Anker", + "anchor_completion": "Ab Erledigung", + "anchor_planned": "Ab geplantem Datum (kein Drift)", + "edit_object": "Objekt bearbeiten", + "name": "Name", + "manufacturer_optional": "Hersteller (optional)", + "model_optional": "Modell (optional)", + "serial_number_optional": "Seriennummer (optional)", + "serial_number_label": "S/N", + "documentation_url_label": "Handbuch", + "object_notes_label": "Notizen", + "sort_due_date": "Fälligkeit", + "sort_object": "Objekt-Name", + "sort_type": "Typ", + "sort_task_name": "Aufgaben-Name", + "all_objects": "Alle Objekte", + "tasks_lower": "Aufgaben", + "no_tasks_yet": "Noch keine Aufgaben", + "add_first_task": "Erste Aufgabe hinzufügen", + "trigger_configuration": "Trigger-Konfiguration", + "entity_id": "Entitäts-ID", + "comma_separated": "kommagetrennt", + "entity_logic": "Entitäts-Logik", + "entity_logic_any": "Beliebige Entität löst aus", + "entity_logic_all": "Alle Entitäten müssen auslösen", + "entities": "Entitäten", + "attribute_optional": "Attribut (optional, leer = Zustand)", + "use_entity_state": "Entitäts-Zustand verwenden (kein Attribut)", + "trigger_above": "Auslösen wenn über", + "trigger_below": "Auslösen wenn unter", + "for_at_least_minutes": "Für mindestens (Minuten)", + "safety_interval_days": "Sicherheitsintervall (Tage, optional)", + "safety_interval": "Sicherheitsintervall (optional)", + "delta_mode": "Delta-Modus", + "from_state_optional": "Von Zustand (optional)", + "to_state_optional": "Zu Zustand (optional)", + "documentation_url_optional": "Dokumentation URL (optional)", + "object_notes_optional": "Notizen (optional)", + "nfc_tag_id_optional": "NFC-Tag-ID (optional)", + "nfc_tags_empty_help": "Noch keine NFC-Tags in Home Assistant registriert.", + "nfc_tags_open_settings": "Tag-Einstellungen öffnen", + "nfc_tags_refresh": "Aktualisieren", + "environmental_entity_optional": "Umgebungs-Sensor (optional)", + "environmental_entity_helper": "z.B. sensor.aussentemperatur — passt das Intervall an Umgebungswerte an", + "environmental_attribute_optional": "Umgebungs-Attribut (optional)", + "nfc_tag_id": "NFC-Tag-ID", + "nfc_linked": "NFC-Tag verknüpft", + "nfc_link_hint": "Klicken um NFC-Tag zu verknüpfen", + "responsible_user": "Verantwortlicher Benutzer", + "shared_with": "Geteilt mit (Rotation)", + "shared_with_help": "Mehrere Personen auswählen, die sich diese Aufgabe teilen; der/die Verantwortliche wechselt bei jedem Abschluss.", + "rotation_strategy": "Rotation", + "rotation_none": "Keine Rotation", + "rotation_round_robin": "Reihum", + "rotation_least_completed": "Am wenigsten erledigt", + "rotation_random": "Zufällig", + "no_user_assigned": "(Kein Benutzer zugewiesen)", + "all_users": "Alle Benutzer", + "my_tasks": "Meine Aufgaben", + "tab_calendar": "Kalender", + "cal_no_events": "Keine Wartung", + "cal_window_7": "7 Tage", + "cal_window_14": "14 Tage", + "cal_window_30": "30 Tage", + "cal_window_365": "1 Jahr", + "cal_every_n_days": "alle {n} Tage", + "cal_source_time": "Zeit-basiert", + "cal_source_time_adaptive": "Zeit-basiert (adaptiv)", + "cal_source_sensor": "Sensor-basiert", + "cal_predicted": "vorhergesagt", + "cal_confidence_high": "hohe Genauigkeit", + "cal_confidence_medium": "mittlere Genauigkeit", + "cal_confidence_low": "niedrige Genauigkeit", + "budget_monthly": "Monatsbudget", + "budget_yearly": "Jahresbudget", + "groups": "Gruppen", + "new_group": "Neue Gruppe", + "edit_group": "Gruppe bearbeiten", + "no_groups": "Keine Gruppen vorhanden", + "delete_group": "Gruppe löschen", + "delete_group_confirm": "Gruppe '{name}' wirklich löschen?", + "group_select_tasks": "Aufgaben auswählen", + "group_name_required": "Name erforderlich", + "description_optional": "Beschreibung (optional)", + "selected": "Ausgewählt", + "loading_chart": "Daten werden geladen...", + "hide_outliers": "Ausreißer ausblenden (Messfehler)", + "was_maintenance_needed": "War diese Wartung nötig?", + "feedback_needed": "Nötig", + "feedback_not_needed": "Nicht nötig", + "feedback_not_sure": "Unsicher", + "suggested_interval": "Empfohlenes Intervall", + "apply_suggestion": "Übernehmen", + "reanalyze": "Neu analysieren", + "reanalyze_result": "Neue Analyse", + "reanalyze_insufficient_data": "Nicht genügend Daten für eine Empfehlung", + "data_points": "Datenpunkte", + "dismiss_suggestion": "Verwerfen", + "confidence_low": "Niedrig", + "confidence_medium": "Mittel", + "confidence_high": "Hoch", + "recommended": "empfohlen", + "seasonal_awareness": "Saisonale Anpassung", + "edit_seasonal_overrides": "Saison-Faktoren bearbeiten", + "seasonal_overrides_title": "Saisonale Faktoren (Override)", + "seasonal_overrides_hint": "Faktor pro Monat (0.1–5.0). Leer = automatisch gelernt.", + "seasonal_override_invalid": "Ungültiger Wert", + "seasonal_override_range": "Faktor muss zwischen 0.1 und 5.0 liegen", + "clear_all": "Alle zurücksetzen", + "seasonal_chart_title": "Saisonale Faktoren", + "seasonal_learned": "Gelernt", + "seasonal_manual": "Manuell", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mär", + "month_apr": "Apr", + "month_may": "Mai", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dez", + "sensor_prediction": "Sensorvorhersage", + "degradation_trend": "Trend", + "trend_rising": "Steigend", + "trend_falling": "Fallend", + "trend_stable": "Stabil", + "trend_insufficient_data": "Unzureichende Daten", + "days_until_threshold": "Tage bis Schwellwert", + "threshold_exceeded": "Schwellwert überschritten", + "environmental_adjustment": "Umgebungsfaktor", + "sensor_prediction_urgency": "Sensor prognostiziert Schwellwert in ~{days} Tagen", + "day_short": "Tag", + "weibull_reliability_curve": "Zuverlässigkeitskurve", + "weibull_failure_probability": "Ausfallwahrscheinlichkeit", + "weibull_r_squared": "Güte R²", + "beta_early_failures": "Frühausfälle", + "beta_random_failures": "Zufällige Ausfälle", + "beta_wear_out": "Verschleiß", + "beta_highly_predictable": "Hochvorhersagbar", + "confidence_interval": "Konfidenzintervall", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisch", + "current_interval_marker": "Aktuelles Intervall", + "recommended_marker": "Empfohlen", + "characteristic_life": "Charakteristische Lebensdauer", + "chart_mini_sparkline": "Trend-Sparkline", + "chart_history": "Kosten- und Dauer-Verlauf", + "chart_seasonal": "Saisonfaktoren, 12 Monate", + "chart_weibull": "Weibull-Zuverlässigkeitskurve", + "chart_sparkline": "Sensor-Triggerwert-Verlauf", + "days_progress": "Tagesfortschritt", + "qr_code": "QR-Code", + "qr_generating": "QR-Code wird generiert…", + "qr_error": "QR-Code konnte nicht generiert werden.", + "qr_error_no_url": "Keine HA-URL konfiguriert. Bitte unter Einstellungen → System → Netzwerk eine externe oder interne URL setzen.", + "save_error": "Fehler beim Speichern. Bitte erneut versuchen.", + "qr_print": "Drucken", + "qr_download": "SVG herunterladen", + "qr_action": "Aktion beim Scannen", + "qr_action_view": "Wartungsinfo anzeigen", + "qr_action_complete": "Wartung als erledigt markieren", + "qr_url_mode": "Link-Typ", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Übersicht", + "analysis": "Analyse", + "recent_activities": "Letzte Aktivitäten", + "search_notes": "Notizen durchsuchen", + "avg_cost": "Ø Kosten", + "no_advanced_features": "Keine erweiterten Funktionen aktiviert", + "no_advanced_features_hint": "Aktiviere „Adaptive Intervalle“ oder „Saisonale Muster“ in den Integrationseinstellungen, um hier Analysedaten zu sehen.", + "analysis_not_enough_data": "Noch nicht genügend Daten für die Analyse vorhanden.", + "analysis_not_enough_data_hint": "Die Weibull-Analyse benötigt mindestens 5 abgeschlossene Wartungen, saisonale Muster werden nach 6+ Datenpunkten pro Monat sichtbar.", + "analysis_manual_task_hint": "Manuelle Aufgaben ohne Intervall erzeugen keine Analysedaten.", + "completions": "Abschlüsse", + "current": "Aktuell", + "shorter": "Kürzer", + "longer": "Länger", + "normal": "Normal", + "disabled": "Deaktiviert", + "compound_logic": "Verknüpfungslogik", + "compound": "Verknüpft (mehrere Bedingungen)", + "compound_logic_and": "UND — alle Bedingungen müssen auslösen", + "compound_logic_or": "ODER — eine Bedingung genügt", + "compound_help": "Mehrere Sensor-Bedingungen zu einem Auslöser kombinieren.", + "compound_no_conditions": "Noch keine Bedingungen — mindestens eine hinzufügen.", + "compound_add_condition": "Bedingung hinzufügen", + "compound_condition": "Bedingung", + "compound_remove_condition": "Bedingung entfernen", + "card_title": "Titel", + "card_show_header": "Kopfzeile mit Statistiken anzeigen", + "card_show_actions": "Aktionsbuttons anzeigen", + "card_compact": "Kompaktmodus", + "card_max_items": "Max. Einträge (0 = alle)", + "card_filter_status": "Nach Status filtern", + "card_filter_status_help": "Leer = alle Status zeigen.", + "card_filter_objects": "Nach Objekten filtern", + "card_filter_objects_help": "Leer = alle Objekte zeigen.", + "card_filter_entities": "Nach Entitäten filtern (entity_ids)", + "card_filter_entities_help": "Wähle Sensor-/Binary-Sensor-Entitäten dieser Integration. Leer = alle.", + "card_loading_objects": "Lade Objekte…", + "card_load_error": "Objekte konnten nicht geladen werden — bitte WS-Verbindung prüfen.", + "card_no_tasks_title": "Noch keine Wartungsaufgaben", + "card_no_tasks_cta": "→ Im Maintenance-Panel anlegen", + "no_objects": "Keine Objekte vorhanden.", + "action_error": "Aktion fehlgeschlagen. Bitte erneut versuchen.", + "area_id_optional": "Bereich (optional)", + "installation_date_optional": "Installationsdatum (optional)", + "warranty_expiry_optional": "Garantie-Ablauf (optional)", + "warranty": "Garantie", + "warranty_valid_until": "gültig bis {date}", + "warranty_expires_in": "läuft in {days} Tagen ab", + "warranty_expired": "abgelaufen", + "cal_past_windows": "Vergangene Fenster", + "cal_forward_windows": "Zukünftige Fenster", + "history_edit_title": "Verlaufseintrag bearbeiten", + "history_edit_timestamp": "Zeitstempel", + "manufacturer": "Hersteller", + "model": "Modell", + "area": "Bereich", + "actions": "Aktionen", + "view_mode_label": "Ansicht", + "view_cards": "Kartenansicht", + "view_table": "Tabellenansicht", + "objects_table_columns_label": "Spalten der Objekt-Tabelle", + "objects_table_columns_hint": "Wähle die Spalten für die Tabellenansicht der Objektübersicht.", + "custom_icon_optional": "Icon (optional, z.B. mdi:wrench)", + "task_enabled": "Aufgabe aktiviert", + "skip_reason_prompt": "Aufgabe überspringen?", + "reason_optional": "Grund (optional)", + "reset_date_prompt": "Aufgabe als ausgeführt markieren?", + "reset_date_optional": "Letztes Erledigungs-Datum (optional, Standard: heute)", + "notes_label": "Notizen", + "documentation_label": "Dokumentation", + "no_nfc_tag": "— Kein Tag —", + "dashboard": "Dashboard", + "tab_today": "Heute", + "palette_placeholder": "Objekte und Aufgaben suchen…", + "palette_no_results": "Keine Treffer", + "palette_hint": "↑↓ Navigieren · Enter Öffnen · Esc Schließen", + "today_all_caught_up": "Alles erledigt! Diese Woche steht nichts an.", + "today_overdue": "Überfällig", + "today_due_today": "Heute fällig", + "today_this_week": "Diese Woche", + "settings": "Einstellungen", + "settings_features": "Erweiterte Funktionen", + "settings_features_desc": "Erweiterte Funktionen ein- oder ausschalten. Deaktivieren blendet sie in der Oberfläche aus, löscht aber keine Daten.", + "feat_adaptive": "Adaptive Intervalle", + "feat_adaptive_desc": "Optimale Intervalle aus Wartungshistorie lernen", + "feat_predictions": "Sensorvorhersagen", + "feat_predictions_desc": "Trigger-Datum anhand von Sensordegradation vorhersagen", + "feat_seasonal": "Saisonale Anpassungen", + "feat_seasonal_desc": "Intervalle basierend auf saisonalen Mustern anpassen", + "feat_environmental": "Umgebungskorrelation", + "feat_environmental_desc": "Intervalle mit Temperatur/Luftfeuchtigkeit korrelieren", + "feat_budget": "Budgetverfolgung", + "feat_budget_desc": "Monatliche und jährliche Wartungsausgaben verfolgen", + "feat_groups": "Aufgabengruppen", + "feat_groups_desc": "Aufgaben in logische Gruppen organisieren", + "feat_checklists": "Checklisten", + "feat_checklists_desc": "Mehrstufige Verfahren zur Aufgabenerlediung", + "settings_general": "Allgemein", + "settings_default_warning": "Standard-Warntage", + "settings_panel_enabled": "Seitenleisten-Panel", + "settings_panel_title": "Panel-Titel", + "settings_notifications": "Benachrichtigungen", + "settings_notify_service": "Benachrichtigungsdienst", + "test_notification": "Test-Benachrichtigung", + "send_test": "Test senden", + "testing": "Sende…", + "test_notification_success": "Test-Benachrichtigung gesendet", + "test_notification_failed": "Test-Benachrichtigung fehlgeschlagen", + "settings_notify_due_soon": "Bei baldiger Fälligkeit benachrichtigen", + "settings_notify_overdue": "Bei Überfälligkeit benachrichtigen", + "settings_notify_triggered": "Bei Auslösung benachrichtigen", + "settings_interval_hours": "Wiederholungsintervall (Stunden, 0 = einmalig)", + "settings_quiet_hours": "Ruhezeiten", + "settings_quiet_start": "Beginn", + "settings_quiet_end": "Ende", + "settings_max_per_day": "Max. Benachrichtigungen pro Tag (0 = unbegrenzt)", + "settings_bundling": "Benachrichtigungen bündeln", + "settings_bundle_threshold": "Bündelungsschwelle", + "settings_reminder_leads": "Zusätzliche Erinnerungen (Tage vor Fälligkeit)", + "settings_reminder_leads_hint": "Kommagetrennte Vorlaufzeiten, z. B. 14, 3, 0 — am jeweiligen Tag wird eine zusätzliche Erinnerung gesendet. Leer = aus.", + "settings_actions": "Mobile Aktionsbuttons", + "settings_action_complete": "\"Erledigt\"-Button anzeigen", + "settings_action_skip": "\"Überspringen\"-Button anzeigen", + "settings_action_snooze": "\"Schlummern\"-Button anzeigen", + "settings_weekly_digest": "Wöchentliche Zusammenfassung", + "settings_weekly_digest_hint": "Eine Zusammenfassungs-Benachrichtigung am Montagmorgen, wenn Aufgaben anstehen.", + "settings_warranty_reminder": "Garantie-Ablauf-Erinnerung", + "settings_warranty_reminder_days": "Tage vor Ablauf", + "settings_warranty_reminder_hint": "Einmal benachrichtigen, wenn die Garantie eines Objekts so viele Tage vor Ablauf steht.", + "settings_snooze_hours": "Schlummerdauer (Stunden)", + "settings_budget": "Budget", + "settings_currency": "Währung", + "settings_budget_monthly": "Monatsbudget", + "settings_budget_yearly": "Jahresbudget", + "settings_budget_alerts": "Budget-Warnungen", + "settings_budget_threshold": "Warnschwelle (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "JSON exportieren", + "settings_export_yaml": "YAML exportieren", + "settings_export_csv": "CSV exportieren", + "settings_import_csv": "CSV importieren", + "settings_import_placeholder": "JSON- oder CSV-Inhalt hier einfügen…", + "settings_import_btn": "Importieren", + "settings_import_success": "{count} Objekte erfolgreich importiert.", + "settings_export_success": "Export heruntergeladen.", + "settings_saved": "Einstellung gespeichert.", + "settings_include_history": "Verlauf einbeziehen", + "sort_alphabetical": "Alphabetisch", + "sort_due_soonest": "Frühestens fällig", + "sort_task_count": "Aufgaben-Anzahl", + "sort_area": "Bereich", + "sort_assigned_user": "Verantwortlicher", + "sort_group": "Gruppe", + "groupby_none": "Keine Gruppierung", + "groupby_area": "Nach Bereich", + "groupby_group": "Nach Gruppe", + "groupby_user": "Nach Verantwortlichem", + "filter_label": "Filter", + "user_label": "Benutzer", + "sort_label": "Sortierung", + "group_by_label": "Gruppieren nach", + "state_value_help": "Verwende den HA-Zustandswert (meist kleingeschrieben, z. B. \"on\"/\"off\"). Groß-/Kleinschreibung wird beim Speichern normalisiert.", + "target_changes_help": "Anzahl der passenden Übergänge, nach denen der Trigger auslöst (Standard: 1).", + "qr_print_title": "QR-Codes drucken", + "qr_print_desc": "Erzeuge eine Druckseite mit QR-Codes zum Ausschneiden und Anbringen an den Geräten.", + "qr_print_load": "Objekte laden", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekte", + "qr_print_actions": "Aktionen", + "qr_print_url_mode": "Link-Typ", + "qr_print_estimate": "Geschätzte QR-Codes", + "qr_print_over_limit": "Obergrenze ist 200, bitte Filter eingrenzen", + "qr_print_generate": "QR-Codes erzeugen", + "qr_print_generating": "Erzeuge…", + "qr_print_ready": "QR-Codes bereit", + "qr_print_print_button": "Drucken", + "qr_print_empty": "Keine QR-Codes zu erzeugen", + "qr_action_skip": "Überspringen", + "vacation_title": "Urlaubsmodus", + "vacation_active": "aktiv", + "vacation_ended": "beendet", + "vacation_desc": "Plane deinen Urlaub: Benachrichtigungen werden während des Zeitraums plus Puffer-Tagen pausiert. Du kannst pro Aufgabe Ausnahmen festlegen.", + "vacation_enable": "Urlaubsmodus aktivieren", + "vacation_start": "Beginn", + "vacation_end": "Ende", + "vacation_buffer": "Puffer (Tage)", + "vacation_exempt_title": "Trotz Urlaubsmodus benachrichtigen", + "vacation_exempt_desc": "Wähle Aufgaben aus, für die auch im Urlaub Benachrichtigungen kommen sollen (z. B. kritische Pool-Chemie).", + "vacation_load_tasks": "Aufgaben laden", + "vacation_preview_btn": "Vorschau anzeigen", + "vacation_preview_affected": "Aufgaben betroffen", + "vacation_event_due_soon": "wird bald fällig", + "vacation_event_overdue": "wird überfällig", + "vacation_event_triggered_est": "Sensor-Trigger möglich", + "vacation_sensor_based": "(sensorbasiert)", + "vacation_action_notify": "Trotzdem benachrichtigen", + "vacation_action_unsilence": "Wieder stummschalten", + "vacation_marked_complete": "Als erledigt markiert", + "vacation_marked_skip": "Übersprungen", + "vacation_end_now": "Urlaub jetzt beenden", + "add": "Hinzufügen", + "show_stats": "Statistiken + Diagramme", + "hide_stats": "Statistiken ausblenden", + "adaptive_no_data": "Noch nicht genug Completion-Historie für die adaptive Auswertung. Schließe diese Aufgabe ein paar Mal mehr ab, um Intervall-Empfehlungen und Zuverlässigkeitskurven freizuschalten.", + "suggestion_applied": "Vorgeschlagenes Intervall übernommen", + "vacation_mode": "Urlaubsmodus", + "vacation_status_active": "Aktiv", + "vacation_status_scheduled": "Geplant", + "vacation_status_inactive": "Inaktiv", + "vacation_end_now_confirm": "Urlaub sofort beenden?", + "vacation_exempt_count": "ausgenommen", + "vacation_advanced": "Erweitert…", + "vacation_open_panel": "Im Panel öffnen", + "enable": "Aktivieren", + "saved": "Gespeichert", + "budget_monthly_set": "Monatsbudget setzen", + "budget_yearly_set": "Jahresbudget setzen", + "budget_advanced": "Währung, Alarme…", + "budget_open_panel": "Im Panel öffnen", + "groups_empty": "Keine Gruppen vorhanden.", + "group_new_placeholder": "Gruppe hinzufügen…", + "group_delete_confirm": "Gruppe \"{name}\" löschen?", + "groups_manage_tasks": "Aufgaben-Zuordnungen verwalten…", + "groups_open_panel": "Im Panel öffnen", + "unassigned": "Nicht zugewiesen", + "no_area": "Kein Bereich", + "has_overdue": "Überfällige Aufgaben", + "object": "Objekt", + "settings_panel_access": "Panel-Zugriff", + "settings_panel_access_desc": "Admins haben immer Vollzugriff. Um Erstellen, Bearbeiten und Löschen an bestimmte Non-Admins zu delegieren, aktiviere dies und wähle sie unten aus — alle anderen sehen nur Abhaken/Überspringen.", + "settings_operator_write": "Ausgewählten Nutzern Erstellen, Bearbeiten & Löschen erlauben", + "settings_operator_write_desc": "Aus: nur Admins können Inhalte ändern. Ein: die ausgewählten Nutzer unten erhalten ebenfalls Vollzugriff.", + "no_non_admin_users": "Keine Non-Admin-User gefunden. Lege welche unter Einstellungen → Personen an.", + "owner_label": "Owner", + "feat_completion_actions": "Completion-Actions", + "feat_completion_actions_desc": "Pro Aufgabe HA-Action beim Abschluss konfigurieren + Quick-Complete-QR mit voreingestellten Werten.", + "on_complete_action_title": "Beim Abschluss: HA-Action auslösen (optional)", + "on_complete_action_desc": "Ruft beim Erledigen der Aufgabe einen HA-Service auf — z. B. einen Zähler am Gerät zurücksetzen.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Ziel-Entität", + "on_complete_action_target_hint": "Achtung: Domain der Entität muss zum Service passen — z. B. 'button.press' nur für button.*, 'counter.increment' nur für counter.*, 'input_button.press' nur für input_button.* etc. Bei Mismatch fired die Aktion nicht (HA loggt 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Daten (JSON, optional)", + "on_complete_action_test": "Konfiguration prüfen", + "on_complete_action_test_success": "✓ Konfiguration gültig (Aktion wird erst beim Abschluss ausgeführt)", + "on_complete_action_test_failed": "Konfigurationsfehler", + "quick_complete_defaults_title": "Schnell-Abschluss-Standardwerte (für QR-Scans, optional)", + "quick_complete_defaults_desc": "Voreingestellte Werte für Schnell-Abschluss-QR. Ohne Werte öffnet der QR den Abschluss-Dialog.", + "quick_complete_defaults_notes": "Notizen", + "quick_complete_defaults_cost": "Kosten", + "quick_complete_defaults_duration": "Dauer (Minuten)", + "quick_complete_defaults_feedback_none": "Kein Feedback", + "quick_complete_defaults_feedback_needed": "War notwendig", + "quick_complete_defaults_feedback_not_needed": "War nicht notwendig", + "quick_complete_success": "Schnell als erledigt markiert", + "show_all_objects": "Alle Objekte anzeigen", + "show_all_tasks": "Filter zurücksetzen — alle Aufgaben anzeigen", + "filter_to_overdue": "Auf überfällige Aufgaben filtern", + "filter_to_due_soon": "Auf bald fällige Aufgaben filtern", + "filter_to_triggered": "Auf ausgelöste Aufgaben filtern", + "open_task": "Aufgabe öffnen", + "show_details": "Verlauf + Statistik anzeigen", + "hide_details": "Details ausblenden", + "history_empty": "Noch keine Einträge.", + "history_edit_button": "Eintrag bearbeiten", + "total_cost": "Gesamtkosten", + "times_performed": "Erledigt", + "older_entries": "ältere", + "open_in_panel": "Im Wartungspanel öffnen", + "skip_reason": "Übersprungen-Grund (optional)", + "reset_to_date": "last_performed setzen auf", + "delete_task_confirm": "Diese Aufgabe und ihren Verlauf löschen?", + "delete_object_confirm": "Dieses Objekt und alle seine Aufgaben löschen?", + "loading": "Laden…", + "archive": "Archivieren", + "undo": "Rückgängig", + "task_archived": "Aufgabe archiviert", + "object_archived": "Objekt archiviert", + "unarchive": "Wiederherstellen", + "archived": "Archiviert", + "show_archived": "Archivierte anzeigen", + "hide_archived": "Archivierte ausblenden", + "confirm_archive_object": "Dieses Objekt und seine Aufgaben archivieren? Verlauf bleibt erhalten, kann später wiederhergestellt werden.", + "settings_archive": "Archiv & Aufbewahrung", + "settings_archive_desc": "Erledigte einmalige Aufgaben archivieren statt löschen. Archivierte Einträge sind ausgeblendet und inaktiv, behalten aber Verlauf und Kosten.", + "settings_archive_oneoff_days": "Erledigte einmalige Aufgaben automatisch archivieren nach (Tage, 0 = aus)", + "settings_delete_archived_oneoff_days": "Archivierte einmalige Aufgaben automatisch löschen nach (Tage, 0 = nie)", + "archive_object": "Objekt archivieren", + "unarchive_object": "Objekt wiederherstellen", + "documents": "Dokumente", + "documents_empty": "Noch keine Dokumente.", + "doc_upload": "Datei hochladen", + "doc_uploading": "Wird hochgeladen…", + "doc_add_link": "Link hinzufügen", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (optional)", + "doc_open": "Öffnen", + "doc_delete_confirm": "„{name}“ löschen?", + "doc_too_large": "Datei ist zu groß (max. 25 MB).", + "doc_upload_failed": "Upload fehlgeschlagen.", + "completion_photo_optional": "Abschlussfoto (optional)", + "add_photo": "Foto hinzufügen", + "uploading": "Wird hochgeladen…", + "remove": "Entfernen", + "doc_deduped": "Bereits anderswo gespeichert — geteilt, kein zusätzlicher Speicher.", + "doc_dup_in_object": "Diese Datei ist bereits an diesem Objekt angehängt.", + "doc_link_invalid": "Nur http/https-Links sind erlaubt.", + "doc_cat_manual": "Anleitung", + "doc_cat_warranty": "Garantie", + "doc_cat_invoice": "Rechnung", + "doc_cat_spare_parts": "Ersatzteile", + "doc_cat_photo": "Foto", + "doc_cat_other": "Sonstiges", + "doc_link_badge": "Link", + "doc_storage_title": "Dokumentenspeicher", + "doc_storage_saved": "Durch Deduplizierung gespart", + "doc_storage_refresh": "Aktualisieren", + "doc_download": "Herunterladen", + "doc_close": "Schließen", + "doc_camera": "Foto aufnehmen", + "doc_drop_hint": "Dateien hier ablegen", + "doc_task_none": "Keine Dokumente mit dieser Aufgabe verknüpft.", + "doc_link_existing": "Dokument verknüpfen…", + "doc_attach": "Verknüpfen", + "doc_unlink": "Verknüpfung lösen", + "doc_page": "Seite", + "chart_range_7d": "7T", + "chart_range_30d": "30T", + "chart_range_90d": "90T", + "chart_range_1y": "1J", + "chart_since_service": "seit letzter Wartung", + "chart_no_stats": "Keine Langzeit-Statistik für diese Entity — es werden nur Werte aus Wartungseinträgen angezeigt", + "auto_complete_on_recovery": "Bei Sensor-Erholung automatisch abschließen", + "auto_complete_on_recovery_help": "Verbucht eine Wartung (setzt „zuletzt durchgeführt“), sobald sich der Auslöser von selbst auflöst — z. B. Salz nachgefüllt, Filter getauscht.", + "doc_search": "Dokumente durchsuchen…", + "doc_search_none": "Keine passenden Dokumente", + "link_device_optional": "Mit bestehendem Gerät verknüpfen (optional)", + "parent_object_optional": "Übergeordnetes Objekt (optional)", + "parent_none": "(Kein übergeordnetes Objekt)", + "paused": "Pausiert", + "pause_object": "Pausieren", + "resume_object": "Fortsetzen", + "pause_until_prompt": "Zeitpläne dieses Objekts einfrieren — nichts wird fällig und nichts benachrichtigt, bis es fortgesetzt wird. Optional ein Datum für die automatische Fortsetzung festlegen.", + "pause_until_label": "Fortsetzen am (optional)", + "object_paused": "Objekt pausiert", + "object_resumed": "Objekt fortgesetzt — Zeitpläne neu gestartet", + "object_paused_badge": "Pausiert", + "paused_until_label": "bis", + "replace_object": "Ersetzen…", + "replace_object_prompt": "Dieses Objekt ausmustern und einen Nachfolger anlegen. Historie und Kosten bleiben am alten Objekt archiviert; Aufgaben und Dokumente wandern zum neuen, Zähler starten frisch.", + "replace_name_label": "Name des Nachfolgers", + "object_replaced": "Objekt ersetzt — Nachfolger angelegt", + "reading_unit_label": "Ableseeinheit (z. B. kWh, m³)", + "reading_unit_help": "Wird beim Erledigen dieser Aufgabe neben dem erfassten Wert angezeigt.", + "reading_value_label": "Ablesewert", + "reading_label": "Ablesung", + "settings_templates_label": "Vorlagen-Galerie", + "settings_templates_hint": "Vorlagen abwählen, die du nie brauchst — sie verschwinden aus den \"Aus Vorlage\"-Auswahlen (Panel und Config-Flow). Sonst ändert sich nichts; jederzeit wieder aktivierbar.", + "worksheet": "Arbeitsblatt", + "worksheet_scan_view": "Scannen, um die Aufgabe zu öffnen", + "worksheet_scan_complete": "Scannen zum Erledigen", + "worksheet_manual_excerpt": "Handbuch-Auszug", + "worksheet_pages": "Seiten", + "worksheet_printed": "Gedruckt", + "worksheet_never": "Nie" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/en.json b/custom_components/maintenance_supporter/frontend-src/locales/en.json new file mode 100644 index 0000000..2b373cb --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/en.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Maintenance", + "objects": "Objects", + "tasks": "Tasks", + "overdue": "Overdue", + "due_soon": "Due Soon", + "triggered": "Triggered", + "trigger_replaced": "Trigger replaced", + "ok": "OK", + "all": "All", + "new_object": "+ New Object", + "templates_from": "From template", + "templates_title": "Start from a template", + "templates_task_count": "{n} tasks", + "template_created": "Created from template", + "onboard_hint": "Add your first object to start tracking maintenance.", + "edit": "Edit", + "duplicate": "Duplicate", + "task_duplicated": "Task duplicated", + "object_duplicated": "Object duplicated", + "delete": "Delete", + "add_task": "+ Add Task", + "complete": "Complete", + "completed": "Completed", + "skip": "Skip", + "skipped": "Skipped", + "missed": "Missed", + "reset": "Reset", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Cancel", + "bulk_select": "Select", + "bulk_select_all": "Select all", + "bulk_n_selected": "{n} selected", + "bulk_completed": "{n} tasks completed", + "bulk_archived": "{n} tasks archived", + "completing": "Completing…", + "interval": "Interval", + "warning": "Warning", + "last_performed": "Last performed", + "next_due": "Next due", + "days_until_due": "Days until due", + "avg_duration": "Avg duration", + "trigger": "Trigger", + "trigger_type": "Trigger type", + "threshold_above": "Upper limit", + "threshold_below": "Lower limit", + "threshold": "Threshold", + "counter": "Counter", + "state_change": "State change", + "runtime": "Runtime", + "runtime_hours": "Target runtime (hours)", + "target_value": "Target value", + "baseline": "Baseline", + "target_changes": "Target changes", + "for_minutes": "For (minutes)", + "time_based": "Time-based", + "sensor_based": "Sensor-based", + "manual": "Manual", + "one_time": "One-time", + "weekdays": "Weekdays", + "nth_weekday": "Nth weekday of month", + "day_of_month": "Day of month", + "recurrence_on_days": "Repeat on", + "recurrence_occurrence": "Occurrence", + "recurrence_weekday": "Weekday", + "recurrence_day": "Day of month (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1st", + "ord_2": "2nd", + "ord_3": "3rd", + "ord_4": "4th", + "ord_5": "5th", + "ord_last": "Last", + "day_word": "Day", + "interval_value": "Interval", + "interval_unit": "Unit", + "unit_days": "Days", + "unit_weeks": "Weeks", + "unit_months": "Months", + "unit_years": "Years", + "due_date": "Due date", + "cleaning": "Cleaning", + "inspection": "Inspection", + "replacement": "Replacement", + "calibration": "Calibration", + "service": "Service", + "reading": "Reading", + "custom": "Custom", + "history": "History", + "cost": "Cost", + "report_button": "Report", + "report_title": "Maintenance report", + "report_generated": "Generated", + "report_times_done": "Done", + "report_total_cost": "Total cost", + "report_every": "every {n} {unit}", + "report_notes": "Notes", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Schedule", + "duration": "Duration", + "both": "Both", + "trigger_val": "Trigger value", + "complete_title": "Complete: ", + "checklist": "Checklist", + "checklist_steps_optional": "Checklist steps (optional)", + "checklist_placeholder": "Clean filter\nReplace seal\nTest pressure", + "checklist_help": "One step per line. Max 100 items.", + "err_too_long": "{field}: too long (max {n} characters)", + "err_too_short": "{field}: too short (min {n} characters)", + "err_value_too_high": "{field}: too large (max {n})", + "err_value_too_low": "{field}: too small (min {n})", + "err_required": "{field}: required", + "err_wrong_type": "{field}: wrong type (expected: {type})", + "err_invalid_choice": "{field}: not an allowed value", + "err_invalid_value": "{field}: invalid value", + "feat_schedule_time": "Time-of-day scheduling", + "feat_schedule_time_desc": "Tasks become overdue at a specific time of day instead of midnight.", + "schedule_time_optional": "Due at time (optional, HH:MM)", + "schedule_time_help": "Empty = midnight (default). HA timezone.", + "at_time": "at", + "notes_optional": "Notes (optional)", + "cost_optional": "Cost (optional)", + "duration_minutes": "Duration in minutes (optional)", + "days": "days", + "day": "day", + "today": "Today", + "d_overdue": "d overdue", + "no_tasks": "No maintenance tasks yet. Create an object to get started.", + "no_tasks_short": "No tasks", + "no_history": "No history entries yet.", + "show_all": "Show all", + "cost_duration_chart": "Cost & Duration", + "installed": "Installed", + "confirm_delete_object": "Delete this object and all its tasks?", + "confirm_delete_task": "Delete this task?", + "min": "Min", + "max": "Max", + "save": "Save", + "saving": "Saving…", + "edit_task": "Edit Task", + "new_task": "New Maintenance Task", + "task_name": "Task name", + "maintenance_type": "Maintenance type", + "priority": "Priority", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Low", + "priority_normal": "Normal", + "priority_high": "High", + "schedule_type": "Schedule type", + "interval_days": "Interval (days)", + "warning_days": "Warning days", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Last performed (optional)", + "interval_anchor": "Interval anchor", + "anchor_completion": "From completion date", + "anchor_planned": "From planned date (no drift)", + "edit_object": "Edit Object", + "name": "Name", + "manufacturer_optional": "Manufacturer (optional)", + "model_optional": "Model (optional)", + "serial_number_optional": "Serial number (optional)", + "serial_number_label": "S/N", + "documentation_url_label": "Manual", + "object_notes_label": "Notes", + "sort_due_date": "Due date", + "sort_object": "Object name", + "sort_type": "Type", + "sort_task_name": "Task name", + "all_objects": "All objects", + "tasks_lower": "tasks", + "no_tasks_yet": "No tasks yet", + "add_first_task": "Add first task", + "trigger_configuration": "Trigger Configuration", + "entity_id": "Entity ID", + "comma_separated": "comma-separated", + "entity_logic": "Entity logic", + "entity_logic_any": "Any entity triggers", + "entity_logic_all": "All entities must trigger", + "entities": "entities", + "attribute_optional": "Attribute (optional, blank = state)", + "use_entity_state": "Use entity state (no attribute)", + "trigger_above": "Trigger above", + "trigger_below": "Trigger below", + "for_at_least_minutes": "For at least (minutes)", + "safety_interval_days": "Safety interval (days, optional)", + "safety_interval": "Safety interval (optional)", + "delta_mode": "Delta mode", + "from_state_optional": "From state (optional)", + "to_state_optional": "To state (optional)", + "documentation_url_optional": "Documentation URL (optional)", + "object_notes_optional": "Notes (optional)", + "nfc_tag_id_optional": "NFC Tag ID (optional)", + "nfc_tags_empty_help": "No NFC tags registered in Home Assistant yet.", + "nfc_tags_open_settings": "Open Tags settings", + "nfc_tags_refresh": "Refresh", + "environmental_entity_optional": "Environmental sensor (optional)", + "environmental_entity_helper": "e.g. sensor.outdoor_temperature — adjusts the interval based on environmental conditions", + "environmental_attribute_optional": "Environmental attribute (optional)", + "nfc_tag_id": "NFC Tag ID", + "nfc_linked": "NFC tag linked", + "nfc_link_hint": "Click to link NFC tag", + "responsible_user": "Responsible User", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(No user assigned)", + "all_users": "All Users", + "my_tasks": "My Tasks", + "tab_calendar": "Calendar", + "cal_no_events": "No maintenance", + "cal_window_7": "7 days", + "cal_window_14": "14 days", + "cal_window_30": "30 days", + "cal_window_365": "1 year", + "cal_every_n_days": "every {n} days", + "cal_source_time": "Time-based", + "cal_source_time_adaptive": "Time-based (adaptive)", + "cal_source_sensor": "Sensor-based", + "cal_predicted": "predicted", + "cal_confidence_high": "high confidence", + "cal_confidence_medium": "medium confidence", + "cal_confidence_low": "low confidence", + "budget_monthly": "Monthly budget", + "budget_yearly": "Yearly budget", + "groups": "Groups", + "new_group": "New group", + "edit_group": "Edit group", + "no_groups": "No groups yet", + "delete_group": "Delete group", + "delete_group_confirm": "Delete group '{name}'?", + "group_select_tasks": "Select tasks", + "group_name_required": "Name is required", + "description_optional": "Description (optional)", + "selected": "Selected", + "loading_chart": "Loading chart data...", + "hide_outliers": "Hide outliers (sensor glitches)", + "was_maintenance_needed": "Was this maintenance needed?", + "feedback_needed": "Needed", + "feedback_not_needed": "Not needed", + "feedback_not_sure": "Not sure", + "suggested_interval": "Suggested interval", + "apply_suggestion": "Apply", + "reanalyze": "Re-analyze", + "reanalyze_result": "New analysis", + "reanalyze_insufficient_data": "Not enough data to produce a recommendation", + "data_points": "data points", + "dismiss_suggestion": "Dismiss", + "confidence_low": "Low", + "confidence_medium": "Medium", + "confidence_high": "High", + "recommended": "recommended", + "seasonal_awareness": "Seasonal Awareness", + "edit_seasonal_overrides": "Edit seasonal factors", + "seasonal_overrides_title": "Seasonal factors (override)", + "seasonal_overrides_hint": "Factor per month (0.1–5.0). Empty = learned automatically.", + "seasonal_override_invalid": "Invalid value", + "seasonal_override_range": "Factor must be between 0.1 and 5.0", + "clear_all": "Clear all", + "seasonal_chart_title": "Seasonal Factors", + "seasonal_learned": "Learned", + "seasonal_manual": "Manual", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "May", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Oct", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensor Prediction", + "degradation_trend": "Trend", + "trend_rising": "Rising", + "trend_falling": "Falling", + "trend_stable": "Stable", + "trend_insufficient_data": "Insufficient data", + "days_until_threshold": "Days until threshold", + "threshold_exceeded": "Threshold exceeded", + "environmental_adjustment": "Environmental factor", + "sensor_prediction_urgency": "Sensor predicts threshold in ~{days} days", + "day_short": "day", + "weibull_reliability_curve": "Reliability Curve", + "weibull_failure_probability": "Failure Probability", + "weibull_r_squared": "Fit R²", + "beta_early_failures": "Early Failures", + "beta_random_failures": "Random Failures", + "beta_wear_out": "Wear-out", + "beta_highly_predictable": "Highly Predictable", + "confidence_interval": "Confidence Interval", + "confidence_conservative": "Conservative", + "confidence_aggressive": "Optimistic", + "current_interval_marker": "Current interval", + "recommended_marker": "Recommended", + "characteristic_life": "Characteristic life", + "chart_mini_sparkline": "Trend sparkline", + "chart_history": "Cost and duration history", + "chart_seasonal": "Seasonal factors, 12 months", + "chart_weibull": "Weibull reliability curve", + "chart_sparkline": "Sensor trigger value chart", + "days_progress": "Days progress", + "qr_code": "QR Code", + "qr_generating": "Generating QR code…", + "qr_error": "Failed to generate QR code.", + "qr_error_no_url": "No HA URL configured. Please set an external or internal URL in Settings → System → Network.", + "save_error": "Failed to save. Please try again.", + "qr_print": "Print", + "qr_download": "Download SVG", + "qr_action": "Action on scan", + "qr_action_view": "View maintenance info", + "qr_action_complete": "Mark maintenance as complete", + "qr_url_mode": "Link type", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "Server URL", + "overview": "Overview", + "analysis": "Analysis", + "recent_activities": "Recent Activities", + "search_notes": "Search notes", + "avg_cost": "Avg Cost", + "no_advanced_features": "No advanced features enabled", + "no_advanced_features_hint": "Enable “Adaptive Intervals” or “Seasonal Patterns” in the integration settings to see analysis data here.", + "analysis_not_enough_data": "Not enough data for analysis yet.", + "analysis_not_enough_data_hint": "Weibull analysis requires at least 5 completed maintenances; seasonal patterns become visible after 6+ data points per month.", + "analysis_manual_task_hint": "Manual tasks without an interval do not generate analysis data.", + "completions": "completions", + "current": "Current", + "shorter": "Shorter", + "longer": "Longer", + "normal": "Normal", + "disabled": "Disabled", + "compound_logic": "Compound logic", + "compound": "Compound (multiple conditions)", + "compound_logic_and": "AND — all conditions must trigger", + "compound_logic_or": "OR — any condition triggers", + "compound_help": "Combine several sensor conditions into one trigger.", + "compound_no_conditions": "No conditions yet — add at least one.", + "compound_add_condition": "Add condition", + "compound_condition": "Condition", + "compound_remove_condition": "Remove condition", + "card_title": "Title", + "card_show_header": "Show header with statistics", + "card_show_actions": "Show action buttons", + "card_compact": "Compact mode", + "card_max_items": "Max items (0 = all)", + "card_filter_status": "Filter by status", + "card_filter_status_help": "Empty = show all statuses.", + "card_filter_objects": "Filter by objects", + "card_filter_objects_help": "Empty = show all objects.", + "card_filter_entities": "Filter by entities (entity_ids)", + "card_filter_entities_help": "Pick sensor / binary_sensor entities from this integration. Empty = all.", + "card_loading_objects": "Loading objects…", + "card_load_error": "Could not load objects — check the WebSocket connection.", + "card_no_tasks_title": "No maintenance tasks yet", + "card_no_tasks_cta": "→ Create one in the Maintenance panel", + "no_objects": "No objects yet.", + "action_error": "Action failed. Please try again.", + "area_id_optional": "Area (optional)", + "installation_date_optional": "Installation date (optional)", + "warranty_expiry_optional": "Warranty expiry (optional)", + "warranty": "Warranty", + "warranty_valid_until": "valid until {date}", + "warranty_expires_in": "expires in {days} days", + "warranty_expired": "expired", + "cal_past_windows": "Past windows", + "cal_forward_windows": "Forward windows", + "history_edit_title": "Edit history entry", + "history_edit_timestamp": "Timestamp", + "manufacturer": "Manufacturer", + "model": "Model", + "area": "Area", + "actions": "Actions", + "view_mode_label": "View", + "view_cards": "Card view", + "view_table": "Table view", + "objects_table_columns_label": "Objects table columns", + "objects_table_columns_hint": "Choose which columns appear in the objects table view.", + "custom_icon_optional": "Icon (optional, e.g. mdi:wrench)", + "task_enabled": "Task enabled", + "skip_reason_prompt": "Skip this task?", + "reason_optional": "Reason (optional)", + "reset_date_prompt": "Mark task as performed?", + "reset_date_optional": "Last performed date (optional, defaults to today)", + "notes_label": "Notes", + "documentation_label": "Documentation", + "no_nfc_tag": "— No tag —", + "dashboard": "Dashboard", + "tab_today": "Today", + "palette_placeholder": "Search objects and tasks…", + "palette_no_results": "No matches", + "palette_hint": "↑↓ to navigate · Enter to open · Esc to close", + "today_all_caught_up": "All caught up! Nothing due this week.", + "today_overdue": "Overdue", + "today_due_today": "Due today", + "today_this_week": "This week", + "settings": "Settings", + "settings_features": "Advanced Features", + "settings_features_desc": "Enable or disable advanced features. Disabling hides them from the UI but does not delete data.", + "feat_adaptive": "Adaptive Scheduling", + "feat_adaptive_desc": "Learn optimal intervals from maintenance history", + "feat_predictions": "Sensor Predictions", + "feat_predictions_desc": "Predict trigger dates from sensor degradation", + "feat_seasonal": "Seasonal Adjustments", + "feat_seasonal_desc": "Adjust intervals based on seasonal patterns", + "feat_environmental": "Environmental Correlation", + "feat_environmental_desc": "Correlate intervals with temperature/humidity", + "feat_budget": "Budget Tracking", + "feat_budget_desc": "Track monthly and yearly maintenance spending", + "feat_groups": "Task Groups", + "feat_groups_desc": "Organize tasks into logical groups", + "feat_checklists": "Checklists", + "feat_checklists_desc": "Multi-step procedures for task completion", + "settings_general": "General", + "settings_default_warning": "Default warning days", + "settings_panel_enabled": "Sidebar panel", + "settings_panel_title": "Sidebar panel title", + "settings_notifications": "Notifications", + "settings_notify_service": "Notification service", + "test_notification": "Test notification", + "send_test": "Send test", + "testing": "Sending…", + "test_notification_success": "Test notification sent", + "test_notification_failed": "Test notification failed", + "settings_notify_due_soon": "Notify when due soon", + "settings_notify_overdue": "Notify when overdue", + "settings_notify_triggered": "Notify when triggered", + "settings_interval_hours": "Repeat interval (hours, 0 = once)", + "settings_quiet_hours": "Quiet hours", + "settings_quiet_start": "Start", + "settings_quiet_end": "End", + "settings_max_per_day": "Max notifications per day (0 = unlimited)", + "settings_bundling": "Bundle notifications", + "settings_bundle_threshold": "Bundle threshold", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobile Action Buttons", + "settings_action_complete": "Show 'Complete' button", + "settings_action_skip": "Show 'Skip' button", + "settings_action_snooze": "Show 'Snooze' button", + "settings_weekly_digest": "Weekly digest", + "settings_weekly_digest_hint": "A single summary notification on Monday morning when tasks are due.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Snooze duration (hours)", + "settings_budget": "Budget", + "settings_currency": "Currency", + "settings_budget_monthly": "Monthly budget", + "settings_budget_yearly": "Yearly budget", + "settings_budget_alerts": "Budget alerts", + "settings_budget_threshold": "Alert threshold (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Export JSON", + "settings_export_yaml": "Export YAML", + "settings_export_csv": "Export CSV", + "settings_import_csv": "Import CSV", + "settings_import_placeholder": "Paste JSON or CSV content here…", + "settings_import_btn": "Import", + "settings_import_success": "{count} objects imported successfully.", + "settings_export_success": "Export downloaded.", + "settings_saved": "Setting saved.", + "settings_include_history": "Include history", + "sort_alphabetical": "Alphabetical", + "sort_due_soonest": "Due soonest", + "sort_task_count": "Task count", + "sort_area": "Area", + "sort_assigned_user": "Assigned user", + "sort_group": "Group", + "groupby_none": "No grouping", + "groupby_area": "By area", + "groupby_group": "By group", + "groupby_user": "By user", + "filter_label": "Filter", + "user_label": "User", + "sort_label": "Sort", + "group_by_label": "Group by", + "state_value_help": "Use the HA state value (usually lowercase, e.g. \"on\"/\"off\"). Case is normalised on save.", + "target_changes_help": "Number of matching transitions before the trigger fires (default: 1).", + "qr_print_title": "Print QR codes", + "qr_print_desc": "Generate a printable page of QR codes to cut out and stick on your equipment.", + "qr_print_load": "Load objects", + "qr_print_filter": "Filter", + "qr_print_objects": "Objects", + "qr_print_actions": "Actions", + "qr_print_url_mode": "Link type", + "qr_print_estimate": "Estimated QR codes", + "qr_print_over_limit": "cap is 200, narrow the filter", + "qr_print_generate": "Generate QR codes", + "qr_print_generating": "Generating…", + "qr_print_ready": "QR codes ready", + "qr_print_print_button": "Print", + "qr_print_empty": "Nothing to generate", + "qr_action_skip": "Skip", + "vacation_title": "Vacation mode", + "vacation_active": "active", + "vacation_ended": "ended", + "vacation_desc": "Plan a vacation: notifications are paused during the period plus a buffer of days. You can opt specific tasks back in.", + "vacation_enable": "Enable vacation mode", + "vacation_start": "Start", + "vacation_end": "End", + "vacation_buffer": "Buffer (days)", + "vacation_exempt_title": "Notify anyway during vacation", + "vacation_exempt_desc": "Pick tasks that should still notify during vacation (e.g. critical pool chemistry).", + "vacation_load_tasks": "Load tasks", + "vacation_preview_btn": "Show preview", + "vacation_preview_affected": "tasks affected", + "vacation_event_due_soon": "becomes due soon", + "vacation_event_overdue": "becomes overdue", + "vacation_event_triggered_est": "sensor trigger possible", + "vacation_sensor_based": "(sensor-based)", + "vacation_action_notify": "Notify anyway", + "vacation_action_unsilence": "Silence again", + "vacation_marked_complete": "Marked complete", + "vacation_marked_skip": "Skipped", + "vacation_end_now": "End vacation now", + "add": "Add", + "show_stats": "Show stats + graphs", + "hide_stats": "Hide stats", + "adaptive_no_data": "Not enough completion history yet for adaptive analysis. Complete this task a few more times to unlock interval recommendations and reliability charts.", + "suggestion_applied": "Suggested interval applied", + "vacation_mode": "Vacation mode", + "vacation_status_active": "Active now", + "vacation_status_scheduled": "Scheduled", + "vacation_status_inactive": "Inactive", + "vacation_end_now_confirm": "End vacation immediately?", + "vacation_exempt_count": "exempt", + "vacation_advanced": "Advanced…", + "vacation_open_panel": "Open in panel", + "enable": "Enable", + "saved": "Saved", + "budget_monthly_set": "Set monthly", + "budget_yearly_set": "Set yearly", + "budget_advanced": "Currency, alerts…", + "budget_open_panel": "Open in panel", + "groups_empty": "No groups yet.", + "group_new_placeholder": "Add group…", + "group_delete_confirm": "Delete group \"{name}\"?", + "groups_manage_tasks": "Manage task assignments…", + "groups_open_panel": "Open in panel", + "unassigned": "Unassigned", + "no_area": "No area", + "has_overdue": "Has overdue tasks", + "object": "Object", + "settings_panel_access": "Panel access", + "settings_panel_access_desc": "Admins always have full access. To delegate create, edit and delete to specific non-admins, switch this on and pick them below — everyone else sees only Complete and Skip.", + "settings_operator_write": "Allow selected users to create, edit & delete", + "settings_operator_write_desc": "Off: only admins can change content. On: the selected users below get full access too.", + "no_non_admin_users": "No non-admin users found. Add some in Settings → People.", + "owner_label": "Owner", + "feat_completion_actions": "Completion actions", + "feat_completion_actions_desc": "Per-task HA action on complete + quick-complete QR with pre-set values.", + "on_complete_action_title": "On complete: trigger HA action (optional)", + "on_complete_action_desc": "Calls an HA service when the task is completed — e.g. reset a counter on the device.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Target entity", + "on_complete_action_target_hint": "Note: the entity domain must match the service — e.g. 'button.press' only works on button.*, 'counter.increment' only on counter.*, 'input_button.press' only on input_button.* etc. On a mismatch the action will silently fail (HA logs 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Data (JSON, optional)", + "on_complete_action_test": "Validate configuration", + "on_complete_action_test_success": "✓ Configuration valid (action will fire only on task completion)", + "on_complete_action_test_failed": "Failed", + "quick_complete_defaults_title": "Quick-complete defaults (for QR scans, optional)", + "quick_complete_defaults_desc": "Pre-set values for quick-complete QR scans. Without these, the QR opens the complete dialog.", + "quick_complete_defaults_notes": "Notes", + "quick_complete_defaults_cost": "Cost", + "quick_complete_defaults_duration": "Duration (minutes)", + "quick_complete_defaults_feedback_none": "No feedback", + "quick_complete_defaults_feedback_needed": "Was needed", + "quick_complete_defaults_feedback_not_needed": "Not needed", + "quick_complete_success": "Quickly marked complete", + "show_all_objects": "Show all objects", + "show_all_tasks": "Clear filter — show all tasks", + "filter_to_overdue": "Filter task list to overdue only", + "filter_to_due_soon": "Filter task list to due-soon only", + "filter_to_triggered": "Filter task list to triggered only", + "open_task": "Open task", + "show_details": "Show history + stats", + "hide_details": "Hide details", + "history_empty": "No history yet.", + "history_edit_button": "Edit entry", + "total_cost": "Total cost", + "times_performed": "Performed", + "older_entries": "older", + "open_in_panel": "Open in Maintenance panel", + "skip_reason": "Skip reason (optional)", + "reset_to_date": "Reset last_performed to", + "delete_task_confirm": "Delete this task and its history?", + "delete_object_confirm": "Delete this object and all its tasks?", + "loading": "Loading…", + "archive": "Archive", + "undo": "Undo", + "task_archived": "Task archived", + "object_archived": "Object archived", + "unarchive": "Unarchive", + "archived": "Archived", + "show_archived": "Show archived", + "hide_archived": "Hide archived", + "confirm_archive_object": "Archive this object and its tasks? They keep their history and can be unarchived later.", + "settings_archive": "Archive & Retention", + "settings_archive_desc": "Retire completed one-off tasks without deleting them. Archived items are hidden and inert but keep their history and cost.", + "settings_archive_oneoff_days": "Auto-archive completed one-off tasks after (days, 0 = off)", + "settings_delete_archived_oneoff_days": "Auto-delete archived one-off tasks after (days, 0 = never)", + "archive_object": "Archive object", + "unarchive_object": "Unarchive object", + "documents": "Documents", + "documents_empty": "No documents yet.", + "doc_upload": "Upload file", + "doc_uploading": "Uploading…", + "doc_add_link": "Add link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Title (optional)", + "doc_open": "Open", + "doc_delete_confirm": "Delete \"{name}\"?", + "doc_too_large": "File is too large (max 25 MB).", + "doc_upload_failed": "Upload failed.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Already stored elsewhere — shared, no extra space used.", + "doc_dup_in_object": "This file is already attached to this object.", + "doc_link_invalid": "Only http/https links are allowed.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Warranty", + "doc_cat_invoice": "Invoice", + "doc_cat_spare_parts": "Spare parts", + "doc_cat_photo": "Photo", + "doc_cat_other": "Other", + "doc_link_badge": "Link", + "doc_storage_title": "Document storage", + "doc_storage_saved": "Saved via deduplication", + "doc_storage_refresh": "Refresh", + "doc_download": "Download", + "doc_close": "Close", + "doc_camera": "Take photo", + "doc_drop_hint": "Drop files here", + "doc_task_none": "No documents linked to this task.", + "doc_link_existing": "Link a document…", + "doc_attach": "Link", + "doc_unlink": "Unlink", + "doc_page": "Page", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1y", + "chart_since_service": "since last service", + "chart_no_stats": "No long-term statistics for this entity — showing maintenance-event values only", + "auto_complete_on_recovery": "Auto-complete when the sensor recovers", + "auto_complete_on_recovery_help": "Records a completion (sets last performed) when the trigger clears itself — e.g. salt refilled, filter replaced.", + "doc_search": "Search documents…", + "doc_search_none": "No matching documents", + "link_device_optional": "Link to existing device (optional)", + "parent_object_optional": "Parent object (optional)", + "parent_none": "(No parent)", + "paused": "Paused", + "pause_object": "Pause", + "resume_object": "Resume", + "pause_until_prompt": "Freeze this object's schedules — nothing becomes due and nothing notifies until it is resumed. Optionally set an auto-resume date.", + "pause_until_label": "Resume on (optional)", + "object_paused": "Object paused", + "object_resumed": "Object resumed — schedules restarted", + "object_paused_badge": "Paused", + "paused_until_label": "until", + "replace_object": "Replace…", + "replace_object_prompt": "Retire this object and create a successor. History and costs stay archived on the old one; tasks and documents carry over to the new one, counters start fresh.", + "replace_name_label": "Successor name", + "object_replaced": "Object replaced — successor created", + "reading_unit_label": "Reading unit (e.g. kWh, m³)", + "reading_unit_help": "Shown next to the recorded value when completing this task.", + "reading_value_label": "Reading value", + "reading_label": "Reading", + "settings_templates_label": "Template gallery", + "settings_templates_hint": "Untick templates you'll never need — they disappear from the \"From template\" pickers (panel and config flow). Nothing else changes; you can re-enable them any time.", + "worksheet": "Work sheet", + "worksheet_scan_view": "Scan to open the task", + "worksheet_scan_complete": "Scan to complete", + "worksheet_manual_excerpt": "Manual excerpt", + "worksheet_pages": "pages", + "worksheet_printed": "Printed", + "worksheet_never": "Never" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/es.json b/custom_components/maintenance_supporter/frontend-src/locales/es.json new file mode 100644 index 0000000..bd44e4b --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/es.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Mantenimiento", + "objects": "Objetos", + "tasks": "Tareas", + "overdue": "Vencida", + "due_soon": "Próxima", + "triggered": "Activada", + "ok": "OK", + "all": "Todos", + "new_object": "+ Nuevo objeto", + "templates_from": "Desde plantilla", + "templates_title": "Empezar desde una plantilla", + "templates_task_count": "{n} tareas", + "template_created": "Creado desde plantilla", + "onboard_hint": "Añade tu primer objeto para empezar a seguir el mantenimiento.", + "edit": "Editar", + "duplicate": "Duplicar", + "task_duplicated": "Tarea duplicada", + "object_duplicated": "Objeto duplicado", + "delete": "Eliminar", + "add_task": "+ Tarea", + "complete": "Completada", + "completed": "Completada", + "skip": "Omitir", + "skipped": "Omitida", + "missed": "Missed", + "reset": "Restablecer", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Cancelar", + "bulk_select": "Seleccionar", + "bulk_select_all": "Seleccionar todo", + "bulk_n_selected": "{n} seleccionados", + "bulk_completed": "{n} tareas completadas", + "bulk_archived": "{n} tareas archivadas", + "completing": "Completando…", + "interval": "Intervalo", + "warning": "Aviso", + "last_performed": "Última ejecución", + "next_due": "Próximo vencimiento", + "days_until_due": "Días hasta vencimiento", + "avg_duration": "Ø Duración", + "trigger": "Disparador", + "trigger_type": "Tipo de disparador", + "threshold_above": "Límite superior", + "threshold_below": "Límite inferior", + "threshold": "Umbral", + "counter": "Contador", + "state_change": "Cambio de estado", + "runtime": "Tiempo de funcionamiento", + "runtime_hours": "Duración objetivo (horas)", + "target_value": "Valor objetivo", + "baseline": "Línea base", + "target_changes": "Cambios objetivo", + "for_minutes": "Durante (minutos)", + "time_based": "Temporal", + "sensor_based": "Sensor", + "manual": "Manual", + "one_time": "Una vez", + "weekdays": "Días de la semana", + "nth_weekday": "N-ésimo día de la semana del mes", + "day_of_month": "Día del mes", + "recurrence_on_days": "Repetir los", + "recurrence_occurrence": "Aparición", + "recurrence_weekday": "Día de la semana", + "recurrence_day": "Día del mes (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.º", + "ord_2": "2.º", + "ord_3": "3.º", + "ord_4": "4.º", + "ord_5": "5.º", + "ord_last": "Último", + "day_word": "Día", + "interval_value": "Intervalo", + "interval_unit": "Unidad", + "unit_days": "Días", + "unit_weeks": "Semanas", + "unit_months": "Meses", + "unit_years": "Años", + "due_date": "Fecha de vencimiento", + "cleaning": "Limpieza", + "inspection": "Inspección", + "replacement": "Sustitución", + "calibration": "Calibración", + "service": "Servicio", + "reading": "Lectura", + "custom": "Personalizado", + "history": "Historial", + "cost": "Coste", + "report_button": "Informe", + "report_title": "Informe de mantenimiento", + "report_generated": "Generado", + "report_times_done": "Hecho", + "report_total_cost": "Coste total", + "report_every": "cada {n} {unit}", + "report_notes": "Notas", + "report_col_type": "Tipo", + "report_col_status": "Estado", + "report_col_schedule": "Programación", + "duration": "Duración", + "both": "Ambos", + "trigger_val": "Valor del disparador", + "complete_title": "Completada: ", + "checklist": "Lista de verificación", + "checklist_steps_optional": "Pasos de la lista de verificación (opcional)", + "checklist_placeholder": "Limpiar filtro\nReemplazar junta\nProbar presión", + "checklist_help": "Un paso por línea. Máx. 100 elementos.", + "err_too_long": "{field}: demasiado largo (máx. {n} caracteres)", + "err_too_short": "{field}: demasiado corto (mín. {n} caracteres)", + "err_value_too_high": "{field}: demasiado grande (máx. {n})", + "err_value_too_low": "{field}: demasiado pequeño (mín. {n})", + "err_required": "{field}: campo obligatorio", + "err_wrong_type": "{field}: tipo incorrecto (esperado: {type})", + "err_invalid_choice": "{field}: valor no permitido", + "err_invalid_value": "{field}: valor inválido", + "feat_schedule_time": "Programación por hora", + "feat_schedule_time_desc": "Las tareas vencen a una hora específica en lugar de medianoche.", + "schedule_time_optional": "Vence a las (opcional, HH:MM)", + "schedule_time_help": "Vacío = medianoche (predeterminado). Zona horaria HA.", + "at_time": "a las", + "notes_optional": "Notas (opcional)", + "cost_optional": "Coste (opcional)", + "duration_minutes": "Duración en minutos (opcional)", + "days": "días", + "day": "día", + "today": "Hoy", + "d_overdue": "d vencida", + "no_tasks": "No hay tareas de mantenimiento. Cree un objeto para empezar.", + "no_tasks_short": "Sin tareas", + "no_history": "Sin entradas en el historial.", + "show_all": "Mostrar todo", + "cost_duration_chart": "Costes & Duración", + "installed": "Instalado", + "confirm_delete_object": "¿Eliminar este objeto y todas sus tareas?", + "confirm_delete_task": "¿Eliminar esta tarea?", + "min": "Mín", + "max": "Máx", + "save": "Guardar", + "saving": "Guardando…", + "edit_task": "Editar tarea", + "new_task": "Nueva tarea de mantenimiento", + "task_name": "Nombre de la tarea", + "maintenance_type": "Tipo de mantenimiento", + "priority": "Prioridad", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Baja", + "priority_normal": "Normal", + "priority_high": "Alta", + "schedule_type": "Tipo de planificación", + "interval_days": "Intervalo (días)", + "warning_days": "Días de aviso", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Última ejecución (opcional)", + "interval_anchor": "Anclaje del intervalo", + "anchor_completion": "Desde la fecha de finalización", + "anchor_planned": "Desde la fecha planificada (sin desviación)", + "edit_object": "Editar objeto", + "name": "Nombre", + "manufacturer_optional": "Fabricante (opcional)", + "model_optional": "Modelo (opcional)", + "serial_number_optional": "Número de serie (opcional)", + "serial_number_label": "N/S", + "documentation_url_label": "Manual", + "object_notes_label": "Notas", + "sort_due_date": "Vencimiento", + "sort_object": "Nombre del objeto", + "sort_type": "Tipo", + "sort_task_name": "Nombre de la tarea", + "all_objects": "Todos los objetos", + "tasks_lower": "tareas", + "no_tasks_yet": "Aún no hay tareas", + "add_first_task": "Agregar primera tarea", + "trigger_configuration": "Configuración del disparador", + "entity_id": "ID de entidad", + "comma_separated": "separados por comas", + "entity_logic": "Lógica de entidad", + "entity_logic_any": "Cualquier entidad activa", + "entity_logic_all": "Todas las entidades deben activar", + "entities": "entidades", + "attribute_optional": "Atributo (opcional, vacío = estado)", + "use_entity_state": "Usar estado de la entidad (sin atributo)", + "trigger_above": "Activar por encima de", + "trigger_below": "Activar por debajo de", + "for_at_least_minutes": "Durante al menos (minutos)", + "safety_interval_days": "Intervalo de seguridad (días, opcional)", + "safety_interval": "Intervalo de seguridad (opcional)", + "delta_mode": "Modo delta", + "from_state_optional": "Desde estado (opcional)", + "to_state_optional": "Hasta estado (opcional)", + "documentation_url_optional": "URL de documentación (opcional)", + "object_notes_optional": "Notas (opcional)", + "nfc_tag_id_optional": "ID de etiqueta NFC (opcional)", + "nfc_tags_empty_help": "Aún no hay tags NFC registrados en Home Assistant.", + "nfc_tags_open_settings": "Abrir configuración de tags", + "nfc_tags_refresh": "Actualizar", + "environmental_entity_optional": "Sensor ambiental (opcional)", + "environmental_entity_helper": "p.ej. sensor.temperatura_exterior — ajusta el intervalo según las condiciones ambientales", + "environmental_attribute_optional": "Atributo ambiental (opcional)", + "nfc_tag_id": "ID de etiqueta NFC", + "nfc_linked": "Etiqueta NFC vinculada", + "nfc_link_hint": "Clic para vincular etiqueta NFC", + "responsible_user": "Usuario responsable", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ningún usuario asignado)", + "all_users": "Todos los usuarios", + "my_tasks": "Mis tareas", + "tab_calendar": "Calendario", + "cal_no_events": "Sin mantenimiento", + "cal_window_7": "7 días", + "cal_window_14": "14 días", + "cal_window_30": "30 días", + "cal_window_365": "1 año", + "cal_every_n_days": "cada {n} días", + "cal_source_time": "Basado en tiempo", + "cal_source_time_adaptive": "Basado en tiempo (adaptativo)", + "cal_source_sensor": "Basado en sensor", + "cal_predicted": "predicho", + "cal_confidence_high": "alta confianza", + "cal_confidence_medium": "confianza media", + "cal_confidence_low": "baja confianza", + "budget_monthly": "Presupuesto mensual", + "budget_yearly": "Presupuesto anual", + "groups": "Grupos", + "new_group": "Nuevo grupo", + "edit_group": "Editar grupo", + "no_groups": "Sin grupos todavía", + "delete_group": "Eliminar grupo", + "delete_group_confirm": "¿Eliminar el grupo '{name}'?", + "group_select_tasks": "Seleccionar tareas", + "group_name_required": "Nombre requerido", + "description_optional": "Descripción (opcional)", + "selected": "Seleccionado", + "loading_chart": "Cargando datos...", + "hide_outliers": "Ocultar valores atípicos (fallos del sensor)", + "was_maintenance_needed": "¿Era necesario este mantenimiento?", + "feedback_needed": "Necesario", + "feedback_not_needed": "No necesario", + "feedback_not_sure": "No seguro", + "suggested_interval": "Intervalo sugerido", + "apply_suggestion": "Aplicar", + "reanalyze": "Reanalizar", + "reanalyze_result": "Nuevo análisis", + "reanalyze_insufficient_data": "Datos insuficientes para una recomendación", + "data_points": "puntos de datos", + "dismiss_suggestion": "Descartar", + "confidence_low": "Baja", + "confidence_medium": "Media", + "confidence_high": "Alta", + "recommended": "recomendado", + "seasonal_awareness": "Conciencia estacional", + "edit_seasonal_overrides": "Editar factores estacionales", + "seasonal_overrides_title": "Factores estacionales (override)", + "seasonal_overrides_hint": "Factor por mes (0.1–5.0). Vacío = aprendido automáticamente.", + "seasonal_override_invalid": "Valor no válido", + "seasonal_override_range": "El factor debe estar entre 0.1 y 5.0", + "clear_all": "Borrar todo", + "seasonal_chart_title": "Factores estacionales", + "seasonal_learned": "Aprendido", + "seasonal_manual": "Manual", + "month_jan": "Ene", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Abr", + "month_may": "May", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Ago", + "month_sep": "Sep", + "month_oct": "Oct", + "month_nov": "Nov", + "month_dec": "Dic", + "sensor_prediction": "Predicción del sensor", + "degradation_trend": "Tendencia", + "trend_rising": "En aumento", + "trend_falling": "En descenso", + "trend_stable": "Estable", + "trend_insufficient_data": "Datos insuficientes", + "days_until_threshold": "Días hasta el umbral", + "threshold_exceeded": "Umbral superado", + "environmental_adjustment": "Factor ambiental", + "sensor_prediction_urgency": "El sensor predice el umbral en ~{days} días", + "day_short": "día", + "weibull_reliability_curve": "Curva de fiabilidad", + "weibull_failure_probability": "Probabilidad de fallo", + "weibull_r_squared": "Ajuste R²", + "beta_early_failures": "Fallos tempranos", + "beta_random_failures": "Fallos aleatorios", + "beta_wear_out": "Desgaste", + "beta_highly_predictable": "Altamente predecible", + "confidence_interval": "Intervalo de confianza", + "confidence_conservative": "Conservador", + "confidence_aggressive": "Optimista", + "current_interval_marker": "Intervalo actual", + "recommended_marker": "Recomendado", + "characteristic_life": "Vida característica", + "chart_mini_sparkline": "Sparkline de tendencia", + "chart_history": "Historial de costes y duración", + "chart_seasonal": "Factores estacionales, 12 meses", + "chart_weibull": "Curva de fiabilidad Weibull", + "chart_sparkline": "Gráfico de valor del disparador", + "days_progress": "Progreso en días", + "qr_code": "Código QR", + "qr_generating": "Generando código QR…", + "qr_error": "No se pudo generar el código QR.", + "qr_error_no_url": "No hay URL de HA configurada. Establezca una URL externa o interna en Ajustes → Sistema → Red.", + "save_error": "Error al guardar. Inténtelo de nuevo.", + "qr_print": "Imprimir", + "qr_download": "Descargar SVG", + "qr_action": "Acción al escanear", + "qr_action_view": "Ver info de mantenimiento", + "qr_action_complete": "Marcar mantenimiento como completado", + "qr_url_mode": "Tipo de enlace", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "URL del servidor", + "overview": "Resumen", + "analysis": "Análisis", + "recent_activities": "Actividades recientes", + "search_notes": "Buscar en notas", + "avg_cost": "Ø Coste", + "no_advanced_features": "Sin funciones avanzadas activadas", + "no_advanced_features_hint": "Active “Intervalos Adaptativos” o “Patrones Estacionales” en la configuración de la integración para ver datos de análisis aquí.", + "analysis_not_enough_data": "Aún no hay suficientes datos para el análisis.", + "analysis_not_enough_data_hint": "El análisis Weibull requiere al menos 5 mantenimientos completados; los patrones estacionales son visibles tras 6+ puntos de datos por mes.", + "analysis_manual_task_hint": "Las tareas manuales sin intervalo no generan datos de análisis.", + "completions": "finalizaciones", + "current": "Actual", + "shorter": "Más corto", + "longer": "Más largo", + "normal": "Normal", + "disabled": "Desactivado", + "compound_logic": "Lógica compuesta", + "compound": "Compuesto (varias condiciones)", + "compound_logic_and": "Y — todas las condiciones deben activarse", + "compound_logic_or": "O — basta con una condición", + "compound_help": "Combina varias condiciones de sensores en un disparador.", + "compound_no_conditions": "Aún no hay condiciones — añade al menos una.", + "compound_add_condition": "Añadir condición", + "compound_condition": "Condición", + "compound_remove_condition": "Eliminar condición", + "card_title": "Título", + "card_show_header": "Mostrar encabezado con estadísticas", + "card_show_actions": "Mostrar botones de acción", + "card_compact": "Modo compacto", + "card_max_items": "Máx. elementos (0 = todos)", + "card_filter_status": "Filtrar por estado", + "card_filter_status_help": "Vacío = mostrar todos los estados.", + "card_filter_objects": "Filtrar por objetos", + "card_filter_objects_help": "Vacío = mostrar todos los objetos.", + "card_filter_entities": "Filtrar por entidades (entity_ids)", + "card_filter_entities_help": "Selecciona entidades sensor / binary_sensor de esta integración. Vacío = todas.", + "card_loading_objects": "Cargando objetos…", + "card_load_error": "No se pudieron cargar los objetos — verifica la conexión WebSocket.", + "card_no_tasks_title": "Aún no hay tareas de mantenimiento", + "card_no_tasks_cta": "→ Crea una en el panel Mantenimiento", + "no_objects": "Aún no hay objetos.", + "action_error": "Acción fallida. Inténtelo de nuevo.", + "area_id_optional": "Área (opcional)", + "installation_date_optional": "Fecha de instalación (opcional)", + "warranty_expiry_optional": "Vencimiento de garantía (opcional)", + "warranty": "Garantía", + "warranty_valid_until": "válida hasta {date}", + "warranty_expires_in": "vence en {days} días", + "warranty_expired": "vencida", + "cal_past_windows": "Ventanas pasadas", + "cal_forward_windows": "Ventanas futuras", + "history_edit_title": "Editar entrada del historial", + "history_edit_timestamp": "Marca de tiempo", + "manufacturer": "Fabricante", + "model": "Modelo", + "area": "Área", + "actions": "Acciones", + "view_mode_label": "Vista", + "view_cards": "Vista de tarjetas", + "view_table": "Vista de tabla", + "objects_table_columns_label": "Columnas de la tabla de objetos", + "objects_table_columns_hint": "Elige qué columnas aparecen en la vista de tabla de objetos.", + "custom_icon_optional": "Icono (opcional, ej. mdi:wrench)", + "task_enabled": "Tarea habilitada", + "skip_reason_prompt": "¿Omitir esta tarea?", + "reason_optional": "Motivo (opcional)", + "reset_date_prompt": "¿Marcar la tarea como realizada?", + "reset_date_optional": "Fecha de última ejecución (opcional, por defecto: hoy)", + "notes_label": "Notas", + "documentation_label": "Documentación", + "no_nfc_tag": "— Sin etiqueta —", + "dashboard": "Panel", + "tab_today": "Hoy", + "palette_placeholder": "Buscar objetos y tareas…", + "palette_no_results": "Sin resultados", + "palette_hint": "↑↓ navegar · Enter abrir · Esc cerrar", + "today_all_caught_up": "¡Todo al día! Nada pendiente esta semana.", + "today_overdue": "Vencidas", + "today_due_today": "Vence hoy", + "today_this_week": "Esta semana", + "settings": "Ajustes", + "settings_features": "Funciones avanzadas", + "settings_features_desc": "Active o desactive funciones avanzadas. Desactivar las oculta de la interfaz pero no elimina datos.", + "feat_adaptive": "Planificación adaptativa", + "feat_adaptive_desc": "Aprender intervalos óptimos del historial de mantenimiento", + "feat_predictions": "Predicciones de sensor", + "feat_predictions_desc": "Predecir fechas de activación por degradación del sensor", + "feat_seasonal": "Ajustes estacionales", + "feat_seasonal_desc": "Ajustar intervalos según patrones estacionales", + "feat_environmental": "Correlación ambiental", + "feat_environmental_desc": "Correlacionar intervalos con temperatura/humedad", + "feat_budget": "Seguimiento de presupuesto", + "feat_budget_desc": "Seguir los gastos de mantenimiento mensuales y anuales", + "feat_groups": "Grupos de tareas", + "feat_groups_desc": "Organizar tareas en grupos lógicos", + "feat_checklists": "Listas de verificación", + "feat_checklists_desc": "Procedimientos de varios pasos para completar tareas", + "settings_general": "General", + "settings_default_warning": "Días de aviso predeterminados", + "settings_panel_enabled": "Panel lateral", + "settings_panel_title": "Título del panel lateral", + "settings_notifications": "Notificaciones", + "settings_notify_service": "Servicio de notificación", + "test_notification": "Notificación de prueba", + "send_test": "Enviar prueba", + "testing": "Enviando…", + "test_notification_success": "Notificación de prueba enviada", + "test_notification_failed": "La notificación de prueba falló", + "settings_notify_due_soon": "Notificar cuando esté próxima", + "settings_notify_overdue": "Notificar cuando esté vencida", + "settings_notify_triggered": "Notificar cuando se active", + "settings_interval_hours": "Intervalo de repetición (horas, 0 = una vez)", + "settings_quiet_hours": "Horas de silencio", + "settings_quiet_start": "Inicio", + "settings_quiet_end": "Fin", + "settings_max_per_day": "Máx. notificaciones por día (0 = ilimitado)", + "settings_bundling": "Agrupar notificaciones", + "settings_bundle_threshold": "Umbral de agrupación", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Botones de acción móviles", + "settings_action_complete": "Mostrar botón 'Completada'", + "settings_action_skip": "Mostrar botón 'Omitir'", + "settings_action_snooze": "Mostrar botón 'Posponer'", + "settings_weekly_digest": "Resumen semanal", + "settings_weekly_digest_hint": "Una notificación de resumen el lunes por la mañana cuando hay tareas.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Duración de posposición (horas)", + "settings_budget": "Presupuesto", + "settings_currency": "Moneda", + "settings_budget_monthly": "Presupuesto mensual", + "settings_budget_yearly": "Presupuesto anual", + "settings_budget_alerts": "Alertas de presupuesto", + "settings_budget_threshold": "Umbral de alerta (%)", + "settings_import_export": "Importar / Exportar", + "settings_export_json": "Exportar JSON", + "settings_export_yaml": "Exportar YAML", + "settings_export_csv": "Exportar CSV", + "settings_import_csv": "Importar CSV", + "settings_import_placeholder": "Pegue el contenido JSON o CSV aquí…", + "settings_import_btn": "Importar", + "settings_import_success": "{count} objetos importados correctamente.", + "settings_export_success": "Exportación descargada.", + "settings_saved": "Ajuste guardado.", + "settings_include_history": "Incluir historial", + "sort_alphabetical": "Alfabético", + "sort_due_soonest": "Próximo a vencer", + "sort_task_count": "Cantidad de tareas", + "sort_area": "Área", + "sort_assigned_user": "Usuario asignado", + "sort_group": "Grupo", + "groupby_none": "Sin agrupación", + "groupby_area": "Por área", + "groupby_group": "Por grupo", + "groupby_user": "Por usuario", + "filter_label": "Filtro", + "user_label": "Usuario", + "sort_label": "Ordenar", + "group_by_label": "Agrupar por", + "state_value_help": "Usa el valor de estado de HA (normalmente en minúsculas, p. ej. \"on\"/\"off\"). Las mayúsculas se normalizan al guardar.", + "target_changes_help": "Número de transiciones coincidentes antes de que se dispare el trigger (predeterminado: 1).", + "qr_print_title": "Imprimir códigos QR", + "qr_print_desc": "Genera una página imprimible de códigos QR para recortar y pegar en tus equipos.", + "qr_print_load": "Cargar objetos", + "qr_print_filter": "Filtro", + "qr_print_objects": "Objetos", + "qr_print_actions": "Acciones", + "qr_print_url_mode": "Tipo de enlace", + "qr_print_estimate": "Códigos QR estimados", + "qr_print_over_limit": "máximo 200, reduce el filtro", + "qr_print_generate": "Generar códigos QR", + "qr_print_generating": "Generando…", + "qr_print_ready": "Códigos QR listos", + "qr_print_print_button": "Imprimir", + "qr_print_empty": "Nada que generar", + "qr_action_skip": "Omitir", + "vacation_title": "Modo vacaciones", + "vacation_active": "activo", + "vacation_ended": "terminado", + "vacation_desc": "Planifica tus vacaciones: las notificaciones se pausan durante el período más unos días de margen. Puedes excluir tareas concretas.", + "vacation_enable": "Activar modo vacaciones", + "vacation_start": "Inicio", + "vacation_end": "Fin", + "vacation_buffer": "Margen (días)", + "vacation_exempt_title": "Notificar igual durante vacaciones", + "vacation_exempt_desc": "Selecciona tareas que deben notificar también en vacaciones (p. ej. química crítica de piscina).", + "vacation_load_tasks": "Cargar tareas", + "vacation_preview_btn": "Mostrar vista previa", + "vacation_preview_affected": "tareas afectadas", + "vacation_event_due_soon": "vencerá pronto", + "vacation_event_overdue": "se volverá vencida", + "vacation_event_triggered_est": "posible activación de sensor", + "vacation_sensor_based": "(basado en sensor)", + "vacation_action_notify": "Notificar igual", + "vacation_action_unsilence": "Silenciar de nuevo", + "vacation_marked_complete": "Marcado como completado", + "vacation_marked_skip": "Omitido", + "vacation_end_now": "Terminar vacaciones ahora", + "unassigned": "Sin asignar", + "no_area": "Sin área", + "has_overdue": "Tareas vencidas", + "object": "Objeto", + "settings_panel_access": "Acceso al panel", + "settings_panel_access_desc": "Los administradores siempre tienen acceso completo. Para delegar crear, editar y eliminar a no administradores específicos, activa esto y selecciónalos abajo — los demás solo ven Completar y Omitir.", + "settings_operator_write": "Permitir a los usuarios seleccionados crear, editar y eliminar", + "settings_operator_write_desc": "Desactivado: solo los administradores pueden cambiar el contenido. Activado: los usuarios seleccionados abajo también tienen acceso completo.", + "no_non_admin_users": "No se encontraron usuarios no administradores. Añade alguno en Ajustes → Personas.", + "owner_label": "Propietario", + "feat_completion_actions": "Acciones de finalización", + "feat_completion_actions_desc": "Acción HA por tarea al completar + QR de finalización rápida con valores prefijados.", + "on_complete_action_title": "Al completar: ejecutar acción HA (opcional)", + "on_complete_action_desc": "Llama un servicio HA cuando se completa la tarea — p. ej. reiniciar un contador del dispositivo.", + "on_complete_action_service": "Servicio", + "on_complete_action_target": "Entidad objetivo", + "on_complete_action_data": "Datos (JSON, opcional)", + "on_complete_action_test": "Probar acción", + "on_complete_action_test_success": "Éxito", + "on_complete_action_test_failed": "Fallido", + "quick_complete_defaults_title": "Valores predeterminados de finalización rápida (para escaneos QR, opcional)", + "quick_complete_defaults_desc": "Valores prefijados para QR de finalización rápida. Sin ellos, el QR abre el diálogo de completar.", + "quick_complete_defaults_notes": "Notas", + "quick_complete_defaults_cost": "Coste", + "quick_complete_defaults_duration": "Duración (minutos)", + "quick_complete_defaults_feedback_none": "Sin feedback", + "quick_complete_defaults_feedback_needed": "Era necesario", + "quick_complete_defaults_feedback_not_needed": "No era necesario", + "quick_complete_success": "Completado rápido", + "trigger_replaced": "Disparador reemplazado", + "add": "Añadir", + "show_stats": "Mostrar estadísticas + gráficos", + "hide_stats": "Ocultar estadísticas", + "adaptive_no_data": "Aún no hay suficiente historial de finalizaciones para el análisis adaptativo. Completa esta tarea unas cuantas veces más para desbloquear las recomendaciones de intervalo y los gráficos de fiabilidad.", + "suggestion_applied": "Intervalo sugerido aplicado", + "vacation_mode": "Modo vacaciones", + "vacation_status_active": "Activo ahora", + "vacation_status_scheduled": "Programado", + "vacation_status_inactive": "Inactivo", + "vacation_end_now_confirm": "¿Finalizar las vacaciones de inmediato?", + "vacation_exempt_count": "exentas", + "vacation_advanced": "Avanzado…", + "vacation_open_panel": "Abrir en el panel", + "enable": "Activar", + "saved": "Guardado", + "budget_monthly_set": "Definir mensual", + "budget_yearly_set": "Definir anual", + "budget_advanced": "Moneda, alertas…", + "budget_open_panel": "Abrir en el panel", + "groups_empty": "Aún no hay grupos.", + "group_new_placeholder": "Añadir grupo…", + "group_delete_confirm": "¿Eliminar el grupo \"{name}\"?", + "groups_manage_tasks": "Gestionar asignaciones de tareas…", + "groups_open_panel": "Abrir en el panel", + "on_complete_action_target_hint": "Nota: el dominio de la entidad debe coincidir con el servicio — p. ej. 'button.press' solo funciona en button.*, 'counter.increment' solo en counter.*, 'input_button.press' solo en input_button.* etc. Si no coinciden, la acción falla silenciosamente (HA registra 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Mostrar todos los objetos", + "show_all_tasks": "Quitar filtro — mostrar todas las tareas", + "filter_to_overdue": "Filtrar la lista solo a vencidas", + "filter_to_due_soon": "Filtrar la lista solo a próximas a vencer", + "filter_to_triggered": "Filtrar la lista solo a activadas", + "open_task": "Abrir tarea", + "show_details": "Mostrar historial + estadísticas", + "hide_details": "Ocultar detalles", + "history_empty": "Aún no hay historial.", + "history_edit_button": "Editar entrada", + "total_cost": "Coste total", + "times_performed": "Realizada", + "older_entries": "anteriores", + "open_in_panel": "Abrir en el panel de Mantenimiento", + "skip_reason": "Motivo de omisión (opcional)", + "reset_to_date": "Restablecer última ejecución a", + "delete_task_confirm": "¿Eliminar esta tarea y su historial?", + "delete_object_confirm": "¿Eliminar este objeto y todas sus tareas?", + "loading": "Cargando…", + "archive": "Archivar", + "undo": "Deshacer", + "task_archived": "Tarea archivada", + "object_archived": "Objeto archivado", + "unarchive": "Desarchivar", + "archived": "Archivado", + "show_archived": "Mostrar archivados", + "hide_archived": "Ocultar archivados", + "confirm_archive_object": "¿Archivar este objeto y sus tareas? Se conserva el historial y se puede restaurar más tarde.", + "settings_archive": "Archivado y retención", + "settings_archive_desc": "Retira las tareas únicas completadas sin eliminarlas. Los elementos archivados se ocultan y quedan inactivos, pero conservan su historial y coste.", + "settings_archive_oneoff_days": "Archivar automáticamente las tareas únicas completadas tras (días, 0 = desactivado)", + "settings_delete_archived_oneoff_days": "Eliminar automáticamente las tareas únicas archivadas tras (días, 0 = nunca)", + "archive_object": "Archivar objeto", + "unarchive_object": "Desarchivar objeto", + "documents": "Documentos", + "documents_empty": "Aún no hay documentos.", + "doc_upload": "Subir archivo", + "doc_uploading": "Subiendo…", + "doc_add_link": "Añadir enlace", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Título (opcional)", + "doc_open": "Abrir", + "doc_delete_confirm": "¿Eliminar «{name}»?", + "doc_too_large": "El archivo es demasiado grande (máx. 25 MB).", + "doc_upload_failed": "Error al subir.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Ya almacenado en otro lugar — compartido, sin espacio adicional.", + "doc_dup_in_object": "Este archivo ya está adjunto a este objeto.", + "doc_link_invalid": "Solo se permiten enlaces http/https.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Garantía", + "doc_cat_invoice": "Factura", + "doc_cat_spare_parts": "Repuestos", + "doc_cat_photo": "Foto", + "doc_cat_other": "Otro", + "doc_link_badge": "Enlace", + "doc_storage_title": "Almacenamiento de documentos", + "doc_storage_saved": "Ahorrado por deduplicación", + "doc_storage_refresh": "Actualizar", + "doc_download": "Descargar", + "doc_close": "Cerrar", + "doc_camera": "Tomar foto", + "doc_drop_hint": "Suelta los archivos aquí", + "doc_task_none": "Ningún documento vinculado a esta tarea.", + "doc_link_existing": "Vincular un documento…", + "doc_attach": "Vincular", + "doc_unlink": "Desvincular", + "doc_page": "Página", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1a", + "chart_since_service": "desde el último mantenimiento", + "chart_no_stats": "No hay estadísticas a largo plazo para esta entidad — solo se muestran valores de eventos de mantenimiento", + "auto_complete_on_recovery": "Completar automáticamente cuando el sensor se recupere", + "auto_complete_on_recovery_help": "Registra una finalización (fija la última realización) cuando el disparador se resuelve solo — p. ej., sal rellenada, filtro cambiado.", + "doc_search": "Buscar documentos…", + "doc_search_none": "Ningún documento coincidente", + "link_device_optional": "Vincular a dispositivo existente (opcional)", + "parent_object_optional": "Objeto principal (opcional)", + "parent_none": "(Sin objeto principal)", + "paused": "Pausado", + "pause_object": "Pausar", + "resume_object": "Reanudar", + "pause_until_prompt": "Congela los calendarios de este objeto: nada vence ni notifica hasta reanudarlo. Opcionalmente, fija una fecha de reanudación automática.", + "pause_until_label": "Reanudar el (opcional)", + "object_paused": "Objeto pausado", + "object_resumed": "Objeto reanudado — calendarios reiniciados", + "object_paused_badge": "Pausado", + "paused_until_label": "hasta", + "replace_object": "Sustituir…", + "replace_object_prompt": "Retira este objeto y crea un sucesor. El historial y los costes quedan archivados en el antiguo; las tareas y los documentos pasan al nuevo, los contadores empiezan de cero.", + "replace_name_label": "Nombre del sucesor", + "object_replaced": "Objeto sustituido — sucesor creado", + "reading_unit_label": "Unidad de lectura (p. ej. kWh, m³)", + "reading_unit_help": "Se muestra junto al valor registrado al completar esta tarea.", + "reading_value_label": "Valor de lectura", + "reading_label": "Lectura", + "settings_templates_label": "Galería de plantillas", + "settings_templates_hint": "Desmarca las plantillas que nunca necesitarás: desaparecen de los selectores \"Desde plantilla\" (panel y flujo de configuración). Nada más cambia; puedes reactivarlas cuando quieras.", + "worksheet": "Hoja de trabajo", + "worksheet_scan_view": "Escanea para abrir la tarea", + "worksheet_scan_complete": "Escanea para completar", + "worksheet_manual_excerpt": "Extracto del manual", + "worksheet_pages": "páginas", + "worksheet_printed": "Impreso", + "worksheet_never": "Nunca" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/fi.json b/custom_components/maintenance_supporter/frontend-src/locales/fi.json new file mode 100644 index 0000000..e641728 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/fi.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Huolto", + "objects": "Kohteet", + "tasks": "Tehtävät", + "overdue": "Myöhässä", + "due_soon": "Pian erääntyy", + "triggered": "Laukaistu", + "trigger_replaced": "Laukaisin korvattu", + "ok": "OK", + "all": "Kaikki", + "new_object": "+ Uusi kohde", + "templates_from": "Mallista", + "templates_title": "Aloita mallista", + "templates_task_count": "{n} tehtävää", + "template_created": "Luotu mallista", + "onboard_hint": "Lisää ensimmäinen kohde ja aloita huollon seuranta.", + "edit": "Muokkaa", + "duplicate": "Monista", + "task_duplicated": "Tehtävä monistettu", + "object_duplicated": "Kohde monistettu", + "delete": "Poista", + "add_task": "+ Lisää tehtävä", + "complete": "Valmis", + "completed": "Suoritettu", + "skip": "Ohita", + "skipped": "Ohitettu", + "missed": "Missed", + "reset": "Nollaa", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Peruuta", + "bulk_select": "Valitse", + "bulk_select_all": "Valitse kaikki", + "bulk_n_selected": "{n} valittu", + "bulk_completed": "{n} tehtävää valmiina", + "bulk_archived": "{n} tehtävää arkistoitu", + "completing": "Suoritetaan…", + "interval": "Väli", + "warning": "Varoitus", + "last_performed": "Viimeksi suoritettu", + "next_due": "Seuraava eräpäivä", + "days_until_due": "Päiviä eräpäivään", + "avg_duration": "Keskim. kesto", + "trigger": "Laukaisin", + "trigger_type": "Laukaisimen tyyppi", + "threshold_above": "Yläraja", + "threshold_below": "Alaraja", + "threshold": "Kynnysarvo", + "counter": "Laskuri", + "state_change": "Tilan muutos", + "runtime": "Käyttöaika", + "runtime_hours": "Tavoitekäyttöaika (tuntia)", + "target_value": "Tavoitearvo", + "baseline": "Lähtötaso", + "target_changes": "Tavoitemuutokset", + "for_minutes": "Vähintään (minuuttia)", + "time_based": "Aikaperusteinen", + "sensor_based": "Anturiperusteinen", + "manual": "Manuaalinen", + "one_time": "Kertaluonteinen", + "weekdays": "Viikonpäivät", + "nth_weekday": "Kuukauden n:s viikonpäivä", + "day_of_month": "Kuukauden päivä", + "recurrence_on_days": "Toista päivinä", + "recurrence_occurrence": "Esiintymä", + "recurrence_weekday": "Viikonpäivä", + "recurrence_day": "Kuukauden päivä (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Viimeinen", + "day_word": "Päivä", + "interval_value": "Väli", + "interval_unit": "Yksikkö", + "unit_days": "Päivää", + "unit_weeks": "Viikkoa", + "unit_months": "Kuukautta", + "unit_years": "Vuotta", + "due_date": "Eräpäivä", + "cleaning": "Puhdistus", + "inspection": "Tarkastus", + "replacement": "Vaihto", + "calibration": "Kalibrointi", + "service": "Huolto", + "reading": "Lukema", + "custom": "Mukautettu", + "history": "Historia", + "cost": "Kustannus", + "report_button": "Raportti", + "report_title": "Huoltoraportti", + "report_generated": "Luotu", + "report_times_done": "Tehty", + "report_total_cost": "Kokonaiskustannus", + "report_every": "joka {n}. {unit}", + "report_notes": "Muistiinpanot", + "report_col_type": "Tyyppi", + "report_col_status": "Tila", + "report_col_schedule": "Aikataulu", + "duration": "Kesto", + "both": "Molemmat", + "trigger_val": "Laukaisuarvo", + "complete_title": "Suorita: ", + "checklist": "Tarkistuslista", + "checklist_steps_optional": "Tarkistuslistan vaiheet (valinnainen)", + "checklist_placeholder": "Puhdista suodatin\nVaihda tiiviste\nTarkista paine", + "checklist_help": "Yksi vaihe riviä kohden. Enintään 100 kohdetta.", + "err_too_long": "{field}: liian pitkä (enint. {n} merkkiä)", + "err_too_short": "{field}: liian lyhyt (väh. {n} merkkiä)", + "err_value_too_high": "{field}: liian suuri (enint. {n})", + "err_value_too_low": "{field}: liian pieni (väh. {n})", + "err_required": "{field}: pakollinen", + "err_wrong_type": "{field}: väärä tyyppi (odotettiin: {type})", + "err_invalid_choice": "{field}: ei sallittu arvo", + "err_invalid_value": "{field}: virheellinen arvo", + "feat_schedule_time": "Kellonajan ajoitus", + "feat_schedule_time_desc": "Tehtävät erääntyvät tiettyyn kellonaikaan keskiyön sijaan.", + "schedule_time_optional": "Erääntyy klo (valinnainen, HH:MM)", + "schedule_time_help": "Tyhjä = keskiyö (oletus). HA:n aikavyöhyke.", + "at_time": "klo", + "notes_optional": "Muistiinpanot (valinnainen)", + "cost_optional": "Kustannus (valinnainen)", + "duration_minutes": "Kesto minuutteina (valinnainen)", + "days": "päivää", + "day": "päivä", + "today": "Tänään", + "d_overdue": "pv myöhässä", + "no_tasks": "Ei vielä huoltotehtäviä. Luo kohde aloittaaksesi.", + "no_tasks_short": "Ei tehtäviä", + "no_history": "Ei vielä historiatietoja.", + "show_all": "Näytä kaikki", + "cost_duration_chart": "Kustannus ja kesto", + "installed": "Asennettu", + "confirm_delete_object": "Poistetaanko tämä kohde ja kaikki sen tehtävät?", + "confirm_delete_task": "Poistetaanko tämä tehtävä?", + "min": "Min", + "max": "Maks", + "save": "Tallenna", + "saving": "Tallennetaan…", + "edit_task": "Muokkaa tehtävää", + "new_task": "Uusi huoltotehtävä", + "task_name": "Tehtävän nimi", + "maintenance_type": "Huoltotyyppi", + "priority": "Prioriteetti", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Matala", + "priority_normal": "Normaali", + "priority_high": "Korkea", + "schedule_type": "Ajoitustyyppi", + "interval_days": "Väli (päivää)", + "warning_days": "Varoituspäivät", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Viimeksi suoritettu (valinnainen)", + "interval_anchor": "Välin ankkuri", + "anchor_completion": "Suorituspäivästä", + "anchor_planned": "Suunnitellusta päivästä (ei poikkeamaa)", + "edit_object": "Muokkaa kohdetta", + "name": "Nimi", + "manufacturer_optional": "Valmistaja (valinnainen)", + "model_optional": "Malli (valinnainen)", + "serial_number_optional": "Sarjanumero (valinnainen)", + "serial_number_label": "S/N", + "documentation_url_label": "Käyttöohje", + "object_notes_label": "Muistiinpanot", + "sort_due_date": "Eräpäivä", + "sort_object": "Kohteen nimi", + "sort_type": "Tyyppi", + "sort_task_name": "Tehtävän nimi", + "all_objects": "Kaikki kohteet", + "tasks_lower": "tehtävää", + "no_tasks_yet": "Ei vielä tehtäviä", + "add_first_task": "Lisää ensimmäinen tehtävä", + "trigger_configuration": "Laukaisimen asetukset", + "entity_id": "Entiteetin tunnus", + "comma_separated": "pilkuilla eroteltu", + "entity_logic": "Entiteettilogiikka", + "entity_logic_any": "Mikä tahansa entiteetti laukaisee", + "entity_logic_all": "Kaikkien entiteettien on laukaistava", + "entities": "entiteettiä", + "attribute_optional": "Attribuutti (valinnainen, tyhjä = tila)", + "use_entity_state": "Käytä entiteetin tilaa (ei attribuuttia)", + "trigger_above": "Laukaise yli", + "trigger_below": "Laukaise alle", + "for_at_least_minutes": "Vähintään (minuuttia)", + "safety_interval_days": "Turvaväli (päivää, valinnainen)", + "safety_interval": "Turvaväli (valinnainen)", + "delta_mode": "Delta-tila", + "from_state_optional": "Lähtötilasta (valinnainen)", + "to_state_optional": "Kohdetilaan (valinnainen)", + "documentation_url_optional": "Dokumentaation URL (valinnainen)", + "object_notes_optional": "Muistiinpanot (valinnainen)", + "nfc_tag_id_optional": "NFC-tunnisteen tunnus (valinnainen)", + "nfc_tags_empty_help": "Home Assistantiin ei ole vielä rekisteröity NFC-tunnisteita.", + "nfc_tags_open_settings": "Avaa tunnisteiden asetukset", + "nfc_tags_refresh": "Päivitä", + "environmental_entity_optional": "Ympäristöanturi (valinnainen)", + "environmental_entity_helper": "esim. sensor.outdoor_temperature — säätää väliä ympäristöolosuhteiden mukaan", + "environmental_attribute_optional": "Ympäristöattribuutti (valinnainen)", + "nfc_tag_id": "NFC-tunnisteen tunnus", + "nfc_linked": "NFC-tunniste linkitetty", + "nfc_link_hint": "Linkitä NFC-tunniste napsauttamalla", + "responsible_user": "Vastuukäyttäjä", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ei käyttäjää määritetty)", + "all_users": "Kaikki käyttäjät", + "my_tasks": "Omat tehtävät", + "tab_calendar": "Kalenteri", + "cal_no_events": "Ei huoltoja", + "cal_window_7": "7 päivää", + "cal_window_14": "14 päivää", + "cal_window_30": "30 päivää", + "cal_window_365": "1 vuosi", + "cal_every_n_days": "{n} päivän välein", + "cal_source_time": "Aikaperusteinen", + "cal_source_time_adaptive": "Aikaperusteinen (mukautuva)", + "cal_source_sensor": "Anturiperusteinen", + "cal_predicted": "ennustettu", + "cal_confidence_high": "korkea luottamus", + "cal_confidence_medium": "keskitason luottamus", + "cal_confidence_low": "matala luottamus", + "budget_monthly": "Kuukausibudjetti", + "budget_yearly": "Vuosibudjetti", + "groups": "Ryhmät", + "new_group": "Uusi ryhmä", + "edit_group": "Muokkaa ryhmää", + "no_groups": "Ei vielä ryhmiä", + "delete_group": "Poista ryhmä", + "delete_group_confirm": "Poistetaanko ryhmä '{name}'?", + "group_select_tasks": "Valitse tehtävät", + "group_name_required": "Nimi on pakollinen", + "description_optional": "Kuvaus (valinnainen)", + "selected": "Valittu", + "loading_chart": "Ladataan kaaviotietoja...", + "hide_outliers": "Piilota poikkeamat (anturivirheet)", + "was_maintenance_needed": "Oliko tämä huolto tarpeen?", + "feedback_needed": "Tarpeen", + "feedback_not_needed": "Ei tarpeen", + "feedback_not_sure": "Ei varma", + "suggested_interval": "Ehdotettu väli", + "apply_suggestion": "Käytä", + "reanalyze": "Analysoi uudelleen", + "reanalyze_result": "Uusi analyysi", + "reanalyze_insufficient_data": "Ei tarpeeksi tietoja suosituksen tekemiseen", + "data_points": "tietopistettä", + "dismiss_suggestion": "Hylkää", + "confidence_low": "Matala", + "confidence_medium": "Keskitaso", + "confidence_high": "Korkea", + "recommended": "suositeltu", + "seasonal_awareness": "Kausivaihtelun huomiointi", + "edit_seasonal_overrides": "Muokkaa kausikertoimia", + "seasonal_overrides_title": "Kausikertoimet (ohitus)", + "seasonal_overrides_hint": "Kerroin kuukautta kohden (0,1–5,0). Tyhjä = opitaan automaattisesti.", + "seasonal_override_invalid": "Virheellinen arvo", + "seasonal_override_range": "Kertoimen on oltava välillä 0,1–5,0", + "clear_all": "Tyhjennä kaikki", + "seasonal_chart_title": "Kausikertoimet", + "seasonal_learned": "Opittu", + "seasonal_manual": "Manuaalinen", + "month_jan": "Tammi", + "month_feb": "Helmi", + "month_mar": "Maalis", + "month_apr": "Huhti", + "month_may": "Touko", + "month_jun": "Kesä", + "month_jul": "Heinä", + "month_aug": "Elo", + "month_sep": "Syys", + "month_oct": "Loka", + "month_nov": "Marras", + "month_dec": "Joulu", + "sensor_prediction": "Anturiennuste", + "degradation_trend": "Trendi", + "trend_rising": "Nouseva", + "trend_falling": "Laskeva", + "trend_stable": "Vakaa", + "trend_insufficient_data": "Riittämättömät tiedot", + "days_until_threshold": "Päiviä kynnysarvoon", + "threshold_exceeded": "Kynnysarvo ylitetty", + "environmental_adjustment": "Ympäristökerroin", + "sensor_prediction_urgency": "Anturi ennustaa kynnysarvon noin {days} päivässä", + "day_short": "pv", + "weibull_reliability_curve": "Luotettavuuskäyrä", + "weibull_failure_probability": "Vikaantumistodennäköisyys", + "weibull_r_squared": "Sovitus R²", + "beta_early_failures": "Varhaiset viat", + "beta_random_failures": "Satunnaiset viat", + "beta_wear_out": "Kuluminen", + "beta_highly_predictable": "Erittäin ennustettava", + "confidence_interval": "Luottamusväli", + "confidence_conservative": "Varovainen", + "confidence_aggressive": "Optimistinen", + "current_interval_marker": "Nykyinen väli", + "recommended_marker": "Suositeltu", + "characteristic_life": "Ominaiselinikä", + "chart_mini_sparkline": "Trendikäyrä", + "chart_history": "Kustannus- ja kestohistoria", + "chart_seasonal": "Kausikertoimet, 12 kuukautta", + "chart_weibull": "Weibull-luotettavuuskäyrä", + "chart_sparkline": "Anturin laukaisuarvon kaavio", + "days_progress": "Päivien edistyminen", + "qr_code": "QR-koodi", + "qr_generating": "Luodaan QR-koodia…", + "qr_error": "QR-koodin luonti epäonnistui.", + "qr_error_no_url": "HA:n URL-osoitetta ei ole määritetty. Aseta ulkoinen tai sisäinen URL kohdassa Asetukset → Järjestelmä → Verkko.", + "save_error": "Tallennus epäonnistui. Yritä uudelleen.", + "qr_print": "Tulosta", + "qr_download": "Lataa SVG", + "qr_action": "Toiminto skannattaessa", + "qr_action_view": "Näytä huoltotiedot", + "qr_action_complete": "Merkitse huolto valmiiksi", + "qr_url_mode": "Linkin tyyppi", + "qr_mode_companion": "Companion-sovellus", + "qr_mode_local": "Paikallinen (mDNS)", + "qr_mode_server": "Palvelimen URL", + "overview": "Yleiskatsaus", + "analysis": "Analyysi", + "recent_activities": "Viimeaikaiset toimet", + "search_notes": "Hae muistiinpanoja", + "avg_cost": "Keskim. kustannus", + "no_advanced_features": "Lisäominaisuuksia ei ole otettu käyttöön", + "no_advanced_features_hint": "Ota käyttöön ”Mukautuvat välit” tai ”Kausivaihtelut” integraation asetuksissa nähdäksesi analyysitiedot täällä.", + "analysis_not_enough_data": "Ei vielä tarpeeksi tietoja analyysiin.", + "analysis_not_enough_data_hint": "Weibull-analyysi vaatii vähintään 5 suoritettua huoltoa; kausivaihtelut näkyvät kun kuukautta kohden on yli 6 tietopistettä.", + "analysis_manual_task_hint": "Manuaaliset tehtävät ilman väliä eivät tuota analyysitietoja.", + "completions": "suoritusta", + "current": "Nykyinen", + "shorter": "Lyhyempi", + "longer": "Pidempi", + "normal": "Normaali", + "disabled": "Pois käytöstä", + "compound_logic": "Yhdistelmälogiikka", + "compound": "Yhdistetty (useita ehtoja)", + "compound_logic_and": "JA — kaikkien ehtojen on täytyttävä", + "compound_logic_or": "TAI — mikä tahansa ehto riittää", + "compound_help": "Yhdistä useita anturiehtoja yhdeksi laukaisimeksi.", + "compound_no_conditions": "Ei vielä ehtoja — lisää vähintään yksi.", + "compound_add_condition": "Lisää ehto", + "compound_condition": "Ehto", + "compound_remove_condition": "Poista ehto", + "card_title": "Otsikko", + "card_show_header": "Näytä otsikko tilastoineen", + "card_show_actions": "Näytä toimintopainikkeet", + "card_compact": "Tiivis tila", + "card_max_items": "Enimmäismäärä (0 = kaikki)", + "card_filter_status": "Suodata tilan mukaan", + "card_filter_status_help": "Tyhjä = näytä kaikki tilat.", + "card_filter_objects": "Suodata kohteiden mukaan", + "card_filter_objects_help": "Tyhjä = näytä kaikki kohteet.", + "card_filter_entities": "Suodata entiteettien mukaan (entity_id)", + "card_filter_entities_help": "Valitse sensor- / binary_sensor-entiteetit tästä integraatiosta. Tyhjä = kaikki.", + "card_loading_objects": "Ladataan kohteita…", + "card_load_error": "Kohteita ei voitu ladata — tarkista WebSocket-yhteys.", + "card_no_tasks_title": "Ei vielä huoltotehtäviä", + "card_no_tasks_cta": "→ Luo sellainen Huolto-paneelissa", + "no_objects": "Ei vielä kohteita.", + "action_error": "Toiminto epäonnistui. Yritä uudelleen.", + "area_id_optional": "Alue (valinnainen)", + "installation_date_optional": "Asennuspäivä (valinnainen)", + "warranty_expiry_optional": "Takuun päättyminen (valinnainen)", + "warranty": "Takuu", + "warranty_valid_until": "voimassa {date} asti", + "warranty_expires_in": "päättyy {days} päivän kuluttua", + "warranty_expired": "päättynyt", + "cal_past_windows": "Menneet jaksot", + "cal_forward_windows": "Tulevat jaksot", + "history_edit_title": "Muokkaa historiamerkintää", + "history_edit_timestamp": "Aikaleima", + "manufacturer": "Valmistaja", + "model": "Malli", + "area": "Alue", + "actions": "Toiminnot", + "view_mode_label": "Näkymä", + "view_cards": "Korttinäkymä", + "view_table": "Taulukkonäkymä", + "objects_table_columns_label": "Kohdetaulukon sarakkeet", + "objects_table_columns_hint": "Valitse, mitkä sarakkeet näkyvät kohdetaulukkonäkymässä.", + "custom_icon_optional": "Kuvake (valinnainen, esim. mdi:wrench)", + "task_enabled": "Tehtävä käytössä", + "skip_reason_prompt": "Ohitetaanko tämä tehtävä?", + "reason_optional": "Syy (valinnainen)", + "reset_date_prompt": "Merkitäänkö tehtävä suoritetuksi?", + "reset_date_optional": "Viimeksi suoritettu -päivä (valinnainen, oletus tänään)", + "notes_label": "Muistiinpanot", + "documentation_label": "Dokumentaatio", + "no_nfc_tag": "— Ei tunnistetta —", + "dashboard": "Kojelauta", + "tab_today": "Tänään", + "palette_placeholder": "Hae kohteita ja tehtäviä…", + "palette_no_results": "Ei osumia", + "palette_hint": "↑↓ liiku · Enter avaa · Esc sulje", + "today_all_caught_up": "Kaikki hoidettu! Tällä viikolla ei ole mitään.", + "today_overdue": "Myöhässä", + "today_due_today": "Erääntyy tänään", + "today_this_week": "Tällä viikolla", + "settings": "Asetukset", + "settings_features": "Lisäominaisuudet", + "settings_features_desc": "Ota lisäominaisuudet käyttöön tai pois käytöstä. Poistaminen piilottaa ne käyttöliittymästä mutta ei poista tietoja.", + "feat_adaptive": "Mukautuva ajoitus", + "feat_adaptive_desc": "Opi optimaaliset välit huoltohistoriasta", + "feat_predictions": "Anturiennusteet", + "feat_predictions_desc": "Ennusta laukaisupäivät anturin kulumisesta", + "feat_seasonal": "Kausisäädöt", + "feat_seasonal_desc": "Säädä välejä kausivaihteluiden mukaan", + "feat_environmental": "Ympäristökorrelaatio", + "feat_environmental_desc": "Korreloi välit lämpötilan/kosteuden kanssa", + "feat_budget": "Budjettiseuranta", + "feat_budget_desc": "Seuraa kuukausittaisia ja vuosittaisia huoltokuluja", + "feat_groups": "Tehtäväryhmät", + "feat_groups_desc": "Järjestä tehtävät loogisiin ryhmiin", + "feat_checklists": "Tarkistuslistat", + "feat_checklists_desc": "Monivaiheiset menettelyt tehtävän suorittamiseen", + "settings_general": "Yleiset", + "settings_default_warning": "Oletusvaroituspäivät", + "settings_panel_enabled": "Sivupalkin paneeli", + "settings_panel_title": "Sivupalkin paneelin otsikko", + "settings_notifications": "Ilmoitukset", + "settings_notify_service": "Ilmoituspalvelu", + "test_notification": "Testi-ilmoitus", + "send_test": "Lähetä testi", + "testing": "Lähetetään…", + "test_notification_success": "Testi-ilmoitus lähetetty", + "test_notification_failed": "Testi-ilmoitus epäonnistui", + "settings_notify_due_soon": "Ilmoita, kun erääntyy pian", + "settings_notify_overdue": "Ilmoita, kun myöhässä", + "settings_notify_triggered": "Ilmoita, kun laukaistu", + "settings_interval_hours": "Toistoväli (tuntia, 0 = kerran)", + "settings_quiet_hours": "Hiljaiset tunnit", + "settings_quiet_start": "Alku", + "settings_quiet_end": "Loppu", + "settings_max_per_day": "Ilmoituksia enintään päivässä (0 = rajoittamaton)", + "settings_bundling": "Niputa ilmoitukset", + "settings_bundle_threshold": "Niputuksen kynnys", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobiilitoimintopainikkeet", + "settings_action_complete": "Näytä Valmis-painike", + "settings_action_skip": "Näytä Ohita-painike", + "settings_action_snooze": "Näytä Torkku-painike", + "settings_weekly_digest": "Viikkokooste", + "settings_weekly_digest_hint": "Yksi kooste-ilmoitus maanantaiaamuna, kun tehtäviä on.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Torkun kesto (tuntia)", + "settings_budget": "Budjetti", + "settings_currency": "Valuutta", + "settings_budget_monthly": "Kuukausibudjetti", + "settings_budget_yearly": "Vuosibudjetti", + "settings_budget_alerts": "Budjettihälytykset", + "settings_budget_threshold": "Hälytyskynnys (%)", + "settings_import_export": "Tuonti / Vienti", + "settings_export_json": "Vie JSON", + "settings_export_yaml": "Vie YAML", + "settings_export_csv": "Vie CSV", + "settings_import_csv": "Tuo CSV", + "settings_import_placeholder": "Liitä JSON- tai CSV-sisältö tähän…", + "settings_import_btn": "Tuo", + "settings_import_success": "{count} kohdetta tuotu onnistuneesti.", + "settings_export_success": "Vienti ladattu.", + "settings_saved": "Asetus tallennettu.", + "settings_include_history": "Sisällytä historia", + "sort_alphabetical": "Aakkosjärjestys", + "sort_due_soonest": "Erääntyy pian", + "sort_task_count": "Tehtävien määrä", + "sort_area": "Alue", + "sort_assigned_user": "Määritetty käyttäjä", + "sort_group": "Ryhmä", + "groupby_none": "Ei ryhmittelyä", + "groupby_area": "Alueen mukaan", + "groupby_group": "Ryhmän mukaan", + "groupby_user": "Käyttäjän mukaan", + "filter_label": "Suodata", + "user_label": "Käyttäjä", + "sort_label": "Lajittele", + "group_by_label": "Ryhmittele", + "state_value_help": "Käytä HA:n tila-arvoa (yleensä pienillä kirjaimilla, esim. \"on\"/\"off\"). Kirjainkoko normalisoidaan tallennettaessa.", + "target_changes_help": "Vastaavien siirtymien määrä ennen laukaisua (oletus: 1).", + "qr_print_title": "Tulosta QR-koodit", + "qr_print_desc": "Luo tulostettava sivu QR-koodeja, jotka voit leikata ja kiinnittää laitteisiin.", + "qr_print_load": "Lataa kohteet", + "qr_print_filter": "Suodata", + "qr_print_objects": "Kohteet", + "qr_print_actions": "Toiminnot", + "qr_print_url_mode": "Linkin tyyppi", + "qr_print_estimate": "Arvioitu QR-koodien määrä", + "qr_print_over_limit": "raja on 200, rajaa suodatinta", + "qr_print_generate": "Luo QR-koodit", + "qr_print_generating": "Luodaan…", + "qr_print_ready": "QR-koodit valmiina", + "qr_print_print_button": "Tulosta", + "qr_print_empty": "Ei mitään luotavaa", + "qr_action_skip": "Ohita", + "vacation_title": "Lomatila", + "vacation_active": "aktiivinen", + "vacation_ended": "päättynyt", + "vacation_desc": "Suunnittele loma: ilmoitukset keskeytetään jakson ajaksi plus puskuripäivien ajaksi. Voit ottaa tietyt tehtävät takaisin käyttöön.", + "vacation_enable": "Ota lomatila käyttöön", + "vacation_start": "Alku", + "vacation_end": "Loppu", + "vacation_buffer": "Puskuri (päivää)", + "vacation_exempt_title": "Ilmoita silti loman aikana", + "vacation_exempt_desc": "Valitse tehtävät, joista ilmoitetaan silti loman aikana (esim. kriittinen altaan kemia).", + "vacation_load_tasks": "Lataa tehtävät", + "vacation_preview_btn": "Näytä esikatselu", + "vacation_preview_affected": "tehtävää koskee", + "vacation_event_due_soon": "erääntyy pian", + "vacation_event_overdue": "menee myöhässä", + "vacation_event_triggered_est": "anturin laukaisu mahdollinen", + "vacation_sensor_based": "(anturiperusteinen)", + "vacation_action_notify": "Ilmoita silti", + "vacation_action_unsilence": "Vaimenna uudelleen", + "vacation_marked_complete": "Merkitty valmiiksi", + "vacation_marked_skip": "Ohitettu", + "vacation_end_now": "Lopeta loma nyt", + "add": "Lisää", + "show_stats": "Näytä tilastot ja kaaviot", + "hide_stats": "Piilota tilastot", + "adaptive_no_data": "Ei vielä tarpeeksi suoritushistoriaa mukautuvaan analyysiin. Suorita tämä tehtävä muutaman kerran lisää avataksesi väliehdotukset ja luotettavuuskaaviot.", + "suggestion_applied": "Ehdotettu väli otettu käyttöön", + "vacation_mode": "Lomatila", + "vacation_status_active": "Aktiivinen nyt", + "vacation_status_scheduled": "Ajoitettu", + "vacation_status_inactive": "Ei aktiivinen", + "vacation_end_now_confirm": "Lopetetaanko loma heti?", + "vacation_exempt_count": "poikkeusta", + "vacation_advanced": "Lisäasetukset…", + "vacation_open_panel": "Avaa paneelissa", + "enable": "Ota käyttöön", + "saved": "Tallennettu", + "budget_monthly_set": "Aseta kuukausittain", + "budget_yearly_set": "Aseta vuosittain", + "budget_advanced": "Valuutta, hälytykset…", + "budget_open_panel": "Avaa paneelissa", + "groups_empty": "Ei vielä ryhmiä.", + "group_new_placeholder": "Lisää ryhmä…", + "group_delete_confirm": "Poistetaanko ryhmä \"{name}\"?", + "groups_manage_tasks": "Hallinnoi tehtävien määrityksiä…", + "groups_open_panel": "Avaa paneelissa", + "unassigned": "Ei määritetty", + "no_area": "Ei aluetta", + "has_overdue": "Sisältää myöhässä olevia tehtäviä", + "object": "Kohde", + "settings_panel_access": "Paneelin käyttöoikeus", + "settings_panel_access_desc": "Ylläpitäjillä on aina täydet oikeudet. Delegoidaksesi luonnin, muokkauksen ja poiston tietyille ei-ylläpitäjille, kytke tämä päälle ja valitse heidät alta — muut näkevät vain Valmis ja Ohita.", + "settings_operator_write": "Salli valittujen käyttäjien luoda, muokata ja poistaa", + "settings_operator_write_desc": "Pois: vain ylläpitäjät voivat muuttaa sisältöä. Päällä: myös alla valitut käyttäjät saavat täydet oikeudet.", + "no_non_admin_users": "Ei-ylläpitäjäkäyttäjiä ei löytynyt. Lisää heitä kohdassa Asetukset → Henkilöt.", + "owner_label": "Omistaja", + "feat_completion_actions": "Suoritustoiminnot", + "feat_completion_actions_desc": "Tehtäväkohtainen HA-toiminto suorituksen yhteydessä + pikasuoritus-QR esiasetetuilla arvoilla.", + "on_complete_action_title": "Suorituksen yhteydessä: laukaise HA-toiminto (valinnainen)", + "on_complete_action_desc": "Kutsuu HA-palvelua, kun tehtävä suoritetaan — esim. nollaa laitteen laskuri.", + "on_complete_action_service": "Palvelu", + "on_complete_action_target": "Kohde-entiteetti", + "on_complete_action_target_hint": "Huom: entiteetin toimialueen on vastattava palvelua — esim. 'button.press' toimii vain button.*-entiteeteillä, 'counter.increment' vain counter.*-entiteeteillä, 'input_button.press' vain input_button.*-entiteeteillä jne. Epäsopivuuden sattuessa toiminto epäonnistuu hiljaisesti (HA kirjaa 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Tiedot (JSON, valinnainen)", + "on_complete_action_test": "Tarkista asetukset", + "on_complete_action_test_success": "✓ Asetukset kelvolliset (toiminto laukeaa vain tehtävän suorituksen yhteydessä)", + "on_complete_action_test_failed": "Epäonnistui", + "quick_complete_defaults_title": "Pikasuorituksen oletukset (QR-skannauksille, valinnainen)", + "quick_complete_defaults_desc": "Esiasetetut arvot pikasuoritus-QR-skannauksille. Ilman näitä QR avaa suoritusikkunan.", + "quick_complete_defaults_notes": "Muistiinpanot", + "quick_complete_defaults_cost": "Kustannus", + "quick_complete_defaults_duration": "Kesto (minuuttia)", + "quick_complete_defaults_feedback_none": "Ei palautetta", + "quick_complete_defaults_feedback_needed": "Oli tarpeen", + "quick_complete_defaults_feedback_not_needed": "Ei tarpeen", + "quick_complete_success": "Merkitty nopeasti valmiiksi", + "show_all_objects": "Näytä kaikki kohteet", + "show_all_tasks": "Tyhjennä suodatin — näytä kaikki tehtävät", + "filter_to_overdue": "Suodata tehtävälista vain myöhässä oleviin", + "filter_to_due_soon": "Suodata tehtävälista vain pian erääntyviin", + "filter_to_triggered": "Suodata tehtävälista vain laukaistuihin", + "open_task": "Avaa tehtävä", + "show_details": "Näytä historia ja tilastot", + "hide_details": "Piilota tiedot", + "history_empty": "Ei vielä historiaa.", + "history_edit_button": "Muokkaa merkintää", + "total_cost": "Kokonaiskustannus", + "times_performed": "Suoritettu", + "older_entries": "vanhempaa", + "open_in_panel": "Avaa Huolto-paneelissa", + "skip_reason": "Ohituksen syy (valinnainen)", + "reset_to_date": "Nollaa viimeksi suoritettu -päivä", + "delete_task_confirm": "Poistetaanko tämä tehtävä ja sen historia?", + "delete_object_confirm": "Poistetaanko tämä kohde ja kaikki sen tehtävät?", + "loading": "Ladataan…", + "archive": "Arkistoi", + "undo": "Kumoa", + "task_archived": "Tehtävä arkistoitu", + "object_archived": "Kohde arkistoitu", + "unarchive": "Palauta arkistosta", + "archived": "Arkistoitu", + "show_archived": "Näytä arkistoidut", + "hide_archived": "Piilota arkistoidut", + "confirm_archive_object": "Arkistoidaanko tämä kohde ja sen tehtävät? Historia säilyy ja sen voi palauttaa myöhemmin.", + "settings_archive": "Arkistointi ja säilytys", + "settings_archive_desc": "Siirrä valmiit kertatehtävät arkistoon poistamatta niitä. Arkistoidut kohteet ovat piilossa ja passiivisia, mutta säilyttävät historian ja kustannukset.", + "settings_archive_oneoff_days": "Arkistoi valmiit kertatehtävät automaattisesti (päivää, 0 = pois)", + "settings_delete_archived_oneoff_days": "Poista arkistoidut kertatehtävät automaattisesti (päivää, 0 = ei koskaan)", + "archive_object": "Arkistoi kohde", + "unarchive_object": "Palauta kohde arkistosta", + "documents": "Asiakirjat", + "documents_empty": "Ei vielä asiakirjoja.", + "doc_upload": "Lataa tiedosto", + "doc_uploading": "Ladataan…", + "doc_add_link": "Lisää linkki", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Otsikko (valinnainen)", + "doc_open": "Avaa", + "doc_delete_confirm": "Poistetaanko \"{name}\"?", + "doc_too_large": "Tiedosto on liian suuri (enint. 25 Mt).", + "doc_upload_failed": "Lataus epäonnistui.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Jo tallennettu muualle — jaettu, ei lisätilaa.", + "doc_dup_in_object": "Tämä tiedosto on jo liitetty tähän kohteeseen.", + "doc_link_invalid": "Vain http/https-linkit ovat sallittuja.", + "doc_cat_manual": "Käyttöohje", + "doc_cat_warranty": "Takuu", + "doc_cat_invoice": "Lasku", + "doc_cat_spare_parts": "Varaosat", + "doc_cat_photo": "Valokuva", + "doc_cat_other": "Muu", + "doc_link_badge": "Linkki", + "doc_storage_title": "Asiakirjojen tallennustila", + "doc_storage_saved": "Säästetty deduplikoinnilla", + "doc_storage_refresh": "Päivitä", + "doc_download": "Lataa", + "doc_close": "Sulje", + "doc_camera": "Ota kuva", + "doc_drop_hint": "Pudota tiedostot tähän", + "doc_task_none": "Tähän tehtävään ei ole liitetty asiakirjoja.", + "doc_link_existing": "Liitä asiakirja…", + "doc_attach": "Liitä", + "doc_unlink": "Poista liitos", + "doc_page": "Sivu", + "chart_range_7d": "7pv", + "chart_range_30d": "30pv", + "chart_range_90d": "90pv", + "chart_range_1y": "1v", + "chart_since_service": "edellisen huollon jälkeen", + "chart_no_stats": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään vain huoltotapahtumien arvot", + "auto_complete_on_recovery": "Suorita automaattisesti, kun anturi palautuu", + "auto_complete_on_recovery_help": "Kirjaa suorituksen (asettaa viimeksi tehdyn), kun laukaisin poistuu itsestään — esim. suola lisätty, suodatin vaihdettu.", + "doc_search": "Hae asiakirjoja…", + "doc_search_none": "Ei vastaavia asiakirjoja", + "link_device_optional": "Liitä olemassa olevaan laitteeseen (valinnainen)", + "parent_object_optional": "Yläobjekti (valinnainen)", + "parent_none": "(Ei yläobjektia)", + "paused": "Keskeytetty", + "pause_object": "Keskeytä", + "resume_object": "Jatka", + "pause_until_prompt": "Jäädytä tämän kohteen aikataulut — mikään ei eräänny eikä ilmoita ennen jatkamista. Aseta halutessasi automaattisen jatkamisen päivämäärä.", + "pause_until_label": "Jatka (valinnainen)", + "object_paused": "Kohde keskeytetty", + "object_resumed": "Kohde jatkettu — aikataulut käynnistetty uudelleen", + "object_paused_badge": "Keskeytetty", + "paused_until_label": "asti", + "replace_object": "Korvaa…", + "replace_object_prompt": "Poista tämä kohde käytöstä ja luo seuraaja. Historia ja kustannukset pysyvät arkistoituina vanhassa; tehtävät ja asiakirjat siirtyvät uuteen, laskurit alkavat alusta.", + "replace_name_label": "Seuraajan nimi", + "object_replaced": "Kohde korvattu — seuraaja luotu", + "reading_unit_label": "Lukeman yksikkö (esim. kWh, m³)", + "reading_unit_help": "Näytetään kirjatun arvon vieressä tehtävää suoritettaessa.", + "reading_value_label": "Lukema", + "reading_label": "Lukema", + "settings_templates_label": "Mallipohjagalleria", + "settings_templates_hint": "Poista valinta mallipohjista, joita et koskaan tarvitse — ne katoavat \"Mallipohjasta\"-valitsimista (paneeli ja config flow). Mikään muu ei muutu; voi ottaa käyttöön uudelleen milloin tahansa.", + "worksheet": "Työlomake", + "worksheet_scan_view": "Skannaa avataksesi tehtävän", + "worksheet_scan_complete": "Skannaa suorittaaksesi", + "worksheet_manual_excerpt": "Ote käyttöohjeesta", + "worksheet_pages": "sivut", + "worksheet_printed": "Tulostettu", + "worksheet_never": "Ei koskaan" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/fr.json b/custom_components/maintenance_supporter/frontend-src/locales/fr.json new file mode 100644 index 0000000..46cfd17 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/fr.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Maintenance", + "objects": "Objets", + "tasks": "Tâches", + "overdue": "En retard", + "due_soon": "Bientôt dû", + "triggered": "Déclenché", + "ok": "OK", + "all": "Tous", + "new_object": "+ Nouvel objet", + "templates_from": "Depuis un modèle", + "templates_title": "Partir d'un modèle", + "templates_task_count": "{n} tâches", + "template_created": "Créé depuis un modèle", + "onboard_hint": "Ajoutez votre premier objet pour suivre l'entretien.", + "edit": "Modifier", + "duplicate": "Dupliquer", + "task_duplicated": "Tâche dupliquée", + "object_duplicated": "Objet dupliqué", + "delete": "Supprimer", + "add_task": "+ Tâche", + "complete": "Terminé", + "completed": "Terminé", + "skip": "Passer", + "skipped": "Ignoré", + "missed": "Missed", + "reset": "Réinitialiser", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annuler", + "bulk_select": "Sélectionner", + "bulk_select_all": "Tout sélectionner", + "bulk_n_selected": "{n} sélectionné(s)", + "bulk_completed": "{n} tâches terminées", + "bulk_archived": "{n} tâches archivées", + "completing": "En cours…", + "interval": "Intervalle", + "warning": "Avertissement", + "last_performed": "Dernière exécution", + "next_due": "Prochaine échéance", + "days_until_due": "Jours restants", + "avg_duration": "Ø Durée", + "trigger": "Déclencheur", + "trigger_type": "Type de déclencheur", + "threshold_above": "Limite supérieure", + "threshold_below": "Limite inférieure", + "threshold": "Seuil", + "counter": "Compteur", + "state_change": "Changement d'état", + "runtime": "Durée de fonctionnement", + "runtime_hours": "Durée cible (heures)", + "target_value": "Valeur cible", + "baseline": "Ligne de base", + "target_changes": "Changements cibles", + "for_minutes": "Pendant (minutes)", + "time_based": "Temporel", + "sensor_based": "Capteur", + "manual": "Manuel", + "one_time": "Unique", + "weekdays": "Jours de semaine", + "nth_weekday": "N-ième jour de la semaine du mois", + "day_of_month": "Jour du mois", + "recurrence_on_days": "Répéter le", + "recurrence_occurrence": "Occurrence", + "recurrence_weekday": "Jour de semaine", + "recurrence_day": "Jour du mois (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1er", + "ord_2": "2e", + "ord_3": "3e", + "ord_4": "4e", + "ord_5": "5e", + "ord_last": "Dernier", + "day_word": "Jour", + "interval_value": "Intervalle", + "interval_unit": "Unité", + "unit_days": "Jours", + "unit_weeks": "Semaines", + "unit_months": "Mois", + "unit_years": "Années", + "due_date": "Date d'échéance", + "cleaning": "Nettoyage", + "inspection": "Inspection", + "replacement": "Remplacement", + "calibration": "Étalonnage", + "service": "Service", + "reading": "Relevé", + "custom": "Personnalisé", + "history": "Historique", + "cost": "Coût", + "report_button": "Rapport", + "report_title": "Rapport d'entretien", + "report_generated": "Généré", + "report_times_done": "Effectué", + "report_total_cost": "Coût total", + "report_every": "tous les {n} {unit}", + "report_notes": "Notes", + "report_col_type": "Type", + "report_col_status": "Statut", + "report_col_schedule": "Planning", + "duration": "Durée", + "both": "Les deux", + "trigger_val": "Valeur du déclencheur", + "complete_title": "Terminé : ", + "checklist": "Checklist", + "checklist_steps_optional": "Étapes de la checklist (optionnel)", + "checklist_placeholder": "Nettoyer le filtre\nRemplacer le joint\nTester la pression", + "checklist_help": "Une étape par ligne. Max 100 éléments.", + "err_too_long": "{field} : trop long (max {n} caractères)", + "err_too_short": "{field} : trop court (min {n} caractères)", + "err_value_too_high": "{field} : trop grand (max {n})", + "err_value_too_low": "{field} : trop petit (min {n})", + "err_required": "{field} : champ obligatoire", + "err_wrong_type": "{field} : mauvais type (attendu : {type})", + "err_invalid_choice": "{field} : valeur non autorisée", + "err_invalid_value": "{field} : valeur invalide", + "feat_schedule_time": "Planification à l'heure", + "feat_schedule_time_desc": "Les tâches arrivent à échéance à une heure précise plutôt qu'à minuit.", + "schedule_time_optional": "Échéance à l'heure (optionnel, HH:MM)", + "schedule_time_help": "Vide = minuit (défaut). Fuseau horaire HA.", + "at_time": "à", + "notes_optional": "Notes (optionnel)", + "cost_optional": "Coût (optionnel)", + "duration_minutes": "Durée en minutes (optionnel)", + "days": "jours", + "day": "jour", + "today": "Aujourd'hui", + "d_overdue": "j en retard", + "no_tasks": "Aucune tâche de maintenance. Créez un objet pour commencer.", + "no_tasks_short": "Aucune tâche", + "no_history": "Aucun historique.", + "show_all": "Tout afficher", + "cost_duration_chart": "Coûts & Durée", + "installed": "Installé", + "confirm_delete_object": "Supprimer cet objet et toutes ses tâches ?", + "confirm_delete_task": "Supprimer cette tâche ?", + "min": "Min", + "max": "Max", + "save": "Enregistrer", + "saving": "Enregistrement…", + "edit_task": "Modifier la tâche", + "new_task": "Nouvelle tâche de maintenance", + "task_name": "Nom de la tâche", + "maintenance_type": "Type de maintenance", + "priority": "Priorité", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Basse", + "priority_normal": "Normale", + "priority_high": "Haute", + "schedule_type": "Type de planification", + "interval_days": "Intervalle (jours)", + "warning_days": "Jours d'avertissement", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Dernière exécution (optionnel)", + "interval_anchor": "Ancrage de l'intervalle", + "anchor_completion": "Depuis la date de réalisation", + "anchor_planned": "Depuis la date prévue (sans dérive)", + "edit_object": "Modifier l'objet", + "name": "Nom", + "manufacturer_optional": "Fabricant (optionnel)", + "model_optional": "Modèle (optionnel)", + "serial_number_optional": "Numéro de série (optionnel)", + "serial_number_label": "N/S", + "documentation_url_label": "Manuel", + "object_notes_label": "Notes", + "sort_due_date": "Échéance", + "sort_object": "Nom de l'objet", + "sort_type": "Type", + "sort_task_name": "Nom de la tâche", + "all_objects": "Tous les objets", + "tasks_lower": "tâches", + "no_tasks_yet": "Pas encore de tâches", + "add_first_task": "Ajouter la première tâche", + "trigger_configuration": "Configuration du déclencheur", + "entity_id": "ID d'entité", + "comma_separated": "séparé par des virgules", + "entity_logic": "Logique d'entité", + "entity_logic_any": "N'importe quelle entité déclenche", + "entity_logic_all": "Toutes les entités doivent déclencher", + "entities": "entités", + "attribute_optional": "Attribut (optionnel, vide = état)", + "use_entity_state": "Utiliser l'état de l'entité (pas d'attribut)", + "trigger_above": "Déclencher au-dessus de", + "trigger_below": "Déclencher en dessous de", + "for_at_least_minutes": "Pendant au moins (minutes)", + "safety_interval_days": "Intervalle de sécurité (jours, optionnel)", + "safety_interval": "Intervalle de sécurité (optionnel)", + "delta_mode": "Mode delta", + "from_state_optional": "État source (optionnel)", + "to_state_optional": "État cible (optionnel)", + "documentation_url_optional": "URL de documentation (optionnel)", + "object_notes_optional": "Notes (facultatif)", + "nfc_tag_id_optional": "ID tag NFC (optionnel)", + "nfc_tags_empty_help": "Aucun tag NFC enregistré dans Home Assistant pour le moment.", + "nfc_tags_open_settings": "Ouvrir les réglages des tags", + "nfc_tags_refresh": "Actualiser", + "environmental_entity_optional": "Capteur d'environnement (optionnel)", + "environmental_entity_helper": "ex. sensor.temperature_exterieure — ajuste l'intervalle selon les conditions environnementales", + "environmental_attribute_optional": "Attribut d'environnement (optionnel)", + "nfc_tag_id": "ID tag NFC", + "nfc_linked": "Tag NFC lié", + "nfc_link_hint": "Cliquer pour associer un tag NFC", + "responsible_user": "Utilisateur responsable", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Aucun utilisateur assigné)", + "all_users": "Tous les utilisateurs", + "my_tasks": "Mes tâches", + "tab_calendar": "Calendrier", + "cal_no_events": "Aucun entretien", + "cal_window_7": "7 jours", + "cal_window_14": "14 jours", + "cal_window_30": "30 jours", + "cal_window_365": "1 an", + "cal_every_n_days": "tous les {n} jours", + "cal_source_time": "Basé sur le temps", + "cal_source_time_adaptive": "Basé sur le temps (adaptatif)", + "cal_source_sensor": "Basé sur capteur", + "cal_predicted": "prédit", + "cal_confidence_high": "haute confiance", + "cal_confidence_medium": "confiance moyenne", + "cal_confidence_low": "faible confiance", + "budget_monthly": "Budget mensuel", + "budget_yearly": "Budget annuel", + "groups": "Groupes", + "new_group": "Nouveau groupe", + "edit_group": "Modifier le groupe", + "no_groups": "Aucun groupe pour l'instant", + "delete_group": "Supprimer le groupe", + "delete_group_confirm": "Supprimer le groupe '{name}' ?", + "group_select_tasks": "Sélectionner les tâches", + "group_name_required": "Nom requis", + "description_optional": "Description (optionnel)", + "selected": "Sélectionné", + "loading_chart": "Chargement des données...", + "hide_outliers": "Masquer les valeurs aberrantes (erreurs capteur)", + "was_maintenance_needed": "Cette maintenance était-elle nécessaire ?", + "feedback_needed": "Nécessaire", + "feedback_not_needed": "Pas nécessaire", + "feedback_not_sure": "Pas sûr", + "suggested_interval": "Intervalle suggéré", + "apply_suggestion": "Appliquer", + "reanalyze": "Réanalyser", + "reanalyze_result": "Nouvelle analyse", + "reanalyze_insufficient_data": "Données insuffisantes pour une recommandation", + "data_points": "points de données", + "dismiss_suggestion": "Ignorer", + "confidence_low": "Faible", + "confidence_medium": "Moyen", + "confidence_high": "Élevé", + "recommended": "recommandé", + "seasonal_awareness": "Conscience saisonnière", + "edit_seasonal_overrides": "Modifier les facteurs saisonniers", + "seasonal_overrides_title": "Facteurs saisonniers (override)", + "seasonal_overrides_hint": "Facteur par mois (0.1–5.0). Vide = appris automatiquement.", + "seasonal_override_invalid": "Valeur invalide", + "seasonal_override_range": "Le facteur doit être entre 0.1 et 5.0", + "clear_all": "Tout effacer", + "seasonal_chart_title": "Facteurs saisonniers", + "seasonal_learned": "Appris", + "seasonal_manual": "Manuel", + "month_jan": "Jan", + "month_feb": "Fév", + "month_mar": "Mar", + "month_apr": "Avr", + "month_may": "Mai", + "month_jun": "Juin", + "month_jul": "Juil", + "month_aug": "Août", + "month_sep": "Sep", + "month_oct": "Oct", + "month_nov": "Nov", + "month_dec": "Déc", + "sensor_prediction": "Prédiction capteur", + "degradation_trend": "Tendance", + "trend_rising": "En hausse", + "trend_falling": "En baisse", + "trend_stable": "Stable", + "trend_insufficient_data": "Données insuffisantes", + "days_until_threshold": "Jours avant le seuil", + "threshold_exceeded": "Seuil dépassé", + "environmental_adjustment": "Facteur environnemental", + "sensor_prediction_urgency": "Le capteur prévoit le seuil dans ~{days} jours", + "day_short": "jour", + "weibull_reliability_curve": "Courbe de fiabilité", + "weibull_failure_probability": "Probabilité de défaillance", + "weibull_r_squared": "Ajustement R²", + "beta_early_failures": "Défaillances précoces", + "beta_random_failures": "Défaillances aléatoires", + "beta_wear_out": "Usure", + "beta_highly_predictable": "Très prévisible", + "confidence_interval": "Intervalle de confiance", + "confidence_conservative": "Conservateur", + "confidence_aggressive": "Optimiste", + "current_interval_marker": "Intervalle actuel", + "recommended_marker": "Recommandé", + "characteristic_life": "Durée de vie caractéristique", + "chart_mini_sparkline": "Sparkline de tendance", + "chart_history": "Historique coûts et durée", + "chart_seasonal": "Facteurs saisonniers, 12 mois", + "chart_weibull": "Courbe de fiabilité Weibull", + "chart_sparkline": "Graphique valeur déclencheur", + "days_progress": "Progression en jours", + "qr_code": "QR Code", + "qr_generating": "Génération du QR code…", + "qr_error": "Impossible de générer le QR code.", + "qr_error_no_url": "Aucune URL HA configurée. Veuillez définir une URL externe ou interne dans Paramètres → Système → Réseau.", + "save_error": "Échec de l'enregistrement. Veuillez réessayer.", + "qr_print": "Imprimer", + "qr_download": "Télécharger SVG", + "qr_action": "Action au scan", + "qr_action_view": "Afficher les infos de maintenance", + "qr_action_complete": "Marquer la maintenance comme terminée", + "qr_url_mode": "Type de lien", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "URL serveur", + "overview": "Aperçu", + "analysis": "Analyse", + "recent_activities": "Activités récentes", + "search_notes": "Rechercher dans les notes", + "avg_cost": "Ø Coût", + "no_advanced_features": "Aucune fonction avancée activée", + "no_advanced_features_hint": "Activez « Intervalles adaptatifs » ou « Tendances saisonnières » dans les paramètres de l'intégration pour voir les données d'analyse ici.", + "analysis_not_enough_data": "Pas encore assez de données pour l'analyse.", + "analysis_not_enough_data_hint": "L'analyse Weibull nécessite au moins 5 maintenances terminées ; les tendances saisonnières apparaissent après 6+ points par mois.", + "analysis_manual_task_hint": "Les tâches manuelles sans intervalle ne génèrent pas de données d'analyse.", + "completions": "réalisations", + "current": "Actuel", + "shorter": "Plus court", + "longer": "Plus long", + "normal": "Normal", + "disabled": "Désactivé", + "compound_logic": "Logique composée", + "compound": "Composé (plusieurs conditions)", + "compound_logic_and": "ET — toutes les conditions doivent se déclencher", + "compound_logic_or": "OU — une seule condition suffit", + "compound_help": "Combinez plusieurs conditions de capteurs en un déclencheur.", + "compound_no_conditions": "Aucune condition — ajoutez-en au moins une.", + "compound_add_condition": "Ajouter une condition", + "compound_condition": "Condition", + "compound_remove_condition": "Supprimer la condition", + "card_title": "Titre", + "card_show_header": "Afficher l'en-tête avec statistiques", + "card_show_actions": "Afficher les boutons d'action", + "card_compact": "Mode compact", + "card_max_items": "Nombre max (0 = tous)", + "card_filter_status": "Filtrer par statut", + "card_filter_status_help": "Vide = afficher tous les statuts.", + "card_filter_objects": "Filtrer par objets", + "card_filter_objects_help": "Vide = afficher tous les objets.", + "card_filter_entities": "Filtrer par entités (entity_ids)", + "card_filter_entities_help": "Choisissez des entités sensor / binary_sensor de cette intégration. Vide = toutes.", + "card_loading_objects": "Chargement des objets…", + "card_load_error": "Impossible de charger les objets — vérifiez la connexion WebSocket.", + "card_no_tasks_title": "Aucune tâche de maintenance pour l'instant", + "card_no_tasks_cta": "→ Créez-en une dans le panneau Maintenance", + "no_objects": "Aucun objet pour l'instant.", + "action_error": "Action échouée. Veuillez réessayer.", + "area_id_optional": "Zone (optionnel)", + "installation_date_optional": "Date d'installation (optionnel)", + "warranty_expiry_optional": "Fin de garantie (optionnel)", + "warranty": "Garantie", + "warranty_valid_until": "valable jusqu'au {date}", + "warranty_expires_in": "expire dans {days} jours", + "warranty_expired": "expirée", + "cal_past_windows": "Fenêtres passées", + "cal_forward_windows": "Fenêtres à venir", + "history_edit_title": "Modifier l'entrée d'historique", + "history_edit_timestamp": "Horodatage", + "manufacturer": "Fabricant", + "model": "Modèle", + "area": "Zone", + "actions": "Actions", + "view_mode_label": "Vue", + "view_cards": "Vue cartes", + "view_table": "Vue tableau", + "objects_table_columns_label": "Colonnes du tableau d'objets", + "objects_table_columns_hint": "Choisissez les colonnes affichées dans la vue tableau des objets.", + "custom_icon_optional": "Icône (optionnel, ex. mdi:wrench)", + "task_enabled": "Tâche activée", + "skip_reason_prompt": "Ignorer cette tâche ?", + "reason_optional": "Raison (optionnel)", + "reset_date_prompt": "Marquer la tâche comme effectuée ?", + "reset_date_optional": "Date de dernière exécution (optionnel, défaut : aujourd'hui)", + "notes_label": "Notes", + "documentation_label": "Documentation", + "no_nfc_tag": "— Aucun tag —", + "dashboard": "Tableau de bord", + "tab_today": "Aujourd'hui", + "palette_placeholder": "Rechercher objets et tâches…", + "palette_no_results": "Aucun résultat", + "palette_hint": "↑↓ naviguer · Entrée ouvrir · Échap fermer", + "today_all_caught_up": "Tout est à jour ! Rien cette semaine.", + "today_overdue": "En retard", + "today_due_today": "À faire aujourd'hui", + "today_this_week": "Cette semaine", + "settings": "Paramètres", + "settings_features": "Fonctions avancées", + "settings_features_desc": "Activez ou désactivez les fonctions avancées. La désactivation les masque dans l'interface mais ne supprime pas les données.", + "feat_adaptive": "Planification adaptative", + "feat_adaptive_desc": "Apprendre les intervalles optimaux à partir de l'historique", + "feat_predictions": "Prédictions capteurs", + "feat_predictions_desc": "Prédire les dates de déclenchement par dégradation des capteurs", + "feat_seasonal": "Ajustements saisonniers", + "feat_seasonal_desc": "Ajuster les intervalles selon les tendances saisonnières", + "feat_environmental": "Corrélation environnementale", + "feat_environmental_desc": "Corréler les intervalles avec la température/humidité", + "feat_budget": "Suivi budgétaire", + "feat_budget_desc": "Suivre les dépenses de maintenance mensuelles et annuelles", + "feat_groups": "Groupes de tâches", + "feat_groups_desc": "Organiser les tâches en groupes logiques", + "feat_checklists": "Checklists", + "feat_checklists_desc": "Procédures multi-étapes pour la réalisation des tâches", + "settings_general": "Général", + "settings_default_warning": "Jours d'avertissement par défaut", + "settings_panel_enabled": "Panneau latéral", + "settings_panel_title": "Titre du panneau latéral", + "settings_notifications": "Notifications", + "settings_notify_service": "Service de notification", + "test_notification": "Notification de test", + "send_test": "Envoyer le test", + "testing": "Envoi en cours…", + "test_notification_success": "Notification de test envoyée", + "test_notification_failed": "Échec de la notification de test", + "settings_notify_due_soon": "Notifier quand bientôt dû", + "settings_notify_overdue": "Notifier quand en retard", + "settings_notify_triggered": "Notifier quand déclenché", + "settings_interval_hours": "Intervalle de répétition (heures, 0 = une fois)", + "settings_quiet_hours": "Heures de silence", + "settings_quiet_start": "Début", + "settings_quiet_end": "Fin", + "settings_max_per_day": "Max notifications par jour (0 = illimité)", + "settings_bundling": "Regrouper les notifications", + "settings_bundle_threshold": "Seuil de regroupement", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Boutons d'action mobiles", + "settings_action_complete": "Afficher le bouton 'Terminé'", + "settings_action_skip": "Afficher le bouton 'Passer'", + "settings_action_snooze": "Afficher le bouton 'Reporter'", + "settings_weekly_digest": "Récapitulatif hebdomadaire", + "settings_weekly_digest_hint": "Une notification récapitulative le lundi matin quand des tâches sont dues.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Durée de report (heures)", + "settings_budget": "Budget", + "settings_currency": "Devise", + "settings_budget_monthly": "Budget mensuel", + "settings_budget_yearly": "Budget annuel", + "settings_budget_alerts": "Alertes budgétaires", + "settings_budget_threshold": "Seuil d'alerte (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Exporter JSON", + "settings_export_yaml": "Exporter YAML", + "settings_export_csv": "Exporter CSV", + "settings_import_csv": "Importer CSV", + "settings_import_placeholder": "Collez le contenu JSON ou CSV ici…", + "settings_import_btn": "Importer", + "settings_import_success": "{count} objets importés avec succès.", + "settings_export_success": "Export téléchargé.", + "settings_saved": "Paramètre enregistré.", + "settings_include_history": "Inclure l'historique", + "sort_alphabetical": "Alphabétique", + "sort_due_soonest": "Échéance la plus proche", + "sort_task_count": "Nombre de tâches", + "sort_area": "Zone", + "sort_assigned_user": "Utilisateur affecté", + "sort_group": "Groupe", + "groupby_none": "Aucun groupement", + "groupby_area": "Par zone", + "groupby_group": "Par groupe", + "groupby_user": "Par utilisateur", + "filter_label": "Filtre", + "user_label": "Utilisateur", + "sort_label": "Tri", + "group_by_label": "Grouper par", + "state_value_help": "Utilisez la valeur d'état HA (généralement en minuscules, p. ex. \"on\"/\"off\"). La casse est normalisée à l'enregistrement.", + "target_changes_help": "Nombre de transitions correspondantes avant le déclenchement (par défaut : 1).", + "qr_print_title": "Imprimer les QR codes", + "qr_print_desc": "Générer une page imprimable de QR codes à découper et coller sur votre équipement.", + "qr_print_load": "Charger les objets", + "qr_print_filter": "Filtre", + "qr_print_objects": "Objets", + "qr_print_actions": "Actions", + "qr_print_url_mode": "Type de lien", + "qr_print_estimate": "QR codes estimés", + "qr_print_over_limit": "limite à 200, affinez le filtre", + "qr_print_generate": "Générer les QR codes", + "qr_print_generating": "Génération…", + "qr_print_ready": "QR codes prêts", + "qr_print_print_button": "Imprimer", + "qr_print_empty": "Rien à générer", + "qr_action_skip": "Passer", + "vacation_title": "Mode vacances", + "vacation_active": "actif", + "vacation_ended": "terminé", + "vacation_desc": "Planifiez vos vacances : les notifications sont mises en pause pendant la période plus quelques jours tampon. Vous pouvez réactiver certaines tâches.", + "vacation_enable": "Activer le mode vacances", + "vacation_start": "Début", + "vacation_end": "Fin", + "vacation_buffer": "Tampon (jours)", + "vacation_exempt_title": "Notifier malgré les vacances", + "vacation_exempt_desc": "Choisissez les tâches qui doivent quand même notifier pendant les vacances (p. ex. chimie de piscine critique).", + "vacation_load_tasks": "Charger les tâches", + "vacation_preview_btn": "Afficher l'aperçu", + "vacation_preview_affected": "tâches concernées", + "vacation_event_due_soon": "bientôt dû", + "vacation_event_overdue": "deviendra en retard", + "vacation_event_triggered_est": "déclencheur capteur possible", + "vacation_sensor_based": "(basé sur capteur)", + "vacation_action_notify": "Notifier quand même", + "vacation_action_unsilence": "Mettre en silence", + "vacation_marked_complete": "Marqué comme terminé", + "vacation_marked_skip": "Passé", + "vacation_end_now": "Terminer les vacances maintenant", + "unassigned": "Non assigné", + "no_area": "Aucune zone", + "has_overdue": "Tâches en retard", + "object": "Objet", + "settings_panel_access": "Accès au panneau", + "settings_panel_access_desc": "Les administrateurs ont toujours un accès complet. Pour déléguer la création, la modification et la suppression à des non-administrateurs spécifiques, activez ceci et sélectionnez-les ci-dessous — les autres ne voient que Terminer et Ignorer.", + "settings_operator_write": "Autoriser les utilisateurs sélectionnés à créer, modifier et supprimer", + "settings_operator_write_desc": "Désactivé : seuls les administrateurs peuvent modifier le contenu. Activé : les utilisateurs ci-dessous ont aussi un accès complet.", + "no_non_admin_users": "Aucun utilisateur non administrateur trouvé. Ajoutez-en dans Paramètres → Personnes.", + "owner_label": "Propriétaire", + "feat_completion_actions": "Actions de finalisation", + "feat_completion_actions_desc": "Action HA par tâche lors de la finalisation + QR de finalisation rapide avec valeurs pré-définies.", + "on_complete_action_title": "À la finalisation : déclencher une action HA (optionnel)", + "on_complete_action_desc": "Appelle un service HA quand la tâche est terminée — p. ex. réinitialiser un compteur sur l'appareil.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Entité cible", + "on_complete_action_data": "Données (JSON, optionnel)", + "on_complete_action_test": "Tester l'action", + "on_complete_action_test_success": "Réussi", + "on_complete_action_test_failed": "Échoué", + "quick_complete_defaults_title": "Valeurs par défaut pour finalisation rapide (scans QR, optionnel)", + "quick_complete_defaults_desc": "Valeurs pré-définies pour les scans QR de finalisation rapide. Sans ces valeurs, le QR ouvre la boîte de dialogue.", + "quick_complete_defaults_notes": "Notes", + "quick_complete_defaults_cost": "Coût", + "quick_complete_defaults_duration": "Durée (minutes)", + "quick_complete_defaults_feedback_none": "Aucun feedback", + "quick_complete_defaults_feedback_needed": "Était nécessaire", + "quick_complete_defaults_feedback_not_needed": "Non nécessaire", + "quick_complete_success": "Terminé rapidement", + "trigger_replaced": "Déclencheur remplacé", + "add": "Ajouter", + "show_stats": "Afficher les statistiques + graphiques", + "hide_stats": "Masquer les statistiques", + "adaptive_no_data": "Pas encore assez d'historique de réalisations pour l'analyse adaptative. Réalisez cette tâche encore quelques fois pour débloquer les recommandations d'intervalle et les courbes de fiabilité.", + "suggestion_applied": "Intervalle suggéré appliqué", + "vacation_mode": "Mode vacances", + "vacation_status_active": "Actif maintenant", + "vacation_status_scheduled": "Planifié", + "vacation_status_inactive": "Inactif", + "vacation_end_now_confirm": "Terminer les vacances immédiatement ?", + "vacation_exempt_count": "exemptées", + "vacation_advanced": "Avancé…", + "vacation_open_panel": "Ouvrir dans le panneau", + "enable": "Activer", + "saved": "Enregistré", + "budget_monthly_set": "Définir mensuel", + "budget_yearly_set": "Définir annuel", + "budget_advanced": "Devise, alertes…", + "budget_open_panel": "Ouvrir dans le panneau", + "groups_empty": "Aucun groupe pour le moment.", + "group_new_placeholder": "Ajouter un groupe…", + "group_delete_confirm": "Supprimer le groupe « {name} » ?", + "groups_manage_tasks": "Gérer les affectations de tâches…", + "groups_open_panel": "Ouvrir dans le panneau", + "on_complete_action_target_hint": "Note : le domaine de l'entité doit correspondre au service — p. ex. 'button.press' ne fonctionne que sur button.*, 'counter.increment' seulement sur counter.*, 'input_button.press' seulement sur input_button.* etc. En cas de non-correspondance, l'action échoue silencieusement (HA journalise 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Afficher tous les objets", + "show_all_tasks": "Effacer le filtre — afficher toutes les tâches", + "filter_to_overdue": "Filtrer la liste sur les tâches en retard uniquement", + "filter_to_due_soon": "Filtrer la liste sur les tâches bientôt dues uniquement", + "filter_to_triggered": "Filtrer la liste sur les tâches déclenchées uniquement", + "open_task": "Ouvrir la tâche", + "show_details": "Afficher l'historique + statistiques", + "hide_details": "Masquer les détails", + "history_empty": "Aucun historique pour le moment.", + "history_edit_button": "Modifier l'entrée", + "total_cost": "Coût total", + "times_performed": "Réalisée", + "older_entries": "plus anciennes", + "open_in_panel": "Ouvrir dans le panneau Maintenance", + "skip_reason": "Raison du saut (facultatif)", + "reset_to_date": "Réinitialiser la dernière réalisation au", + "delete_task_confirm": "Supprimer cette tâche et son historique ?", + "delete_object_confirm": "Supprimer cet objet et toutes ses tâches ?", + "loading": "Chargement…", + "archive": "Archiver", + "undo": "Annuler", + "task_archived": "Tâche archivée", + "object_archived": "Objet archivé", + "unarchive": "Désarchiver", + "archived": "Archivé", + "show_archived": "Afficher les archivés", + "hide_archived": "Masquer les archivés", + "confirm_archive_object": "Archiver cet objet et ses tâches ? L'historique est conservé et peut être restauré plus tard.", + "settings_archive": "Archivage et conservation", + "settings_archive_desc": "Mettez de côté les tâches ponctuelles terminées sans les supprimer. Les éléments archivés sont masqués et inactifs, mais conservent leur historique et leurs coûts.", + "settings_archive_oneoff_days": "Archiver automatiquement les tâches ponctuelles terminées après (jours, 0 = désactivé)", + "settings_delete_archived_oneoff_days": "Supprimer automatiquement les tâches ponctuelles archivées après (jours, 0 = jamais)", + "archive_object": "Archiver l'objet", + "unarchive_object": "Désarchiver l'objet", + "documents": "Documents", + "documents_empty": "Aucun document pour l'instant.", + "doc_upload": "Téléverser un fichier", + "doc_uploading": "Téléversement…", + "doc_add_link": "Ajouter un lien", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titre (facultatif)", + "doc_open": "Ouvrir", + "doc_delete_confirm": "Supprimer « {name} » ?", + "doc_too_large": "Fichier trop volumineux (max. 25 Mo).", + "doc_upload_failed": "Échec du téléversement.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Déjà stocké ailleurs — partagé, aucun espace supplémentaire.", + "doc_dup_in_object": "Ce fichier est déjà attaché à cet objet.", + "doc_link_invalid": "Seuls les liens http/https sont autorisés.", + "doc_cat_manual": "Manuel", + "doc_cat_warranty": "Garantie", + "doc_cat_invoice": "Facture", + "doc_cat_spare_parts": "Pièces détachées", + "doc_cat_photo": "Photo", + "doc_cat_other": "Autre", + "doc_link_badge": "Lien", + "doc_storage_title": "Stockage des documents", + "doc_storage_saved": "Économisé par déduplication", + "doc_storage_refresh": "Actualiser", + "doc_download": "Télécharger", + "doc_close": "Fermer", + "doc_camera": "Prendre une photo", + "doc_drop_hint": "Déposez les fichiers ici", + "doc_task_none": "Aucun document lié à cette tâche.", + "doc_link_existing": "Lier un document…", + "doc_attach": "Lier", + "doc_unlink": "Dissocier", + "doc_page": "Page", + "chart_range_7d": "7j", + "chart_range_30d": "30j", + "chart_range_90d": "90j", + "chart_range_1y": "1a", + "chart_since_service": "depuis le dernier entretien", + "chart_no_stats": "Pas de statistiques à long terme pour cette entité — seules les valeurs des entretiens sont affichées", + "auto_complete_on_recovery": "Terminer automatiquement quand le capteur se rétablit", + "auto_complete_on_recovery_help": "Enregistre une exécution (met à jour la dernière réalisation) lorsque le déclencheur se résout de lui-même — p. ex. sel rechargé, filtre remplacé.", + "doc_search": "Rechercher des documents…", + "doc_search_none": "Aucun document correspondant", + "link_device_optional": "Lier à un appareil existant (optionnel)", + "parent_object_optional": "Objet parent (optionnel)", + "parent_none": "(Aucun parent)", + "paused": "En pause", + "pause_object": "Mettre en pause", + "resume_object": "Reprendre", + "pause_until_prompt": "Gèle les calendriers de cet objet — rien n'arrive à échéance et rien ne notifie jusqu'à la reprise. Optionnellement, fixez une date de reprise automatique.", + "pause_until_label": "Reprendre le (optionnel)", + "object_paused": "Objet mis en pause", + "object_resumed": "Objet repris — calendriers relancés", + "object_paused_badge": "En pause", + "paused_until_label": "jusqu'au", + "replace_object": "Remplacer…", + "replace_object_prompt": "Retire cet objet et crée un successeur. L'historique et les coûts restent archivés sur l'ancien ; les tâches et documents passent au nouveau, les compteurs repartent de zéro.", + "replace_name_label": "Nom du successeur", + "object_replaced": "Objet remplacé — successeur créé", + "reading_unit_label": "Unité de relevé (p. ex. kWh, m³)", + "reading_unit_help": "Affichée à côté de la valeur enregistrée lors de l'exécution de cette tâche.", + "reading_value_label": "Valeur relevée", + "reading_label": "Relevé", + "settings_templates_label": "Galerie de modèles", + "settings_templates_hint": "Décochez les modèles dont vous n'aurez jamais besoin — ils disparaissent des sélecteurs « Depuis un modèle » (panneau et config flow). Rien d'autre ne change ; réactivables à tout moment.", + "worksheet": "Fiche de travail", + "worksheet_scan_view": "Scanner pour ouvrir la tâche", + "worksheet_scan_complete": "Scanner pour terminer", + "worksheet_manual_excerpt": "Extrait du manuel", + "worksheet_pages": "pages", + "worksheet_printed": "Imprimé", + "worksheet_never": "Jamais" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/hi.json b/custom_components/maintenance_supporter/frontend-src/locales/hi.json new file mode 100644 index 0000000..0b0ca9d --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/hi.json @@ -0,0 +1,696 @@ +{ + "maintenance": "रखरखाव", + "objects": "वस्तुएँ", + "tasks": "कार्य", + "overdue": "अतिदेय", + "due_soon": "जल्द देय", + "triggered": "ट्रिगर हुआ", + "trigger_replaced": "ट्रिगर बदला गया", + "ok": "ठीक", + "all": "सभी", + "new_object": "+ नई वस्तु", + "templates_from": "टेम्पलेट से", + "templates_title": "टेम्पलेट से शुरू करें", + "templates_task_count": "{n} कार्य", + "template_created": "टेम्पलेट से बनाया गया", + "onboard_hint": "रखरखाव ट्रैक करने के लिए अपना पहला ऑब्जेक्ट जोड़ें।", + "edit": "संपादित करें", + "duplicate": "डुप्लिकेट करें", + "task_duplicated": "कार्य डुप्लिकेट किया गया", + "object_duplicated": "ऑब्जेक्ट डुप्लिकेट किया गया", + "delete": "हटाएँ", + "add_task": "+ कार्य जोड़ें", + "complete": "पूर्ण करें", + "completed": "पूर्ण हुआ", + "skip": "छोड़ें", + "skipped": "छोड़ा गया", + "missed": "Missed", + "reset": "रीसेट करें", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "रद्द करें", + "bulk_select": "चुनें", + "bulk_select_all": "सभी चुनें", + "bulk_n_selected": "{n} चयनित", + "bulk_completed": "{n} कार्य पूर्ण", + "bulk_archived": "{n} कार्य संग्रहित", + "completing": "पूर्ण किया जा रहा है…", + "interval": "अंतराल", + "warning": "चेतावनी", + "last_performed": "अंतिम बार किया गया", + "next_due": "अगली देय", + "days_until_due": "देय तक दिन", + "avg_duration": "औसत अवधि", + "trigger": "ट्रिगर", + "trigger_type": "ट्रिगर प्रकार", + "threshold_above": "ऊपरी सीमा", + "threshold_below": "निचली सीमा", + "threshold": "सीमा", + "counter": "काउंटर", + "state_change": "स्थिति परिवर्तन", + "runtime": "रनटाइम", + "runtime_hours": "लक्ष्य रनटाइम (घंटे)", + "target_value": "लक्ष्य मान", + "baseline": "आधार रेखा", + "target_changes": "लक्ष्य परिवर्तन", + "for_minutes": "के लिए (मिनट)", + "time_based": "समय-आधारित", + "sensor_based": "सेंसर-आधारित", + "manual": "मैनुअल", + "one_time": "एक बार", + "weekdays": "सप्ताह के दिन", + "nth_weekday": "महीने का N-वाँ सप्ताह-दिन", + "day_of_month": "महीने का दिन", + "recurrence_on_days": "दोहराएँ", + "recurrence_occurrence": "घटना", + "recurrence_weekday": "सप्ताह-दिन", + "recurrence_day": "महीने का दिन (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "पहला", + "ord_2": "दूसरा", + "ord_3": "तीसरा", + "ord_4": "चौथा", + "ord_5": "पाँचवाँ", + "ord_last": "अंतिम", + "day_word": "दिन", + "interval_value": "अंतराल", + "interval_unit": "इकाई", + "unit_days": "दिन", + "unit_weeks": "सप्ताह", + "unit_months": "महीने", + "unit_years": "वर्ष", + "due_date": "देय तिथि", + "cleaning": "सफ़ाई", + "inspection": "निरीक्षण", + "replacement": "प्रतिस्थापन", + "calibration": "अंशांकन", + "service": "सर्विस", + "reading": "रीडिंग", + "custom": "कस्टम", + "history": "इतिहास", + "cost": "लागत", + "report_button": "रिपोर्ट", + "report_title": "रखरखाव रिपोर्ट", + "report_generated": "जनरेट किया गया", + "report_times_done": "पूर्ण", + "report_total_cost": "कुल लागत", + "report_every": "हर {n} {unit}", + "report_notes": "नोट्स", + "report_col_type": "प्रकार", + "report_col_status": "स्थिति", + "report_col_schedule": "शेड्यूल", + "duration": "अवधि", + "both": "दोनों", + "trigger_val": "ट्रिगर मान", + "complete_title": "पूर्ण करें: ", + "checklist": "चेकलिस्ट", + "checklist_steps_optional": "चेकलिस्ट चरण (वैकल्पिक)", + "checklist_placeholder": "फ़िल्टर साफ़ करें\nसील बदलें\nदबाव जाँचें", + "checklist_help": "प्रति पंक्ति एक चरण। अधिकतम 100 आइटम।", + "err_too_long": "{field}: बहुत लंबा (अधिकतम {n} अक्षर)", + "err_too_short": "{field}: बहुत छोटा (न्यूनतम {n} अक्षर)", + "err_value_too_high": "{field}: बहुत बड़ा (अधिकतम {n})", + "err_value_too_low": "{field}: बहुत छोटा (न्यूनतम {n})", + "err_required": "{field}: आवश्यक", + "err_wrong_type": "{field}: गलत प्रकार (अपेक्षित: {type})", + "err_invalid_choice": "{field}: अनुमत मान नहीं है", + "err_invalid_value": "{field}: अमान्य मान", + "feat_schedule_time": "दिन का समय निर्धारण", + "feat_schedule_time_desc": "कार्य मध्यरात्रि के बजाय दिन के एक विशिष्ट समय पर अतिदेय हो जाते हैं।", + "schedule_time_optional": "इस समय देय (वैकल्पिक, HH:MM)", + "schedule_time_help": "खाली = मध्यरात्रि (डिफ़ॉल्ट)। HA समय क्षेत्र।", + "at_time": "बजे", + "notes_optional": "टिप्पणियाँ (वैकल्पिक)", + "cost_optional": "लागत (वैकल्पिक)", + "duration_minutes": "मिनटों में अवधि (वैकल्पिक)", + "days": "दिन", + "day": "दिन", + "today": "आज", + "d_overdue": "दिन अतिदेय", + "no_tasks": "अभी कोई रखरखाव कार्य नहीं। शुरू करने के लिए एक वस्तु बनाएँ।", + "no_tasks_short": "कोई कार्य नहीं", + "no_history": "अभी कोई इतिहास प्रविष्टि नहीं।", + "show_all": "सभी दिखाएँ", + "cost_duration_chart": "लागत और अवधि", + "installed": "स्थापित", + "confirm_delete_object": "इस वस्तु और इसके सभी कार्यों को हटाएँ?", + "confirm_delete_task": "इस कार्य को हटाएँ?", + "min": "न्यूनतम", + "max": "अधिकतम", + "save": "सहेजें", + "saving": "सहेजा जा रहा है…", + "edit_task": "कार्य संपादित करें", + "new_task": "नया रखरखाव कार्य", + "task_name": "कार्य का नाम", + "maintenance_type": "रखरखाव प्रकार", + "priority": "प्राथमिकता", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "कम", + "priority_normal": "सामान्य", + "priority_high": "उच्च", + "schedule_type": "अनुसूची प्रकार", + "interval_days": "अंतराल (दिन)", + "warning_days": "चेतावनी दिन", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "अंतिम बार किया गया (वैकल्पिक)", + "interval_anchor": "अंतराल आधार", + "anchor_completion": "पूर्ण तिथि से", + "anchor_planned": "नियोजित तिथि से (कोई विचलन नहीं)", + "edit_object": "वस्तु संपादित करें", + "name": "नाम", + "manufacturer_optional": "निर्माता (वैकल्पिक)", + "model_optional": "मॉडल (वैकल्पिक)", + "serial_number_optional": "सीरियल नंबर (वैकल्पिक)", + "serial_number_label": "S/N", + "documentation_url_label": "मैनुअल", + "object_notes_label": "टिप्पणियाँ", + "sort_due_date": "देय तिथि", + "sort_object": "वस्तु का नाम", + "sort_type": "प्रकार", + "sort_task_name": "कार्य का नाम", + "all_objects": "सभी वस्तुएँ", + "tasks_lower": "कार्य", + "no_tasks_yet": "अभी कोई कार्य नहीं", + "add_first_task": "पहला कार्य जोड़ें", + "trigger_configuration": "ट्रिगर कॉन्फ़िगरेशन", + "entity_id": "एंटिटी ID", + "comma_separated": "अल्पविराम से अलग", + "entity_logic": "एंटिटी लॉजिक", + "entity_logic_any": "कोई भी एंटिटी ट्रिगर करती है", + "entity_logic_all": "सभी एंटिटी ट्रिगर होनी चाहिए", + "entities": "एंटिटी", + "attribute_optional": "विशेषता (वैकल्पिक, खाली = स्थिति)", + "use_entity_state": "एंटिटी स्थिति का उपयोग करें (कोई विशेषता नहीं)", + "trigger_above": "इससे ऊपर ट्रिगर करें", + "trigger_below": "इससे नीचे ट्रिगर करें", + "for_at_least_minutes": "कम से कम (मिनट)", + "safety_interval_days": "सुरक्षा अंतराल (दिन, वैकल्पिक)", + "safety_interval": "सुरक्षा अंतराल (वैकल्पिक)", + "delta_mode": "डेल्टा मोड", + "from_state_optional": "किस स्थिति से (वैकल्पिक)", + "to_state_optional": "किस स्थिति तक (वैकल्पिक)", + "documentation_url_optional": "दस्तावेज़ URL (वैकल्पिक)", + "object_notes_optional": "टिप्पणियाँ (वैकल्पिक)", + "nfc_tag_id_optional": "NFC टैग ID (वैकल्पिक)", + "nfc_tags_empty_help": "Home Assistant में अभी कोई NFC टैग पंजीकृत नहीं है।", + "nfc_tags_open_settings": "टैग सेटिंग्स खोलें", + "nfc_tags_refresh": "रिफ़्रेश करें", + "environmental_entity_optional": "पर्यावरण सेंसर (वैकल्पिक)", + "environmental_entity_helper": "उदा. sensor.outdoor_temperature — पर्यावरणीय स्थितियों के आधार पर अंतराल समायोजित करता है", + "environmental_attribute_optional": "पर्यावरण विशेषता (वैकल्पिक)", + "nfc_tag_id": "NFC टैग ID", + "nfc_linked": "NFC टैग लिंक किया गया", + "nfc_link_hint": "NFC टैग लिंक करने के लिए क्लिक करें", + "responsible_user": "जिम्मेदार उपयोगकर्ता", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(कोई उपयोगकर्ता असाइन नहीं)", + "all_users": "सभी उपयोगकर्ता", + "my_tasks": "मेरे कार्य", + "tab_calendar": "कैलेंडर", + "cal_no_events": "कोई रखरखाव नहीं", + "cal_window_7": "7 दिन", + "cal_window_14": "14 दिन", + "cal_window_30": "30 दिन", + "cal_window_365": "1 वर्ष", + "cal_every_n_days": "हर {n} दिन", + "cal_source_time": "समय-आधारित", + "cal_source_time_adaptive": "समय-आधारित (अनुकूली)", + "cal_source_sensor": "सेंसर-आधारित", + "cal_predicted": "अनुमानित", + "cal_confidence_high": "उच्च विश्वास", + "cal_confidence_medium": "मध्यम विश्वास", + "cal_confidence_low": "निम्न विश्वास", + "budget_monthly": "मासिक बजट", + "budget_yearly": "वार्षिक बजट", + "groups": "समूह", + "new_group": "नया समूह", + "edit_group": "समूह संपादित करें", + "no_groups": "अभी कोई समूह नहीं", + "delete_group": "समूह हटाएँ", + "delete_group_confirm": "समूह '{name}' हटाएँ?", + "group_select_tasks": "कार्य चुनें", + "group_name_required": "नाम आवश्यक है", + "description_optional": "विवरण (वैकल्पिक)", + "selected": "चयनित", + "loading_chart": "चार्ट डेटा लोड हो रहा है...", + "hide_outliers": "आउटलायर छिपाएँ (सेंसर गड़बड़)", + "was_maintenance_needed": "क्या यह रखरखाव आवश्यक था?", + "feedback_needed": "आवश्यक था", + "feedback_not_needed": "आवश्यक नहीं", + "feedback_not_sure": "निश्चित नहीं", + "suggested_interval": "सुझाया गया अंतराल", + "apply_suggestion": "लागू करें", + "reanalyze": "पुनः विश्लेषण करें", + "reanalyze_result": "नया विश्लेषण", + "reanalyze_insufficient_data": "अनुशंसा देने के लिए पर्याप्त डेटा नहीं", + "data_points": "डेटा बिंदु", + "dismiss_suggestion": "खारिज करें", + "confidence_low": "निम्न", + "confidence_medium": "मध्यम", + "confidence_high": "उच्च", + "recommended": "अनुशंसित", + "seasonal_awareness": "मौसमी जागरूकता", + "edit_seasonal_overrides": "मौसमी कारक संपादित करें", + "seasonal_overrides_title": "मौसमी कारक (ओवरराइड)", + "seasonal_overrides_hint": "प्रति माह कारक (0.1–5.0)। खाली = स्वचालित रूप से सीखा गया।", + "seasonal_override_invalid": "अमान्य मान", + "seasonal_override_range": "कारक 0.1 और 5.0 के बीच होना चाहिए", + "clear_all": "सभी साफ़ करें", + "seasonal_chart_title": "मौसमी कारक", + "seasonal_learned": "सीखा गया", + "seasonal_manual": "मैनुअल", + "month_jan": "जन", + "month_feb": "फ़र", + "month_mar": "मार्च", + "month_apr": "अप्रैल", + "month_may": "मई", + "month_jun": "जून", + "month_jul": "जुल", + "month_aug": "अग", + "month_sep": "सित", + "month_oct": "अक्टू", + "month_nov": "नव", + "month_dec": "दिस", + "sensor_prediction": "सेंसर पूर्वानुमान", + "degradation_trend": "रुझान", + "trend_rising": "बढ़ता हुआ", + "trend_falling": "घटता हुआ", + "trend_stable": "स्थिर", + "trend_insufficient_data": "अपर्याप्त डेटा", + "days_until_threshold": "सीमा तक दिन", + "threshold_exceeded": "सीमा पार हुई", + "environmental_adjustment": "पर्यावरण कारक", + "sensor_prediction_urgency": "सेंसर ~{days} दिनों में सीमा का अनुमान लगाता है", + "day_short": "दिन", + "weibull_reliability_curve": "विश्वसनीयता वक्र", + "weibull_failure_probability": "विफलता संभावना", + "weibull_r_squared": "फ़िट R²", + "beta_early_failures": "प्रारंभिक विफलताएँ", + "beta_random_failures": "यादृच्छिक विफलताएँ", + "beta_wear_out": "घिसाव", + "beta_highly_predictable": "अत्यधिक पूर्वानुमेय", + "confidence_interval": "विश्वास अंतराल", + "confidence_conservative": "रूढ़िवादी", + "confidence_aggressive": "आशावादी", + "current_interval_marker": "वर्तमान अंतराल", + "recommended_marker": "अनुशंसित", + "characteristic_life": "विशेषता आयु", + "chart_mini_sparkline": "रुझान स्पार्कलाइन", + "chart_history": "लागत और अवधि इतिहास", + "chart_seasonal": "मौसमी कारक, 12 महीने", + "chart_weibull": "वीबुल विश्वसनीयता वक्र", + "chart_sparkline": "सेंसर ट्रिगर मान चार्ट", + "days_progress": "दिनों की प्रगति", + "qr_code": "QR कोड", + "qr_generating": "QR कोड बनाया जा रहा है…", + "qr_error": "QR कोड बनाने में विफल।", + "qr_error_no_url": "कोई HA URL कॉन्फ़िगर नहीं है। कृपया सेटिंग्स → सिस्टम → नेटवर्क में एक बाहरी या आंतरिक URL सेट करें।", + "save_error": "सहेजने में विफल। कृपया पुनः प्रयास करें।", + "qr_print": "प्रिंट करें", + "qr_download": "SVG डाउनलोड करें", + "qr_action": "स्कैन पर क्रिया", + "qr_action_view": "रखरखाव जानकारी देखें", + "qr_action_complete": "रखरखाव को पूर्ण के रूप में चिह्नित करें", + "qr_url_mode": "लिंक प्रकार", + "qr_mode_companion": "Companion ऐप", + "qr_mode_local": "लोकल (mDNS)", + "qr_mode_server": "सर्वर URL", + "overview": "अवलोकन", + "analysis": "विश्लेषण", + "recent_activities": "हाल की गतिविधियाँ", + "search_notes": "टिप्पणियाँ खोजें", + "avg_cost": "औसत लागत", + "no_advanced_features": "कोई उन्नत सुविधा सक्षम नहीं", + "no_advanced_features_hint": "यहाँ विश्लेषण डेटा देखने के लिए इंटीग्रेशन सेटिंग्स में “अनुकूली अंतराल” या “मौसमी पैटर्न” सक्षम करें।", + "analysis_not_enough_data": "अभी विश्लेषण के लिए पर्याप्त डेटा नहीं।", + "analysis_not_enough_data_hint": "वीबुल विश्लेषण के लिए कम से कम 5 पूर्ण रखरखाव आवश्यक हैं; मौसमी पैटर्न प्रति माह 6+ डेटा बिंदुओं के बाद दिखाई देते हैं।", + "analysis_manual_task_hint": "अंतराल के बिना मैनुअल कार्य विश्लेषण डेटा उत्पन्न नहीं करते।", + "completions": "पूर्णताएँ", + "current": "वर्तमान", + "shorter": "छोटा", + "longer": "लंबा", + "normal": "सामान्य", + "disabled": "अक्षम", + "compound_logic": "संयुक्त लॉजिक", + "compound": "संयुक्त (कई शर्तें)", + "compound_logic_and": "और — सभी शर्तें पूरी हों", + "compound_logic_or": "या — कोई एक शर्त पर्याप्त", + "compound_help": "कई सेंसर शर्तों को एक ट्रिगर में मिलाएँ।", + "compound_no_conditions": "अभी कोई शर्त नहीं — कम से कम एक जोड़ें।", + "compound_add_condition": "शर्त जोड़ें", + "compound_condition": "शर्त", + "compound_remove_condition": "शर्त हटाएँ", + "card_title": "शीर्षक", + "card_show_header": "सांख्यिकी के साथ हेडर दिखाएँ", + "card_show_actions": "क्रिया बटन दिखाएँ", + "card_compact": "संक्षिप्त मोड", + "card_max_items": "अधिकतम आइटम (0 = सभी)", + "card_filter_status": "स्थिति के अनुसार फ़िल्टर करें", + "card_filter_status_help": "खाली = सभी स्थितियाँ दिखाएँ।", + "card_filter_objects": "वस्तुओं के अनुसार फ़िल्टर करें", + "card_filter_objects_help": "खाली = सभी वस्तुएँ दिखाएँ।", + "card_filter_entities": "एंटिटी के अनुसार फ़िल्टर करें (entity_ids)", + "card_filter_entities_help": "इस इंटीग्रेशन से sensor / binary_sensor एंटिटी चुनें। खाली = सभी।", + "card_loading_objects": "वस्तुएँ लोड हो रही हैं…", + "card_load_error": "वस्तुएँ लोड नहीं हो सकीं — WebSocket कनेक्शन जाँचें।", + "card_no_tasks_title": "अभी कोई रखरखाव कार्य नहीं", + "card_no_tasks_cta": "→ रखरखाव पैनल में एक बनाएँ", + "no_objects": "अभी कोई वस्तु नहीं।", + "action_error": "क्रिया विफल। कृपया पुनः प्रयास करें।", + "area_id_optional": "क्षेत्र (वैकल्पिक)", + "installation_date_optional": "स्थापना तिथि (वैकल्पिक)", + "warranty_expiry_optional": "वारंटी समाप्ति (वैकल्पिक)", + "warranty": "वारंटी", + "warranty_valid_until": "{date} तक वैध", + "warranty_expires_in": "{days} दिनों में समाप्त", + "warranty_expired": "समाप्त", + "cal_past_windows": "पिछली अवधियाँ", + "cal_forward_windows": "आगामी अवधियाँ", + "history_edit_title": "इतिहास प्रविष्टि संपादित करें", + "history_edit_timestamp": "टाइमस्टैम्प", + "manufacturer": "निर्माता", + "model": "मॉडल", + "area": "क्षेत्र", + "actions": "क्रियाएँ", + "view_mode_label": "दृश्य", + "view_cards": "कार्ड दृश्य", + "view_table": "तालिका दृश्य", + "objects_table_columns_label": "वस्तु तालिका कॉलम", + "objects_table_columns_hint": "चुनें कि वस्तु तालिका दृश्य में कौन से कॉलम दिखाई दें।", + "custom_icon_optional": "आइकन (वैकल्पिक, उदा. mdi:wrench)", + "task_enabled": "कार्य सक्षम", + "skip_reason_prompt": "इस कार्य को छोड़ें?", + "reason_optional": "कारण (वैकल्पिक)", + "reset_date_prompt": "कार्य को किया गया के रूप में चिह्नित करें?", + "reset_date_optional": "अंतिम बार किया गया तिथि (वैकल्पिक, डिफ़ॉल्ट आज)", + "notes_label": "टिप्पणियाँ", + "documentation_label": "दस्तावेज़", + "no_nfc_tag": "— कोई टैग नहीं —", + "dashboard": "डैशबोर्ड", + "tab_today": "आज", + "palette_placeholder": "ऑब्जेक्ट और कार्य खोजें…", + "palette_no_results": "कोई मिलान नहीं", + "palette_hint": "↑↓ नेविगेट · Enter खोलें · Esc बंद", + "today_all_caught_up": "सब पूरा! इस सप्ताह कुछ भी बाकी नहीं।", + "today_overdue": "अतिदेय", + "today_due_today": "आज देय", + "today_this_week": "इस सप्ताह", + "settings": "सेटिंग्स", + "settings_features": "उन्नत सुविधाएँ", + "settings_features_desc": "उन्नत सुविधाओं को सक्षम या अक्षम करें। अक्षम करने से वे UI से छिप जाती हैं पर डेटा नहीं हटता।", + "feat_adaptive": "अनुकूली निर्धारण", + "feat_adaptive_desc": "रखरखाव इतिहास से इष्टतम अंतराल सीखें", + "feat_predictions": "सेंसर पूर्वानुमान", + "feat_predictions_desc": "सेंसर क्षरण से ट्रिगर तिथियों का अनुमान लगाएँ", + "feat_seasonal": "मौसमी समायोजन", + "feat_seasonal_desc": "मौसमी पैटर्न के आधार पर अंतराल समायोजित करें", + "feat_environmental": "पर्यावरणीय सहसंबंध", + "feat_environmental_desc": "तापमान/आर्द्रता के साथ अंतराल का सहसंबंध करें", + "feat_budget": "बजट ट्रैकिंग", + "feat_budget_desc": "मासिक और वार्षिक रखरखाव खर्च ट्रैक करें", + "feat_groups": "कार्य समूह", + "feat_groups_desc": "कार्यों को तार्किक समूहों में व्यवस्थित करें", + "feat_checklists": "चेकलिस्ट", + "feat_checklists_desc": "कार्य पूर्णता के लिए बहु-चरणीय प्रक्रियाएँ", + "settings_general": "सामान्य", + "settings_default_warning": "डिफ़ॉल्ट चेतावनी दिन", + "settings_panel_enabled": "साइडबार पैनल", + "settings_panel_title": "साइडबार पैनल शीर्षक", + "settings_notifications": "सूचनाएँ", + "settings_notify_service": "सूचना सेवा", + "test_notification": "परीक्षण सूचना", + "send_test": "परीक्षण भेजें", + "testing": "भेजा जा रहा है…", + "test_notification_success": "परीक्षण सूचना भेजी गई", + "test_notification_failed": "परीक्षण सूचना विफल", + "settings_notify_due_soon": "जल्द देय होने पर सूचित करें", + "settings_notify_overdue": "अतिदेय होने पर सूचित करें", + "settings_notify_triggered": "ट्रिगर होने पर सूचित करें", + "settings_interval_hours": "दोहराव अंतराल (घंटे, 0 = एक बार)", + "settings_quiet_hours": "शांत घंटे", + "settings_quiet_start": "प्रारंभ", + "settings_quiet_end": "समाप्त", + "settings_max_per_day": "प्रति दिन अधिकतम सूचनाएँ (0 = असीमित)", + "settings_bundling": "सूचनाएँ बंडल करें", + "settings_bundle_threshold": "बंडल सीमा", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "मोबाइल क्रिया बटन", + "settings_action_complete": "'पूर्ण करें' बटन दिखाएँ", + "settings_action_skip": "'छोड़ें' बटन दिखाएँ", + "settings_action_snooze": "'स्नूज़' बटन दिखाएँ", + "settings_weekly_digest": "साप्ताहिक सारांश", + "settings_weekly_digest_hint": "सोमवार सुबह एक सारांश सूचना जब कार्य देय हों।", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "स्नूज़ अवधि (घंटे)", + "settings_budget": "बजट", + "settings_currency": "मुद्रा", + "settings_budget_monthly": "मासिक बजट", + "settings_budget_yearly": "वार्षिक बजट", + "settings_budget_alerts": "बजट अलर्ट", + "settings_budget_threshold": "अलर्ट सीमा (%)", + "settings_import_export": "आयात / निर्यात", + "settings_export_json": "JSON निर्यात करें", + "settings_export_yaml": "YAML निर्यात करें", + "settings_export_csv": "CSV निर्यात करें", + "settings_import_csv": "CSV आयात करें", + "settings_import_placeholder": "JSON या CSV सामग्री यहाँ चिपकाएँ…", + "settings_import_btn": "आयात करें", + "settings_import_success": "{count} वस्तुएँ सफलतापूर्वक आयात की गईं।", + "settings_export_success": "निर्यात डाउनलोड किया गया।", + "settings_saved": "सेटिंग सहेजी गई।", + "settings_include_history": "इतिहास शामिल करें", + "sort_alphabetical": "वर्णानुक्रम", + "sort_due_soonest": "सबसे जल्द देय", + "sort_task_count": "कार्य संख्या", + "sort_area": "क्षेत्र", + "sort_assigned_user": "असाइन किया गया उपयोगकर्ता", + "sort_group": "समूह", + "groupby_none": "कोई समूहन नहीं", + "groupby_area": "क्षेत्र के अनुसार", + "groupby_group": "समूह के अनुसार", + "groupby_user": "उपयोगकर्ता के अनुसार", + "filter_label": "फ़िल्टर", + "user_label": "उपयोगकर्ता", + "sort_label": "क्रमबद्ध करें", + "group_by_label": "इसके अनुसार समूहित करें", + "state_value_help": "HA स्थिति मान का उपयोग करें (आमतौर पर लोअरकेस, उदा. \"on\"/\"off\")। सहेजने पर केस सामान्यीकृत हो जाता है।", + "target_changes_help": "ट्रिगर होने से पहले मिलान वाले संक्रमणों की संख्या (डिफ़ॉल्ट: 1)।", + "qr_print_title": "QR कोड प्रिंट करें", + "qr_print_desc": "अपने उपकरण पर काटकर चिपकाने के लिए QR कोड का प्रिंट-योग्य पृष्ठ बनाएँ।", + "qr_print_load": "वस्तुएँ लोड करें", + "qr_print_filter": "फ़िल्टर", + "qr_print_objects": "वस्तुएँ", + "qr_print_actions": "क्रियाएँ", + "qr_print_url_mode": "लिंक प्रकार", + "qr_print_estimate": "अनुमानित QR कोड", + "qr_print_over_limit": "सीमा 200 है, फ़िल्टर को संकीर्ण करें", + "qr_print_generate": "QR कोड बनाएँ", + "qr_print_generating": "बनाया जा रहा है…", + "qr_print_ready": "QR कोड तैयार", + "qr_print_print_button": "प्रिंट करें", + "qr_print_empty": "बनाने के लिए कुछ नहीं", + "qr_action_skip": "छोड़ें", + "vacation_title": "अवकाश मोड", + "vacation_active": "सक्रिय", + "vacation_ended": "समाप्त", + "vacation_desc": "अवकाश की योजना बनाएँ: इस अवधि और कुछ बफ़र दिनों के दौरान सूचनाएँ रुकी रहती हैं। आप विशिष्ट कार्यों को वापस शामिल कर सकते हैं।", + "vacation_enable": "अवकाश मोड सक्षम करें", + "vacation_start": "प्रारंभ", + "vacation_end": "समाप्त", + "vacation_buffer": "बफ़र (दिन)", + "vacation_exempt_title": "अवकाश के दौरान फिर भी सूचित करें", + "vacation_exempt_desc": "ऐसे कार्य चुनें जिनकी अवकाश के दौरान भी सूचना मिलनी चाहिए (उदा. महत्वपूर्ण पूल रसायन)।", + "vacation_load_tasks": "कार्य लोड करें", + "vacation_preview_btn": "पूर्वावलोकन दिखाएँ", + "vacation_preview_affected": "प्रभावित कार्य", + "vacation_event_due_soon": "जल्द देय हो जाता है", + "vacation_event_overdue": "अतिदेय हो जाता है", + "vacation_event_triggered_est": "सेंसर ट्रिगर संभव", + "vacation_sensor_based": "(सेंसर-आधारित)", + "vacation_action_notify": "फिर भी सूचित करें", + "vacation_action_unsilence": "फिर से चुप करें", + "vacation_marked_complete": "पूर्ण के रूप में चिह्नित", + "vacation_marked_skip": "छोड़ा गया", + "vacation_end_now": "अवकाश अभी समाप्त करें", + "add": "जोड़ें", + "show_stats": "सांख्यिकी + ग्राफ़ दिखाएँ", + "hide_stats": "सांख्यिकी छिपाएँ", + "adaptive_no_data": "अनुकूली विश्लेषण के लिए अभी पर्याप्त पूर्णता इतिहास नहीं। अंतराल अनुशंसाएँ और विश्वसनीयता चार्ट अनलॉक करने के लिए इस कार्य को कुछ और बार पूर्ण करें।", + "suggestion_applied": "सुझाया गया अंतराल लागू किया गया", + "vacation_mode": "अवकाश मोड", + "vacation_status_active": "अभी सक्रिय", + "vacation_status_scheduled": "निर्धारित", + "vacation_status_inactive": "निष्क्रिय", + "vacation_end_now_confirm": "अवकाश तुरंत समाप्त करें?", + "vacation_exempt_count": "छूट प्राप्त", + "vacation_advanced": "उन्नत…", + "vacation_open_panel": "पैनल में खोलें", + "enable": "सक्षम करें", + "saved": "सहेजा गया", + "budget_monthly_set": "मासिक सेट करें", + "budget_yearly_set": "वार्षिक सेट करें", + "budget_advanced": "मुद्रा, अलर्ट…", + "budget_open_panel": "पैनल में खोलें", + "groups_empty": "अभी कोई समूह नहीं।", + "group_new_placeholder": "समूह जोड़ें…", + "group_delete_confirm": "समूह \"{name}\" हटाएँ?", + "groups_manage_tasks": "कार्य असाइनमेंट प्रबंधित करें…", + "groups_open_panel": "पैनल में खोलें", + "unassigned": "असाइन नहीं", + "no_area": "कोई क्षेत्र नहीं", + "has_overdue": "अतिदेय कार्य हैं", + "object": "वस्तु", + "settings_panel_access": "पैनल पहुँच", + "settings_panel_access_desc": "व्यवस्थापकों को हमेशा पूर्ण पहुँच होती है। विशिष्ट गैर-व्यवस्थापकों को बनाना, संपादित करना और हटाना सौंपने के लिए, इसे चालू करें और नीचे उन्हें चुनें — बाकी सभी को केवल पूर्ण करें और छोड़ें दिखता है।", + "settings_operator_write": "चयनित उपयोगकर्ताओं को बनाने, संपादित करने और हटाने की अनुमति दें", + "settings_operator_write_desc": "बंद: केवल व्यवस्थापक सामग्री बदल सकते हैं। चालू: नीचे चयनित उपयोगकर्ताओं को भी पूर्ण पहुँच मिलती है।", + "no_non_admin_users": "कोई गैर-व्यवस्थापक उपयोगकर्ता नहीं मिला। सेटिंग्स → लोग में कुछ जोड़ें।", + "owner_label": "स्वामी", + "feat_completion_actions": "पूर्णता क्रियाएँ", + "feat_completion_actions_desc": "पूर्ण होने पर प्रति-कार्य HA क्रिया + पूर्व-निर्धारित मानों के साथ त्वरित-पूर्ण QR।", + "on_complete_action_title": "पूर्ण होने पर: HA क्रिया ट्रिगर करें (वैकल्पिक)", + "on_complete_action_desc": "कार्य पूर्ण होने पर एक HA सेवा कॉल करता है — उदा. डिवाइस पर काउंटर रीसेट करना।", + "on_complete_action_service": "सेवा", + "on_complete_action_target": "लक्ष्य एंटिटी", + "on_complete_action_target_hint": "ध्यान दें: एंटिटी डोमेन सेवा से मेल खाना चाहिए — उदा. 'button.press' केवल button.* पर काम करता है, 'counter.increment' केवल counter.* पर, 'input_button.press' केवल input_button.* पर आदि। मेल न खाने पर क्रिया चुपचाप विफल हो जाएगी (HA लॉग करता है 'Referenced entities ... missing or not currently available')।", + "on_complete_action_data": "डेटा (JSON, वैकल्पिक)", + "on_complete_action_test": "कॉन्फ़िगरेशन सत्यापित करें", + "on_complete_action_test_success": "✓ कॉन्फ़िगरेशन वैध (क्रिया केवल कार्य पूर्ण होने पर चलेगी)", + "on_complete_action_test_failed": "विफल", + "quick_complete_defaults_title": "त्वरित-पूर्ण डिफ़ॉल्ट (QR स्कैन के लिए, वैकल्पिक)", + "quick_complete_defaults_desc": "त्वरित-पूर्ण QR स्कैन के लिए पूर्व-निर्धारित मान। इनके बिना, QR पूर्ण करें संवाद खोलता है।", + "quick_complete_defaults_notes": "टिप्पणियाँ", + "quick_complete_defaults_cost": "लागत", + "quick_complete_defaults_duration": "अवधि (मिनट)", + "quick_complete_defaults_feedback_none": "कोई प्रतिक्रिया नहीं", + "quick_complete_defaults_feedback_needed": "आवश्यक था", + "quick_complete_defaults_feedback_not_needed": "आवश्यक नहीं", + "quick_complete_success": "त्वरित रूप से पूर्ण के रूप में चिह्नित", + "show_all_objects": "सभी वस्तुएँ दिखाएँ", + "show_all_tasks": "फ़िल्टर हटाएँ — सभी कार्य दिखाएँ", + "filter_to_overdue": "कार्य सूची को केवल अतिदेय तक फ़िल्टर करें", + "filter_to_due_soon": "कार्य सूची को केवल जल्द-देय तक फ़िल्टर करें", + "filter_to_triggered": "कार्य सूची को केवल ट्रिगर हुए तक फ़िल्टर करें", + "open_task": "कार्य खोलें", + "show_details": "इतिहास + सांख्यिकी दिखाएँ", + "hide_details": "विवरण छिपाएँ", + "history_empty": "अभी कोई इतिहास नहीं।", + "history_edit_button": "प्रविष्टि संपादित करें", + "total_cost": "कुल लागत", + "times_performed": "किया गया", + "older_entries": "पुराने", + "open_in_panel": "रखरखाव पैनल में खोलें", + "skip_reason": "छोड़ने का कारण (वैकल्पिक)", + "reset_to_date": "अंतिम बार किया गया को रीसेट करें", + "delete_task_confirm": "इस कार्य और इसके इतिहास को हटाएँ?", + "delete_object_confirm": "इस वस्तु और इसके सभी कार्यों को हटाएँ?", + "loading": "लोड हो रहा है…", + "archive": "संग्रह करें", + "undo": "पूर्ववत करें", + "task_archived": "कार्य संग्रहित", + "object_archived": "ऑब्जेक्ट संग्रहित", + "unarchive": "संग्रह से हटाएँ", + "archived": "संग्रहीत", + "show_archived": "संग्रहीत दिखाएँ", + "hide_archived": "संग्रहीत छिपाएँ", + "confirm_archive_object": "इस ऑब्जेक्ट और इसके कार्यों को संग्रहित करें? इतिहास सुरक्षित रहता है और बाद में पुनर्स्थापित किया जा सकता है।", + "settings_archive": "संग्रह और प्रतिधारण", + "settings_archive_desc": "पूर्ण एक-बार के कार्यों को हटाए बिना अलग रखें। संग्रहीत आइटम छिपे और निष्क्रिय रहते हैं, पर इतिहास और लागत बनाए रखते हैं।", + "settings_archive_oneoff_days": "पूर्ण एक-बार के कार्य स्वतः संग्रहित करें (दिन, 0 = बंद)", + "settings_delete_archived_oneoff_days": "संग्रहीत एक-बार के कार्य स्वतः हटाएँ (दिन, 0 = कभी नहीं)", + "archive_object": "ऑब्जेक्ट संग्रहित करें", + "unarchive_object": "ऑब्जेक्ट संग्रह से हटाएँ", + "documents": "दस्तावेज़", + "documents_empty": "अभी तक कोई दस्तावेज़ नहीं।", + "doc_upload": "फ़ाइल अपलोड करें", + "doc_uploading": "अपलोड हो रहा है…", + "doc_add_link": "लिंक जोड़ें", + "doc_link_url": "URL (https://…)", + "doc_link_title": "शीर्षक (वैकल्पिक)", + "doc_open": "खोलें", + "doc_delete_confirm": "\"{name}\" हटाएँ?", + "doc_too_large": "फ़ाइल बहुत बड़ी है (अधिकतम 25 MB)।", + "doc_upload_failed": "अपलोड विफल रहा।", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "पहले से कहीं और संग्रहीत — साझा, कोई अतिरिक्त स्थान नहीं।", + "doc_dup_in_object": "यह फ़ाइल पहले से इस ऑब्जेक्ट से जुड़ी है।", + "doc_link_invalid": "केवल http/https लिंक की अनुमति है।", + "doc_cat_manual": "मैनुअल", + "doc_cat_warranty": "वारंटी", + "doc_cat_invoice": "चालान", + "doc_cat_spare_parts": "स्पेयर पार्ट्स", + "doc_cat_photo": "फ़ोटो", + "doc_cat_other": "अन्य", + "doc_link_badge": "लिंक", + "doc_storage_title": "दस्तावेज़ संग्रहण", + "doc_storage_saved": "डीडुप्लिकेशन से बचत", + "doc_storage_refresh": "ताज़ा करें", + "doc_download": "डाउनलोड करें", + "doc_close": "बंद करें", + "doc_camera": "फ़ोटो लें", + "doc_drop_hint": "फ़ाइलें यहाँ छोड़ें", + "doc_task_none": "इस कार्य से कोई दस्तावेज़ संबद्ध नहीं है।", + "doc_link_existing": "दस्तावेज़ जोड़ें…", + "doc_attach": "जोड़ें", + "doc_unlink": "अलग करें", + "doc_page": "पृष्ठ", + "chart_range_7d": "7दिन", + "chart_range_30d": "30दिन", + "chart_range_90d": "90दिन", + "chart_range_1y": "1वर्ष", + "chart_since_service": "पिछले रखरखाव के बाद से", + "chart_no_stats": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — केवल रखरखाव इवेंट मान दिखाए जा रहे हैं", + "auto_complete_on_recovery": "सेंसर ठीक होने पर स्वतः पूर्ण करें", + "auto_complete_on_recovery_help": "जब ट्रिगर स्वयं हल हो जाता है तो एक पूर्णता दर्ज करता है (अंतिम निष्पादन सेट करता है) — जैसे नमक भरा गया, फ़िल्टर बदला गया।", + "doc_search": "दस्तावेज़ खोजें…", + "doc_search_none": "कोई मेल खाता दस्तावेज़ नहीं", + "link_device_optional": "मौजूदा डिवाइस से लिंक करें (वैकल्पिक)", + "parent_object_optional": "मूल ऑब्जेक्ट (वैकल्पिक)", + "parent_none": "(कोई मूल नहीं)", + "paused": "रोका गया", + "pause_object": "रोकें", + "resume_object": "फिर से शुरू करें", + "pause_until_prompt": "इस ऑब्जेक्ट के शेड्यूल फ़्रीज़ करें — फिर से शुरू करने तक कुछ भी देय नहीं होगा और कोई सूचना नहीं आएगी। वैकल्पिक रूप से स्वतः पुनरारंभ की तारीख़ सेट करें।", + "pause_until_label": "फिर से शुरू करें (वैकल्पिक)", + "object_paused": "ऑब्जेक्ट रोका गया", + "object_resumed": "ऑब्जेक्ट फिर से शुरू — शेड्यूल पुनः आरंभ", + "object_paused_badge": "रोका गया", + "paused_until_label": "तक", + "replace_object": "बदलें…", + "replace_object_prompt": "इस ऑब्जेक्ट को सेवानिवृत्त करें और उत्तराधिकारी बनाएं। इतिहास और लागत पुराने पर संग्रहीत रहती है; कार्य और दस्तावेज़ नए में चले जाते हैं, काउंटर नए सिरे से शुरू होते हैं।", + "replace_name_label": "उत्तराधिकारी का नाम", + "object_replaced": "ऑब्जेक्ट बदला गया — उत्तराधिकारी बनाया गया", + "reading_unit_label": "रीडिंग इकाई (जैसे kWh, m³)", + "reading_unit_help": "इस कार्य को पूरा करते समय दर्ज मान के बगल में दिखाई जाती है।", + "reading_value_label": "रीडिंग मान", + "reading_label": "रीडिंग", + "settings_templates_label": "टेम्पलेट गैलरी", + "settings_templates_hint": "जिन टेम्पलेट की कभी ज़रूरत नहीं होगी उन्हें अनचेक करें — वे \"टेम्पलेट से\" चयनकर्ताओं (पैनल और कॉन्फ़िग फ़्लो) से गायब हो जाएंगे। और कुछ नहीं बदलता; कभी भी फिर से सक्षम कर सकते हैं।", + "worksheet": "कार्य पत्रक", + "worksheet_scan_view": "कार्य खोलने के लिए स्कैन करें", + "worksheet_scan_complete": "पूरा करने के लिए स्कैन करें", + "worksheet_manual_excerpt": "मैनुअल अंश", + "worksheet_pages": "पृष्ठ", + "worksheet_printed": "मुद्रित", + "worksheet_never": "कभी नहीं" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/it.json b/custom_components/maintenance_supporter/frontend-src/locales/it.json new file mode 100644 index 0000000..8397385 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/it.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Manutenzione", + "objects": "Oggetti", + "tasks": "Attività", + "overdue": "Scaduto", + "due_soon": "In scadenza", + "triggered": "Attivato", + "ok": "OK", + "all": "Tutti", + "new_object": "+ Nuovo oggetto", + "templates_from": "Da modello", + "templates_title": "Inizia da un modello", + "templates_task_count": "{n} attività", + "template_created": "Creato da modello", + "onboard_hint": "Aggiungi il primo oggetto per iniziare a tracciare la manutenzione.", + "edit": "Modifica", + "duplicate": "Duplica", + "task_duplicated": "Attività duplicata", + "object_duplicated": "Oggetto duplicato", + "delete": "Elimina", + "add_task": "+ Attività", + "complete": "Completato", + "completed": "Completato", + "skip": "Salta", + "skipped": "Saltato", + "missed": "Missed", + "reset": "Reimposta", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annulla", + "bulk_select": "Seleziona", + "bulk_select_all": "Seleziona tutto", + "bulk_n_selected": "{n} selezionati", + "bulk_completed": "{n} attività completate", + "bulk_archived": "{n} attività archiviate", + "completing": "Completamento…", + "interval": "Intervallo", + "warning": "Avviso", + "last_performed": "Ultima esecuzione", + "next_due": "Prossima scadenza", + "days_until_due": "Giorni alla scadenza", + "avg_duration": "Ø Durata", + "trigger": "Trigger", + "trigger_type": "Tipo di trigger", + "threshold_above": "Limite superiore", + "threshold_below": "Limite inferiore", + "threshold": "Soglia", + "counter": "Contatore", + "state_change": "Cambio di stato", + "runtime": "Tempo di funzionamento", + "runtime_hours": "Durata obiettivo (ore)", + "target_value": "Valore obiettivo", + "baseline": "Linea di base", + "target_changes": "Modifiche obiettivo", + "for_minutes": "Per (minuti)", + "time_based": "Temporale", + "sensor_based": "Sensore", + "manual": "Manuale", + "one_time": "Una tantum", + "weekdays": "Giorni della settimana", + "nth_weekday": "N-esimo giorno della settimana del mese", + "day_of_month": "Giorno del mese", + "recurrence_on_days": "Ripeti il", + "recurrence_occurrence": "Occorrenza", + "recurrence_weekday": "Giorno della settimana", + "recurrence_day": "Giorno del mese (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1º", + "ord_2": "2º", + "ord_3": "3º", + "ord_4": "4º", + "ord_5": "5º", + "ord_last": "Ultimo", + "day_word": "Giorno", + "interval_value": "Intervallo", + "interval_unit": "Unità", + "unit_days": "Giorni", + "unit_weeks": "Settimane", + "unit_months": "Mesi", + "unit_years": "Anni", + "due_date": "Data di scadenza", + "cleaning": "Pulizia", + "inspection": "Ispezione", + "replacement": "Sostituzione", + "calibration": "Calibrazione", + "service": "Servizio", + "reading": "Lettura", + "custom": "Personalizzato", + "history": "Cronologia", + "cost": "Costo", + "report_button": "Report", + "report_title": "Report di manutenzione", + "report_generated": "Generato", + "report_times_done": "Fatto", + "report_total_cost": "Costo totale", + "report_every": "ogni {n} {unit}", + "report_notes": "Note", + "report_col_type": "Tipo", + "report_col_status": "Stato", + "report_col_schedule": "Pianificazione", + "duration": "Durata", + "both": "Entrambi", + "trigger_val": "Valore trigger", + "complete_title": "Completato: ", + "checklist": "Checklist", + "checklist_steps_optional": "Passaggi della checklist (opzionale)", + "checklist_placeholder": "Pulire il filtro\nSostituire la guarnizione\nTestare la pressione", + "checklist_help": "Un passaggio per riga. Max 100 elementi.", + "err_too_long": "{field}: troppo lungo (max {n} caratteri)", + "err_too_short": "{field}: troppo corto (min {n} caratteri)", + "err_value_too_high": "{field}: troppo grande (max {n})", + "err_value_too_low": "{field}: troppo piccolo (min {n})", + "err_required": "{field}: campo obbligatorio", + "err_wrong_type": "{field}: tipo errato (atteso: {type})", + "err_invalid_choice": "{field}: valore non consentito", + "err_invalid_value": "{field}: valore non valido", + "feat_schedule_time": "Pianificazione oraria", + "feat_schedule_time_desc": "Le attività scadono a un'ora specifica anziché a mezzanotte.", + "schedule_time_optional": "Scadenza all'ora (opzionale, HH:MM)", + "schedule_time_help": "Vuoto = mezzanotte (default). Fuso orario HA.", + "at_time": "alle", + "notes_optional": "Note (opzionale)", + "cost_optional": "Costo (opzionale)", + "duration_minutes": "Durata in minuti (opzionale)", + "days": "giorni", + "day": "giorno", + "today": "Oggi", + "d_overdue": "g in ritardo", + "no_tasks": "Nessuna attività di manutenzione. Crea un oggetto per iniziare.", + "no_tasks_short": "Nessuna attività", + "no_history": "Nessuna voce nella cronologia.", + "show_all": "Mostra tutto", + "cost_duration_chart": "Costi & Durata", + "installed": "Installato", + "confirm_delete_object": "Eliminare questo oggetto e tutte le sue attività?", + "confirm_delete_task": "Eliminare questa attività?", + "min": "Min", + "max": "Max", + "save": "Salva", + "saving": "Salvataggio…", + "edit_task": "Modifica attività", + "new_task": "Nuova attività di manutenzione", + "task_name": "Nome attività", + "maintenance_type": "Tipo di manutenzione", + "priority": "Priorità", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Bassa", + "priority_normal": "Normale", + "priority_high": "Alta", + "schedule_type": "Tipo di pianificazione", + "interval_days": "Intervallo (giorni)", + "warning_days": "Giorni di avviso", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Ultima esecuzione (opzionale)", + "interval_anchor": "Ancoraggio intervallo", + "anchor_completion": "Dalla data di completamento", + "anchor_planned": "Dalla data pianificata (nessuna deriva)", + "edit_object": "Modifica oggetto", + "name": "Nome", + "manufacturer_optional": "Produttore (opzionale)", + "model_optional": "Modello (opzionale)", + "serial_number_optional": "Numero di serie (opzionale)", + "serial_number_label": "N/S", + "documentation_url_label": "Manuale", + "object_notes_label": "Note", + "sort_due_date": "Scadenza", + "sort_object": "Nome oggetto", + "sort_type": "Tipo", + "sort_task_name": "Nome attività", + "all_objects": "Tutti gli oggetti", + "tasks_lower": "attività", + "no_tasks_yet": "Nessuna attività", + "add_first_task": "Aggiungi prima attività", + "trigger_configuration": "Configurazione trigger", + "entity_id": "ID entità", + "comma_separated": "separati da virgola", + "entity_logic": "Logica entità", + "entity_logic_any": "Qualsiasi entità attiva", + "entity_logic_all": "Tutte le entità devono attivare", + "entities": "entità", + "attribute_optional": "Attributo (opzionale, vuoto = stato)", + "use_entity_state": "Usa stato dell'entità (nessun attributo)", + "trigger_above": "Attivare sopra", + "trigger_below": "Attivare sotto", + "for_at_least_minutes": "Per almeno (minuti)", + "safety_interval_days": "Intervallo di sicurezza (giorni, opzionale)", + "safety_interval": "Intervallo di sicurezza (opzionale)", + "delta_mode": "Modalità delta", + "from_state_optional": "Dallo stato (opzionale)", + "to_state_optional": "Allo stato (opzionale)", + "documentation_url_optional": "URL documentazione (opzionale)", + "object_notes_optional": "Note (opzionale)", + "nfc_tag_id_optional": "ID tag NFC (opzionale)", + "nfc_tags_empty_help": "Nessun tag NFC ancora registrato in Home Assistant.", + "nfc_tags_open_settings": "Apri impostazioni tag", + "nfc_tags_refresh": "Aggiorna", + "environmental_entity_optional": "Sensore ambientale (opzionale)", + "environmental_entity_helper": "es. sensor.temperatura_esterna — regola l'intervallo in base alle condizioni ambientali", + "environmental_attribute_optional": "Attributo ambientale (opzionale)", + "nfc_tag_id": "ID tag NFC", + "nfc_linked": "Tag NFC collegato", + "nfc_link_hint": "Clicca per collegare un tag NFC", + "responsible_user": "Utente responsabile", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Nessun utente assegnato)", + "all_users": "Tutti gli utenti", + "my_tasks": "Le mie attività", + "tab_calendar": "Calendario", + "cal_no_events": "Nessuna manutenzione", + "cal_window_7": "7 giorni", + "cal_window_14": "14 giorni", + "cal_window_30": "30 giorni", + "cal_window_365": "1 anno", + "cal_every_n_days": "ogni {n} giorni", + "cal_source_time": "Basato sul tempo", + "cal_source_time_adaptive": "Basato sul tempo (adattivo)", + "cal_source_sensor": "Basato su sensore", + "cal_predicted": "previsto", + "cal_confidence_high": "alta confidenza", + "cal_confidence_medium": "media confidenza", + "cal_confidence_low": "bassa confidenza", + "budget_monthly": "Budget mensile", + "budget_yearly": "Budget annuale", + "groups": "Gruppi", + "new_group": "Nuovo gruppo", + "edit_group": "Modifica gruppo", + "no_groups": "Nessun gruppo", + "delete_group": "Elimina gruppo", + "delete_group_confirm": "Eliminare il gruppo '{name}'?", + "group_select_tasks": "Seleziona attività", + "group_name_required": "Nome richiesto", + "description_optional": "Descrizione (opzionale)", + "selected": "Selezionato", + "loading_chart": "Caricamento dati...", + "hide_outliers": "Nascondi valori anomali (errori sensore)", + "was_maintenance_needed": "Questa manutenzione era necessaria?", + "feedback_needed": "Necessaria", + "feedback_not_needed": "Non necessaria", + "feedback_not_sure": "Non sicuro", + "suggested_interval": "Intervallo suggerito", + "apply_suggestion": "Applica", + "reanalyze": "Rianalizza", + "reanalyze_result": "Nuova analisi", + "reanalyze_insufficient_data": "Dati insufficienti per una raccomandazione", + "data_points": "punti dati", + "dismiss_suggestion": "Ignora", + "confidence_low": "Bassa", + "confidence_medium": "Media", + "confidence_high": "Alta", + "recommended": "consigliato", + "seasonal_awareness": "Consapevolezza stagionale", + "edit_seasonal_overrides": "Modifica fattori stagionali", + "seasonal_overrides_title": "Fattori stagionali (override)", + "seasonal_overrides_hint": "Fattore per mese (0.1–5.0). Vuoto = appreso automaticamente.", + "seasonal_override_invalid": "Valore non valido", + "seasonal_override_range": "Il fattore deve essere tra 0.1 e 5.0", + "clear_all": "Cancella tutto", + "seasonal_chart_title": "Fattori stagionali", + "seasonal_learned": "Appreso", + "seasonal_manual": "Manuale", + "month_jan": "Gen", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Mag", + "month_jun": "Giu", + "month_jul": "Lug", + "month_aug": "Ago", + "month_sep": "Set", + "month_oct": "Ott", + "month_nov": "Nov", + "month_dec": "Dic", + "sensor_prediction": "Previsione sensore", + "degradation_trend": "Tendenza", + "trend_rising": "In aumento", + "trend_falling": "In calo", + "trend_stable": "Stabile", + "trend_insufficient_data": "Dati insufficienti", + "days_until_threshold": "Giorni alla soglia", + "threshold_exceeded": "Soglia superata", + "environmental_adjustment": "Fattore ambientale", + "sensor_prediction_urgency": "Il sensore prevede la soglia tra ~{days} giorni", + "day_short": "giorno", + "weibull_reliability_curve": "Curva di affidabilità", + "weibull_failure_probability": "Probabilità di guasto", + "weibull_r_squared": "Adattamento R²", + "beta_early_failures": "Guasti precoci", + "beta_random_failures": "Guasti casuali", + "beta_wear_out": "Usura", + "beta_highly_predictable": "Altamente prevedibile", + "confidence_interval": "Intervallo di confidenza", + "confidence_conservative": "Conservativo", + "confidence_aggressive": "Ottimistico", + "current_interval_marker": "Intervallo attuale", + "recommended_marker": "Consigliato", + "characteristic_life": "Vita caratteristica", + "chart_mini_sparkline": "Sparkline di tendenza", + "chart_history": "Cronologia costi e durata", + "chart_seasonal": "Fattori stagionali, 12 mesi", + "chart_weibull": "Curva di affidabilità Weibull", + "chart_sparkline": "Grafico valore trigger sensore", + "days_progress": "Avanzamento giorni", + "qr_code": "Codice QR", + "qr_generating": "Generazione codice QR…", + "qr_error": "Impossibile generare il codice QR.", + "qr_error_no_url": "Nessun URL HA configurato. Impostare un URL esterno o interno in Impostazioni → Sistema → Rete.", + "save_error": "Salvataggio non riuscito. Riprovare.", + "qr_print": "Stampa", + "qr_download": "Scarica SVG", + "qr_action": "Azione alla scansione", + "qr_action_view": "Visualizza info manutenzione", + "qr_action_complete": "Segna manutenzione come completata", + "qr_url_mode": "Tipo di link", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Locale (mDNS)", + "qr_mode_server": "URL server", + "overview": "Panoramica", + "analysis": "Analisi", + "recent_activities": "Attività recenti", + "search_notes": "Cerca nelle note", + "avg_cost": "Ø Costo", + "no_advanced_features": "Nessuna funzione avanzata attivata", + "no_advanced_features_hint": "Attiva “Intervalli Adattivi” o “Modelli Stagionali” nelle impostazioni dell'integrazione per vedere i dati di analisi qui.", + "analysis_not_enough_data": "Non ci sono ancora abbastanza dati per l'analisi.", + "analysis_not_enough_data_hint": "L'analisi Weibull richiede almeno 5 manutenzioni completate; i modelli stagionali diventano visibili dopo 6+ punti dati al mese.", + "analysis_manual_task_hint": "Le attività manuali senza intervallo non generano dati di analisi.", + "completions": "completamenti", + "current": "Attuale", + "shorter": "Più breve", + "longer": "Più lungo", + "normal": "Normale", + "disabled": "Disattivato", + "compound_logic": "Logica composta", + "compound": "Composto (più condizioni)", + "compound_logic_and": "E — tutte le condizioni devono attivarsi", + "compound_logic_or": "O — basta una condizione", + "compound_help": "Combina più condizioni dei sensori in un unico trigger.", + "compound_no_conditions": "Ancora nessuna condizione — aggiungine almeno una.", + "compound_add_condition": "Aggiungi condizione", + "compound_condition": "Condizione", + "compound_remove_condition": "Rimuovi condizione", + "card_title": "Titolo", + "card_show_header": "Mostra intestazione con statistiche", + "card_show_actions": "Mostra pulsanti azione", + "card_compact": "Modalità compatta", + "card_max_items": "Max elementi (0 = tutti)", + "card_filter_status": "Filtra per stato", + "card_filter_status_help": "Vuoto = mostra tutti gli stati.", + "card_filter_objects": "Filtra per oggetti", + "card_filter_objects_help": "Vuoto = mostra tutti gli oggetti.", + "card_filter_entities": "Filtra per entità (entity_ids)", + "card_filter_entities_help": "Seleziona entità sensor / binary_sensor da questa integrazione. Vuoto = tutte.", + "card_loading_objects": "Caricamento oggetti…", + "card_load_error": "Impossibile caricare gli oggetti — verifica la connessione WebSocket.", + "card_no_tasks_title": "Nessuna attività di manutenzione", + "card_no_tasks_cta": "→ Creane una nel pannello Manutenzione", + "no_objects": "Nessun oggetto ancora.", + "action_error": "Azione fallita. Riprova.", + "area_id_optional": "Area (opzionale)", + "installation_date_optional": "Data di installazione (opzionale)", + "warranty_expiry_optional": "Scadenza garanzia (opzionale)", + "warranty": "Garanzia", + "warranty_valid_until": "valida fino al {date}", + "warranty_expires_in": "scade tra {days} giorni", + "warranty_expired": "scaduta", + "cal_past_windows": "Finestre passate", + "cal_forward_windows": "Finestre future", + "history_edit_title": "Modifica voce dello storico", + "history_edit_timestamp": "Data e ora", + "manufacturer": "Produttore", + "model": "Modello", + "area": "Area", + "actions": "Azioni", + "view_mode_label": "Vista", + "view_cards": "Vista schede", + "view_table": "Vista tabella", + "objects_table_columns_label": "Colonne tabella oggetti", + "objects_table_columns_hint": "Scegli quali colonne mostrare nella vista tabella degli oggetti.", + "custom_icon_optional": "Icona (opzionale, es. mdi:wrench)", + "task_enabled": "Attività abilitata", + "skip_reason_prompt": "Saltare questa attività?", + "reason_optional": "Motivo (opzionale)", + "reset_date_prompt": "Segnare l'attività come eseguita?", + "reset_date_optional": "Data ultima esecuzione (opzionale, predefinito: oggi)", + "notes_label": "Note", + "documentation_label": "Documentazione", + "no_nfc_tag": "— Nessun tag —", + "dashboard": "Dashboard", + "tab_today": "Oggi", + "palette_placeholder": "Cerca oggetti e attività…", + "palette_no_results": "Nessun risultato", + "palette_hint": "↑↓ naviga · Invio apri · Esc chiudi", + "today_all_caught_up": "Tutto fatto! Niente in scadenza questa settimana.", + "today_overdue": "Scadute", + "today_due_today": "In scadenza oggi", + "today_this_week": "Questa settimana", + "settings": "Impostazioni", + "settings_features": "Funzioni avanzate", + "settings_features_desc": "Attiva o disattiva le funzioni avanzate. La disattivazione le nasconde dall'interfaccia ma non elimina i dati.", + "feat_adaptive": "Pianificazione adattiva", + "feat_adaptive_desc": "Impara intervalli ottimali dalla cronologia di manutenzione", + "feat_predictions": "Previsioni sensore", + "feat_predictions_desc": "Prevedi date di attivazione dalla degradazione dei sensori", + "feat_seasonal": "Adeguamenti stagionali", + "feat_seasonal_desc": "Adegua gli intervalli in base ai modelli stagionali", + "feat_environmental": "Correlazione ambientale", + "feat_environmental_desc": "Correla gli intervalli con temperatura/umidità", + "feat_budget": "Monitoraggio budget", + "feat_budget_desc": "Monitora le spese di manutenzione mensili e annuali", + "feat_groups": "Gruppi di attività", + "feat_groups_desc": "Organizza le attività in gruppi logici", + "feat_checklists": "Checklist", + "feat_checklists_desc": "Procedure multi-fase per il completamento delle attività", + "settings_general": "Generale", + "settings_default_warning": "Giorni di avviso predefiniti", + "settings_panel_enabled": "Pannello laterale", + "settings_panel_title": "Titolo pannello laterale", + "settings_notifications": "Notifiche", + "settings_notify_service": "Servizio di notifica", + "test_notification": "Notifica di test", + "send_test": "Invia test", + "testing": "Invio in corso…", + "test_notification_success": "Notifica di test inviata", + "test_notification_failed": "Notifica di test non riuscita", + "settings_notify_due_soon": "Notifica quando in scadenza", + "settings_notify_overdue": "Notifica quando scaduto", + "settings_notify_triggered": "Notifica quando attivato", + "settings_interval_hours": "Intervallo di ripetizione (ore, 0 = una volta)", + "settings_quiet_hours": "Ore di silenzio", + "settings_quiet_start": "Inizio", + "settings_quiet_end": "Fine", + "settings_max_per_day": "Max notifiche al giorno (0 = illimitato)", + "settings_bundling": "Raggruppare le notifiche", + "settings_bundle_threshold": "Soglia di raggruppamento", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Pulsanti azione mobili", + "settings_action_complete": "Mostra pulsante 'Completato'", + "settings_action_skip": "Mostra pulsante 'Salta'", + "settings_action_snooze": "Mostra pulsante 'Posticipa'", + "settings_weekly_digest": "Riepilogo settimanale", + "settings_weekly_digest_hint": "Una notifica di riepilogo il lunedì mattina quando ci sono attività.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Durata posticipo (ore)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Budget mensile", + "settings_budget_yearly": "Budget annuale", + "settings_budget_alerts": "Avvisi budget", + "settings_budget_threshold": "Soglia di avviso (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Esporta JSON", + "settings_export_yaml": "Esporta YAML", + "settings_export_csv": "Esporta CSV", + "settings_import_csv": "Importa CSV", + "settings_import_placeholder": "Incolla il contenuto JSON o CSV qui…", + "settings_import_btn": "Importa", + "settings_import_success": "{count} oggetti importati con successo.", + "settings_export_success": "Export scaricato.", + "settings_saved": "Impostazione salvata.", + "settings_include_history": "Includi cronologia", + "sort_alphabetical": "Alfabetico", + "sort_due_soonest": "Scadenza più vicina", + "sort_task_count": "Numero di attività", + "sort_area": "Area", + "sort_assigned_user": "Utente assegnato", + "sort_group": "Gruppo", + "groupby_none": "Nessun raggruppamento", + "groupby_area": "Per area", + "groupby_group": "Per gruppo", + "groupby_user": "Per utente", + "filter_label": "Filtro", + "user_label": "Utente", + "sort_label": "Ordinamento", + "group_by_label": "Raggruppa per", + "state_value_help": "Usa il valore di stato HA (di solito minuscolo, es. \"on\"/\"off\"). Il case viene normalizzato al salvataggio.", + "target_changes_help": "Numero di transizioni corrispondenti prima che il trigger si attivi (predefinito: 1).", + "qr_print_title": "Stampa codici QR", + "qr_print_desc": "Genera una pagina stampabile di codici QR da ritagliare e applicare sulle apparecchiature.", + "qr_print_load": "Carica oggetti", + "qr_print_filter": "Filtro", + "qr_print_objects": "Oggetti", + "qr_print_actions": "Azioni", + "qr_print_url_mode": "Tipo di link", + "qr_print_estimate": "Codici QR stimati", + "qr_print_over_limit": "limite 200, restringi il filtro", + "qr_print_generate": "Genera codici QR", + "qr_print_generating": "Generazione…", + "qr_print_ready": "Codici QR pronti", + "qr_print_print_button": "Stampa", + "qr_print_empty": "Niente da generare", + "qr_action_skip": "Salta", + "vacation_title": "Modalità vacanza", + "vacation_active": "attiva", + "vacation_ended": "terminata", + "vacation_desc": "Pianifica le tue vacanze: le notifiche vengono messe in pausa durante il periodo più giorni di buffer. Puoi escludere singole attività.", + "vacation_enable": "Attiva modalità vacanza", + "vacation_start": "Inizio", + "vacation_end": "Fine", + "vacation_buffer": "Buffer (giorni)", + "vacation_exempt_title": "Notifica comunque durante le vacanze", + "vacation_exempt_desc": "Scegli attività che devono notificare anche in vacanza (es. chimica della piscina critica).", + "vacation_load_tasks": "Carica attività", + "vacation_preview_btn": "Mostra anteprima", + "vacation_preview_affected": "attività interessate", + "vacation_event_due_soon": "sarà in scadenza", + "vacation_event_overdue": "diventerà scaduta", + "vacation_event_triggered_est": "trigger sensore possibile", + "vacation_sensor_based": "(basato su sensore)", + "vacation_action_notify": "Notifica comunque", + "vacation_action_unsilence": "Silenzia di nuovo", + "vacation_marked_complete": "Segnato come completato", + "vacation_marked_skip": "Saltato", + "vacation_end_now": "Termina vacanza ora", + "unassigned": "Non assegnato", + "no_area": "Nessuna area", + "has_overdue": "Attività scadute", + "object": "Oggetto", + "settings_panel_access": "Accesso al pannello", + "settings_panel_access_desc": "Gli amministratori hanno sempre accesso completo. Per delegare creazione, modifica ed eliminazione a non amministratori specifici, attiva questa opzione e selezionali qui sotto — gli altri vedono solo Completa e Salta.", + "settings_operator_write": "Consenti agli utenti selezionati di creare, modificare ed eliminare", + "settings_operator_write_desc": "Disattivato: solo gli amministratori possono modificare i contenuti. Attivato: anche gli utenti selezionati qui sotto hanno accesso completo.", + "no_non_admin_users": "Nessun utente non amministratore trovato. Aggiungili in Impostazioni → Persone.", + "owner_label": "Proprietario", + "feat_completion_actions": "Azioni di completamento", + "feat_completion_actions_desc": "Azione HA per attività al completamento + QR completamento rapido con valori predefiniti.", + "on_complete_action_title": "Al completamento: attiva azione HA (opzionale)", + "on_complete_action_desc": "Chiama un servizio HA quando l'attività viene completata — es. azzerare un contatore sul dispositivo.", + "on_complete_action_service": "Servizio", + "on_complete_action_target": "Entità target", + "on_complete_action_data": "Dati (JSON, opzionale)", + "on_complete_action_test": "Testa azione", + "on_complete_action_test_success": "Riuscito", + "on_complete_action_test_failed": "Fallito", + "quick_complete_defaults_title": "Valori predefiniti completamento rapido (per scansioni QR, opzionale)", + "quick_complete_defaults_desc": "Valori predefiniti per QR di completamento rapido. Senza, il QR apre la finestra di completamento.", + "quick_complete_defaults_notes": "Note", + "quick_complete_defaults_cost": "Costo", + "quick_complete_defaults_duration": "Durata (minuti)", + "quick_complete_defaults_feedback_none": "Nessun feedback", + "quick_complete_defaults_feedback_needed": "Era necessario", + "quick_complete_defaults_feedback_not_needed": "Non necessario", + "quick_complete_success": "Completato rapidamente", + "trigger_replaced": "Trigger sostituito", + "add": "Aggiungi", + "show_stats": "Mostra statistiche + grafici", + "hide_stats": "Nascondi statistiche", + "adaptive_no_data": "Storico dei completamenti ancora insufficiente per l'analisi adattiva. Completa questa attività ancora qualche volta per sbloccare i suggerimenti sull'intervallo e i grafici di affidabilità.", + "suggestion_applied": "Intervallo suggerito applicato", + "vacation_mode": "Modalità vacanza", + "vacation_status_active": "Attiva ora", + "vacation_status_scheduled": "Pianificata", + "vacation_status_inactive": "Inattiva", + "vacation_end_now_confirm": "Terminare subito la vacanza?", + "vacation_exempt_count": "escluse", + "vacation_advanced": "Avanzate…", + "vacation_open_panel": "Apri nel pannello", + "enable": "Attiva", + "saved": "Salvato", + "budget_monthly_set": "Imposta mensile", + "budget_yearly_set": "Imposta annuale", + "budget_advanced": "Valuta, avvisi…", + "budget_open_panel": "Apri nel pannello", + "groups_empty": "Nessun gruppo ancora.", + "group_new_placeholder": "Aggiungi gruppo…", + "group_delete_confirm": "Eliminare il gruppo \"{name}\"?", + "groups_manage_tasks": "Gestisci assegnazioni attività…", + "groups_open_panel": "Apri nel pannello", + "on_complete_action_target_hint": "Nota: il dominio dell'entità deve corrispondere al servizio — es. 'button.press' funziona solo su button.*, 'counter.increment' solo su counter.*, 'input_button.press' solo su input_button.* ecc. In caso di mancata corrispondenza l'azione fallisce silenziosamente (HA registra 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Mostra tutti gli oggetti", + "show_all_tasks": "Azzera filtro — mostra tutte le attività", + "filter_to_overdue": "Filtra l'elenco solo sulle attività in ritardo", + "filter_to_due_soon": "Filtra l'elenco solo sulle attività in scadenza", + "filter_to_triggered": "Filtra l'elenco solo sulle attività attivate", + "open_task": "Apri attività", + "show_details": "Mostra storico + statistiche", + "hide_details": "Nascondi dettagli", + "history_empty": "Nessuno storico ancora.", + "history_edit_button": "Modifica voce", + "total_cost": "Costo totale", + "times_performed": "Eseguita", + "older_entries": "precedenti", + "open_in_panel": "Apri nel pannello Manutenzione", + "skip_reason": "Motivo del salto (facoltativo)", + "reset_to_date": "Reimposta ultima esecuzione al", + "delete_task_confirm": "Eliminare questa attività e il suo storico?", + "delete_object_confirm": "Eliminare questo oggetto e tutte le sue attività?", + "loading": "Caricamento…", + "archive": "Archivia", + "undo": "Annulla", + "task_archived": "Attività archiviata", + "object_archived": "Oggetto archiviato", + "unarchive": "Annulla archiviazione", + "archived": "Archiviato", + "show_archived": "Mostra archiviati", + "hide_archived": "Nascondi archiviati", + "confirm_archive_object": "Archiviare questo oggetto e le sue attività? La cronologia viene conservata e può essere ripristinata in seguito.", + "settings_archive": "Archiviazione e conservazione", + "settings_archive_desc": "Metti da parte le attività una tantum completate senza eliminarle. Gli elementi archiviati sono nascosti e inattivi, ma conservano cronologia e costi.", + "settings_archive_oneoff_days": "Archivia automaticamente le attività una tantum completate dopo (giorni, 0 = off)", + "settings_delete_archived_oneoff_days": "Elimina automaticamente le attività una tantum archiviate dopo (giorni, 0 = mai)", + "archive_object": "Archivia oggetto", + "unarchive_object": "Annulla archiviazione oggetto", + "documents": "Documenti", + "documents_empty": "Ancora nessun documento.", + "doc_upload": "Carica file", + "doc_uploading": "Caricamento…", + "doc_add_link": "Aggiungi link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titolo (facoltativo)", + "doc_open": "Apri", + "doc_delete_confirm": "Eliminare «{name}»?", + "doc_too_large": "File troppo grande (max 25 MB).", + "doc_upload_failed": "Caricamento non riuscito.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Già archiviato altrove — condiviso, nessuno spazio aggiuntivo.", + "doc_dup_in_object": "Questo file è già allegato a questo oggetto.", + "doc_link_invalid": "Sono consentiti solo link http/https.", + "doc_cat_manual": "Manuale", + "doc_cat_warranty": "Garanzia", + "doc_cat_invoice": "Fattura", + "doc_cat_spare_parts": "Ricambi", + "doc_cat_photo": "Foto", + "doc_cat_other": "Altro", + "doc_link_badge": "Link", + "doc_storage_title": "Archiviazione documenti", + "doc_storage_saved": "Risparmiato con deduplicazione", + "doc_storage_refresh": "Aggiorna", + "doc_download": "Scarica", + "doc_close": "Chiudi", + "doc_camera": "Scatta foto", + "doc_drop_hint": "Trascina qui i file", + "doc_task_none": "Nessun documento collegato a questa attività.", + "doc_link_existing": "Collega un documento…", + "doc_attach": "Collega", + "doc_unlink": "Scollega", + "doc_page": "Pagina", + "chart_range_7d": "7g", + "chart_range_30d": "30g", + "chart_range_90d": "90g", + "chart_range_1y": "1a", + "chart_since_service": "dall'ultima manutenzione", + "chart_no_stats": "Nessuna statistica a lungo termine per questa entità — vengono mostrati solo i valori degli interventi", + "auto_complete_on_recovery": "Completa automaticamente quando il sensore si ripristina", + "auto_complete_on_recovery_help": "Registra un completamento (imposta l'ultima esecuzione) quando il trigger si risolve da solo — ad es. sale ricaricato, filtro sostituito.", + "doc_search": "Cerca documenti…", + "doc_search_none": "Nessun documento corrispondente", + "link_device_optional": "Collega a dispositivo esistente (opzionale)", + "parent_object_optional": "Oggetto principale (opzionale)", + "parent_none": "(Nessun principale)", + "paused": "In pausa", + "pause_object": "Metti in pausa", + "resume_object": "Riprendi", + "pause_until_prompt": "Congela i calendari di questo oggetto: nulla scade e nulla notifica finché non viene ripreso. Facoltativamente imposta una data di ripresa automatica.", + "pause_until_label": "Riprendi il (facoltativo)", + "object_paused": "Oggetto in pausa", + "object_resumed": "Oggetto ripreso — calendari riavviati", + "object_paused_badge": "In pausa", + "paused_until_label": "fino al", + "replace_object": "Sostituisci…", + "replace_object_prompt": "Ritira questo oggetto e crea un successore. Storico e costi restano archiviati sul vecchio; attività e documenti passano al nuovo, i contatori ripartono da zero.", + "replace_name_label": "Nome del successore", + "object_replaced": "Oggetto sostituito — successore creato", + "reading_unit_label": "Unità di lettura (es. kWh, m³)", + "reading_unit_help": "Mostrata accanto al valore registrato al completamento di questa attività.", + "reading_value_label": "Valore letto", + "reading_label": "Lettura", + "settings_templates_label": "Galleria modelli", + "settings_templates_hint": "Deseleziona i modelli che non ti serviranno mai: spariscono dai selettori \"Da modello\" (pannello e config flow). Nient'altro cambia; riattivabili in qualsiasi momento.", + "worksheet": "Scheda di lavoro", + "worksheet_scan_view": "Scansiona per aprire l'attività", + "worksheet_scan_complete": "Scansiona per completare", + "worksheet_manual_excerpt": "Estratto del manuale", + "worksheet_pages": "pagine", + "worksheet_printed": "Stampato", + "worksheet_never": "Mai" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/ja.json b/custom_components/maintenance_supporter/frontend-src/locales/ja.json new file mode 100644 index 0000000..6c87564 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/ja.json @@ -0,0 +1,696 @@ +{ + "maintenance": "メンテナンス", + "objects": "対象", + "tasks": "タスク", + "overdue": "期限超過", + "due_soon": "期限間近", + "triggered": "トリガー発生", + "trigger_replaced": "トリガー解除済み", + "ok": "OK", + "all": "すべて", + "new_object": "+ 新規対象", + "templates_from": "テンプレートから", + "templates_title": "テンプレートから始める", + "templates_task_count": "{n} 件のタスク", + "template_created": "テンプレートから作成しました", + "onboard_hint": "最初のオブジェクトを追加してメンテナンスの記録を始めましょう。", + "edit": "編集", + "duplicate": "複製", + "task_duplicated": "タスクを複製しました", + "object_duplicated": "オブジェクトを複製しました", + "delete": "削除", + "add_task": "+ タスク追加", + "complete": "完了", + "completed": "完了済み", + "skip": "スキップ", + "skipped": "スキップ済み", + "missed": "Missed", + "reset": "リセット", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "キャンセル", + "bulk_select": "選択", + "bulk_select_all": "すべて選択", + "bulk_n_selected": "{n} 件選択", + "bulk_completed": "{n} 件のタスクを完了", + "bulk_archived": "{n} 件のタスクをアーカイブ", + "completing": "完了処理中…", + "interval": "間隔", + "warning": "警告", + "last_performed": "最終実施", + "next_due": "次回期限", + "days_until_due": "期限までの日数", + "avg_duration": "平均所要時間", + "trigger": "トリガー", + "trigger_type": "トリガー種別", + "threshold_above": "上限", + "threshold_below": "下限", + "threshold": "しきい値", + "counter": "カウンター", + "state_change": "状態変化", + "runtime": "稼働時間", + "runtime_hours": "目標稼働時間(時間)", + "target_value": "目標値", + "baseline": "基準値", + "target_changes": "変化回数", + "for_minutes": "継続(分)", + "time_based": "時間ベース", + "sensor_based": "センサーベース", + "manual": "手動", + "one_time": "1回のみ", + "weekdays": "曜日", + "nth_weekday": "毎月第N曜日", + "day_of_month": "毎月の日", + "recurrence_on_days": "繰り返す曜日", + "recurrence_occurrence": "出現週", + "recurrence_weekday": "曜日", + "recurrence_day": "日(1〜31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "第1", + "ord_2": "第2", + "ord_3": "第3", + "ord_4": "第4", + "ord_5": "第5", + "ord_last": "最終", + "day_word": "日", + "interval_value": "間隔", + "interval_unit": "単位", + "unit_days": "日", + "unit_weeks": "週", + "unit_months": "か月", + "unit_years": "年", + "due_date": "期限日", + "cleaning": "清掃", + "inspection": "点検", + "replacement": "交換", + "calibration": "校正", + "service": "保守", + "reading": "読み取り", + "custom": "カスタム", + "history": "履歴", + "cost": "費用", + "report_button": "レポート", + "report_title": "メンテナンスレポート", + "report_generated": "作成日", + "report_times_done": "実施回数", + "report_total_cost": "合計費用", + "report_every": "{n} {unit} ごと", + "report_notes": "メモ", + "report_col_type": "種類", + "report_col_status": "状態", + "report_col_schedule": "スケジュール", + "duration": "所要時間", + "both": "両方", + "trigger_val": "トリガー値", + "complete_title": "完了: ", + "checklist": "チェックリスト", + "checklist_steps_optional": "チェックリストの手順(任意)", + "checklist_placeholder": "フィルター清掃\nシール交換\n圧力テスト", + "checklist_help": "1行に1手順。最大100項目。", + "err_too_long": "{field}: 長すぎます(最大{n}文字)", + "err_too_short": "{field}: 短すぎます(最小{n}文字)", + "err_value_too_high": "{field}: 大きすぎます(最大{n})", + "err_value_too_low": "{field}: 小さすぎます(最小{n})", + "err_required": "{field}: 必須です", + "err_wrong_type": "{field}: 型が違います(期待値: {type})", + "err_invalid_choice": "{field}: 許可されていない値です", + "err_invalid_value": "{field}: 無効な値です", + "feat_schedule_time": "時刻指定スケジュール", + "feat_schedule_time_desc": "タスクが深夜0時ではなく指定した時刻に期限超過となります。", + "schedule_time_optional": "期限の時刻(任意、HH:MM)", + "schedule_time_help": "空欄=深夜0時(既定)。HAのタイムゾーン。", + "at_time": "時刻", + "notes_optional": "メモ(任意)", + "cost_optional": "費用(任意)", + "duration_minutes": "所要時間(分、任意)", + "days": "日", + "day": "日", + "today": "今日", + "d_overdue": "日超過", + "no_tasks": "メンテナンスタスクがまだありません。対象を作成して始めましょう。", + "no_tasks_short": "タスクなし", + "no_history": "履歴がまだありません。", + "show_all": "すべて表示", + "cost_duration_chart": "費用と所要時間", + "installed": "設置済み", + "confirm_delete_object": "この対象とすべてのタスクを削除しますか?", + "confirm_delete_task": "このタスクを削除しますか?", + "min": "最小", + "max": "最大", + "save": "保存", + "saving": "保存中…", + "edit_task": "タスクを編集", + "new_task": "新規メンテナンスタスク", + "task_name": "タスク名", + "maintenance_type": "メンテナンス種別", + "priority": "優先度", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "低", + "priority_normal": "標準", + "priority_high": "高", + "schedule_type": "スケジュール種別", + "interval_days": "間隔(日)", + "warning_days": "警告日数", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "最終実施(任意)", + "interval_anchor": "間隔の起点", + "anchor_completion": "完了日から", + "anchor_planned": "予定日から(ずれなし)", + "edit_object": "対象を編集", + "name": "名前", + "manufacturer_optional": "メーカー(任意)", + "model_optional": "型番(任意)", + "serial_number_optional": "シリアル番号(任意)", + "serial_number_label": "S/N", + "documentation_url_label": "取扱説明書", + "object_notes_label": "メモ", + "sort_due_date": "期限日", + "sort_object": "対象名", + "sort_type": "種別", + "sort_task_name": "タスク名", + "all_objects": "すべての対象", + "tasks_lower": "タスク", + "no_tasks_yet": "タスクがまだありません", + "add_first_task": "最初のタスクを追加", + "trigger_configuration": "トリガー設定", + "entity_id": "エンティティID", + "comma_separated": "カンマ区切り", + "entity_logic": "エンティティ条件", + "entity_logic_any": "いずれかのエンティティでトリガー", + "entity_logic_all": "すべてのエンティティが条件を満たす必要あり", + "entities": "エンティティ", + "attribute_optional": "属性(任意、空欄=状態)", + "use_entity_state": "エンティティの状態を使用(属性なし)", + "trigger_above": "この値を超えたらトリガー", + "trigger_below": "この値を下回ったらトリガー", + "for_at_least_minutes": "最低継続時間(分)", + "safety_interval_days": "安全間隔(日、任意)", + "safety_interval": "安全間隔(任意)", + "delta_mode": "差分モード", + "from_state_optional": "変化前の状態(任意)", + "to_state_optional": "変化後の状態(任意)", + "documentation_url_optional": "ドキュメントURL(任意)", + "object_notes_optional": "メモ(任意)", + "nfc_tag_id_optional": "NFCタグID(任意)", + "nfc_tags_empty_help": "Home AssistantにNFCタグがまだ登録されていません。", + "nfc_tags_open_settings": "タグ設定を開く", + "nfc_tags_refresh": "更新", + "environmental_entity_optional": "環境センサー(任意)", + "environmental_entity_helper": "例: sensor.outdoor_temperature — 環境条件に応じて間隔を調整します", + "environmental_attribute_optional": "環境属性(任意)", + "nfc_tag_id": "NFCタグID", + "nfc_linked": "NFCタグ連携済み", + "nfc_link_hint": "クリックしてNFCタグを連携", + "responsible_user": "担当ユーザー", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(担当者なし)", + "all_users": "すべてのユーザー", + "my_tasks": "自分のタスク", + "tab_calendar": "カレンダー", + "cal_no_events": "メンテナンスなし", + "cal_window_7": "7日間", + "cal_window_14": "14日間", + "cal_window_30": "30日間", + "cal_window_365": "1年間", + "cal_every_n_days": "{n}日ごと", + "cal_source_time": "時間ベース", + "cal_source_time_adaptive": "時間ベース(適応)", + "cal_source_sensor": "センサーベース", + "cal_predicted": "予測", + "cal_confidence_high": "信頼度:高", + "cal_confidence_medium": "信頼度:中", + "cal_confidence_low": "信頼度:低", + "budget_monthly": "月間予算", + "budget_yearly": "年間予算", + "groups": "グループ", + "new_group": "新規グループ", + "edit_group": "グループを編集", + "no_groups": "グループがまだありません", + "delete_group": "グループを削除", + "delete_group_confirm": "グループ「{name}」を削除しますか?", + "group_select_tasks": "タスクを選択", + "group_name_required": "名前は必須です", + "description_optional": "説明(任意)", + "selected": "選択中", + "loading_chart": "グラフデータを読み込み中...", + "hide_outliers": "外れ値を隠す(センサー誤差)", + "was_maintenance_needed": "このメンテナンスは必要でしたか?", + "feedback_needed": "必要だった", + "feedback_not_needed": "不要だった", + "feedback_not_sure": "わからない", + "suggested_interval": "推奨間隔", + "apply_suggestion": "適用", + "reanalyze": "再分析", + "reanalyze_result": "新しい分析結果", + "reanalyze_insufficient_data": "推奨を出すにはデータが不足しています", + "data_points": "データポイント", + "dismiss_suggestion": "閉じる", + "confidence_low": "低", + "confidence_medium": "中", + "confidence_high": "高", + "recommended": "推奨", + "seasonal_awareness": "季節対応", + "edit_seasonal_overrides": "季節係数を編集", + "seasonal_overrides_title": "季節係数(上書き)", + "seasonal_overrides_hint": "月ごとの係数(0.1〜5.0)。空欄=自動学習。", + "seasonal_override_invalid": "無効な値です", + "seasonal_override_range": "係数は0.1〜5.0の範囲で指定してください", + "clear_all": "すべてクリア", + "seasonal_chart_title": "季節係数", + "seasonal_learned": "学習値", + "seasonal_manual": "手動", + "month_jan": "1月", + "month_feb": "2月", + "month_mar": "3月", + "month_apr": "4月", + "month_may": "5月", + "month_jun": "6月", + "month_jul": "7月", + "month_aug": "8月", + "month_sep": "9月", + "month_oct": "10月", + "month_nov": "11月", + "month_dec": "12月", + "sensor_prediction": "センサー予測", + "degradation_trend": "傾向", + "trend_rising": "上昇", + "trend_falling": "下降", + "trend_stable": "安定", + "trend_insufficient_data": "データ不足", + "days_until_threshold": "しきい値までの日数", + "threshold_exceeded": "しきい値超過", + "environmental_adjustment": "環境係数", + "sensor_prediction_urgency": "センサー予測:約{days}日でしきい値到達", + "day_short": "日", + "weibull_reliability_curve": "信頼性曲線", + "weibull_failure_probability": "故障確率", + "weibull_r_squared": "適合度 R²", + "beta_early_failures": "初期故障", + "beta_random_failures": "偶発故障", + "beta_wear_out": "摩耗故障", + "beta_highly_predictable": "高い予測可能性", + "confidence_interval": "信頼区間", + "confidence_conservative": "保守的", + "confidence_aggressive": "楽観的", + "current_interval_marker": "現在の間隔", + "recommended_marker": "推奨", + "characteristic_life": "特性寿命", + "chart_mini_sparkline": "傾向スパークライン", + "chart_history": "費用と所要時間の履歴", + "chart_seasonal": "季節係数(12か月)", + "chart_weibull": "ワイブル信頼性曲線", + "chart_sparkline": "センサートリガー値のグラフ", + "days_progress": "経過日数", + "qr_code": "QRコード", + "qr_generating": "QRコードを生成中…", + "qr_error": "QRコードの生成に失敗しました。", + "qr_error_no_url": "HAのURLが設定されていません。設定 → システム → ネットワークで外部または内部URLを設定してください。", + "save_error": "保存に失敗しました。もう一度お試しください。", + "qr_print": "印刷", + "qr_download": "SVGをダウンロード", + "qr_action": "スキャン時の動作", + "qr_action_view": "メンテナンス情報を表示", + "qr_action_complete": "メンテナンスを完了にする", + "qr_url_mode": "リンク種別", + "qr_mode_companion": "コンパニオンアプリ", + "qr_mode_local": "ローカル(mDNS)", + "qr_mode_server": "サーバーURL", + "overview": "概要", + "analysis": "分析", + "recent_activities": "最近のアクティビティ", + "search_notes": "メモを検索", + "avg_cost": "平均費用", + "no_advanced_features": "高度な機能が有効になっていません", + "no_advanced_features_hint": "統合設定で「適応間隔」または「季節パターン」を有効にすると、ここに分析データが表示されます。", + "analysis_not_enough_data": "分析に必要なデータがまだ不足しています。", + "analysis_not_enough_data_hint": "ワイブル分析には完了済みメンテナンスが最低5件必要です。季節パターンは月あたり6件以上のデータポイントで表示されるようになります。", + "analysis_manual_task_hint": "間隔のない手動タスクは分析データを生成しません。", + "completions": "完了回数", + "current": "現在", + "shorter": "短く", + "longer": "長く", + "normal": "通常", + "disabled": "無効", + "compound_logic": "複合条件", + "compound": "複合(複数条件)", + "compound_logic_and": "AND — すべての条件が必要", + "compound_logic_or": "OR — いずれかの条件で発火", + "compound_help": "複数のセンサー条件を1つのトリガーにまとめます。", + "compound_no_conditions": "条件がありません — 少なくとも1つ追加してください。", + "compound_add_condition": "条件を追加", + "compound_condition": "条件", + "compound_remove_condition": "条件を削除", + "card_title": "タイトル", + "card_show_header": "統計付きヘッダーを表示", + "card_show_actions": "操作ボタンを表示", + "card_compact": "コンパクトモード", + "card_max_items": "最大表示件数(0=すべて)", + "card_filter_status": "状態で絞り込み", + "card_filter_status_help": "空欄=すべての状態を表示。", + "card_filter_objects": "対象で絞り込み", + "card_filter_objects_help": "空欄=すべての対象を表示。", + "card_filter_entities": "エンティティで絞り込み(entity_ids)", + "card_filter_entities_help": "この統合のsensor/binary_sensorエンティティを選択。空欄=すべて。", + "card_loading_objects": "対象を読み込み中…", + "card_load_error": "対象を読み込めませんでした — WebSocket接続を確認してください。", + "card_no_tasks_title": "メンテナンスタスクがまだありません", + "card_no_tasks_cta": "→ メンテナンスパネルで作成しましょう", + "no_objects": "対象がまだありません。", + "action_error": "操作に失敗しました。もう一度お試しください。", + "area_id_optional": "エリア(任意)", + "installation_date_optional": "設置日(任意)", + "warranty_expiry_optional": "保証期限(任意)", + "warranty": "保証", + "warranty_valid_until": "{date}まで有効", + "warranty_expires_in": "あと{days}日で期限切れ", + "warranty_expired": "期限切れ", + "cal_past_windows": "過去の期間", + "cal_forward_windows": "今後の期間", + "history_edit_title": "履歴項目を編集", + "history_edit_timestamp": "日時", + "manufacturer": "メーカー", + "model": "型番", + "area": "エリア", + "actions": "操作", + "view_mode_label": "表示", + "view_cards": "カード表示", + "view_table": "テーブル表示", + "objects_table_columns_label": "対象テーブルの列", + "objects_table_columns_hint": "対象のテーブル表示に表示する列を選択します。", + "custom_icon_optional": "アイコン(任意、例: mdi:wrench)", + "task_enabled": "タスク有効", + "skip_reason_prompt": "このタスクをスキップしますか?", + "reason_optional": "理由(任意)", + "reset_date_prompt": "タスクを実施済みにしますか?", + "reset_date_optional": "最終実施日(任意、既定は今日)", + "notes_label": "メモ", + "documentation_label": "ドキュメント", + "no_nfc_tag": "— タグなし —", + "dashboard": "ダッシュボード", + "tab_today": "今日", + "palette_placeholder": "オブジェクトとタスクを検索…", + "palette_no_results": "一致なし", + "palette_hint": "↑↓ 移動 · Enter 開く · Esc 閉じる", + "today_all_caught_up": "すべて完了!今週の予定はありません。", + "today_overdue": "期限切れ", + "today_due_today": "今日期限", + "today_this_week": "今週", + "settings": "設定", + "settings_features": "高度な機能", + "settings_features_desc": "高度な機能の有効/無効を切り替えます。無効にするとUIから非表示になりますが、データは削除されません。", + "feat_adaptive": "適応スケジューリング", + "feat_adaptive_desc": "メンテナンス履歴から最適な間隔を学習します", + "feat_predictions": "センサー予測", + "feat_predictions_desc": "センサーの劣化からトリガー日を予測します", + "feat_seasonal": "季節調整", + "feat_seasonal_desc": "季節パターンに基づいて間隔を調整します", + "feat_environmental": "環境相関", + "feat_environmental_desc": "温度・湿度と間隔を相関させます", + "feat_budget": "予算管理", + "feat_budget_desc": "月間・年間のメンテナンス費用を管理します", + "feat_groups": "タスクグループ", + "feat_groups_desc": "タスクを論理的なグループに整理します", + "feat_checklists": "チェックリスト", + "feat_checklists_desc": "タスク完了のための複数手順の手続き", + "settings_general": "一般", + "settings_default_warning": "既定の警告日数", + "settings_panel_enabled": "サイドバーパネル", + "settings_panel_title": "サイドバーパネルのタイトル", + "settings_notifications": "通知", + "settings_notify_service": "通知サービス", + "test_notification": "テスト通知", + "send_test": "テスト送信", + "testing": "送信中…", + "test_notification_success": "テスト通知を送信しました", + "test_notification_failed": "テスト通知に失敗しました", + "settings_notify_due_soon": "期限間近で通知", + "settings_notify_overdue": "期限超過で通知", + "settings_notify_triggered": "トリガー発生で通知", + "settings_interval_hours": "繰り返し間隔(時間、0=1回のみ)", + "settings_quiet_hours": "通知停止時間", + "settings_quiet_start": "開始", + "settings_quiet_end": "終了", + "settings_max_per_day": "1日あたりの最大通知数(0=無制限)", + "settings_bundling": "通知をまとめる", + "settings_bundle_threshold": "まとめるしきい値", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "モバイル操作ボタン", + "settings_action_complete": "「完了」ボタンを表示", + "settings_action_skip": "「スキップ」ボタンを表示", + "settings_action_snooze": "「スヌーズ」ボタンを表示", + "settings_weekly_digest": "週間まとめ", + "settings_weekly_digest_hint": "タスクがある月曜の朝に1件のまとめ通知。", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "スヌーズ時間(時間)", + "settings_budget": "予算", + "settings_currency": "通貨", + "settings_budget_monthly": "月間予算", + "settings_budget_yearly": "年間予算", + "settings_budget_alerts": "予算アラート", + "settings_budget_threshold": "アラートしきい値(%)", + "settings_import_export": "インポート/エクスポート", + "settings_export_json": "JSONをエクスポート", + "settings_export_yaml": "YAMLをエクスポート", + "settings_export_csv": "CSVをエクスポート", + "settings_import_csv": "CSVをインポート", + "settings_import_placeholder": "JSONまたはCSVの内容をここに貼り付け…", + "settings_import_btn": "インポート", + "settings_import_success": "{count}件の対象を正常にインポートしました。", + "settings_export_success": "エクスポートをダウンロードしました。", + "settings_saved": "設定を保存しました。", + "settings_include_history": "履歴を含める", + "sort_alphabetical": "アルファベット順", + "sort_due_soonest": "期限が近い順", + "sort_task_count": "タスク数", + "sort_area": "エリア", + "sort_assigned_user": "担当ユーザー", + "sort_group": "グループ", + "groupby_none": "グループ化なし", + "groupby_area": "エリア別", + "groupby_group": "グループ別", + "groupby_user": "ユーザー別", + "filter_label": "絞り込み", + "user_label": "ユーザー", + "sort_label": "並べ替え", + "group_by_label": "グループ化", + "state_value_help": "HAの状態値を使用してください(通常は小文字、例: \"on\"/\"off\")。保存時に大文字小文字は正規化されます。", + "target_changes_help": "トリガーが発生するまでに一致する遷移の回数(既定: 1)。", + "qr_print_title": "QRコードを印刷", + "qr_print_desc": "切り取って機器に貼れるQRコードの印刷ページを生成します。", + "qr_print_load": "対象を読み込む", + "qr_print_filter": "絞り込み", + "qr_print_objects": "対象", + "qr_print_actions": "操作", + "qr_print_url_mode": "リンク種別", + "qr_print_estimate": "QRコードの推定数", + "qr_print_over_limit": "上限は200件です。絞り込んでください", + "qr_print_generate": "QRコードを生成", + "qr_print_generating": "生成中…", + "qr_print_ready": "QRコードの準備完了", + "qr_print_print_button": "印刷", + "qr_print_empty": "生成する対象がありません", + "qr_action_skip": "スキップ", + "vacation_title": "休暇モード", + "vacation_active": "有効", + "vacation_ended": "終了済み", + "vacation_desc": "休暇を計画します。期間中とその前後のバッファ日数の間、通知が一時停止されます。特定のタスクは通知対象に戻せます。", + "vacation_enable": "休暇モードを有効にする", + "vacation_start": "開始", + "vacation_end": "終了", + "vacation_buffer": "バッファ(日)", + "vacation_exempt_title": "休暇中でも通知する", + "vacation_exempt_desc": "休暇中でも通知すべきタスクを選択します(例: 重要なプールの水質管理)。", + "vacation_load_tasks": "タスクを読み込む", + "vacation_preview_btn": "プレビューを表示", + "vacation_preview_affected": "件のタスクが影響を受けます", + "vacation_event_due_soon": "期限間近になります", + "vacation_event_overdue": "期限超過になります", + "vacation_event_triggered_est": "センサートリガーの可能性あり", + "vacation_sensor_based": "(センサーベース)", + "vacation_action_notify": "通知する", + "vacation_action_unsilence": "再び停止する", + "vacation_marked_complete": "完了にしました", + "vacation_marked_skip": "スキップしました", + "vacation_end_now": "今すぐ休暇を終了", + "add": "追加", + "show_stats": "統計とグラフを表示", + "hide_stats": "統計を非表示", + "adaptive_no_data": "適応分析に必要な完了履歴がまだ不足しています。このタスクをあと数回完了すると、間隔の推奨と信頼性グラフが利用できるようになります。", + "suggestion_applied": "推奨間隔を適用しました", + "vacation_mode": "休暇モード", + "vacation_status_active": "現在有効", + "vacation_status_scheduled": "予定済み", + "vacation_status_inactive": "無効", + "vacation_end_now_confirm": "今すぐ休暇を終了しますか?", + "vacation_exempt_count": "件 除外", + "vacation_advanced": "詳細…", + "vacation_open_panel": "パネルで開く", + "enable": "有効にする", + "saved": "保存しました", + "budget_monthly_set": "月間を設定", + "budget_yearly_set": "年間を設定", + "budget_advanced": "通貨、アラート…", + "budget_open_panel": "パネルで開く", + "groups_empty": "グループがまだありません。", + "group_new_placeholder": "グループを追加…", + "group_delete_confirm": "グループ「{name}」を削除しますか?", + "groups_manage_tasks": "タスクの割り当てを管理…", + "groups_open_panel": "パネルで開く", + "unassigned": "未割り当て", + "no_area": "エリアなし", + "has_overdue": "期限超過のタスクあり", + "object": "対象", + "settings_panel_access": "パネルアクセス", + "settings_panel_access_desc": "管理者は常にフルアクセスできます。作成・編集・削除を特定の非管理者に委任するには、これをオンにして下記で選択してください。それ以外のユーザーには完了とスキップのみが表示されます。", + "settings_operator_write": "選択したユーザーに作成・編集・削除を許可", + "settings_operator_write_desc": "オフ:管理者のみがコンテンツを変更できます。オン:下記で選択したユーザーもフルアクセスできます。", + "no_non_admin_users": "非管理者ユーザーが見つかりません。設定 → ユーザーで追加してください。", + "owner_label": "オーナー", + "feat_completion_actions": "完了時アクション", + "feat_completion_actions_desc": "タスクごとの完了時HAアクション+事前設定値付きのクイック完了QR。", + "on_complete_action_title": "完了時: HAアクションを実行(任意)", + "on_complete_action_desc": "タスク完了時にHAサービスを呼び出します — 例: デバイスのカウンターをリセット。", + "on_complete_action_service": "サービス", + "on_complete_action_target": "対象エンティティ", + "on_complete_action_target_hint": "注意: エンティティのドメインはサービスと一致する必要があります — 例: 'button.press'はbutton.*のみ、'counter.increment'はcounter.*のみ、'input_button.press'はinput_button.*のみで動作します。不一致の場合、アクションは何も表示せずに失敗します(HAのログに「Referenced entities ... missing or not currently available」と記録されます)。", + "on_complete_action_data": "データ(JSON、任意)", + "on_complete_action_test": "設定を検証", + "on_complete_action_test_success": "✓ 設定は有効です(アクションはタスク完了時のみ実行されます)", + "on_complete_action_test_failed": "失敗しました", + "quick_complete_defaults_title": "クイック完了の既定値(QRスキャン用、任意)", + "quick_complete_defaults_desc": "クイック完了QRスキャンの事前設定値。設定しない場合、QRは完了ダイアログを開きます。", + "quick_complete_defaults_notes": "メモ", + "quick_complete_defaults_cost": "費用", + "quick_complete_defaults_duration": "所要時間(分)", + "quick_complete_defaults_feedback_none": "フィードバックなし", + "quick_complete_defaults_feedback_needed": "必要だった", + "quick_complete_defaults_feedback_not_needed": "不要だった", + "quick_complete_success": "クイック完了にしました", + "show_all_objects": "すべての対象を表示", + "show_all_tasks": "絞り込みを解除 — すべてのタスクを表示", + "filter_to_overdue": "タスク一覧を期限超過のみに絞り込む", + "filter_to_due_soon": "タスク一覧を期限間近のみに絞り込む", + "filter_to_triggered": "タスク一覧をトリガー発生のみに絞り込む", + "open_task": "タスクを開く", + "show_details": "履歴と統計を表示", + "hide_details": "詳細を非表示", + "history_empty": "履歴がまだありません。", + "history_edit_button": "項目を編集", + "total_cost": "費用合計", + "times_performed": "実施回数", + "older_entries": "以前", + "open_in_panel": "メンテナンスパネルで開く", + "skip_reason": "スキップ理由(任意)", + "reset_to_date": "最終実施日を次の日付にリセット", + "delete_task_confirm": "このタスクと履歴を削除しますか?", + "delete_object_confirm": "この対象とすべてのタスクを削除しますか?", + "loading": "読み込み中…", + "archive": "アーカイブ", + "undo": "元に戻す", + "task_archived": "タスクをアーカイブしました", + "object_archived": "オブジェクトをアーカイブしました", + "unarchive": "アーカイブ解除", + "archived": "アーカイブ済み", + "show_archived": "アーカイブを表示", + "hide_archived": "アーカイブを非表示", + "confirm_archive_object": "このオブジェクトとそのタスクをアーカイブしますか?履歴は保持され、後で元に戻せます。", + "settings_archive": "アーカイブと保持", + "settings_archive_desc": "完了した単発タスクを削除せずに片付けます。アーカイブされた項目は非表示で動作しませんが、履歴とコストは保持されます。", + "settings_archive_oneoff_days": "完了した単発タスクを自動アーカイブする日数(0 = 無効)", + "settings_delete_archived_oneoff_days": "アーカイブ済み単発タスクを自動削除する日数(0 = しない)", + "archive_object": "オブジェクトをアーカイブ", + "unarchive_object": "オブジェクトのアーカイブを解除", + "documents": "ドキュメント", + "documents_empty": "まだドキュメントがありません。", + "doc_upload": "ファイルをアップロード", + "doc_uploading": "アップロード中…", + "doc_add_link": "リンクを追加", + "doc_link_url": "URL (https://…)", + "doc_link_title": "タイトル(任意)", + "doc_open": "開く", + "doc_delete_confirm": "「{name}」を削除しますか?", + "doc_too_large": "ファイルが大きすぎます(最大 25 MB)。", + "doc_upload_failed": "アップロードに失敗しました。", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "既に別の場所に保存済み — 共有され、追加容量はかかりません。", + "doc_dup_in_object": "このファイルは既にこのオブジェクトに添付されています。", + "doc_link_invalid": "http/https のリンクのみ許可されています。", + "doc_cat_manual": "取扱説明書", + "doc_cat_warranty": "保証書", + "doc_cat_invoice": "請求書", + "doc_cat_spare_parts": "交換部品", + "doc_cat_photo": "写真", + "doc_cat_other": "その他", + "doc_link_badge": "リンク", + "doc_storage_title": "ドキュメントストレージ", + "doc_storage_saved": "重複排除による節約", + "doc_storage_refresh": "更新", + "doc_download": "ダウンロード", + "doc_close": "閉じる", + "doc_camera": "写真を撮る", + "doc_drop_hint": "ここにファイルをドロップ", + "doc_task_none": "このタスクに紐付いたドキュメントはありません。", + "doc_link_existing": "ドキュメントを紐付け…", + "doc_attach": "紐付け", + "doc_unlink": "紐付け解除", + "doc_page": "ページ", + "chart_range_7d": "7日", + "chart_range_30d": "30日", + "chart_range_90d": "90日", + "chart_range_1y": "1年", + "chart_since_service": "前回のメンテナンス以降", + "chart_no_stats": "このエンティティには長期統計がありません — メンテナンス記録の値のみ表示しています", + "auto_complete_on_recovery": "センサー回復時に自動完了", + "auto_complete_on_recovery_help": "トリガーが自然に解消されたとき(塩の補充、フィルター交換など)に完了を記録し、最終実施日を更新します。", + "doc_search": "ドキュメントを検索…", + "doc_search_none": "一致するドキュメントはありません", + "link_device_optional": "既存のデバイスにリンク(任意)", + "parent_object_optional": "親オブジェクト(任意)", + "parent_none": "(親なし)", + "paused": "一時停止中", + "pause_object": "一時停止", + "resume_object": "再開", + "pause_until_prompt": "このオブジェクトのスケジュールを凍結します。再開するまで期限も通知も発生しません。必要に応じて自動再開日を設定できます。", + "pause_until_label": "再開日(任意)", + "object_paused": "オブジェクトを一時停止しました", + "object_resumed": "オブジェクトを再開しました — スケジュールを再スタート", + "object_paused_badge": "一時停止中", + "paused_until_label": "まで", + "replace_object": "置き換え…", + "replace_object_prompt": "このオブジェクトを退役させ、後継を作成します。履歴とコストは旧オブジェクトにアーカイブされたまま残り、タスクと文書は新しいオブジェクトへ引き継がれ、カウンターはリセットされます。", + "replace_name_label": "後継の名前", + "object_replaced": "オブジェクトを置き換えました — 後継を作成", + "reading_unit_label": "検針単位(例: kWh、m³)", + "reading_unit_help": "このタスクの完了時に記録値の横に表示されます。", + "reading_value_label": "検針値", + "reading_label": "検針", + "settings_templates_label": "テンプレートギャラリー", + "settings_templates_hint": "不要なテンプレートのチェックを外すと、「テンプレートから」の選択肢(パネルと設定フロー)から消えます。それ以外は何も変わらず、いつでも再有効化できます。", + "worksheet": "作業シート", + "worksheet_scan_view": "スキャンしてタスクを開く", + "worksheet_scan_complete": "スキャンして完了", + "worksheet_manual_excerpt": "マニュアル抜粋", + "worksheet_pages": "ページ", + "worksheet_printed": "印刷日", + "worksheet_never": "未実施" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/nb.json b/custom_components/maintenance_supporter/frontend-src/locales/nb.json new file mode 100644 index 0000000..dacefde --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/nb.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Vedlikehold", + "objects": "Objekter", + "tasks": "Oppgaver", + "overdue": "Forfalt", + "due_soon": "Forfaller snart", + "triggered": "Utløst", + "trigger_replaced": "Utløser erstattet", + "ok": "OK", + "all": "Alle", + "new_object": "+ Nytt objekt", + "templates_from": "Fra mal", + "templates_title": "Start fra en mal", + "templates_task_count": "{n} oppgaver", + "template_created": "Opprettet fra mal", + "onboard_hint": "Legg til ditt første objekt for å spore vedlikehold.", + "edit": "Rediger", + "duplicate": "Dupliser", + "task_duplicated": "Oppgave duplisert", + "object_duplicated": "Objekt duplisert", + "delete": "Slett", + "add_task": "+ Legg til oppgave", + "complete": "Fullfør", + "completed": "Fullført", + "skip": "Hopp over", + "skipped": "Hoppet over", + "missed": "Missed", + "reset": "Tilbakestill", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Avbryt", + "bulk_select": "Velg", + "bulk_select_all": "Velg alle", + "bulk_n_selected": "{n} valgt", + "bulk_completed": "{n} oppgaver fullført", + "bulk_archived": "{n} oppgaver arkivert", + "completing": "Fullfører…", + "interval": "Intervall", + "warning": "Advarsel", + "last_performed": "Sist utført", + "next_due": "Neste forfall", + "days_until_due": "Dager til forfall", + "avg_duration": "Gj.snittlig varighet", + "trigger": "Utløser", + "trigger_type": "Utløsertype", + "threshold_above": "Øvre grense", + "threshold_below": "Nedre grense", + "threshold": "Terskel", + "counter": "Teller", + "state_change": "Tilstandsendring", + "runtime": "Driftstid", + "runtime_hours": "Måldriftstid (timer)", + "target_value": "Målverdi", + "baseline": "Utgangspunkt", + "target_changes": "Antall endringer", + "for_minutes": "I (minutter)", + "time_based": "Tidsbasert", + "sensor_based": "Sensorbasert", + "manual": "Manuell", + "one_time": "Engangs", + "weekdays": "Ukedager", + "nth_weekday": "N-te ukedag i måneden", + "day_of_month": "Dag i måneden", + "recurrence_on_days": "Gjenta på", + "recurrence_occurrence": "Forekomst", + "recurrence_weekday": "Ukedag", + "recurrence_day": "Dag i måneden (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Siste", + "day_word": "Dag", + "interval_value": "Intervall", + "interval_unit": "Enhet", + "unit_days": "Dager", + "unit_weeks": "Uker", + "unit_months": "Måneder", + "unit_years": "År", + "due_date": "Forfallsdato", + "cleaning": "Rengjøring", + "inspection": "Inspeksjon", + "replacement": "Utskifting", + "calibration": "Kalibrering", + "service": "Service", + "reading": "Avlesning", + "custom": "Egendefinert", + "history": "Historikk", + "cost": "Kostnad", + "report_button": "Rapport", + "report_title": "Vedlikeholdsrapport", + "report_generated": "Generert", + "report_times_done": "Utført", + "report_total_cost": "Totalkostnad", + "report_every": "hver {n}. {unit}", + "report_notes": "Notater", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Tidsplan", + "duration": "Varighet", + "both": "Begge", + "trigger_val": "Utløserverdi", + "complete_title": "Fullfør: ", + "checklist": "Sjekkliste", + "checklist_steps_optional": "Sjekklistetrinn (valgfritt)", + "checklist_placeholder": "Rengjør filter\nBytt pakning\nTest trykk", + "checklist_help": "Ett trinn per linje. Maks 100 elementer.", + "err_too_long": "{field}: for lang (maks {n} tegn)", + "err_too_short": "{field}: for kort (min {n} tegn)", + "err_value_too_high": "{field}: for stor (maks {n})", + "err_value_too_low": "{field}: for liten (min {n})", + "err_required": "{field}: påkrevd", + "err_wrong_type": "{field}: feil type (forventet: {type})", + "err_invalid_choice": "{field}: ikke en tillatt verdi", + "err_invalid_value": "{field}: ugyldig verdi", + "feat_schedule_time": "Planlegging med klokkeslett", + "feat_schedule_time_desc": "Oppgaver forfaller på et bestemt klokkeslett i stedet for ved midnatt.", + "schedule_time_optional": "Forfaller kl. (valgfritt, TT:MM)", + "schedule_time_help": "Tom = midnatt (standard). HA-tidssone.", + "at_time": "kl.", + "notes_optional": "Notater (valgfritt)", + "cost_optional": "Kostnad (valgfritt)", + "duration_minutes": "Varighet i minutter (valgfritt)", + "days": "dager", + "day": "dag", + "today": "I dag", + "d_overdue": "d forfalt", + "no_tasks": "Ingen vedlikeholdsoppgaver ennå. Opprett et objekt for å komme i gang.", + "no_tasks_short": "Ingen oppgaver", + "no_history": "Ingen historikkoppføringer ennå.", + "show_all": "Vis alle", + "cost_duration_chart": "Kostnad og varighet", + "installed": "Installert", + "confirm_delete_object": "Slette dette objektet og alle dets oppgaver?", + "confirm_delete_task": "Slette denne oppgaven?", + "min": "Min", + "max": "Maks", + "save": "Lagre", + "saving": "Lagrer…", + "edit_task": "Rediger oppgave", + "new_task": "Ny vedlikeholdsoppgave", + "task_name": "Oppgavenavn", + "maintenance_type": "Vedlikeholdstype", + "priority": "Prioritet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Lav", + "priority_normal": "Normal", + "priority_high": "Høy", + "schedule_type": "Planleggingstype", + "interval_days": "Intervall (dager)", + "warning_days": "Varslingsdager", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Sist utført (valgfritt)", + "interval_anchor": "Intervallforankring", + "anchor_completion": "Fra fullføringsdato", + "anchor_planned": "Fra planlagt dato (ingen avdrift)", + "edit_object": "Rediger objekt", + "name": "Navn", + "manufacturer_optional": "Produsent (valgfritt)", + "model_optional": "Modell (valgfritt)", + "serial_number_optional": "Serienummer (valgfritt)", + "serial_number_label": "S/N", + "documentation_url_label": "Bruksanvisning", + "object_notes_label": "Notater", + "sort_due_date": "Forfallsdato", + "sort_object": "Objektnavn", + "sort_type": "Type", + "sort_task_name": "Oppgavenavn", + "all_objects": "Alle objekter", + "tasks_lower": "oppgaver", + "no_tasks_yet": "Ingen oppgaver ennå", + "add_first_task": "Legg til første oppgave", + "trigger_configuration": "Utløserkonfigurasjon", + "entity_id": "Entitets-ID", + "comma_separated": "kommaseparert", + "entity_logic": "Entitetslogikk", + "entity_logic_any": "Hvilken som helst entitet utløser", + "entity_logic_all": "Alle entiteter må utløse", + "entities": "entiteter", + "attribute_optional": "Attributt (valgfritt, tomt = tilstand)", + "use_entity_state": "Bruk entitetstilstand (ingen attributt)", + "trigger_above": "Utløs over", + "trigger_below": "Utløs under", + "for_at_least_minutes": "I minst (minutter)", + "safety_interval_days": "Sikkerhetsintervall (dager, valgfritt)", + "safety_interval": "Sikkerhetsintervall (valgfritt)", + "delta_mode": "Deltamodus", + "from_state_optional": "Fra tilstand (valgfritt)", + "to_state_optional": "Til tilstand (valgfritt)", + "documentation_url_optional": "Dokumentasjons-URL (valgfritt)", + "object_notes_optional": "Notater (valgfritt)", + "nfc_tag_id_optional": "NFC-brikke-ID (valgfritt)", + "nfc_tags_empty_help": "Ingen NFC-brikker registrert i Home Assistant ennå.", + "nfc_tags_open_settings": "Åpne brikkeinnstillinger", + "nfc_tags_refresh": "Oppdater", + "environmental_entity_optional": "Miljøsensor (valgfritt)", + "environmental_entity_helper": "f.eks. sensor.outdoor_temperature — justerer intervallet basert på miljøforhold", + "environmental_attribute_optional": "Miljøattributt (valgfritt)", + "nfc_tag_id": "NFC-brikke-ID", + "nfc_linked": "NFC-brikke koblet", + "nfc_link_hint": "Klikk for å koble NFC-brikke", + "responsible_user": "Ansvarlig bruker", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ingen bruker tildelt)", + "all_users": "Alle brukere", + "my_tasks": "Mine oppgaver", + "tab_calendar": "Kalender", + "cal_no_events": "Ingen vedlikehold", + "cal_window_7": "7 dager", + "cal_window_14": "14 dager", + "cal_window_30": "30 dager", + "cal_window_365": "1 år", + "cal_every_n_days": "hver {n}. dag", + "cal_source_time": "Tidsbasert", + "cal_source_time_adaptive": "Tidsbasert (adaptiv)", + "cal_source_sensor": "Sensorbasert", + "cal_predicted": "forutsagt", + "cal_confidence_high": "høy sikkerhet", + "cal_confidence_medium": "middels sikkerhet", + "cal_confidence_low": "lav sikkerhet", + "budget_monthly": "Månedsbudsjett", + "budget_yearly": "Årsbudsjett", + "groups": "Grupper", + "new_group": "Ny gruppe", + "edit_group": "Rediger gruppe", + "no_groups": "Ingen grupper ennå", + "delete_group": "Slett gruppe", + "delete_group_confirm": "Slette gruppen '{name}'?", + "group_select_tasks": "Velg oppgaver", + "group_name_required": "Navn er påkrevd", + "description_optional": "Beskrivelse (valgfritt)", + "selected": "Valgt", + "loading_chart": "Laster diagramdata...", + "hide_outliers": "Skjul avvik (sensorfeil)", + "was_maintenance_needed": "Var dette vedlikeholdet nødvendig?", + "feedback_needed": "Nødvendig", + "feedback_not_needed": "Ikke nødvendig", + "feedback_not_sure": "Usikker", + "suggested_interval": "Foreslått intervall", + "apply_suggestion": "Bruk", + "reanalyze": "Analyser på nytt", + "reanalyze_result": "Ny analyse", + "reanalyze_insufficient_data": "Ikke nok data til å gi en anbefaling", + "data_points": "datapunkter", + "dismiss_suggestion": "Avvis", + "confidence_low": "Lav", + "confidence_medium": "Middels", + "confidence_high": "Høy", + "recommended": "anbefalt", + "seasonal_awareness": "Sesongbevissthet", + "edit_seasonal_overrides": "Rediger sesongfaktorer", + "seasonal_overrides_title": "Sesongfaktorer (overstyring)", + "seasonal_overrides_hint": "Faktor per måned (0,1–5,0). Tom = læres automatisk.", + "seasonal_override_invalid": "Ugyldig verdi", + "seasonal_override_range": "Faktor må være mellom 0,1 og 5,0", + "clear_all": "Fjern alle", + "seasonal_chart_title": "Sesongfaktorer", + "seasonal_learned": "Lært", + "seasonal_manual": "Manuell", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Mai", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Des", + "sensor_prediction": "Sensorprediksjon", + "degradation_trend": "Trend", + "trend_rising": "Stigende", + "trend_falling": "Synkende", + "trend_stable": "Stabil", + "trend_insufficient_data": "Utilstrekkelige data", + "days_until_threshold": "Dager til terskel", + "threshold_exceeded": "Terskel overskredet", + "environmental_adjustment": "Miljøfaktor", + "sensor_prediction_urgency": "Sensoren forutsier terskel om ~{days} dager", + "day_short": "dag", + "weibull_reliability_curve": "Pålitelighetskurve", + "weibull_failure_probability": "Feilsannsynlighet", + "weibull_r_squared": "Tilpasning R²", + "beta_early_failures": "Tidlige feil", + "beta_random_failures": "Tilfeldige feil", + "beta_wear_out": "Slitasje", + "beta_highly_predictable": "Svært forutsigbar", + "confidence_interval": "Konfidensintervall", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisk", + "current_interval_marker": "Nåværende intervall", + "recommended_marker": "Anbefalt", + "characteristic_life": "Karakteristisk levetid", + "chart_mini_sparkline": "Trend-minigraf", + "chart_history": "Kostnads- og varighetshistorikk", + "chart_seasonal": "Sesongfaktorer, 12 måneder", + "chart_weibull": "Weibull-pålitelighetskurve", + "chart_sparkline": "Diagram over sensorutløserverdi", + "days_progress": "Dagsfremdrift", + "qr_code": "QR-kode", + "qr_generating": "Genererer QR-kode…", + "qr_error": "Kunne ikke generere QR-kode.", + "qr_error_no_url": "Ingen HA-URL konfigurert. Angi en ekstern eller intern URL under Innstillinger → System → Nettverk.", + "save_error": "Kunne ikke lagre. Prøv igjen.", + "qr_print": "Skriv ut", + "qr_download": "Last ned SVG", + "qr_action": "Handling ved skanning", + "qr_action_view": "Vis vedlikeholdsinfo", + "qr_action_complete": "Merk vedlikehold som fullført", + "qr_url_mode": "Lenketype", + "qr_mode_companion": "Companion-app", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Oversikt", + "analysis": "Analyse", + "recent_activities": "Nylige aktiviteter", + "search_notes": "Søk i notater", + "avg_cost": "Gj.snittlig kostnad", + "no_advanced_features": "Ingen avanserte funksjoner aktivert", + "no_advanced_features_hint": "Aktiver «Adaptive intervaller» eller «Sesongmønstre» i integrasjonsinnstillingene for å se analysedata her.", + "analysis_not_enough_data": "Ikke nok data for analyse ennå.", + "analysis_not_enough_data_hint": "Weibull-analyse krever minst 5 fullførte vedlikehold; sesongmønstre blir synlige etter 6+ datapunkter per måned.", + "analysis_manual_task_hint": "Manuelle oppgaver uten intervall genererer ikke analysedata.", + "completions": "fullføringer", + "current": "Nåværende", + "shorter": "Kortere", + "longer": "Lengre", + "normal": "Normal", + "disabled": "Deaktivert", + "compound_logic": "Sammensatt logikk", + "compound": "Sammensatt (flere betingelser)", + "compound_logic_and": "OG — alle betingelser må utløses", + "compound_logic_or": "ELLER — én betingelse er nok", + "compound_help": "Kombiner flere sensorbetingelser til én utløser.", + "compound_no_conditions": "Ingen betingelser ennå — legg til minst én.", + "compound_add_condition": "Legg til betingelse", + "compound_condition": "Betingelse", + "compound_remove_condition": "Fjern betingelse", + "card_title": "Tittel", + "card_show_header": "Vis topptekst med statistikk", + "card_show_actions": "Vis handlingsknapper", + "card_compact": "Kompakt modus", + "card_max_items": "Maks elementer (0 = alle)", + "card_filter_status": "Filtrer etter status", + "card_filter_status_help": "Tom = vis alle statuser.", + "card_filter_objects": "Filtrer etter objekter", + "card_filter_objects_help": "Tom = vis alle objekter.", + "card_filter_entities": "Filtrer etter entiteter (entity_ids)", + "card_filter_entities_help": "Velg sensor-/binary_sensor-entiteter fra denne integrasjonen. Tom = alle.", + "card_loading_objects": "Laster objekter…", + "card_load_error": "Kunne ikke laste objekter — kontroller WebSocket-tilkoblingen.", + "card_no_tasks_title": "Ingen vedlikeholdsoppgaver ennå", + "card_no_tasks_cta": "→ Opprett en i vedlikeholdspanelet", + "no_objects": "Ingen objekter ennå.", + "action_error": "Handlingen mislyktes. Prøv igjen.", + "area_id_optional": "Område (valgfritt)", + "installation_date_optional": "Installasjonsdato (valgfritt)", + "warranty_expiry_optional": "Garanti utløper (valgfritt)", + "warranty": "Garanti", + "warranty_valid_until": "gyldig til {date}", + "warranty_expires_in": "utløper om {days} dager", + "warranty_expired": "utløpt", + "cal_past_windows": "Tidligere vinduer", + "cal_forward_windows": "Fremtidige vinduer", + "history_edit_title": "Rediger historikkoppføring", + "history_edit_timestamp": "Tidsstempel", + "manufacturer": "Produsent", + "model": "Modell", + "area": "Område", + "actions": "Handlinger", + "view_mode_label": "Visning", + "view_cards": "Kortvisning", + "view_table": "Tabellvisning", + "objects_table_columns_label": "Kolonner i objekttabell", + "objects_table_columns_hint": "Velg hvilke kolonner som vises i tabellvisningen for objekter.", + "custom_icon_optional": "Ikon (valgfritt, f.eks. mdi:wrench)", + "task_enabled": "Oppgave aktivert", + "skip_reason_prompt": "Hoppe over denne oppgaven?", + "reason_optional": "Årsak (valgfritt)", + "reset_date_prompt": "Merke oppgaven som utført?", + "reset_date_optional": "Sist utført-dato (valgfritt, standard er i dag)", + "notes_label": "Notater", + "documentation_label": "Dokumentasjon", + "no_nfc_tag": "— Ingen brikke —", + "dashboard": "Dashbord", + "tab_today": "I dag", + "palette_placeholder": "Søk objekter og oppgaver…", + "palette_no_results": "Ingen treff", + "palette_hint": "↑↓ naviger · Enter åpne · Esc lukk", + "today_all_caught_up": "Alt er ajour! Ingenting forfaller denne uken.", + "today_overdue": "Forfalt", + "today_due_today": "Forfaller i dag", + "today_this_week": "Denne uken", + "settings": "Innstillinger", + "settings_features": "Avanserte funksjoner", + "settings_features_desc": "Aktiver eller deaktiver avanserte funksjoner. Deaktivering skjuler dem fra grensesnittet, men sletter ikke data.", + "feat_adaptive": "Adaptiv planlegging", + "feat_adaptive_desc": "Lær optimale intervaller fra vedlikeholdshistorikken", + "feat_predictions": "Sensorprediksjoner", + "feat_predictions_desc": "Forutsi utløserdatoer fra sensorforringelse", + "feat_seasonal": "Sesongjusteringer", + "feat_seasonal_desc": "Juster intervaller basert på sesongmønstre", + "feat_environmental": "Miljøkorrelasjon", + "feat_environmental_desc": "Korreler intervaller med temperatur/fuktighet", + "feat_budget": "Budsjettsporing", + "feat_budget_desc": "Spor månedlige og årlige vedlikeholdsutgifter", + "feat_groups": "Oppgavegrupper", + "feat_groups_desc": "Organiser oppgaver i logiske grupper", + "feat_checklists": "Sjekklister", + "feat_checklists_desc": "Flertrinnsprosedyrer for fullføring av oppgaver", + "settings_general": "Generelt", + "settings_default_warning": "Standard varslingsdager", + "settings_panel_enabled": "Sidepanel", + "settings_panel_title": "Tittel på sidepanel", + "settings_notifications": "Varsler", + "settings_notify_service": "Varslingstjeneste", + "test_notification": "Testvarsel", + "send_test": "Send test", + "testing": "Sender…", + "test_notification_success": "Testvarsel sendt", + "test_notification_failed": "Testvarsel mislyktes", + "settings_notify_due_soon": "Varsle når noe forfaller snart", + "settings_notify_overdue": "Varsle når noe er forfalt", + "settings_notify_triggered": "Varsle når noe utløses", + "settings_interval_hours": "Gjentaksintervall (timer, 0 = én gang)", + "settings_quiet_hours": "Stilletimer", + "settings_quiet_start": "Start", + "settings_quiet_end": "Slutt", + "settings_max_per_day": "Maks varsler per dag (0 = ubegrenset)", + "settings_bundling": "Samle varsler", + "settings_bundle_threshold": "Samlingsterskel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Handlingsknapper på mobil", + "settings_action_complete": "Vis «Fullfør»-knapp", + "settings_action_skip": "Vis «Hopp over»-knapp", + "settings_action_snooze": "Vis «Slumre»-knapp", + "settings_weekly_digest": "Ukentlig oversikt", + "settings_weekly_digest_hint": "Ett sammendragsvarsel mandag morgen når oppgaver forfaller.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Slumrevarighet (timer)", + "settings_budget": "Budsjett", + "settings_currency": "Valuta", + "settings_budget_monthly": "Månedsbudsjett", + "settings_budget_yearly": "Årsbudsjett", + "settings_budget_alerts": "Budsjettvarsler", + "settings_budget_threshold": "Varslingsterskel (%)", + "settings_import_export": "Import / eksport", + "settings_export_json": "Eksporter JSON", + "settings_export_yaml": "Eksporter YAML", + "settings_export_csv": "Eksporter CSV", + "settings_import_csv": "Importer CSV", + "settings_import_placeholder": "Lim inn JSON- eller CSV-innhold her…", + "settings_import_btn": "Importer", + "settings_import_success": "{count} objekter importert.", + "settings_export_success": "Eksport lastet ned.", + "settings_saved": "Innstilling lagret.", + "settings_include_history": "Inkluder historikk", + "sort_alphabetical": "Alfabetisk", + "sort_due_soonest": "Forfaller først", + "sort_task_count": "Antall oppgaver", + "sort_area": "Område", + "sort_assigned_user": "Tildelt bruker", + "sort_group": "Gruppe", + "groupby_none": "Ingen gruppering", + "groupby_area": "Etter område", + "groupby_group": "Etter gruppe", + "groupby_user": "Etter bruker", + "filter_label": "Filter", + "user_label": "Bruker", + "sort_label": "Sorter", + "group_by_label": "Grupper etter", + "state_value_help": "Bruk HA-tilstandsverdien (vanligvis små bokstaver, f.eks. \"on\"/\"off\"). Store/små bokstaver normaliseres ved lagring.", + "target_changes_help": "Antall samsvarende overganger før utløseren aktiveres (standard: 1).", + "qr_print_title": "Skriv ut QR-koder", + "qr_print_desc": "Generer en utskrivbar side med QR-koder som kan klippes ut og festes på utstyret ditt.", + "qr_print_load": "Last objekter", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekter", + "qr_print_actions": "Handlinger", + "qr_print_url_mode": "Lenketype", + "qr_print_estimate": "Estimerte QR-koder", + "qr_print_over_limit": "grensen er 200, snevre inn filteret", + "qr_print_generate": "Generer QR-koder", + "qr_print_generating": "Genererer…", + "qr_print_ready": "QR-koder klare", + "qr_print_print_button": "Skriv ut", + "qr_print_empty": "Ingenting å generere", + "qr_action_skip": "Hopp over", + "vacation_title": "Feriemodus", + "vacation_active": "aktiv", + "vacation_ended": "avsluttet", + "vacation_desc": "Planlegg en ferie: varsler settes på pause i perioden pluss en buffer på noen dager. Du kan inkludere bestemte oppgaver igjen.", + "vacation_enable": "Aktiver feriemodus", + "vacation_start": "Start", + "vacation_end": "Slutt", + "vacation_buffer": "Buffer (dager)", + "vacation_exempt_title": "Varsle likevel under ferie", + "vacation_exempt_desc": "Velg oppgaver som fortsatt skal varsle under ferie (f.eks. kritisk bassengkjemi).", + "vacation_load_tasks": "Last oppgaver", + "vacation_preview_btn": "Vis forhåndsvisning", + "vacation_preview_affected": "oppgaver berørt", + "vacation_event_due_soon": "forfaller snart", + "vacation_event_overdue": "blir forfalt", + "vacation_event_triggered_est": "sensorutløsning mulig", + "vacation_sensor_based": "(sensorbasert)", + "vacation_action_notify": "Varsle likevel", + "vacation_action_unsilence": "Demp igjen", + "vacation_marked_complete": "Merket som fullført", + "vacation_marked_skip": "Hoppet over", + "vacation_end_now": "Avslutt ferie nå", + "add": "Legg til", + "show_stats": "Vis statistikk + grafer", + "hide_stats": "Skjul statistikk", + "adaptive_no_data": "Ikke nok fullføringshistorikk ennå for adaptiv analyse. Fullfør denne oppgaven noen flere ganger for å låse opp intervallanbefalinger og pålitelighetsdiagrammer.", + "suggestion_applied": "Foreslått intervall tatt i bruk", + "vacation_mode": "Feriemodus", + "vacation_status_active": "Aktiv nå", + "vacation_status_scheduled": "Planlagt", + "vacation_status_inactive": "Inaktiv", + "vacation_end_now_confirm": "Avslutte ferien umiddelbart?", + "vacation_exempt_count": "unntatt", + "vacation_advanced": "Avansert…", + "vacation_open_panel": "Åpne i panel", + "enable": "Aktiver", + "saved": "Lagret", + "budget_monthly_set": "Angi månedlig", + "budget_yearly_set": "Angi årlig", + "budget_advanced": "Valuta, varsler…", + "budget_open_panel": "Åpne i panel", + "groups_empty": "Ingen grupper ennå.", + "group_new_placeholder": "Legg til gruppe…", + "group_delete_confirm": "Slette gruppen \"{name}\"?", + "groups_manage_tasks": "Administrer oppgavetildelinger…", + "groups_open_panel": "Åpne i panel", + "unassigned": "Ikke tildelt", + "no_area": "Intet område", + "has_overdue": "Har forfalte oppgaver", + "object": "Objekt", + "settings_panel_access": "Paneltilgang", + "settings_panel_access_desc": "Administratorer har alltid full tilgang. For å delegere opprettelse, redigering og sletting til bestemte ikke-administratorer, slå dette på og velg dem nedenfor — alle andre ser kun Fullfør og Hopp over.", + "settings_operator_write": "Tillat valgte brukere å opprette, redigere og slette", + "settings_operator_write_desc": "Av: bare administratorer kan endre innhold. På: de valgte brukerne nedenfor får også full tilgang.", + "no_non_admin_users": "Ingen ikke-administratorbrukere funnet. Legg til noen under Innstillinger → Personer.", + "owner_label": "Eier", + "feat_completion_actions": "Fullføringshandlinger", + "feat_completion_actions_desc": "HA-handling per oppgave ved fullføring + hurtigfullføring med QR og forhåndsinnstilte verdier.", + "on_complete_action_title": "Ved fullføring: utløs HA-handling (valgfritt)", + "on_complete_action_desc": "Kaller en HA-tjeneste når oppgaven fullføres — f.eks. tilbakestille en teller på enheten.", + "on_complete_action_service": "Tjeneste", + "on_complete_action_target": "Målentitet", + "on_complete_action_target_hint": "Merk: entitetsdomenet må samsvare med tjenesten — f.eks. fungerer 'button.press' kun på button.*, 'counter.increment' kun på counter.*, 'input_button.press' kun på input_button.* osv. Ved manglende samsvar mislykkes handlingen i det stille (HA logger 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Data (JSON, valgfritt)", + "on_complete_action_test": "Valider konfigurasjon", + "on_complete_action_test_success": "✓ Konfigurasjon gyldig (handlingen utløses kun ved fullføring av oppgaven)", + "on_complete_action_test_failed": "Mislyktes", + "quick_complete_defaults_title": "Standardverdier for hurtigfullføring (for QR-skanninger, valgfritt)", + "quick_complete_defaults_desc": "Forhåndsinnstilte verdier for hurtigfullføring via QR-skanning. Uten disse åpner QR-koden fullføringsdialogen.", + "quick_complete_defaults_notes": "Notater", + "quick_complete_defaults_cost": "Kostnad", + "quick_complete_defaults_duration": "Varighet (minutter)", + "quick_complete_defaults_feedback_none": "Ingen tilbakemelding", + "quick_complete_defaults_feedback_needed": "Var nødvendig", + "quick_complete_defaults_feedback_not_needed": "Ikke nødvendig", + "quick_complete_success": "Raskt merket som fullført", + "show_all_objects": "Vis alle objekter", + "show_all_tasks": "Fjern filter — vis alle oppgaver", + "filter_to_overdue": "Filtrer oppgavelisten til kun forfalte", + "filter_to_due_soon": "Filtrer oppgavelisten til kun forfaller snart", + "filter_to_triggered": "Filtrer oppgavelisten til kun utløste", + "open_task": "Åpne oppgave", + "show_details": "Vis historikk + statistikk", + "hide_details": "Skjul detaljer", + "history_empty": "Ingen historikk ennå.", + "history_edit_button": "Rediger oppføring", + "total_cost": "Total kostnad", + "times_performed": "Utført", + "older_entries": "eldre", + "open_in_panel": "Åpne i vedlikeholdspanelet", + "skip_reason": "Årsak for å hoppe over (valgfritt)", + "reset_to_date": "Tilbakestill sist utført til", + "delete_task_confirm": "Slette denne oppgaven og dens historikk?", + "delete_object_confirm": "Slette dette objektet og alle dets oppgaver?", + "loading": "Laster…", + "archive": "Arkivér", + "undo": "Angre", + "task_archived": "Oppgave arkivert", + "object_archived": "Objekt arkivert", + "unarchive": "Gjenopprett", + "archived": "Arkivert", + "show_archived": "Vis arkiverte", + "hide_archived": "Skjul arkiverte", + "confirm_archive_object": "Arkivere dette objektet og oppgavene? Historikken beholdes og kan gjenopprettes senere.", + "settings_archive": "Arkiv og oppbevaring", + "settings_archive_desc": "Legg bort fullførte engangsoppgaver uten å slette dem. Arkiverte elementer er skjult og inaktive, men beholder historikk og kostnad.", + "settings_archive_oneoff_days": "Arkivér fullførte engangsoppgaver automatisk etter (dager, 0 = av)", + "settings_delete_archived_oneoff_days": "Slett arkiverte engangsoppgaver automatisk etter (dager, 0 = aldri)", + "archive_object": "Arkivér objekt", + "unarchive_object": "Gjenopprett objekt", + "documents": "Dokumenter", + "documents_empty": "Ingen dokumenter ennå.", + "doc_upload": "Last opp fil", + "doc_uploading": "Laster opp…", + "doc_add_link": "Legg til lenke", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Tittel (valgfritt)", + "doc_open": "Åpne", + "doc_delete_confirm": "Slette \"{name}\"?", + "doc_too_large": "Filen er for stor (maks. 25 MB).", + "doc_upload_failed": "Opplasting mislyktes.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Allerede lagret et annet sted — delt, ingen ekstra plass.", + "doc_dup_in_object": "Denne filen er allerede knyttet til dette objektet.", + "doc_link_invalid": "Kun http/https-lenker er tillatt.", + "doc_cat_manual": "Bruksanvisning", + "doc_cat_warranty": "Garanti", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Reservedeler", + "doc_cat_photo": "Foto", + "doc_cat_other": "Annet", + "doc_link_badge": "Lenke", + "doc_storage_title": "Dokumentlagring", + "doc_storage_saved": "Spart ved deduplisering", + "doc_storage_refresh": "Oppdater", + "doc_download": "Last ned", + "doc_close": "Lukk", + "doc_camera": "Ta bilde", + "doc_drop_hint": "Slipp filer her", + "doc_task_none": "Ingen dokumenter knyttet til denne oppgaven.", + "doc_link_existing": "Knytt et dokument…", + "doc_attach": "Knytt", + "doc_unlink": "Fjern kobling", + "doc_page": "Side", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1å", + "chart_since_service": "siden siste vedlikehold", + "chart_no_stats": "Ingen langtidsstatistikk for denne enheten — viser bare verdier fra vedlikeholdsoppføringer", + "auto_complete_on_recovery": "Fullfør automatisk når sensoren normaliseres", + "auto_complete_on_recovery_help": "Registrerer en fullføring (setter sist utført) når utløseren løser seg selv — f.eks. salt etterfylt, filter byttet.", + "doc_search": "Søk i dokumenter…", + "doc_search_none": "Ingen samsvarende dokumenter", + "link_device_optional": "Koble til eksisterende enhet (valgfritt)", + "parent_object_optional": "Overordnet objekt (valgfritt)", + "parent_none": "(Ingen overordnet)", + "paused": "Satt på pause", + "pause_object": "Sett på pause", + "resume_object": "Gjenoppta", + "pause_until_prompt": "Frys objektets planer — ingenting forfaller og ingenting varsler før det gjenopptas. Angi eventuelt en dato for automatisk gjenopptakelse.", + "pause_until_label": "Gjenoppta den (valgfritt)", + "object_paused": "Objekt satt på pause", + "object_resumed": "Objekt gjenopptatt — planer startet på nytt", + "object_paused_badge": "På pause", + "paused_until_label": "til", + "replace_object": "Erstatt…", + "replace_object_prompt": "Pensjoner dette objektet og opprett en etterfølger. Historikk og kostnader forblir arkivert på det gamle; oppgaver og dokumenter følger med til det nye, tellere starter på nytt.", + "replace_name_label": "Etterfølgerens navn", + "object_replaced": "Objekt erstattet — etterfølger opprettet", + "reading_unit_label": "Avlesningsenhet (f.eks. kWh, m³)", + "reading_unit_help": "Vises ved siden av den registrerte verdien når oppgaven fullføres.", + "reading_value_label": "Avlest verdi", + "reading_label": "Avlesning", + "settings_templates_label": "Malgalleri", + "settings_templates_hint": "Fjern haken for maler du aldri trenger — de forsvinner fra \"Fra mal\"-velgerne (panel og config flow). Ellers endres ingenting; kan aktiveres igjen når som helst.", + "worksheet": "Arbeidsark", + "worksheet_scan_view": "Skann for å åpne oppgaven", + "worksheet_scan_complete": "Skann for å fullføre", + "worksheet_manual_excerpt": "Utdrag fra manualen", + "worksheet_pages": "sider", + "worksheet_printed": "Skrevet ut", + "worksheet_never": "Aldri" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/nl.json b/custom_components/maintenance_supporter/frontend-src/locales/nl.json new file mode 100644 index 0000000..ed43fbd --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/nl.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Onderhoud", + "objects": "Objecten", + "tasks": "Taken", + "overdue": "Achterstallig", + "due_soon": "Binnenkort", + "triggered": "Geactiveerd", + "ok": "OK", + "all": "Alle", + "new_object": "+ Nieuw object", + "templates_from": "Uit sjabloon", + "templates_title": "Begin met een sjabloon", + "templates_task_count": "{n} taken", + "template_created": "Aangemaakt uit sjabloon", + "onboard_hint": "Voeg je eerste object toe om onderhoud te volgen.", + "edit": "Bewerken", + "duplicate": "Dupliceren", + "task_duplicated": "Taak gedupliceerd", + "object_duplicated": "Object gedupliceerd", + "delete": "Verwijderen", + "add_task": "+ Taak", + "complete": "Voltooid", + "completed": "Voltooid", + "skip": "Overslaan", + "skipped": "Overgeslagen", + "missed": "Missed", + "reset": "Resetten", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annuleren", + "bulk_select": "Selecteren", + "bulk_select_all": "Alles selecteren", + "bulk_n_selected": "{n} geselecteerd", + "bulk_completed": "{n} taken voltooid", + "bulk_archived": "{n} taken gearchiveerd", + "completing": "Wordt voltooid…", + "interval": "Interval", + "warning": "Waarschuwing", + "last_performed": "Laatst uitgevoerd", + "next_due": "Volgende keer", + "days_until_due": "Dagen tot vervaldatum", + "avg_duration": "Ø Duur", + "trigger": "Trigger", + "trigger_type": "Triggertype", + "threshold_above": "Bovengrens", + "threshold_below": "Ondergrens", + "threshold": "Drempelwaarde", + "counter": "Teller", + "state_change": "Statuswijziging", + "runtime": "Looptijd", + "runtime_hours": "Doellooptijd (uren)", + "target_value": "Doelwaarde", + "baseline": "Basislijn", + "target_changes": "Doelwijzigingen", + "for_minutes": "Voor (minuten)", + "time_based": "Tijdgebaseerd", + "sensor_based": "Sensorgebaseerd", + "manual": "Handmatig", + "one_time": "Eenmalig", + "weekdays": "Weekdagen", + "nth_weekday": "Zoveelste weekdag van de maand", + "day_of_month": "Dag van de maand", + "recurrence_on_days": "Herhalen op", + "recurrence_occurrence": "Voorkomen", + "recurrence_weekday": "Weekdag", + "recurrence_day": "Dag van de maand (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1e", + "ord_2": "2e", + "ord_3": "3e", + "ord_4": "4e", + "ord_5": "5e", + "ord_last": "Laatste", + "day_word": "Dag", + "interval_value": "Interval", + "interval_unit": "Eenheid", + "unit_days": "Dagen", + "unit_weeks": "Weken", + "unit_months": "Maanden", + "unit_years": "Jaren", + "due_date": "Vervaldatum", + "cleaning": "Reiniging", + "inspection": "Inspectie", + "replacement": "Vervanging", + "calibration": "Kalibratie", + "service": "Service", + "reading": "Aflezing", + "custom": "Aangepast", + "history": "Geschiedenis", + "cost": "Kosten", + "report_button": "Rapport", + "report_title": "Onderhoudsrapport", + "report_generated": "Gegenereerd", + "report_times_done": "Gedaan", + "report_total_cost": "Totale kosten", + "report_every": "elke {n} {unit}", + "report_notes": "Notities", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Schema", + "duration": "Duur", + "both": "Beide", + "trigger_val": "Triggerwaarde", + "complete_title": "Voltooid: ", + "checklist": "Checklist", + "checklist_steps_optional": "Checklist-stappen (optioneel)", + "checklist_placeholder": "Filter schoonmaken\nPakking vervangen\nDruk testen", + "checklist_help": "Eén stap per regel. Max. 100 items.", + "err_too_long": "{field}: te lang (max. {n} tekens)", + "err_too_short": "{field}: te kort (min. {n} tekens)", + "err_value_too_high": "{field}: te groot (max. {n})", + "err_value_too_low": "{field}: te klein (min. {n})", + "err_required": "{field}: verplicht", + "err_wrong_type": "{field}: verkeerd type (verwacht: {type})", + "err_invalid_choice": "{field}: niet-toegestane waarde", + "err_invalid_value": "{field}: ongeldige waarde", + "feat_schedule_time": "Tijd-van-dag-planning", + "feat_schedule_time_desc": "Taken vervallen op een specifieke tijd in plaats van middernacht.", + "schedule_time_optional": "Vervaldagstijd (optioneel, HH:MM)", + "schedule_time_help": "Leeg = middernacht (standaard). HA-tijdzone.", + "at_time": "om", + "notes_optional": "Notities (optioneel)", + "cost_optional": "Kosten (optioneel)", + "duration_minutes": "Duur in minuten (optioneel)", + "days": "dagen", + "day": "dag", + "today": "Vandaag", + "d_overdue": "d achterstallig", + "no_tasks": "Geen onderhoudstaken. Maak een object aan om te beginnen.", + "no_tasks_short": "Geen taken", + "no_history": "Nog geen geschiedenisitems.", + "show_all": "Alles tonen", + "cost_duration_chart": "Kosten & Duur", + "installed": "Geïnstalleerd", + "confirm_delete_object": "Dit object en alle bijbehorende taken verwijderen?", + "confirm_delete_task": "Deze taak verwijderen?", + "min": "Min", + "max": "Max", + "save": "Opslaan", + "saving": "Opslaan…", + "edit_task": "Taak bewerken", + "new_task": "Nieuwe onderhoudstaak", + "task_name": "Taaknaam", + "maintenance_type": "Onderhoudstype", + "priority": "Prioriteit", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Laag", + "priority_normal": "Normaal", + "priority_high": "Hoog", + "schedule_type": "Planningstype", + "interval_days": "Interval (dagen)", + "warning_days": "Waarschuwingsdagen", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Laatst uitgevoerd (optioneel)", + "interval_anchor": "Interval-anker", + "anchor_completion": "Vanaf voltooiing", + "anchor_planned": "Vanaf geplande datum (geen drift)", + "edit_object": "Object bewerken", + "name": "Naam", + "manufacturer_optional": "Fabrikant (optioneel)", + "model_optional": "Model (optioneel)", + "serial_number_optional": "Serienummer (optioneel)", + "serial_number_label": "S/N", + "documentation_url_label": "Handleiding", + "object_notes_label": "Notities", + "sort_due_date": "Vervaldatum", + "sort_object": "Objectnaam", + "sort_type": "Type", + "sort_task_name": "Taaknaam", + "all_objects": "Alle objecten", + "tasks_lower": "taken", + "no_tasks_yet": "Nog geen taken", + "add_first_task": "Eerste taak toevoegen", + "trigger_configuration": "Triggerconfiguratie", + "entity_id": "Entiteits-ID", + "comma_separated": "kommagescheiden", + "entity_logic": "Entiteitslogica", + "entity_logic_any": "Elke entiteit triggert", + "entity_logic_all": "Alle entiteiten moeten triggeren", + "entities": "entiteiten", + "attribute_optional": "Attribuut (optioneel, leeg = status)", + "use_entity_state": "Entiteitsstatus gebruiken (geen attribuut)", + "trigger_above": "Activeren als boven", + "trigger_below": "Activeren als onder", + "for_at_least_minutes": "Voor minstens (minuten)", + "safety_interval_days": "Veiligheidsinterval (dagen, optioneel)", + "safety_interval": "Veiligheidsinterval (optioneel)", + "delta_mode": "Deltamodus", + "from_state_optional": "Van status (optioneel)", + "to_state_optional": "Naar status (optioneel)", + "documentation_url_optional": "Documentatie-URL (optioneel)", + "object_notes_optional": "Notities (optioneel)", + "nfc_tag_id_optional": "NFC-tag-ID (optioneel)", + "nfc_tags_empty_help": "Nog geen NFC-tags geregistreerd in Home Assistant.", + "nfc_tags_open_settings": "Tag-instellingen openen", + "nfc_tags_refresh": "Vernieuwen", + "environmental_entity_optional": "Omgevingssensor (optioneel)", + "environmental_entity_helper": "bv. sensor.buitentemperatuur — past het interval aan op basis van omgevingswaarden", + "environmental_attribute_optional": "Omgevingsattribuut (optioneel)", + "nfc_tag_id": "NFC-tag-ID", + "nfc_linked": "NFC-tag gekoppeld", + "nfc_link_hint": "Klik om NFC-tag te koppelen", + "responsible_user": "Verantwoordelijke gebruiker", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Geen gebruiker toegewezen)", + "all_users": "Alle gebruikers", + "my_tasks": "Mijn taken", + "tab_calendar": "Kalender", + "cal_no_events": "Geen onderhoud", + "cal_window_7": "7 dagen", + "cal_window_14": "14 dagen", + "cal_window_30": "30 dagen", + "cal_window_365": "1 jaar", + "cal_every_n_days": "elke {n} dagen", + "cal_source_time": "Tijd-gebaseerd", + "cal_source_time_adaptive": "Tijd-gebaseerd (adaptief)", + "cal_source_sensor": "Sensor-gebaseerd", + "cal_predicted": "voorspeld", + "cal_confidence_high": "hoge zekerheid", + "cal_confidence_medium": "gemiddelde zekerheid", + "cal_confidence_low": "lage zekerheid", + "budget_monthly": "Maandbudget", + "budget_yearly": "Jaarbudget", + "groups": "Groepen", + "new_group": "Nieuwe groep", + "edit_group": "Groep bewerken", + "no_groups": "Nog geen groepen", + "delete_group": "Groep verwijderen", + "delete_group_confirm": "Groep '{name}' verwijderen?", + "group_select_tasks": "Taken selecteren", + "group_name_required": "Naam vereist", + "description_optional": "Beschrijving (optioneel)", + "selected": "Geselecteerd", + "loading_chart": "Grafiekgegevens laden...", + "hide_outliers": "Uitschieters verbergen (sensorfouten)", + "was_maintenance_needed": "Was dit onderhoud nodig?", + "feedback_needed": "Nodig", + "feedback_not_needed": "Niet nodig", + "feedback_not_sure": "Niet zeker", + "suggested_interval": "Voorgesteld interval", + "apply_suggestion": "Toepassen", + "reanalyze": "Opnieuw analyseren", + "reanalyze_result": "Nieuwe analyse", + "reanalyze_insufficient_data": "Onvoldoende gegevens voor een aanbeveling", + "data_points": "datapunten", + "dismiss_suggestion": "Negeren", + "confidence_low": "Laag", + "confidence_medium": "Gemiddeld", + "confidence_high": "Hoog", + "recommended": "aanbevolen", + "seasonal_awareness": "Seizoensbewustzijn", + "edit_seasonal_overrides": "Seizoensfactoren bewerken", + "seasonal_overrides_title": "Seizoensfactoren (override)", + "seasonal_overrides_hint": "Factor per maand (0.1–5.0). Leeg = automatisch geleerd.", + "seasonal_override_invalid": "Ongeldige waarde", + "seasonal_override_range": "Factor moet tussen 0.1 en 5.0 liggen", + "clear_all": "Alles wissen", + "seasonal_chart_title": "Seizoensfactoren", + "seasonal_learned": "Geleerd", + "seasonal_manual": "Handmatig", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mrt", + "month_apr": "Apr", + "month_may": "Mei", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensorvoorspelling", + "degradation_trend": "Trend", + "trend_rising": "Stijgend", + "trend_falling": "Dalend", + "trend_stable": "Stabiel", + "trend_insufficient_data": "Onvoldoende gegevens", + "days_until_threshold": "Dagen tot drempelwaarde", + "threshold_exceeded": "Drempelwaarde overschreden", + "environmental_adjustment": "Omgevingsfactor", + "sensor_prediction_urgency": "Sensor voorspelt drempelwaarde in ~{days} dagen", + "day_short": "dag", + "weibull_reliability_curve": "Betrouwbaarheidscurve", + "weibull_failure_probability": "Faalkans", + "weibull_r_squared": "Fit R²", + "beta_early_failures": "Vroege uitval", + "beta_random_failures": "Willekeurige uitval", + "beta_wear_out": "Slijtage", + "beta_highly_predictable": "Zeer voorspelbaar", + "confidence_interval": "Betrouwbaarheidsinterval", + "confidence_conservative": "Conservatief", + "confidence_aggressive": "Optimistisch", + "current_interval_marker": "Huidig interval", + "recommended_marker": "Aanbevolen", + "characteristic_life": "Karakteristieke levensduur", + "chart_mini_sparkline": "Trend-sparkline", + "chart_history": "Kosten- en duurgeschiedenis", + "chart_seasonal": "Seizoensfactoren, 12 maanden", + "chart_weibull": "Weibull-betrouwbaarheidscurve", + "chart_sparkline": "Sensor-triggerwaardegrafiek", + "days_progress": "Dagenvoortgang", + "qr_code": "QR-code", + "qr_generating": "QR-code genereren…", + "qr_error": "QR-code kon niet worden gegenereerd.", + "qr_error_no_url": "Geen HA-URL geconfigureerd. Stel een externe of interne URL in via Instellingen → Systeem → Netwerk.", + "save_error": "Opslaan mislukt. Probeer het opnieuw.", + "qr_print": "Afdrukken", + "qr_download": "SVG downloaden", + "qr_action": "Actie bij scannen", + "qr_action_view": "Onderhoudsinfo bekijken", + "qr_action_complete": "Onderhoud als voltooid markeren", + "qr_url_mode": "Linktype", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokaal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Overzicht", + "analysis": "Analyse", + "recent_activities": "Recente activiteiten", + "search_notes": "Notities doorzoeken", + "avg_cost": "Ø Kosten", + "no_advanced_features": "Geen geavanceerde functies ingeschakeld", + "no_advanced_features_hint": "Schakel „Adaptieve Intervallen” of „Seizoenpatronen” in via de integratie-instellingen om hier analysegegevens te zien.", + "analysis_not_enough_data": "Nog niet genoeg gegevens voor analyse.", + "analysis_not_enough_data_hint": "Weibull-analyse vereist minstens 5 voltooide onderhoudsbeurten; seizoenspatronen worden zichtbaar na 6+ datapunten per maand.", + "analysis_manual_task_hint": "Handmatige taken zonder interval genereren geen analysegegevens.", + "completions": "voltooiingen", + "current": "Huidig", + "shorter": "Korter", + "longer": "Langer", + "normal": "Normaal", + "disabled": "Uitgeschakeld", + "compound_logic": "Samengestelde logica", + "compound": "Samengesteld (meerdere voorwaarden)", + "compound_logic_and": "EN — alle voorwaarden moeten activeren", + "compound_logic_or": "OF — één voorwaarde is genoeg", + "compound_help": "Combineer meerdere sensorvoorwaarden tot één trigger.", + "compound_no_conditions": "Nog geen voorwaarden — voeg er ten minste één toe.", + "compound_add_condition": "Voorwaarde toevoegen", + "compound_condition": "Voorwaarde", + "compound_remove_condition": "Voorwaarde verwijderen", + "card_title": "Titel", + "card_show_header": "Koptekst met statistieken tonen", + "card_show_actions": "Actieknoppen tonen", + "card_compact": "Compacte modus", + "card_max_items": "Max items (0 = alle)", + "card_filter_status": "Filteren op status", + "card_filter_status_help": "Leeg = alle statussen tonen.", + "card_filter_objects": "Filteren op objecten", + "card_filter_objects_help": "Leeg = alle objecten tonen.", + "card_filter_entities": "Filteren op entiteiten (entity_ids)", + "card_filter_entities_help": "Kies sensor/binary_sensor entiteiten van deze integratie. Leeg = alle.", + "card_loading_objects": "Objecten laden…", + "card_load_error": "Objecten konden niet worden geladen — controleer de WebSocket-verbinding.", + "card_no_tasks_title": "Nog geen onderhoudstaken", + "card_no_tasks_cta": "→ Maak er een aan in het Maintenance-paneel", + "no_objects": "Nog geen objecten.", + "action_error": "Actie mislukt. Probeer het opnieuw.", + "area_id_optional": "Gebied (optioneel)", + "installation_date_optional": "Installatiedatum (optioneel)", + "warranty_expiry_optional": "Garantie verloopt (optioneel)", + "warranty": "Garantie", + "warranty_valid_until": "geldig tot {date}", + "warranty_expires_in": "verloopt over {days} dagen", + "warranty_expired": "verlopen", + "cal_past_windows": "Eerdere vensters", + "cal_forward_windows": "Toekomstige vensters", + "history_edit_title": "Geschiedenisitem bewerken", + "history_edit_timestamp": "Tijdstempel", + "manufacturer": "Fabrikant", + "model": "Model", + "area": "Gebied", + "actions": "Acties", + "view_mode_label": "Weergave", + "view_cards": "Kaartweergave", + "view_table": "Tabelweergave", + "objects_table_columns_label": "Kolommen objecttabel", + "objects_table_columns_hint": "Kies welke kolommen in de tabelweergave van objecten verschijnen.", + "custom_icon_optional": "Icoon (optioneel, bijv. mdi:wrench)", + "task_enabled": "Taak ingeschakeld", + "skip_reason_prompt": "Deze taak overslaan?", + "reason_optional": "Reden (optioneel)", + "reset_date_prompt": "Taak markeren als uitgevoerd?", + "reset_date_optional": "Laatste uitvoeringsdatum (optioneel, standaard vandaag)", + "notes_label": "Notities", + "documentation_label": "Documentatie", + "no_nfc_tag": "— Geen tag —", + "dashboard": "Dashboard", + "tab_today": "Vandaag", + "palette_placeholder": "Objecten en taken zoeken…", + "palette_no_results": "Geen resultaten", + "palette_hint": "↑↓ navigeren · Enter openen · Esc sluiten", + "today_all_caught_up": "Helemaal bij! Niets te doen deze week.", + "today_overdue": "Achterstallig", + "today_due_today": "Vandaag te doen", + "today_this_week": "Deze week", + "settings": "Instellingen", + "settings_features": "Geavanceerde functies", + "settings_features_desc": "Schakel geavanceerde functies in of uit. Uitschakelen verbergt ze in de interface maar verwijdert geen gegevens.", + "feat_adaptive": "Adaptieve planning", + "feat_adaptive_desc": "Leer optimale intervallen uit onderhoudsgeschiedenis", + "feat_predictions": "Sensorvoorspellingen", + "feat_predictions_desc": "Voorspel triggerdatums op basis van sensordegradatie", + "feat_seasonal": "Seizoensaanpassingen", + "feat_seasonal_desc": "Pas intervallen aan op seizoenspatronen", + "feat_environmental": "Omgevingscorrelatie", + "feat_environmental_desc": "Correleer intervallen met temperatuur/vochtigheid", + "feat_budget": "Budgetbeheer", + "feat_budget_desc": "Volg maandelijkse en jaarlijkse onderhoudsuitgaven", + "feat_groups": "Taakgroepen", + "feat_groups_desc": "Organiseer taken in logische groepen", + "feat_checklists": "Checklists", + "feat_checklists_desc": "Meerstaps procedures voor taakvoltooiing", + "settings_general": "Algemeen", + "settings_default_warning": "Standaard waarschuwingsdagen", + "settings_panel_enabled": "Zijbalkpaneel", + "settings_panel_title": "Titel zijbalkpaneel", + "settings_notifications": "Meldingen", + "settings_notify_service": "Meldingsservice", + "test_notification": "Testmelding", + "send_test": "Test versturen", + "testing": "Verzenden…", + "test_notification_success": "Testmelding verzonden", + "test_notification_failed": "Testmelding mislukt", + "settings_notify_due_soon": "Melding bij bijna verlopen", + "settings_notify_overdue": "Melding bij achterstallig", + "settings_notify_triggered": "Melding bij geactiveerd", + "settings_interval_hours": "Herhalingsinterval (uren, 0 = eenmalig)", + "settings_quiet_hours": "Stille uren", + "settings_quiet_start": "Start", + "settings_quiet_end": "Einde", + "settings_max_per_day": "Max meldingen per dag (0 = onbeperkt)", + "settings_bundling": "Meldingen bundelen", + "settings_bundle_threshold": "Bundeldrempel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobiele actieknoppen", + "settings_action_complete": "Knop 'Voltooid' tonen", + "settings_action_skip": "Knop 'Overslaan' tonen", + "settings_action_snooze": "Knop 'Snooze' tonen", + "settings_weekly_digest": "Wekelijks overzicht", + "settings_weekly_digest_hint": "Eén samenvattingsmelding op maandagochtend als er taken zijn.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Snoozeduur (uren)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Maandbudget", + "settings_budget_yearly": "Jaarbudget", + "settings_budget_alerts": "Budgetwaarschuwingen", + "settings_budget_threshold": "Waarschuwingsdrempel (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "JSON exporteren", + "settings_export_yaml": "YAML exporteren", + "settings_export_csv": "CSV exporteren", + "settings_import_csv": "CSV importeren", + "settings_import_placeholder": "Plak JSON- of CSV-inhoud hier…", + "settings_import_btn": "Importeren", + "settings_import_success": "{count} objecten succesvol geïmporteerd.", + "settings_export_success": "Export gedownload.", + "settings_saved": "Instelling opgeslagen.", + "settings_include_history": "Geschiedenis meenemen", + "sort_alphabetical": "Alfabetisch", + "sort_due_soonest": "Eerst vervallend", + "sort_task_count": "Aantal taken", + "sort_area": "Gebied", + "sort_assigned_user": "Toegewezen gebruiker", + "sort_group": "Groep", + "groupby_none": "Geen groepering", + "groupby_area": "Per gebied", + "groupby_group": "Per groep", + "groupby_user": "Per gebruiker", + "filter_label": "Filter", + "user_label": "Gebruiker", + "sort_label": "Sorteren", + "group_by_label": "Groeperen op", + "state_value_help": "Gebruik de HA-statuswaarde (meestal in kleine letters, bv. \"on\"/\"off\"). Hoofdletters worden bij opslaan genormaliseerd.", + "target_changes_help": "Aantal overeenkomende overgangen voordat de trigger wordt geactiveerd (standaard: 1).", + "qr_print_title": "QR-codes afdrukken", + "qr_print_desc": "Genereer een afdrukpagina met QR-codes om uit te knippen en op je apparaten te plakken.", + "qr_print_load": "Objecten laden", + "qr_print_filter": "Filter", + "qr_print_objects": "Objecten", + "qr_print_actions": "Acties", + "qr_print_url_mode": "Linktype", + "qr_print_estimate": "Geschatte QR-codes", + "qr_print_over_limit": "max is 200, beperk de filter", + "qr_print_generate": "QR-codes genereren", + "qr_print_generating": "Genereren…", + "qr_print_ready": "QR-codes klaar", + "qr_print_print_button": "Afdrukken", + "qr_print_empty": "Niets te genereren", + "qr_action_skip": "Overslaan", + "vacation_title": "Vakantiemodus", + "vacation_active": "actief", + "vacation_ended": "beëindigd", + "vacation_desc": "Plan je vakantie: meldingen worden gepauzeerd tijdens de periode plus een buffer van dagen. Je kunt per taak uitzonderingen instellen.", + "vacation_enable": "Vakantiemodus inschakelen", + "vacation_start": "Begin", + "vacation_end": "Einde", + "vacation_buffer": "Buffer (dagen)", + "vacation_exempt_title": "Toch melden tijdens vakantie", + "vacation_exempt_desc": "Kies taken die ook tijdens vakantie meldingen moeten geven (bv. kritische zwembadchemie).", + "vacation_load_tasks": "Taken laden", + "vacation_preview_btn": "Voorvertoning", + "vacation_preview_affected": "taken betrokken", + "vacation_event_due_soon": "wordt binnenkort verschuldigd", + "vacation_event_overdue": "wordt achterstallig", + "vacation_event_triggered_est": "sensortrigger mogelijk", + "vacation_sensor_based": "(sensor-gebaseerd)", + "vacation_action_notify": "Toch melden", + "vacation_action_unsilence": "Weer dempen", + "vacation_marked_complete": "Als voltooid gemarkeerd", + "vacation_marked_skip": "Overgeslagen", + "vacation_end_now": "Vakantie nu beëindigen", + "unassigned": "Niet toegewezen", + "no_area": "Geen gebied", + "has_overdue": "Heeft achterstallige taken", + "object": "Object", + "settings_panel_access": "Paneel-toegang", + "settings_panel_access_desc": "Admins hebben altijd volledige toegang. Om aanmaken, bewerken en verwijderen aan specifieke niet-admins te delegeren, schakel dit in en selecteer hen hieronder — alle anderen zien alleen Voltooien en Overslaan.", + "settings_operator_write": "Geselecteerde gebruikers laten aanmaken, bewerken en verwijderen", + "settings_operator_write_desc": "Uit: alleen admins kunnen inhoud wijzigen. Aan: de geselecteerde gebruikers hieronder krijgen ook volledige toegang.", + "no_non_admin_users": "Geen niet-admin gebruikers gevonden. Voeg ze toe in Instellingen → Personen.", + "owner_label": "Eigenaar", + "feat_completion_actions": "Voltooiings-acties", + "feat_completion_actions_desc": "Per taak HA-actie bij voltooien + snel-voltooien-QR met vooraf ingestelde waarden.", + "on_complete_action_title": "Bij voltooien: HA-actie uitvoeren (optioneel)", + "on_complete_action_desc": "Roept een HA-service aan wanneer de taak is voltooid — bv. een teller op het apparaat resetten.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Doel-entiteit", + "on_complete_action_data": "Data (JSON, optioneel)", + "on_complete_action_test": "Actie testen", + "on_complete_action_test_success": "Geslaagd", + "on_complete_action_test_failed": "Mislukt", + "quick_complete_defaults_title": "Snel-voltooien-standaardwaarden (voor QR-scans, optioneel)", + "quick_complete_defaults_desc": "Vooraf ingestelde waarden voor snel-voltooien-QR. Zonder deze opent de QR de voltooi-dialoog.", + "quick_complete_defaults_notes": "Notities", + "quick_complete_defaults_cost": "Kosten", + "quick_complete_defaults_duration": "Duur (minuten)", + "quick_complete_defaults_feedback_none": "Geen feedback", + "quick_complete_defaults_feedback_needed": "Was nodig", + "quick_complete_defaults_feedback_not_needed": "Niet nodig", + "quick_complete_success": "Snel als voltooid gemarkeerd", + "trigger_replaced": "Trigger vervangen", + "add": "Toevoegen", + "show_stats": "Statistieken + grafieken tonen", + "hide_stats": "Statistieken verbergen", + "adaptive_no_data": "Nog niet genoeg voltooiingsgeschiedenis voor adaptieve analyse. Voer deze taak nog een paar keer uit om intervaladviezen en betrouwbaarheidsgrafieken te ontgrendelen.", + "suggestion_applied": "Voorgesteld interval toegepast", + "vacation_mode": "Vakantiemodus", + "vacation_status_active": "Nu actief", + "vacation_status_scheduled": "Gepland", + "vacation_status_inactive": "Inactief", + "vacation_end_now_confirm": "Vakantie onmiddellijk beëindigen?", + "vacation_exempt_count": "uitgezonderd", + "vacation_advanced": "Geavanceerd…", + "vacation_open_panel": "Openen in paneel", + "enable": "Inschakelen", + "saved": "Opgeslagen", + "budget_monthly_set": "Maandelijks instellen", + "budget_yearly_set": "Jaarlijks instellen", + "budget_advanced": "Valuta, waarschuwingen…", + "budget_open_panel": "Openen in paneel", + "groups_empty": "Nog geen groepen.", + "group_new_placeholder": "Groep toevoegen…", + "group_delete_confirm": "Groep \"{name}\" verwijderen?", + "groups_manage_tasks": "Taaktoewijzingen beheren…", + "groups_open_panel": "Openen in paneel", + "on_complete_action_target_hint": "Let op: het domein van de entiteit moet bij de service passen — bijv. 'button.press' werkt alleen op button.*, 'counter.increment' alleen op counter.*, 'input_button.press' alleen op input_button.* enz. Bij een mismatch mislukt de actie stil (HA logt 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Alle objecten tonen", + "show_all_tasks": "Filter wissen — alle taken tonen", + "filter_to_overdue": "Takenlijst filteren op alleen achterstallig", + "filter_to_due_soon": "Takenlijst filteren op alleen binnenkort verschuldigd", + "filter_to_triggered": "Takenlijst filteren op alleen geactiveerd", + "open_task": "Taak openen", + "show_details": "Geschiedenis + statistieken tonen", + "hide_details": "Details verbergen", + "history_empty": "Nog geen geschiedenis.", + "history_edit_button": "Item bewerken", + "total_cost": "Totale kosten", + "times_performed": "Uitgevoerd", + "older_entries": "ouder", + "open_in_panel": "Openen in Onderhoudspaneel", + "skip_reason": "Reden voor overslaan (optioneel)", + "reset_to_date": "Laatst uitgevoerd terugzetten op", + "delete_task_confirm": "Deze taak en de geschiedenis verwijderen?", + "delete_object_confirm": "Dit object en alle taken verwijderen?", + "loading": "Laden…", + "archive": "Archiveren", + "undo": "Ongedaan maken", + "task_archived": "Taak gearchiveerd", + "object_archived": "Object gearchiveerd", + "unarchive": "Dearchiveren", + "archived": "Gearchiveerd", + "show_archived": "Gearchiveerde tonen", + "hide_archived": "Gearchiveerde verbergen", + "confirm_archive_object": "Dit object en zijn taken archiveren? De geschiedenis blijft behouden en kan later worden hersteld.", + "settings_archive": "Archief & bewaring", + "settings_archive_desc": "Voltooide eenmalige taken archiveren in plaats van verwijderen. Gearchiveerde items zijn verborgen en inactief, maar behouden geschiedenis en kosten.", + "settings_archive_oneoff_days": "Voltooide eenmalige taken automatisch archiveren na (dagen, 0 = uit)", + "settings_delete_archived_oneoff_days": "Gearchiveerde eenmalige taken automatisch verwijderen na (dagen, 0 = nooit)", + "archive_object": "Object archiveren", + "unarchive_object": "Object dearchiveren", + "documents": "Documenten", + "documents_empty": "Nog geen documenten.", + "doc_upload": "Bestand uploaden", + "doc_uploading": "Uploaden…", + "doc_add_link": "Link toevoegen", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (optioneel)", + "doc_open": "Openen", + "doc_delete_confirm": "\"{name}\" verwijderen?", + "doc_too_large": "Bestand is te groot (max. 25 MB).", + "doc_upload_failed": "Uploaden mislukt.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Al elders opgeslagen — gedeeld, geen extra ruimte.", + "doc_dup_in_object": "Dit bestand is al aan dit object gekoppeld.", + "doc_link_invalid": "Alleen http/https-links zijn toegestaan.", + "doc_cat_manual": "Handleiding", + "doc_cat_warranty": "Garantie", + "doc_cat_invoice": "Factuur", + "doc_cat_spare_parts": "Onderdelen", + "doc_cat_photo": "Foto", + "doc_cat_other": "Overig", + "doc_link_badge": "Link", + "doc_storage_title": "Documentopslag", + "doc_storage_saved": "Bespaard door deduplicatie", + "doc_storage_refresh": "Vernieuwen", + "doc_download": "Downloaden", + "doc_close": "Sluiten", + "doc_camera": "Foto maken", + "doc_drop_hint": "Sleep bestanden hierheen", + "doc_task_none": "Geen documenten aan deze taak gekoppeld.", + "doc_link_existing": "Document koppelen…", + "doc_attach": "Koppelen", + "doc_unlink": "Ontkoppelen", + "doc_page": "Pagina", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1j", + "chart_since_service": "sinds laatste onderhoud", + "chart_no_stats": "Geen langetermijnstatistieken voor deze entiteit — alleen waarden uit onderhoudsregels worden getoond", + "auto_complete_on_recovery": "Automatisch voltooien wanneer de sensor herstelt", + "auto_complete_on_recovery_help": "Registreert een voltooiing (stelt laatst uitgevoerd in) wanneer de trigger zichzelf oplost — bijv. zout bijgevuld, filter vervangen.", + "doc_search": "Documenten zoeken…", + "doc_search_none": "Geen overeenkomende documenten", + "link_device_optional": "Koppelen aan bestaand apparaat (optioneel)", + "parent_object_optional": "Bovenliggend object (optioneel)", + "parent_none": "(Geen bovenliggend object)", + "paused": "Gepauzeerd", + "pause_object": "Pauzeren", + "resume_object": "Hervatten", + "pause_until_prompt": "Bevries de schema's van dit object — niets wordt verwacht en niets meldt tot het wordt hervat. Stel optioneel een datum voor automatisch hervatten in.", + "pause_until_label": "Hervatten op (optioneel)", + "object_paused": "Object gepauzeerd", + "object_resumed": "Object hervat — schema's herstart", + "object_paused_badge": "Gepauzeerd", + "paused_until_label": "tot", + "replace_object": "Vervangen…", + "replace_object_prompt": "Dit object uitfaseren en een opvolger aanmaken. Geschiedenis en kosten blijven gearchiveerd bij het oude; taken en documenten gaan mee naar het nieuwe, tellers beginnen opnieuw.", + "replace_name_label": "Naam van de opvolger", + "object_replaced": "Object vervangen — opvolger aangemaakt", + "reading_unit_label": "Meeteenheid (bijv. kWh, m³)", + "reading_unit_help": "Wordt naast de vastgelegde waarde getoond bij het afronden van deze taak.", + "reading_value_label": "Meterstand", + "reading_label": "Meting", + "settings_templates_label": "Sjabloongalerij", + "settings_templates_hint": "Vink sjablonen uit die je nooit nodig hebt — ze verdwijnen uit de \"Vanuit sjabloon\"-keuzes (paneel en config flow). Verder verandert er niets; altijd weer in te schakelen.", + "worksheet": "Werkblad", + "worksheet_scan_view": "Scan om de taak te openen", + "worksheet_scan_complete": "Scan om af te ronden", + "worksheet_manual_excerpt": "Handleidingfragment", + "worksheet_pages": "pagina's", + "worksheet_printed": "Afgedrukt", + "worksheet_never": "Nooit" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/pl.json b/custom_components/maintenance_supporter/frontend-src/locales/pl.json new file mode 100644 index 0000000..e95f4e5 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/pl.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Konserwacja", + "objects": "Obiekty", + "tasks": "Zadania", + "overdue": "Zaległe", + "due_soon": "Wkrótce", + "triggered": "Wyzwolone", + "ok": "OK", + "all": "Wszystkie", + "new_object": "+ Nowy obiekt", + "templates_from": "Z szablonu", + "templates_title": "Zacznij od szablonu", + "templates_task_count": "{n} zadań", + "template_created": "Utworzono z szablonu", + "onboard_hint": "Dodaj pierwszy obiekt, aby śledzić konserwację.", + "edit": "Edytuj", + "duplicate": "Duplikuj", + "task_duplicated": "Zadanie zduplikowane", + "object_duplicated": "Obiekt zduplikowany", + "delete": "Usuń", + "add_task": "+ Dodaj zadanie", + "complete": "Wykonaj", + "completed": "Wykonano", + "skip": "Pomiń", + "skipped": "Pominięte", + "missed": "Missed", + "reset": "Resetuj", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Anuluj", + "bulk_select": "Zaznacz", + "bulk_select_all": "Zaznacz wszystko", + "bulk_n_selected": "Zaznaczono {n}", + "bulk_completed": "Ukończono {n} zadań", + "bulk_archived": "Zarchiwizowano {n} zadań", + "completing": "Wykonywanie…", + "interval": "Interwał", + "warning": "Ostrzeżenie", + "last_performed": "Ostatnio wykonane", + "next_due": "Następny termin", + "days_until_due": "Dni do terminu", + "avg_duration": "Śr. czas trwania", + "trigger": "Wyzwalacz", + "trigger_type": "Typ wyzwalacza", + "threshold_above": "Górny limit", + "threshold_below": "Dolny limit", + "threshold": "Próg", + "counter": "Licznik", + "state_change": "Zmiana stanu", + "runtime": "Czas pracy", + "runtime_hours": "Docelowy czas pracy (godziny)", + "target_value": "Wartość docelowa", + "baseline": "Wartość bazowa", + "target_changes": "Docelowa liczba zmian", + "for_minutes": "Przez (minuty)", + "time_based": "Czasowy", + "sensor_based": "Oparty na czujniku", + "manual": "Ręczny", + "one_time": "Jednorazowo", + "weekdays": "Dni tygodnia", + "nth_weekday": "N-ty dzień tygodnia w miesiącu", + "day_of_month": "Dzień miesiąca", + "recurrence_on_days": "Powtarzaj w", + "recurrence_occurrence": "Wystąpienie", + "recurrence_weekday": "Dzień tygodnia", + "recurrence_day": "Dzień miesiąca (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Ostatni", + "day_word": "Dzień", + "interval_value": "Interwał", + "interval_unit": "Jednostka", + "unit_days": "Dni", + "unit_weeks": "Tygodnie", + "unit_months": "Miesiące", + "unit_years": "Lata", + "due_date": "Termin", + "cleaning": "Czyszczenie", + "inspection": "Inspekcja", + "replacement": "Wymiana", + "calibration": "Kalibracja", + "service": "Serwis", + "reading": "Odczyt", + "custom": "Niestandardowy", + "history": "Historia", + "cost": "Koszt", + "report_button": "Raport", + "report_title": "Raport konserwacji", + "report_generated": "Wygenerowano", + "report_times_done": "Wykonano", + "report_total_cost": "Koszt całkowity", + "report_every": "co {n} {unit}", + "report_notes": "Notatki", + "report_col_type": "Typ", + "report_col_status": "Status", + "report_col_schedule": "Harmonogram", + "duration": "Czas trwania", + "both": "Oba", + "trigger_val": "Wartość wyzwalacza", + "complete_title": "Wykonaj: ", + "checklist": "Lista kontrolna", + "checklist_steps_optional": "Kroki listy kontrolnej (opcjonalne)", + "checklist_placeholder": "Wyczyść filtr\nWymień uszczelkę\nSprawdź ciśnienie", + "checklist_help": "Jeden krok na linię. Maks. 100 elementów.", + "err_too_long": "{field}: za długie (maks. {n} znaków)", + "err_too_short": "{field}: za krótkie (min. {n} znaków)", + "err_value_too_high": "{field}: za duże (maks. {n})", + "err_value_too_low": "{field}: za małe (min. {n})", + "err_required": "{field}: wymagane", + "err_wrong_type": "{field}: zły typ (oczekiwano: {type})", + "err_invalid_choice": "{field}: niedozwolona wartość", + "err_invalid_value": "{field}: nieprawidłowa wartość", + "feat_schedule_time": "Harmonogram według pory dnia", + "feat_schedule_time_desc": "Zadania stają się zaległe o określonej porze dnia zamiast o północy.", + "schedule_time_optional": "Termin o godzinie (opcjonalne, HH:MM)", + "schedule_time_help": "Puste = północ (domyślnie). Strefa czasowa HA.", + "at_time": "o", + "notes_optional": "Notatki (opcjonalne)", + "cost_optional": "Koszt (opcjonalne)", + "duration_minutes": "Czas trwania w minutach (opcjonalne)", + "days": "dni", + "day": "dzień", + "today": "Dzisiaj", + "d_overdue": "d zaległe", + "no_tasks": "Jeszcze brak zadań konserwacyjnych. Utwórz obiekt, aby zacząć.", + "no_tasks_short": "Brak zadań", + "no_history": "Jeszcze brak wpisów historii.", + "show_all": "Pokaż wszystko", + "cost_duration_chart": "Koszt i czas trwania", + "installed": "Zainstalowane", + "confirm_delete_object": "Usunąć ten obiekt i wszystkie jego zadania?", + "confirm_delete_task": "Usunąć to zadanie?", + "min": "Min", + "max": "Maks", + "save": "Zapisz", + "saving": "Zapisywanie…", + "edit_task": "Edytuj zadanie", + "new_task": "Nowe zadanie konserwacyjne", + "task_name": "Nazwa zadania", + "maintenance_type": "Typ konserwacji", + "priority": "Priorytet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Niski", + "priority_normal": "Normalny", + "priority_high": "Wysoki", + "schedule_type": "Typ harmonogramu", + "interval_days": "Interwał (dni)", + "warning_days": "Dni ostrzeżenia", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Ostatnio wykonane (opcjonalne)", + "interval_anchor": "Punkt zaczepienia interwału", + "anchor_completion": "Od daty wykonania", + "anchor_planned": "Od daty planowanej (bez przesunięć)", + "edit_object": "Edytuj obiekt", + "name": "Nazwa", + "manufacturer_optional": "Producent (opcjonalne)", + "model_optional": "Model (opcjonalne)", + "serial_number_optional": "Numer seryjny (opcjonalne)", + "serial_number_label": "S/N", + "documentation_url_label": "Instrukcja", + "object_notes_label": "Notatki", + "sort_due_date": "Termin", + "sort_object": "Nazwa obiektu", + "sort_type": "Typ", + "sort_task_name": "Nazwa zadania", + "all_objects": "Wszystkie obiekty", + "tasks_lower": "zadań", + "no_tasks_yet": "Jeszcze brak zadań", + "add_first_task": "Dodaj pierwsze zadanie", + "trigger_configuration": "Konfiguracja wyzwalacza", + "entity_id": "ID encji", + "comma_separated": "oddzielone przecinkami", + "entity_logic": "Logika encji", + "entity_logic_any": "Wyzwala dowolna encja", + "entity_logic_all": "Wszystkie encje muszą wyzwolić", + "entities": "encje", + "attribute_optional": "Atrybut (opcjonalny, puste = stan)", + "use_entity_state": "Użyj stanu encji (bez atrybutu)", + "trigger_above": "Wyzwól powyżej", + "trigger_below": "Wyzwól poniżej", + "for_at_least_minutes": "Przez co najmniej (minuty)", + "safety_interval_days": "Interwał bezpieczeństwa (dni, opcjonalny)", + "safety_interval": "Interwał bezpieczeństwa (opcjonalny)", + "delta_mode": "Tryb delta", + "from_state_optional": "Ze stanu (opcjonalne)", + "to_state_optional": "Do stanu (opcjonalne)", + "documentation_url_optional": "URL dokumentacji (opcjonalne)", + "object_notes_optional": "Notatki (opcjonalne)", + "nfc_tag_id_optional": "ID tagu NFC (opcjonalne)", + "nfc_tags_empty_help": "W Home Assistant nie zarejestrowano jeszcze tagów NFC.", + "nfc_tags_open_settings": "Otwórz ustawienia tagów", + "nfc_tags_refresh": "Odśwież", + "environmental_entity_optional": "Czujnik środowiskowy (opcjonalne)", + "environmental_entity_helper": "np. sensor.outdoor_temperature — dostosowuje interwał na podstawie warunków środowiskowych", + "environmental_attribute_optional": "Atrybut środowiskowy (opcjonalne)", + "nfc_tag_id": "ID tagu NFC", + "nfc_linked": "Tag NFC powiązany", + "nfc_link_hint": "Kliknij, aby powiązać tag NFC", + "responsible_user": "Odpowiedzialny użytkownik", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Brak przypisanego użytkownika)", + "all_users": "Wszyscy użytkownicy", + "my_tasks": "Moje zadania", + "tab_calendar": "Kalendarz", + "cal_no_events": "Brak konserwacji", + "cal_window_7": "7 dni", + "cal_window_14": "14 dni", + "cal_window_30": "30 dni", + "cal_window_365": "1 rok", + "cal_every_n_days": "co {n} dni", + "cal_source_time": "Czas", + "cal_source_time_adaptive": "Czas (adaptacyjny)", + "cal_source_sensor": "Czujnik", + "cal_predicted": "przewidywane", + "cal_confidence_high": "wysoka pewność", + "cal_confidence_medium": "średnia pewność", + "cal_confidence_low": "niska pewność", + "budget_monthly": "Budżet miesięczny", + "budget_yearly": "Budżet roczny", + "groups": "Grupy", + "new_group": "Nowa grupa", + "edit_group": "Edytuj grupę", + "no_groups": "Jeszcze brak grup", + "delete_group": "Usuń grupę", + "delete_group_confirm": "Usunąć grupę '{name}'?", + "group_select_tasks": "Wybierz zadania", + "group_name_required": "Nazwa jest wymagana", + "description_optional": "Opis (opcjonalny)", + "selected": "Wybrane", + "loading_chart": "Ładowanie danych wykresu...", + "hide_outliers": "Ukryj wartości odstające (błędy czujnika)", + "was_maintenance_needed": "Czy ta konserwacja była potrzebna?", + "feedback_needed": "Potrzebna", + "feedback_not_needed": "Niepotrzebna", + "feedback_not_sure": "Nie jestem pewien", + "suggested_interval": "Sugerowany interwał", + "apply_suggestion": "Zastosuj", + "reanalyze": "Analizuj ponownie", + "reanalyze_result": "Nowa analiza", + "reanalyze_insufficient_data": "Za mało danych do wygenerowania rekomendacji", + "data_points": "punkty danych", + "dismiss_suggestion": "Odrzuć", + "confidence_low": "Niska", + "confidence_medium": "Średnia", + "confidence_high": "Wysoka", + "recommended": "rekomendowane", + "seasonal_awareness": "Świadomość sezonowa", + "edit_seasonal_overrides": "Edytuj czynniki sezonowe", + "seasonal_overrides_title": "Czynniki sezonowe (nadpisanie)", + "seasonal_overrides_hint": "Czynnik na miesiąc (0.1–5.0). Puste = uczone automatycznie.", + "seasonal_override_invalid": "Nieprawidłowa wartość", + "seasonal_override_range": "Czynnik musi być między 0.1 a 5.0", + "clear_all": "Wyczyść wszystko", + "seasonal_chart_title": "Czynniki sezonowe", + "seasonal_learned": "Wyuczone", + "seasonal_manual": "Ręczne", + "month_jan": "Sty", + "month_feb": "Lut", + "month_mar": "Mar", + "month_apr": "Kwi", + "month_may": "Maj", + "month_jun": "Cze", + "month_jul": "Lip", + "month_aug": "Sie", + "month_sep": "Wrz", + "month_oct": "Paź", + "month_nov": "Lis", + "month_dec": "Gru", + "sensor_prediction": "Predykcja czujnika", + "degradation_trend": "Trend", + "trend_rising": "Rosnący", + "trend_falling": "Malejący", + "trend_stable": "Stabilny", + "trend_insufficient_data": "Niewystarczające dane", + "days_until_threshold": "Dni do progu", + "threshold_exceeded": "Próg przekroczony", + "environmental_adjustment": "Czynnik środowiskowy", + "sensor_prediction_urgency": "Czujnik przewiduje próg za ~{days} dni", + "day_short": "d", + "weibull_reliability_curve": "Krzywa niezawodności", + "weibull_failure_probability": "Prawdopodobieństwo awarii", + "weibull_r_squared": "Dopasowanie R²", + "beta_early_failures": "Wczesne awarie", + "beta_random_failures": "Losowe awarie", + "beta_wear_out": "Zużycie", + "beta_highly_predictable": "Wysoce przewidywalne", + "confidence_interval": "Przedział ufności", + "confidence_conservative": "Konserwatywny", + "confidence_aggressive": "Optymistyczny", + "current_interval_marker": "Bieżący interwał", + "recommended_marker": "Rekomendowany", + "characteristic_life": "Charakterystyczna żywotność", + "chart_mini_sparkline": "Wykres trendu", + "chart_history": "Historia kosztów i czasu trwania", + "chart_seasonal": "Czynniki sezonowe, 12 miesięcy", + "chart_weibull": "Krzywa niezawodności Weibulla", + "chart_sparkline": "Wykres wartości wyzwalacza czujnika", + "days_progress": "Postęp dni", + "qr_code": "Kod QR", + "qr_generating": "Generowanie kodu QR…", + "qr_error": "Nie udało się wygenerować kodu QR.", + "qr_error_no_url": "Brak skonfigurowanego URL HA. Ustaw zewnętrzny lub wewnętrzny URL w Ustawienia → System → Sieć.", + "save_error": "Nie udało się zapisać. Spróbuj ponownie.", + "qr_print": "Drukuj", + "qr_download": "Pobierz SVG", + "qr_action": "Akcja przy skanowaniu", + "qr_action_view": "Wyświetl informacje o konserwacji", + "qr_action_complete": "Oznacz konserwację jako wykonaną", + "qr_url_mode": "Typ linku", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokalny (mDNS)", + "qr_mode_server": "URL serwera", + "overview": "Przegląd", + "analysis": "Analiza", + "recent_activities": "Ostatnie aktywności", + "search_notes": "Szukaj w notatkach", + "avg_cost": "Śr. koszt", + "no_advanced_features": "Brak włączonych funkcji zaawansowanych", + "no_advanced_features_hint": "Włącz „Adaptacyjne interwały” lub „Wzorce sezonowe” w ustawieniach integracji, aby zobaczyć tutaj dane analityczne.", + "analysis_not_enough_data": "Jeszcze za mało danych do analizy.", + "analysis_not_enough_data_hint": "Analiza Weibulla wymaga co najmniej 5 wykonanych konserwacji; wzorce sezonowe stają się widoczne po 6+ punktach danych na miesiąc.", + "analysis_manual_task_hint": "Zadania ręczne bez interwału nie generują danych analitycznych.", + "completions": "wykonania", + "current": "Bieżące", + "shorter": "Krótsze", + "longer": "Dłuższe", + "normal": "Normalne", + "disabled": "Wyłączone", + "compound_logic": "Logika złożona", + "compound": "Złożony (wiele warunków)", + "compound_logic_and": "I — wszystkie warunki muszą zadziałać", + "compound_logic_or": "LUB — wystarczy dowolny warunek", + "compound_help": "Połącz kilka warunków czujników w jeden wyzwalacz.", + "compound_no_conditions": "Brak warunków — dodaj co najmniej jeden.", + "compound_add_condition": "Dodaj warunek", + "compound_condition": "Warunek", + "compound_remove_condition": "Usuń warunek", + "card_title": "Tytuł", + "card_show_header": "Pokaż nagłówek ze statystykami", + "card_show_actions": "Pokaż przyciski akcji", + "card_compact": "Tryb kompaktowy", + "card_max_items": "Maks. elementów (0 = wszystkie)", + "card_filter_status": "Filtruj wg statusu", + "card_filter_status_help": "Puste = pokaż wszystkie statusy.", + "card_filter_objects": "Filtruj wg obiektów", + "card_filter_objects_help": "Puste = pokaż wszystkie obiekty.", + "card_filter_entities": "Filtruj wg encji (entity_ids)", + "card_filter_entities_help": "Wybierz encje sensor / binary_sensor z tej integracji. Puste = wszystkie.", + "card_loading_objects": "Ładowanie obiektów…", + "card_load_error": "Nie udało się załadować obiektów — sprawdź połączenie WebSocket.", + "card_no_tasks_title": "Brak zadań konserwacyjnych", + "card_no_tasks_cta": "→ Utwórz w panelu Maintenance", + "no_objects": "Brak obiektów.", + "action_error": "Akcja nie powiodła się. Spróbuj ponownie.", + "area_id_optional": "Obszar (opcjonalny)", + "installation_date_optional": "Data instalacji (opcjonalna)", + "warranty_expiry_optional": "Wygaśnięcie gwarancji (opcjonalnie)", + "warranty": "Gwarancja", + "warranty_valid_until": "ważna do {date}", + "warranty_expires_in": "wygasa za {days} dni", + "warranty_expired": "wygasła", + "cal_past_windows": "Minione okna", + "cal_forward_windows": "Przyszłe okna", + "history_edit_title": "Edytuj wpis historii", + "history_edit_timestamp": "Znacznik czasu", + "manufacturer": "Producent", + "model": "Model", + "area": "Obszar", + "actions": "Akcje", + "view_mode_label": "Widok", + "view_cards": "Widok kart", + "view_table": "Widok tabeli", + "objects_table_columns_label": "Kolumny tabeli obiektów", + "objects_table_columns_hint": "Wybierz, które kolumny mają być widoczne w widoku tabeli obiektów.", + "custom_icon_optional": "Ikona (opcjonalna, np. mdi:wrench)", + "task_enabled": "Zadanie włączone", + "skip_reason_prompt": "Pominąć to zadanie?", + "reason_optional": "Powód (opcjonalny)", + "reset_date_prompt": "Oznaczyć zadanie jako wykonane?", + "reset_date_optional": "Data ostatniego wykonania (opcjonalna, domyślnie dzisiaj)", + "notes_label": "Notatki", + "documentation_label": "Dokumentacja", + "no_nfc_tag": "— Brak tagu —", + "dashboard": "Pulpit", + "tab_today": "Dziś", + "palette_placeholder": "Szukaj obiektów i zadań…", + "palette_no_results": "Brak wyników", + "palette_hint": "↑↓ nawigacja · Enter otwórz · Esc zamknij", + "today_all_caught_up": "Wszystko zrobione! Nic w tym tygodniu.", + "today_overdue": "Zaległe", + "today_due_today": "Na dziś", + "today_this_week": "W tym tygodniu", + "settings": "Ustawienia", + "settings_features": "Funkcje zaawansowane", + "settings_features_desc": "Włącz lub wyłącz funkcje zaawansowane. Wyłączenie ukrywa je z UI, ale nie usuwa danych.", + "feat_adaptive": "Harmonogram adaptacyjny", + "feat_adaptive_desc": "Ucz się optymalnych interwałów z historii konserwacji", + "feat_predictions": "Predykcje czujników", + "feat_predictions_desc": "Przewiduj daty wyzwolenia z degradacji czujnika", + "feat_seasonal": "Korekty sezonowe", + "feat_seasonal_desc": "Dostosuj interwały do wzorców sezonowych", + "feat_environmental": "Korelacja środowiskowa", + "feat_environmental_desc": "Koreluj interwały z temperaturą/wilgotnością", + "feat_budget": "Śledzenie budżetu", + "feat_budget_desc": "Śledź miesięczne i roczne wydatki na konserwację", + "feat_groups": "Grupy zadań", + "feat_groups_desc": "Organizuj zadania w grupy logiczne", + "feat_checklists": "Listy kontrolne", + "feat_checklists_desc": "Wieloetapowe procedury wykonania zadania", + "settings_general": "Ogólne", + "settings_default_warning": "Domyślne dni ostrzeżenia", + "settings_panel_enabled": "Panel boczny", + "settings_panel_title": "Tytuł panelu bocznego", + "settings_notifications": "Powiadomienia", + "settings_notify_service": "Usługa powiadomień", + "test_notification": "Powiadomienie testowe", + "send_test": "Wyślij test", + "testing": "Wysyłanie…", + "test_notification_success": "Powiadomienie testowe wysłane", + "test_notification_failed": "Powiadomienie testowe nie powiodło się", + "settings_notify_due_soon": "Powiadom gdy wkrótce", + "settings_notify_overdue": "Powiadom gdy zaległe", + "settings_notify_triggered": "Powiadom gdy wyzwolone", + "settings_interval_hours": "Interwał powtarzania (godziny, 0 = raz)", + "settings_quiet_hours": "Godziny ciszy", + "settings_quiet_start": "Początek", + "settings_quiet_end": "Koniec", + "settings_max_per_day": "Maks. powiadomień dziennie (0 = bez limitu)", + "settings_bundling": "Grupowanie powiadomień", + "settings_bundle_threshold": "Próg grupowania", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobilne przyciski akcji", + "settings_action_complete": "Pokaż przycisk 'Wykonaj'", + "settings_action_skip": "Pokaż przycisk 'Pomiń'", + "settings_action_snooze": "Pokaż przycisk 'Drzemka'", + "settings_weekly_digest": "Cotygodniowe podsumowanie", + "settings_weekly_digest_hint": "Jedno powiadomienie podsumowujące w poniedziałek rano, gdy są zadania.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Czas drzemki (godziny)", + "settings_budget": "Budżet", + "settings_currency": "Waluta", + "settings_budget_monthly": "Budżet miesięczny", + "settings_budget_yearly": "Budżet roczny", + "settings_budget_alerts": "Alerty budżetowe", + "settings_budget_threshold": "Próg alertu (%)", + "settings_import_export": "Import / Eksport", + "settings_export_json": "Eksportuj JSON", + "settings_export_yaml": "Eksportuj YAML", + "settings_export_csv": "Eksportuj CSV", + "settings_import_csv": "Importuj CSV", + "settings_import_placeholder": "Wklej tutaj zawartość JSON lub CSV…", + "settings_import_btn": "Importuj", + "settings_import_success": "{count} obiektów zaimportowanych pomyślnie.", + "settings_export_success": "Eksport pobrany.", + "settings_saved": "Ustawienie zapisane.", + "settings_include_history": "Dołącz historię", + "sort_alphabetical": "Alfabetycznie", + "sort_due_soonest": "Najbliższy termin", + "sort_task_count": "Liczba zadań", + "sort_area": "Obszar", + "sort_assigned_user": "Przypisany użytkownik", + "sort_group": "Grupa", + "groupby_none": "Bez grupowania", + "groupby_area": "Wg obszaru", + "groupby_group": "Wg grupy", + "groupby_user": "Wg użytkownika", + "filter_label": "Filtr", + "user_label": "Użytkownik", + "sort_label": "Sortowanie", + "group_by_label": "Grupuj wg", + "state_value_help": "Użyj wartości stanu HA (zwykle małymi literami, np. \"on\"/\"off\"). Wielkość liter jest normalizowana przy zapisie.", + "target_changes_help": "Liczba pasujących przejść przed wyzwoleniem (domyślnie: 1).", + "qr_print_title": "Drukuj kody QR", + "qr_print_desc": "Wygeneruj stronę do druku z kodami QR do wycięcia i naklejenia na sprzęcie.", + "qr_print_load": "Załaduj obiekty", + "qr_print_filter": "Filtr", + "qr_print_objects": "Obiekty", + "qr_print_actions": "Akcje", + "qr_print_url_mode": "Typ linku", + "qr_print_estimate": "Szacowane kody QR", + "qr_print_over_limit": "limit 200, zawęź filtr", + "qr_print_generate": "Generuj kody QR", + "qr_print_generating": "Generowanie…", + "qr_print_ready": "Kody QR gotowe", + "qr_print_print_button": "Drukuj", + "qr_print_empty": "Nic do wygenerowania", + "qr_action_skip": "Pomiń", + "vacation_title": "Tryb urlopowy", + "vacation_active": "aktywny", + "vacation_ended": "zakończony", + "vacation_desc": "Zaplanuj urlop: powiadomienia są wstrzymane podczas okresu plus dni bufora. Możesz dodać wyjątki dla wybranych zadań.", + "vacation_enable": "Włącz tryb urlopowy", + "vacation_start": "Początek", + "vacation_end": "Koniec", + "vacation_buffer": "Bufor (dni)", + "vacation_exempt_title": "Powiadamiaj mimo urlopu", + "vacation_exempt_desc": "Wybierz zadania, które mają powiadamiać także w czasie urlopu (np. krytyczna chemia basenu).", + "vacation_load_tasks": "Wczytaj zadania", + "vacation_preview_btn": "Pokaż podgląd", + "vacation_preview_affected": "zadań dotyczy", + "vacation_event_due_soon": "wkrótce będzie wymagane", + "vacation_event_overdue": "stanie się zaległe", + "vacation_event_triggered_est": "możliwe wyzwolenie czujnika", + "vacation_sensor_based": "(czujnikowe)", + "vacation_action_notify": "Powiadamiaj mimo to", + "vacation_action_unsilence": "Ponownie wycisz", + "vacation_marked_complete": "Oznaczono jako wykonane", + "vacation_marked_skip": "Pominięto", + "vacation_end_now": "Zakończ urlop teraz", + "unassigned": "Nieprzypisane", + "no_area": "Brak obszaru", + "has_overdue": "Ma zaległe zadania", + "object": "Obiekt", + "settings_panel_access": "Dostęp do panelu", + "settings_panel_access_desc": "Administratorzy zawsze mają pełny dostęp. Aby delegować tworzenie, edytowanie i usuwanie wybranym użytkownikom bez uprawnień administratora, włącz tę opcję i zaznacz ich poniżej — pozostali widzą tylko Wykonaj i Pomiń.", + "settings_operator_write": "Zezwól wybranym użytkownikom na tworzenie, edycję i usuwanie", + "settings_operator_write_desc": "Wyłączone: tylko administratorzy mogą zmieniać treść. Włączone: wybrani użytkownicy poniżej również mają pełny dostęp.", + "no_non_admin_users": "Nie znaleziono użytkowników nie-admin. Dodaj ich w Ustawienia → Osoby.", + "owner_label": "Właściciel", + "feat_completion_actions": "Akcje po zakończeniu", + "feat_completion_actions_desc": "Akcja HA per zadanie po zakończeniu + QR szybkiego zakończenia z predefiniowanymi wartościami.", + "on_complete_action_title": "Po zakończeniu: wywołaj akcję HA (opcjonalnie)", + "on_complete_action_desc": "Wywołuje usługę HA po zakończeniu zadania — np. zresetuj licznik urządzenia.", + "on_complete_action_service": "Usługa", + "on_complete_action_target": "Encja docelowa", + "on_complete_action_data": "Dane (JSON, opcjonalnie)", + "on_complete_action_test": "Testuj akcję", + "on_complete_action_test_success": "Sukces", + "on_complete_action_test_failed": "Błąd", + "quick_complete_defaults_title": "Wartości domyślne szybkiego zakończenia (dla skanów QR, opcjonalnie)", + "quick_complete_defaults_desc": "Predefiniowane wartości dla QR szybkiego zakończenia. Bez nich QR otwiera okno dialogowe.", + "quick_complete_defaults_notes": "Notatki", + "quick_complete_defaults_cost": "Koszt", + "quick_complete_defaults_duration": "Czas trwania (minuty)", + "quick_complete_defaults_feedback_none": "Brak opinii", + "quick_complete_defaults_feedback_needed": "Było potrzebne", + "quick_complete_defaults_feedback_not_needed": "Nie było potrzebne", + "quick_complete_success": "Szybko oznaczono jako wykonane", + "trigger_replaced": "Wyzwalacz zastąpiony", + "add": "Dodaj", + "show_stats": "Pokaż statystyki + wykresy", + "hide_stats": "Ukryj statystyki", + "adaptive_no_data": "Za mało historii ukończeń do analizy adaptacyjnej. Wykonaj to zadanie jeszcze kilka razy, aby odblokować propozycje interwału i wykresy niezawodności.", + "suggestion_applied": "Zastosowano sugerowany interwał", + "vacation_mode": "Tryb urlopowy", + "vacation_status_active": "Aktywny teraz", + "vacation_status_scheduled": "Zaplanowany", + "vacation_status_inactive": "Nieaktywny", + "vacation_end_now_confirm": "Zakończyć urlop natychmiast?", + "vacation_exempt_count": "wykluczone", + "vacation_advanced": "Zaawansowane…", + "vacation_open_panel": "Otwórz w panelu", + "enable": "Włącz", + "saved": "Zapisano", + "budget_monthly_set": "Ustaw miesięczny", + "budget_yearly_set": "Ustaw roczny", + "budget_advanced": "Waluta, alerty…", + "budget_open_panel": "Otwórz w panelu", + "groups_empty": "Brak grup.", + "group_new_placeholder": "Dodaj grupę…", + "group_delete_confirm": "Usunąć grupę „{name}”?", + "groups_manage_tasks": "Zarządzaj przypisaniami zadań…", + "groups_open_panel": "Otwórz w panelu", + "on_complete_action_target_hint": "Uwaga: domena encji musi pasować do usługi — np. 'button.press' działa tylko na button.*, 'counter.increment' tylko na counter.*, 'input_button.press' tylko na input_button.* itd. W przypadku niezgodności akcja po cichu się nie wykona (HA zapisze w dzienniku 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Pokaż wszystkie obiekty", + "show_all_tasks": "Wyczyść filtr — pokaż wszystkie zadania", + "filter_to_overdue": "Filtruj listę tylko do zaległych", + "filter_to_due_soon": "Filtruj listę tylko do zbliżających się terminów", + "filter_to_triggered": "Filtruj listę tylko do wyzwolonych", + "open_task": "Otwórz zadanie", + "show_details": "Pokaż historię + statystyki", + "hide_details": "Ukryj szczegóły", + "history_empty": "Brak historii.", + "history_edit_button": "Edytuj wpis", + "total_cost": "Koszt całkowity", + "times_performed": "Wykonano", + "older_entries": "starsze", + "open_in_panel": "Otwórz w panelu Konserwacja", + "skip_reason": "Powód pominięcia (opcjonalnie)", + "reset_to_date": "Zresetuj ostatnie wykonanie na", + "delete_task_confirm": "Usunąć to zadanie i jego historię?", + "delete_object_confirm": "Usunąć ten obiekt i wszystkie jego zadania?", + "loading": "Ładowanie…", + "archive": "Archiwizuj", + "undo": "Cofnij", + "task_archived": "Zadanie zarchiwizowane", + "object_archived": "Obiekt zarchiwizowany", + "unarchive": "Przywróć z archiwum", + "archived": "Zarchiwizowane", + "show_archived": "Pokaż zarchiwizowane", + "hide_archived": "Ukryj zarchiwizowane", + "confirm_archive_object": "Zarchiwizować ten obiekt i jego zadania? Historia zostanie zachowana i można ją później przywrócić.", + "settings_archive": "Archiwum i przechowywanie", + "settings_archive_desc": "Odkładaj ukończone zadania jednorazowe bez usuwania. Zarchiwizowane elementy są ukryte i nieaktywne, ale zachowują historię i koszty.", + "settings_archive_oneoff_days": "Automatycznie archiwizuj ukończone zadania jednorazowe po (dni, 0 = wył.)", + "settings_delete_archived_oneoff_days": "Automatycznie usuwaj zarchiwizowane zadania jednorazowe po (dni, 0 = nigdy)", + "archive_object": "Archiwizuj obiekt", + "unarchive_object": "Przywróć obiekt z archiwum", + "documents": "Dokumenty", + "documents_empty": "Brak dokumentów.", + "doc_upload": "Prześlij plik", + "doc_uploading": "Przesyłanie…", + "doc_add_link": "Dodaj link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Tytuł (opcjonalnie)", + "doc_open": "Otwórz", + "doc_delete_confirm": "Usunąć „{name}”?", + "doc_too_large": "Plik jest za duży (maks. 25 MB).", + "doc_upload_failed": "Przesyłanie nie powiodło się.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Już zapisany gdzie indziej — współdzielony, bez dodatkowego miejsca.", + "doc_dup_in_object": "Ten plik jest już dołączony do tego obiektu.", + "doc_link_invalid": "Dozwolone są tylko linki http/https.", + "doc_cat_manual": "Instrukcja", + "doc_cat_warranty": "Gwarancja", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Części zamienne", + "doc_cat_photo": "Zdjęcie", + "doc_cat_other": "Inne", + "doc_link_badge": "Link", + "doc_storage_title": "Magazyn dokumentów", + "doc_storage_saved": "Zaoszczędzone przez deduplikację", + "doc_storage_refresh": "Odśwież", + "doc_download": "Pobierz", + "doc_close": "Zamknij", + "doc_camera": "Zrób zdjęcie", + "doc_drop_hint": "Upuść pliki tutaj", + "doc_task_none": "Brak dokumentów powiązanych z tym zadaniem.", + "doc_link_existing": "Powiąż dokument…", + "doc_attach": "Powiąż", + "doc_unlink": "Odłącz", + "doc_page": "Strona", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1r", + "chart_since_service": "od ostatniej konserwacji", + "chart_no_stats": "Brak statystyk długoterminowych dla tej encji — wyświetlane są tylko wartości z wpisów konserwacji", + "auto_complete_on_recovery": "Zakończ automatycznie, gdy czujnik wróci do normy", + "auto_complete_on_recovery_help": "Rejestruje wykonanie (ustawia ostatnie wykonanie), gdy wyzwalacz sam się rozwiąże — np. uzupełniono sól, wymieniono filtr.", + "doc_search": "Szukaj dokumentów…", + "doc_search_none": "Brak pasujących dokumentów", + "link_device_optional": "Połącz z istniejącym urządzeniem (opcjonalnie)", + "parent_object_optional": "Obiekt nadrzędny (opcjonalnie)", + "parent_none": "(Brak nadrzędnego)", + "paused": "Wstrzymane", + "pause_object": "Wstrzymaj", + "resume_object": "Wznów", + "pause_until_prompt": "Zamroź harmonogramy tego obiektu — nic nie staje się wymagane i nic nie powiadamia do czasu wznowienia. Opcjonalnie ustaw datę automatycznego wznowienia.", + "pause_until_label": "Wznów dnia (opcjonalnie)", + "object_paused": "Obiekt wstrzymany", + "object_resumed": "Obiekt wznowiony — harmonogramy uruchomione od nowa", + "object_paused_badge": "Wstrzymane", + "paused_until_label": "do", + "replace_object": "Zastąp…", + "replace_object_prompt": "Wycofaj ten obiekt i utwórz następcę. Historia i koszty pozostają zarchiwizowane przy starym; zadania i dokumenty przechodzą do nowego, liczniki startują od zera.", + "replace_name_label": "Nazwa następcy", + "object_replaced": "Obiekt zastąpiony — utworzono następcę", + "reading_unit_label": "Jednostka odczytu (np. kWh, m³)", + "reading_unit_help": "Wyświetlana obok zarejestrowanej wartości przy ukończeniu tego zadania.", + "reading_value_label": "Wartość odczytu", + "reading_label": "Odczyt", + "settings_templates_label": "Galeria szablonów", + "settings_templates_hint": "Odznacz szablony, których nigdy nie użyjesz — znikną z wyboru „Z szablonu\" (panel i config flow). Nic więcej się nie zmienia; można je włączyć ponownie w każdej chwili.", + "worksheet": "Karta pracy", + "worksheet_scan_view": "Zeskanuj, aby otworzyć zadanie", + "worksheet_scan_complete": "Zeskanuj, aby ukończyć", + "worksheet_manual_excerpt": "Fragment instrukcji", + "worksheet_pages": "strony", + "worksheet_printed": "Wydrukowano", + "worksheet_never": "Nigdy" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/pt.json b/custom_components/maintenance_supporter/frontend-src/locales/pt.json new file mode 100644 index 0000000..233fdcc --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/pt.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Manutenção", + "objects": "Objetos", + "tasks": "Tarefas", + "overdue": "Atrasada", + "due_soon": "Próxima", + "triggered": "Acionada", + "ok": "OK", + "all": "Todos", + "new_object": "+ Novo objeto", + "templates_from": "De modelo", + "templates_title": "Começar a partir de um modelo", + "templates_task_count": "{n} tarefas", + "template_created": "Criado a partir de modelo", + "onboard_hint": "Adicione o seu primeiro objeto para acompanhar a manutenção.", + "edit": "Editar", + "duplicate": "Duplicar", + "task_duplicated": "Tarefa duplicada", + "object_duplicated": "Objeto duplicado", + "delete": "Eliminar", + "add_task": "+ Tarefa", + "complete": "Concluída", + "completed": "Concluída", + "skip": "Saltar", + "skipped": "Saltada", + "missed": "Missed", + "reset": "Repor", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Cancelar", + "bulk_select": "Selecionar", + "bulk_select_all": "Selecionar tudo", + "bulk_n_selected": "{n} selecionados", + "bulk_completed": "{n} tarefas concluídas", + "bulk_archived": "{n} tarefas arquivadas", + "completing": "A concluir…", + "interval": "Intervalo", + "warning": "Aviso", + "last_performed": "Última execução", + "next_due": "Próximo vencimento", + "days_until_due": "Dias até vencimento", + "avg_duration": "Ø Duração", + "trigger": "Acionador", + "trigger_type": "Tipo de acionador", + "threshold_above": "Limite superior", + "threshold_below": "Limite inferior", + "threshold": "Limiar", + "counter": "Contador", + "state_change": "Mudança de estado", + "runtime": "Tempo de funcionamento", + "runtime_hours": "Duração alvo (horas)", + "target_value": "Valor alvo", + "baseline": "Linha de base", + "target_changes": "Alterações alvo", + "for_minutes": "Durante (minutos)", + "time_based": "Temporal", + "sensor_based": "Sensor", + "manual": "Manual", + "one_time": "Única vez", + "weekdays": "Dias da semana", + "nth_weekday": "N-ésimo dia da semana do mês", + "day_of_month": "Dia do mês", + "recurrence_on_days": "Repetir em", + "recurrence_occurrence": "Ocorrência", + "recurrence_weekday": "Dia da semana", + "recurrence_day": "Dia do mês (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.º", + "ord_2": "2.º", + "ord_3": "3.º", + "ord_4": "4.º", + "ord_5": "5.º", + "ord_last": "Último", + "day_word": "Dia", + "interval_value": "Intervalo", + "interval_unit": "Unidade", + "unit_days": "Dias", + "unit_weeks": "Semanas", + "unit_months": "Meses", + "unit_years": "Anos", + "due_date": "Data de vencimento", + "cleaning": "Limpeza", + "inspection": "Inspeção", + "replacement": "Substituição", + "calibration": "Calibração", + "service": "Serviço", + "reading": "Leitura", + "custom": "Personalizado", + "history": "Histórico", + "cost": "Custo", + "report_button": "Relatório", + "report_title": "Relatório de manutenção", + "report_generated": "Gerado", + "report_times_done": "Feito", + "report_total_cost": "Custo total", + "report_every": "a cada {n} {unit}", + "report_notes": "Notas", + "report_col_type": "Tipo", + "report_col_status": "Estado", + "report_col_schedule": "Agenda", + "duration": "Duração", + "both": "Ambos", + "trigger_val": "Valor do acionador", + "complete_title": "Concluída: ", + "checklist": "Lista de verificação", + "checklist_steps_optional": "Passos da lista de verificação (opcional)", + "checklist_placeholder": "Limpar filtro\nSubstituir vedação\nTestar pressão", + "checklist_help": "Um passo por linha. Máx. 100 itens.", + "err_too_long": "{field}: demasiado longo (máx. {n} caracteres)", + "err_too_short": "{field}: demasiado curto (mín. {n} caracteres)", + "err_value_too_high": "{field}: demasiado grande (máx. {n})", + "err_value_too_low": "{field}: demasiado pequeno (mín. {n})", + "err_required": "{field}: campo obrigatório", + "err_wrong_type": "{field}: tipo incorreto (esperado: {type})", + "err_invalid_choice": "{field}: valor não permitido", + "err_invalid_value": "{field}: valor inválido", + "feat_schedule_time": "Agendamento por hora", + "feat_schedule_time_desc": "Tarefas vencem em um horário específico em vez de meia-noite.", + "schedule_time_optional": "Vence às (opcional, HH:MM)", + "schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário HA.", + "at_time": "às", + "notes_optional": "Notas (opcional)", + "cost_optional": "Custo (opcional)", + "duration_minutes": "Duração em minutos (opcional)", + "days": "dias", + "day": "dia", + "today": "Hoje", + "d_overdue": "d em atraso", + "no_tasks": "Sem tarefas de manutenção. Crie um objeto para começar.", + "no_tasks_short": "Sem tarefas", + "no_history": "Sem entradas no histórico.", + "show_all": "Mostrar tudo", + "cost_duration_chart": "Custos & Duração", + "installed": "Instalado", + "confirm_delete_object": "Eliminar este objeto e todas as suas tarefas?", + "confirm_delete_task": "Eliminar esta tarefa?", + "min": "Mín", + "max": "Máx", + "save": "Guardar", + "saving": "A guardar…", + "edit_task": "Editar tarefa", + "new_task": "Nova tarefa de manutenção", + "task_name": "Nome da tarefa", + "maintenance_type": "Tipo de manutenção", + "priority": "Prioridade", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Baixa", + "priority_normal": "Normal", + "priority_high": "Alta", + "schedule_type": "Tipo de agendamento", + "interval_days": "Intervalo (dias)", + "warning_days": "Dias de aviso", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Última execução (opcional)", + "interval_anchor": "Âncora do intervalo", + "anchor_completion": "A partir da data de conclusão", + "anchor_planned": "A partir da data planeada (sem desvio)", + "edit_object": "Editar objeto", + "name": "Nome", + "manufacturer_optional": "Fabricante (opcional)", + "model_optional": "Modelo (opcional)", + "serial_number_optional": "Número de série (opcional)", + "serial_number_label": "N/S", + "documentation_url_label": "Manual", + "object_notes_label": "Notas", + "sort_due_date": "Vencimento", + "sort_object": "Nome do objeto", + "sort_type": "Tipo", + "sort_task_name": "Nome da tarefa", + "all_objects": "Todos os objetos", + "tasks_lower": "tarefas", + "no_tasks_yet": "Ainda sem tarefas", + "add_first_task": "Adicionar primeira tarefa", + "trigger_configuration": "Configuração do acionador", + "entity_id": "ID da entidade", + "comma_separated": "separados por vírgulas", + "entity_logic": "Lógica da entidade", + "entity_logic_any": "Qualquer entidade aciona", + "entity_logic_all": "Todas as entidades devem acionar", + "entities": "entidades", + "attribute_optional": "Atributo (opcional, vazio = estado)", + "use_entity_state": "Usar estado da entidade (sem atributo)", + "trigger_above": "Acionar acima de", + "trigger_below": "Acionar abaixo de", + "for_at_least_minutes": "Durante pelo menos (minutos)", + "safety_interval_days": "Intervalo de segurança (dias, opcional)", + "safety_interval": "Intervalo de segurança (opcional)", + "delta_mode": "Modo delta", + "from_state_optional": "Do estado (opcional)", + "to_state_optional": "Para o estado (opcional)", + "documentation_url_optional": "URL de documentação (opcional)", + "object_notes_optional": "Notas (opcional)", + "nfc_tag_id_optional": "ID da etiqueta NFC (opcional)", + "nfc_tags_empty_help": "Ainda nenhuma tag NFC registada no Home Assistant.", + "nfc_tags_open_settings": "Abrir configurações de tags", + "nfc_tags_refresh": "Atualizar", + "environmental_entity_optional": "Sensor ambiental (opcional)", + "environmental_entity_helper": "ex. sensor.temperatura_exterior — ajusta o intervalo segundo as condições ambientais", + "environmental_attribute_optional": "Atributo ambiental (opcional)", + "nfc_tag_id": "ID da etiqueta NFC", + "nfc_linked": "Etiqueta NFC associada", + "nfc_link_hint": "Clique para associar etiqueta NFC", + "responsible_user": "Utilizador responsável", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Nenhum utilizador atribuído)", + "all_users": "Todos os utilizadores", + "my_tasks": "As minhas tarefas", + "tab_calendar": "Calendário", + "cal_no_events": "Sem manutenção", + "cal_window_7": "7 dias", + "cal_window_14": "14 dias", + "cal_window_30": "30 dias", + "cal_window_365": "1 ano", + "cal_every_n_days": "a cada {n} dias", + "cal_source_time": "Baseado em tempo", + "cal_source_time_adaptive": "Baseado em tempo (adaptativo)", + "cal_source_sensor": "Baseado em sensor", + "cal_predicted": "previsto", + "cal_confidence_high": "alta confiança", + "cal_confidence_medium": "confiança média", + "cal_confidence_low": "baixa confiança", + "budget_monthly": "Orçamento mensal", + "budget_yearly": "Orçamento anual", + "groups": "Grupos", + "new_group": "Novo grupo", + "edit_group": "Editar grupo", + "no_groups": "Ainda sem grupos", + "delete_group": "Eliminar grupo", + "delete_group_confirm": "Eliminar o grupo '{name}'?", + "group_select_tasks": "Selecionar tarefas", + "group_name_required": "Nome obrigatório", + "description_optional": "Descrição (opcional)", + "selected": "Selecionado", + "loading_chart": "A carregar dados...", + "hide_outliers": "Ocultar valores atípicos (erros do sensor)", + "was_maintenance_needed": "Esta manutenção era necessária?", + "feedback_needed": "Necessária", + "feedback_not_needed": "Não necessária", + "feedback_not_sure": "Não tenho a certeza", + "suggested_interval": "Intervalo sugerido", + "apply_suggestion": "Aplicar", + "reanalyze": "Reanalisar", + "reanalyze_result": "Nova análise", + "reanalyze_insufficient_data": "Dados insuficientes para uma recomendação", + "data_points": "pontos de dados", + "dismiss_suggestion": "Descartar", + "confidence_low": "Baixa", + "confidence_medium": "Média", + "confidence_high": "Alta", + "recommended": "recomendado", + "seasonal_awareness": "Consciência sazonal", + "edit_seasonal_overrides": "Editar fatores sazonais", + "seasonal_overrides_title": "Fatores sazonais (override)", + "seasonal_overrides_hint": "Fator por mês (0.1–5.0). Vazio = aprendido automaticamente.", + "seasonal_override_invalid": "Valor inválido", + "seasonal_override_range": "O fator deve estar entre 0.1 e 5.0", + "clear_all": "Limpar tudo", + "seasonal_chart_title": "Fatores sazonais", + "seasonal_learned": "Aprendido", + "seasonal_manual": "Manual", + "month_jan": "Jan", + "month_feb": "Fev", + "month_mar": "Mar", + "month_apr": "Abr", + "month_may": "Mai", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Ago", + "month_sep": "Set", + "month_oct": "Out", + "month_nov": "Nov", + "month_dec": "Dez", + "sensor_prediction": "Previsão do sensor", + "degradation_trend": "Tendência", + "trend_rising": "A subir", + "trend_falling": "A descer", + "trend_stable": "Estável", + "trend_insufficient_data": "Dados insuficientes", + "days_until_threshold": "Dias até ao limiar", + "threshold_exceeded": "Limiar ultrapassado", + "environmental_adjustment": "Fator ambiental", + "sensor_prediction_urgency": "O sensor prevê o limiar em ~{days} dias", + "day_short": "dia", + "weibull_reliability_curve": "Curva de fiabilidade", + "weibull_failure_probability": "Probabilidade de falha", + "weibull_r_squared": "Ajuste R²", + "beta_early_failures": "Falhas precoces", + "beta_random_failures": "Falhas aleatórias", + "beta_wear_out": "Desgaste", + "beta_highly_predictable": "Altamente previsível", + "confidence_interval": "Intervalo de confiança", + "confidence_conservative": "Conservador", + "confidence_aggressive": "Otimista", + "current_interval_marker": "Intervalo atual", + "recommended_marker": "Recomendado", + "characteristic_life": "Vida característica", + "chart_mini_sparkline": "Sparkline de tendência", + "chart_history": "Histórico de custos e duração", + "chart_seasonal": "Fatores sazonais, 12 meses", + "chart_weibull": "Curva de fiabilidade Weibull", + "chart_sparkline": "Gráfico de valor do acionador", + "days_progress": "Progresso em dias", + "qr_code": "Código QR", + "qr_generating": "A gerar código QR…", + "qr_error": "Não foi possível gerar o código QR.", + "qr_error_no_url": "Nenhum URL do HA configurado. Defina um URL externo ou interno em Definições → Sistema → Rede.", + "save_error": "Erro ao guardar. Tente novamente.", + "qr_print": "Imprimir", + "qr_download": "Transferir SVG", + "qr_action": "Ação ao digitalizar", + "qr_action_view": "Ver informações de manutenção", + "qr_action_complete": "Marcar manutenção como concluída", + "qr_url_mode": "Tipo de ligação", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "URL do servidor", + "overview": "Visão geral", + "analysis": "Análise", + "recent_activities": "Atividades recentes", + "search_notes": "Pesquisar notas", + "avg_cost": "Ø Custo", + "no_advanced_features": "Sem funções avançadas ativadas", + "no_advanced_features_hint": "Ative “Intervalos Adaptativos” ou “Padrões Sazonais” nas definições da integração para ver dados de análise aqui.", + "analysis_not_enough_data": "Ainda não há dados suficientes para a análise.", + "analysis_not_enough_data_hint": "A análise Weibull requer pelo menos 5 manutenções concluídas; os padrões sazonais tornam-se visíveis após 6+ pontos de dados por mês.", + "analysis_manual_task_hint": "Tarefas manuais sem intervalo não geram dados de análise.", + "completions": "conclusões", + "current": "Atual", + "shorter": "Mais curto", + "longer": "Mais longo", + "normal": "Normal", + "disabled": "Desativado", + "compound_logic": "Lógica composta", + "compound": "Composto (várias condições)", + "compound_logic_and": "E — todas as condições devem disparar", + "compound_logic_or": "OU — basta uma condição", + "compound_help": "Combine várias condições de sensores num só disparador.", + "compound_no_conditions": "Ainda sem condições — adicione pelo menos uma.", + "compound_add_condition": "Adicionar condição", + "compound_condition": "Condição", + "compound_remove_condition": "Remover condição", + "card_title": "Título", + "card_show_header": "Mostrar cabeçalho com estatísticas", + "card_show_actions": "Mostrar botões de ação", + "card_compact": "Modo compacto", + "card_max_items": "Máx. itens (0 = todos)", + "card_filter_status": "Filtrar por estado", + "card_filter_status_help": "Vazio = mostrar todos os estados.", + "card_filter_objects": "Filtrar por objetos", + "card_filter_objects_help": "Vazio = mostrar todos os objetos.", + "card_filter_entities": "Filtrar por entidades (entity_ids)", + "card_filter_entities_help": "Selecione entidades sensor / binary_sensor desta integração. Vazio = todas.", + "card_loading_objects": "A carregar objetos…", + "card_load_error": "Não foi possível carregar os objetos — verifique a ligação WebSocket.", + "card_no_tasks_title": "Ainda sem tarefas de manutenção", + "card_no_tasks_cta": "→ Crie uma no painel Manutenção", + "no_objects": "Ainda sem objetos.", + "action_error": "Ação falhada. Tente novamente.", + "area_id_optional": "Área (opcional)", + "installation_date_optional": "Data de instalação (opcional)", + "warranty_expiry_optional": "Validade da garantia (opcional)", + "warranty": "Garantia", + "warranty_valid_until": "válida até {date}", + "warranty_expires_in": "expira em {days} dias", + "warranty_expired": "expirada", + "cal_past_windows": "Janelas passadas", + "cal_forward_windows": "Janelas futuras", + "history_edit_title": "Editar entrada do histórico", + "history_edit_timestamp": "Carimbo de data/hora", + "manufacturer": "Fabricante", + "model": "Modelo", + "area": "Área", + "actions": "Ações", + "view_mode_label": "Vista", + "view_cards": "Vista de cartões", + "view_table": "Vista de tabela", + "objects_table_columns_label": "Colunas da tabela de objetos", + "objects_table_columns_hint": "Escolha que colunas aparecem na vista de tabela de objetos.", + "custom_icon_optional": "Ícone (opcional, ex. mdi:wrench)", + "task_enabled": "Tarefa ativada", + "skip_reason_prompt": "Saltar esta tarefa?", + "reason_optional": "Motivo (opcional)", + "reset_date_prompt": "Marcar tarefa como executada?", + "reset_date_optional": "Data da última execução (opcional, padrão: hoje)", + "notes_label": "Notas", + "documentation_label": "Documentação", + "no_nfc_tag": "— Sem etiqueta —", + "dashboard": "Painel", + "tab_today": "Hoje", + "palette_placeholder": "Pesquisar objetos e tarefas…", + "palette_no_results": "Sem resultados", + "palette_hint": "↑↓ navegar · Enter abrir · Esc fechar", + "today_all_caught_up": "Tudo em dia! Nada para esta semana.", + "today_overdue": "Atrasadas", + "today_due_today": "Vence hoje", + "today_this_week": "Esta semana", + "settings": "Definições", + "settings_features": "Funções avançadas", + "settings_features_desc": "Ative ou desative funções avançadas. Desativar oculta-as da interface mas não elimina dados.", + "feat_adaptive": "Agendamento adaptativo", + "feat_adaptive_desc": "Aprender intervalos ideais a partir do histórico de manutenção", + "feat_predictions": "Previsões do sensor", + "feat_predictions_desc": "Prever datas de acionamento pela degradação do sensor", + "feat_seasonal": "Ajustes sazonais", + "feat_seasonal_desc": "Ajustar intervalos com base em padrões sazonais", + "feat_environmental": "Correlação ambiental", + "feat_environmental_desc": "Correlacionar intervalos com temperatura/humidade", + "feat_budget": "Controlo de orçamento", + "feat_budget_desc": "Acompanhar despesas de manutenção mensais e anuais", + "feat_groups": "Grupos de tarefas", + "feat_groups_desc": "Organizar tarefas em grupos lógicos", + "feat_checklists": "Listas de verificação", + "feat_checklists_desc": "Procedimentos com vários passos para conclusão de tarefas", + "settings_general": "Geral", + "settings_default_warning": "Dias de aviso predefinidos", + "settings_panel_enabled": "Painel lateral", + "settings_panel_title": "Título do painel lateral", + "settings_notifications": "Notificações", + "settings_notify_service": "Serviço de notificação", + "test_notification": "Notificação de teste", + "send_test": "Enviar teste", + "testing": "A enviar…", + "test_notification_success": "Notificação de teste enviada", + "test_notification_failed": "Falha na notificação de teste", + "settings_notify_due_soon": "Notificar quando próxima", + "settings_notify_overdue": "Notificar quando atrasada", + "settings_notify_triggered": "Notificar quando acionada", + "settings_interval_hours": "Intervalo de repetição (horas, 0 = uma vez)", + "settings_quiet_hours": "Horas de silêncio", + "settings_quiet_start": "Início", + "settings_quiet_end": "Fim", + "settings_max_per_day": "Máx. notificações por dia (0 = ilimitado)", + "settings_bundling": "Agrupar notificações", + "settings_bundle_threshold": "Limiar de agrupamento", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Botões de ação móveis", + "settings_action_complete": "Mostrar botão 'Concluída'", + "settings_action_skip": "Mostrar botão 'Saltar'", + "settings_action_snooze": "Mostrar botão 'Adiar'", + "settings_weekly_digest": "Resumo semanal", + "settings_weekly_digest_hint": "Uma notificação de resumo na segunda de manhã quando há tarefas.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Duração do adiamento (horas)", + "settings_budget": "Orçamento", + "settings_currency": "Moeda", + "settings_budget_monthly": "Orçamento mensal", + "settings_budget_yearly": "Orçamento anual", + "settings_budget_alerts": "Alertas de orçamento", + "settings_budget_threshold": "Limiar de alerta (%)", + "settings_import_export": "Importar / Exportar", + "settings_export_json": "Exportar JSON", + "settings_export_yaml": "Exportar YAML", + "settings_export_csv": "Exportar CSV", + "settings_import_csv": "Importar CSV", + "settings_import_placeholder": "Cole o conteúdo JSON ou CSV aqui…", + "settings_import_btn": "Importar", + "settings_import_success": "{count} objetos importados com sucesso.", + "settings_export_success": "Exportação transferida.", + "settings_saved": "Definição guardada.", + "settings_include_history": "Incluir histórico", + "sort_alphabetical": "Alfabético", + "sort_due_soonest": "Vencimento mais próximo", + "sort_task_count": "Quantidade de tarefas", + "sort_area": "Área", + "sort_assigned_user": "Usuário atribuído", + "sort_group": "Grupo", + "filter_label": "Filtro", + "user_label": "Utilizador", + "sort_label": "Ordenar", + "group_by_label": "Agrupar por", + "state_value_help": "Use o valor de estado HA (normalmente em minúsculas, p. ex. \"on\"/\"off\"). As maiúsculas são normalizadas ao guardar.", + "target_changes_help": "Número de transições correspondentes antes do trigger disparar (predefinido: 1).", + "qr_print_title": "Imprimir códigos QR", + "qr_print_desc": "Gera uma página imprimível de códigos QR para recortar e colar nos equipamentos.", + "qr_print_load": "Carregar objetos", + "qr_print_filter": "Filtro", + "qr_print_objects": "Objetos", + "qr_print_actions": "Ações", + "qr_print_url_mode": "Tipo de link", + "qr_print_estimate": "Códigos QR estimados", + "qr_print_over_limit": "máximo 200, restrinja o filtro", + "qr_print_generate": "Gerar códigos QR", + "qr_print_generating": "A gerar…", + "qr_print_ready": "Códigos QR prontos", + "qr_print_print_button": "Imprimir", + "qr_print_empty": "Nada a gerar", + "qr_action_skip": "Saltar", + "vacation_title": "Modo de férias", + "vacation_active": "ativo", + "vacation_ended": "terminado", + "vacation_desc": "Planeia as tuas férias: as notificações são pausadas durante o período mais dias de margem. Podes manter exceções por tarefa.", + "vacation_enable": "Ativar modo de férias", + "vacation_start": "Início", + "vacation_end": "Fim", + "vacation_buffer": "Margem (dias)", + "vacation_exempt_title": "Notificar mesmo em férias", + "vacation_exempt_desc": "Escolhe tarefas que devem notificar mesmo em férias (p. ex. química crítica de piscina).", + "vacation_load_tasks": "Carregar tarefas", + "vacation_preview_btn": "Mostrar pré-visualização", + "vacation_preview_affected": "tarefas afetadas", + "vacation_event_due_soon": "ficará próxima do prazo", + "vacation_event_overdue": "ficará em atraso", + "vacation_event_triggered_est": "possível disparo do sensor", + "vacation_sensor_based": "(baseado em sensor)", + "vacation_action_notify": "Notificar mesmo assim", + "vacation_action_unsilence": "Silenciar novamente", + "vacation_marked_complete": "Marcado como concluído", + "vacation_marked_skip": "Saltado", + "vacation_end_now": "Terminar férias agora", + "groupby_none": "Sem agrupamento", + "groupby_area": "Por área", + "groupby_group": "Por grupo", + "groupby_user": "Por usuário", + "unassigned": "Não atribuído", + "no_area": "Sem área", + "has_overdue": "Tarefas em atraso", + "object": "Objeto", + "settings_panel_access": "Acesso ao painel", + "settings_panel_access_desc": "Administradores sempre têm acesso completo. Para delegar criar, editar e excluir a não administradores específicos, ative isto e selecione-os abaixo — os demais só veem Concluir e Ignorar.", + "settings_operator_write": "Permitir que os usuários selecionados criem, editem e excluam", + "settings_operator_write_desc": "Desativado: apenas administradores podem alterar o conteúdo. Ativado: os usuários selecionados abaixo também têm acesso completo.", + "no_non_admin_users": "Nenhum usuário não administrador encontrado. Adicione em Configurações → Pessoas.", + "owner_label": "Proprietário", + "feat_completion_actions": "Ações de conclusão", + "feat_completion_actions_desc": "Ação HA por tarefa ao concluir + QR de conclusão rápida com valores predefinidos.", + "on_complete_action_title": "Na conclusão: acionar ação HA (opcional)", + "on_complete_action_desc": "Chama um serviço HA quando a tarefa é concluída — p. ex. reiniciar um contador no dispositivo.", + "on_complete_action_service": "Serviço", + "on_complete_action_target": "Entidade alvo", + "on_complete_action_data": "Dados (JSON, opcional)", + "on_complete_action_test": "Testar ação", + "on_complete_action_test_success": "Sucesso", + "on_complete_action_test_failed": "Falhou", + "quick_complete_defaults_title": "Valores predefinidos de conclusão rápida (para scans QR, opcional)", + "quick_complete_defaults_desc": "Valores predefinidos para QR de conclusão rápida. Sem eles, o QR abre o diálogo.", + "quick_complete_defaults_notes": "Notas", + "quick_complete_defaults_cost": "Custo", + "quick_complete_defaults_duration": "Duração (minutos)", + "quick_complete_defaults_feedback_none": "Sem feedback", + "quick_complete_defaults_feedback_needed": "Era necessário", + "quick_complete_defaults_feedback_not_needed": "Não era necessário", + "quick_complete_success": "Concluído rapidamente", + "trigger_replaced": "Acionador substituído", + "add": "Adicionar", + "show_stats": "Mostrar estatísticas + gráficos", + "hide_stats": "Ocultar estatísticas", + "adaptive_no_data": "Ainda não há histórico de conclusões suficiente para a análise adaptativa. Conclua esta tarefa mais algumas vezes para desbloquear as recomendações de intervalo e os gráficos de fiabilidade.", + "suggestion_applied": "Intervalo sugerido aplicado", + "vacation_mode": "Modo de férias", + "vacation_status_active": "Ativo agora", + "vacation_status_scheduled": "Agendado", + "vacation_status_inactive": "Inativo", + "vacation_end_now_confirm": "Terminar as férias imediatamente?", + "vacation_exempt_count": "isentas", + "vacation_advanced": "Avançado…", + "vacation_open_panel": "Abrir no painel", + "enable": "Ativar", + "saved": "Guardado", + "budget_monthly_set": "Definir mensal", + "budget_yearly_set": "Definir anual", + "budget_advanced": "Moeda, alertas…", + "budget_open_panel": "Abrir no painel", + "groups_empty": "Ainda sem grupos.", + "group_new_placeholder": "Adicionar grupo…", + "group_delete_confirm": "Eliminar o grupo \"{name}\"?", + "groups_manage_tasks": "Gerir atribuições de tarefas…", + "groups_open_panel": "Abrir no painel", + "on_complete_action_target_hint": "Nota: o domínio da entidade tem de corresponder ao serviço — p. ex. 'button.press' só funciona em button.*, 'counter.increment' só em counter.*, 'input_button.press' só em input_button.* etc. Em caso de incompatibilidade, a ação falha silenciosamente (o HA regista 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Mostrar todos os objetos", + "show_all_tasks": "Limpar filtro — mostrar todas as tarefas", + "filter_to_overdue": "Filtrar a lista apenas para em atraso", + "filter_to_due_soon": "Filtrar a lista apenas para a vencer em breve", + "filter_to_triggered": "Filtrar a lista apenas para acionadas", + "open_task": "Abrir tarefa", + "show_details": "Mostrar histórico + estatísticas", + "hide_details": "Ocultar detalhes", + "history_empty": "Ainda sem histórico.", + "history_edit_button": "Editar entrada", + "total_cost": "Custo total", + "times_performed": "Realizada", + "older_entries": "mais antigas", + "open_in_panel": "Abrir no painel Manutenção", + "skip_reason": "Motivo para saltar (opcional)", + "reset_to_date": "Repor última execução para", + "delete_task_confirm": "Eliminar esta tarefa e o seu histórico?", + "delete_object_confirm": "Eliminar este objeto e todas as suas tarefas?", + "loading": "A carregar…", + "archive": "Arquivar", + "undo": "Desfazer", + "task_archived": "Tarefa arquivada", + "object_archived": "Objeto arquivado", + "unarchive": "Desarquivar", + "archived": "Arquivado", + "show_archived": "Mostrar arquivados", + "hide_archived": "Ocultar arquivados", + "confirm_archive_object": "Arquivar este objeto e as suas tarefas? O histórico é mantido e pode ser restaurado mais tarde.", + "settings_archive": "Arquivo e retenção", + "settings_archive_desc": "Retire tarefas únicas concluídas sem as eliminar. Os itens arquivados ficam ocultos e inativos, mas mantêm histórico e custos.", + "settings_archive_oneoff_days": "Arquivar automaticamente tarefas únicas concluídas após (dias, 0 = desligado)", + "settings_delete_archived_oneoff_days": "Eliminar automaticamente tarefas únicas arquivadas após (dias, 0 = nunca)", + "archive_object": "Arquivar objeto", + "unarchive_object": "Desarquivar objeto", + "documents": "Documentos", + "documents_empty": "Ainda sem documentos.", + "doc_upload": "Carregar ficheiro", + "doc_uploading": "A carregar…", + "doc_add_link": "Adicionar ligação", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Título (opcional)", + "doc_open": "Abrir", + "doc_delete_confirm": "Eliminar «{name}»?", + "doc_too_large": "Ficheiro demasiado grande (máx. 25 MB).", + "doc_upload_failed": "Falha no carregamento.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Já armazenado noutro local — partilhado, sem espaço extra.", + "doc_dup_in_object": "Este ficheiro já está anexado a este objeto.", + "doc_link_invalid": "Apenas ligações http/https são permitidas.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Garantia", + "doc_cat_invoice": "Fatura", + "doc_cat_spare_parts": "Peças", + "doc_cat_photo": "Foto", + "doc_cat_other": "Outro", + "doc_link_badge": "Ligação", + "doc_storage_title": "Armazenamento de documentos", + "doc_storage_saved": "Poupado por deduplicação", + "doc_storage_refresh": "Atualizar", + "doc_download": "Transferir", + "doc_close": "Fechar", + "doc_camera": "Tirar foto", + "doc_drop_hint": "Solte os ficheiros aqui", + "doc_task_none": "Nenhum documento associado a esta tarefa.", + "doc_link_existing": "Associar um documento…", + "doc_attach": "Associar", + "doc_unlink": "Desassociar", + "doc_page": "Página", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1a", + "chart_since_service": "desde a última manutenção", + "chart_no_stats": "Sem estatísticas de longo prazo para esta entidade — mostrando apenas valores de eventos de manutenção", + "auto_complete_on_recovery": "Concluir automaticamente quando o sensor recuperar", + "auto_complete_on_recovery_help": "Regista uma conclusão (define a última realização) quando o gatilho se resolve sozinho — p. ex., sal reabastecido, filtro substituído.", + "doc_search": "Pesquisar documentos…", + "doc_search_none": "Nenhum documento correspondente", + "link_device_optional": "Associar a dispositivo existente (opcional)", + "parent_object_optional": "Objeto principal (opcional)", + "parent_none": "(Sem objeto principal)", + "paused": "Pausado", + "pause_object": "Pausar", + "resume_object": "Retomar", + "pause_until_prompt": "Congela os agendamentos deste objeto — nada vence nem notifica até ser retomado. Opcionalmente, defina uma data de retoma automática.", + "pause_until_label": "Retomar em (opcional)", + "object_paused": "Objeto pausado", + "object_resumed": "Objeto retomado — agendamentos reiniciados", + "object_paused_badge": "Pausado", + "paused_until_label": "até", + "replace_object": "Substituir…", + "replace_object_prompt": "Retire este objeto e crie um sucessor. O histórico e os custos ficam arquivados no antigo; as tarefas e os documentos passam para o novo, os contadores começam do zero.", + "replace_name_label": "Nome do sucessor", + "object_replaced": "Objeto substituído — sucessor criado", + "reading_unit_label": "Unidade de leitura (p. ex. kWh, m³)", + "reading_unit_help": "Mostrada ao lado do valor registado ao concluir esta tarefa.", + "reading_value_label": "Valor de leitura", + "reading_label": "Leitura", + "settings_templates_label": "Galeria de modelos", + "settings_templates_hint": "Desmarque modelos de que nunca precisará — desaparecem dos seletores \"A partir de modelo\" (painel e config flow). Nada mais muda; reativáveis a qualquer momento.", + "worksheet": "Folha de trabalho", + "worksheet_scan_view": "Digitalize para abrir a tarefa", + "worksheet_scan_complete": "Digitalize para concluir", + "worksheet_manual_excerpt": "Excerto do manual", + "worksheet_pages": "páginas", + "worksheet_printed": "Impresso", + "worksheet_never": "Nunca" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/ru.json b/custom_components/maintenance_supporter/frontend-src/locales/ru.json new file mode 100644 index 0000000..cabacb7 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/ru.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Обслуживание", + "objects": "Объекты", + "tasks": "Задачи", + "overdue": "Просрочено", + "due_soon": "Скоро", + "triggered": "Сработало", + "ok": "OK", + "all": "Все", + "new_object": "+ Новый объект", + "templates_from": "Из шаблона", + "templates_title": "Начать с шаблона", + "templates_task_count": "{n} задач", + "template_created": "Создано из шаблона", + "onboard_hint": "Добавьте первый объект, чтобы отслеживать обслуживание.", + "edit": "Изменить", + "duplicate": "Дублировать", + "task_duplicated": "Задача продублирована", + "object_duplicated": "Объект продублирован", + "delete": "Удалить", + "add_task": "+ Добавить задачу", + "complete": "Выполнить", + "completed": "Выполнено", + "skip": "Пропустить", + "skipped": "Пропущено", + "missed": "Missed", + "reset": "Сбросить", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Отмена", + "bulk_select": "Выбрать", + "bulk_select_all": "Выбрать все", + "bulk_n_selected": "Выбрано: {n}", + "bulk_completed": "Выполнено задач: {n}", + "bulk_archived": "Архивировано задач: {n}", + "completing": "Выполнение…", + "interval": "Интервал", + "warning": "Предупреждение", + "last_performed": "Последнее выполнение", + "next_due": "Следующий срок", + "days_until_due": "Дней до срока", + "avg_duration": "Ср. длительность", + "trigger": "Триггер", + "trigger_type": "Тип триггера", + "threshold_above": "Верхний предел", + "threshold_below": "Нижний предел", + "threshold": "Порог", + "counter": "Счётчик", + "state_change": "Изменение состояния", + "runtime": "Время работы", + "runtime_hours": "Целевое время работы (часы)", + "target_value": "Целевое значение", + "baseline": "Базовое значение", + "target_changes": "Целевые изменения", + "for_minutes": "На (минут)", + "time_based": "По времени", + "sensor_based": "По датчику", + "manual": "Вручную", + "one_time": "Однократно", + "weekdays": "Дни недели", + "nth_weekday": "N-й день недели месяца", + "day_of_month": "День месяца", + "recurrence_on_days": "Повторять в", + "recurrence_occurrence": "Вхождение", + "recurrence_weekday": "День недели", + "recurrence_day": "День месяца (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1-й", + "ord_2": "2-й", + "ord_3": "3-й", + "ord_4": "4-й", + "ord_5": "5-й", + "ord_last": "Последний", + "day_word": "День", + "interval_value": "Интервал", + "interval_unit": "Единица", + "unit_days": "Дни", + "unit_weeks": "Недели", + "unit_months": "Месяцы", + "unit_years": "Годы", + "due_date": "Дата выполнения", + "cleaning": "Чистка", + "inspection": "Осмотр", + "replacement": "Замена", + "calibration": "Калибровка", + "service": "Сервис", + "reading": "Показания", + "custom": "Своё", + "history": "История", + "cost": "Стоимость", + "report_button": "Отчёт", + "report_title": "Отчёт об обслуживании", + "report_generated": "Создан", + "report_times_done": "Выполнено", + "report_total_cost": "Итого", + "report_every": "каждые {n} {unit}", + "report_notes": "Заметки", + "report_col_type": "Тип", + "report_col_status": "Статус", + "report_col_schedule": "Расписание", + "duration": "Длительность", + "both": "Оба", + "trigger_val": "Значение триггера", + "complete_title": "Выполнить: ", + "checklist": "Контрольный список", + "checklist_steps_optional": "Шаги контрольного списка (необязательно)", + "checklist_placeholder": "Очистить фильтр\nЗаменить уплотнитель\nПроверить давление", + "checklist_help": "Один шаг на строку. Макс. 100 элементов.", + "err_too_long": "{field}: слишком длинно (макс. {n} символов)", + "err_too_short": "{field}: слишком коротко (мин. {n} символов)", + "err_value_too_high": "{field}: слишком велико (макс. {n})", + "err_value_too_low": "{field}: слишком мало (мин. {n})", + "err_required": "{field}: обязательное поле", + "err_wrong_type": "{field}: неверный тип (ожидался: {type})", + "err_invalid_choice": "{field}: недопустимое значение", + "err_invalid_value": "{field}: неверное значение", + "feat_schedule_time": "Планирование по времени дня", + "feat_schedule_time_desc": "Задачи становятся просроченными в определённое время дня, а не в полночь.", + "schedule_time_optional": "Срок (необязательно, HH:MM)", + "schedule_time_help": "Пусто = полночь (по умолчанию). Часовой пояс HA.", + "at_time": "в", + "notes_optional": "Примечания (опционально)", + "cost_optional": "Стоимость (опционально)", + "duration_minutes": "Длительность в минутах (опционально)", + "days": "дней", + "day": "день", + "today": "Сегодня", + "d_overdue": "дн. просрочено", + "no_tasks": "Пока нет задач по обслуживанию. Создайте объект, чтобы начать.", + "no_tasks_short": "Нет задач", + "no_history": "Пока нет записей в истории.", + "show_all": "Показать все", + "cost_duration_chart": "Стоимость и длительность", + "installed": "Установлен", + "confirm_delete_object": "Удалить этот объект и все его задачи?", + "confirm_delete_task": "Удалить эту задачу?", + "min": "Мин", + "max": "Макс", + "save": "Сохранить", + "saving": "Сохранение…", + "edit_task": "Изменить задачу", + "new_task": "Новая задача обслуживания", + "task_name": "Название задачи", + "maintenance_type": "Тип обслуживания", + "priority": "Приоритет", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Низкий", + "priority_normal": "Обычный", + "priority_high": "Высокий", + "schedule_type": "Тип расписания", + "interval_days": "Интервал (дни)", + "warning_days": "Дни предупреждения", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "interval_anchor": "Якорь интервала", + "anchor_completion": "От даты выполнения", + "anchor_planned": "От плановой даты (без смещения)", + "edit_object": "Изменить объект", + "name": "Имя", + "manufacturer_optional": "Производитель (опционально)", + "model_optional": "Модель (опционально)", + "serial_number_optional": "Серийный номер (опционально)", + "serial_number_label": "С/Н", + "documentation_url_label": "Руководство", + "object_notes_label": "Заметки", + "sort_due_date": "Срок", + "sort_object": "Имя объекта", + "sort_type": "Тип", + "sort_task_name": "Имя задачи", + "all_objects": "Все объекты", + "tasks_lower": "задач", + "no_tasks_yet": "Пока нет задач", + "add_first_task": "Добавить первую задачу", + "last_performed_optional": "Последнее выполнение (опционально)", + "trigger_configuration": "Настройка триггера", + "entity_id": "ID сущности", + "comma_separated": "через запятую", + "entity_logic": "Логика сущностей", + "entity_logic_any": "Любая сущность срабатывает", + "entity_logic_all": "Все сущности должны сработать", + "entities": "сущности", + "attribute_optional": "Атрибут (опционально, пусто = состояние)", + "use_entity_state": "Использовать состояние сущности (без атрибута)", + "trigger_above": "Срабатывать выше", + "trigger_below": "Срабатывать ниже", + "for_at_least_minutes": "Не менее (минут)", + "safety_interval_days": "Интервал безопасности (дни, опционально)", + "safety_interval": "Интервал безопасности (опционально)", + "delta_mode": "Режим дельты", + "from_state_optional": "Из состояния (опционально)", + "to_state_optional": "В состояние (опционально)", + "documentation_url_optional": "URL документации (опционально)", + "object_notes_optional": "Заметки (опционально)", + "nfc_tag_id_optional": "ID NFC-метки (опционально)", + "nfc_tags_empty_help": "В Home Assistant ещё не зарегистрировано NFC-тегов.", + "nfc_tags_open_settings": "Открыть настройки тегов", + "nfc_tags_refresh": "Обновить", + "environmental_entity_optional": "Датчик окружающей среды (опционально)", + "environmental_entity_helper": "напр. sensor.outdoor_temperature — корректирует интервал в зависимости от условий", + "environmental_attribute_optional": "Атрибут среды (опционально)", + "nfc_tag_id": "ID NFC-метки", + "nfc_linked": "NFC-метка привязана", + "nfc_link_hint": "Нажмите, чтобы привязать NFC-метку", + "responsible_user": "Ответственный пользователь", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Не назначен)", + "all_users": "Все пользователи", + "my_tasks": "Мои задачи", + "tab_calendar": "Календарь", + "cal_no_events": "Без обслуживания", + "cal_window_7": "7 дней", + "cal_window_14": "14 дней", + "cal_window_30": "30 дней", + "cal_window_365": "1 год", + "cal_every_n_days": "каждые {n} дней", + "cal_source_time": "По времени", + "cal_source_time_adaptive": "По времени (адаптивно)", + "cal_source_sensor": "По датчику", + "cal_predicted": "прогноз", + "cal_confidence_high": "высокая надёжность", + "cal_confidence_medium": "средняя надёжность", + "cal_confidence_low": "низкая надёжность", + "budget_monthly": "Месячный бюджет", + "budget_yearly": "Годовой бюджет", + "groups": "Группы", + "new_group": "Новая группа", + "edit_group": "Редактировать группу", + "no_groups": "Групп пока нет", + "delete_group": "Удалить группу", + "delete_group_confirm": "Удалить группу '{name}'?", + "group_select_tasks": "Выбрать задачи", + "group_name_required": "Требуется имя", + "description_optional": "Описание (опционально)", + "selected": "Выбрано", + "loading_chart": "Загрузка данных графика...", + "hide_outliers": "Скрыть выбросы (сбои датчика)", + "was_maintenance_needed": "Требовалось ли это обслуживание?", + "feedback_needed": "Требовалось", + "feedback_not_needed": "Не требовалось", + "feedback_not_sure": "Не уверен", + "suggested_interval": "Рекомендуемый интервал", + "apply_suggestion": "Применить", + "reanalyze": "Повторный анализ", + "reanalyze_result": "Новый анализ", + "reanalyze_insufficient_data": "Недостаточно данных для рекомендации", + "data_points": "точек данных", + "dismiss_suggestion": "Отклонить", + "confidence_low": "Низкая", + "confidence_medium": "Средняя", + "confidence_high": "Высокая", + "recommended": "рекомендуется", + "seasonal_awareness": "Сезонная адаптация", + "edit_seasonal_overrides": "Редактировать сезонные коэффициенты", + "seasonal_overrides_title": "Сезонные коэффициенты (переопределение)", + "seasonal_overrides_hint": "Коэффициент на месяц (0.1–5.0). Пусто = автоматически.", + "seasonal_override_invalid": "Недопустимое значение", + "seasonal_override_range": "Коэффициент должен быть от 0.1 до 5.0", + "clear_all": "Очистить все", + "seasonal_chart_title": "Сезонные факторы", + "seasonal_learned": "Изученные", + "seasonal_manual": "Ручные", + "month_jan": "Янв", + "month_feb": "Фев", + "month_mar": "Мар", + "month_apr": "Апр", + "month_may": "Май", + "month_jun": "Июн", + "month_jul": "Июл", + "month_aug": "Авг", + "month_sep": "Сен", + "month_oct": "Окт", + "month_nov": "Ноя", + "month_dec": "Дек", + "sensor_prediction": "Предсказание по датчику", + "degradation_trend": "Тренд", + "trend_rising": "Растущий", + "trend_falling": "Падающий", + "trend_stable": "Стабильный", + "trend_insufficient_data": "Недостаточно данных", + "days_until_threshold": "Дней до порога", + "threshold_exceeded": "Порог превышен", + "environmental_adjustment": "Фактор среды", + "sensor_prediction_urgency": "Датчик предсказывает порог через ~{days} дней", + "day_short": "дн", + "weibull_reliability_curve": "Кривая надёжности", + "weibull_failure_probability": "Вероятность отказа", + "weibull_r_squared": "Качество аппроксимации R²", + "beta_early_failures": "Ранние отказы", + "beta_random_failures": "Случайные отказы", + "beta_wear_out": "Износ", + "beta_highly_predictable": "Высокая предсказуемость", + "confidence_interval": "Доверительный интервал", + "confidence_conservative": "Консервативный", + "confidence_aggressive": "Оптимистичный", + "current_interval_marker": "Текущий интервал", + "recommended_marker": "Рекомендуемый", + "characteristic_life": "Характеристический срок службы", + "chart_mini_sparkline": "Мини-график тренда", + "chart_history": "История стоимости и длительности", + "chart_seasonal": "Сезонные факторы, 12 месяцев", + "chart_weibull": "Кривая надёжности Вейбулла", + "chart_sparkline": "График значений триггера датчика", + "days_progress": "Прогресс по дням", + "qr_code": "QR-код", + "qr_generating": "Генерация QR-кода…", + "qr_error": "Не удалось сгенерировать QR-код.", + "qr_error_no_url": "URL HA не настроен. Установите внешний или внутренний URL в Настройках → Система → Сеть.", + "save_error": "Не удалось сохранить. Попробуйте ещё раз.", + "qr_print": "Печать", + "qr_download": "Скачать SVG", + "qr_action": "Действие при сканировании", + "qr_action_view": "Просмотр", + "qr_action_complete": "Отметить обслуживание как выполненное", + "qr_url_mode": "Тип ссылки", + "qr_mode_companion": "Приложение-компаньон", + "qr_mode_local": "Локальный (mDNS)", + "qr_mode_server": "URL сервера", + "overview": "Обзор", + "analysis": "Анализ", + "recent_activities": "Недавние действия", + "search_notes": "Поиск по заметкам", + "avg_cost": "Ср. стоимость", + "no_advanced_features": "Расширенные функции не включены", + "no_advanced_features_hint": "Включите «Адаптивные интервалы» или «Сезонные паттерны» в настройках интеграции, чтобы увидеть здесь аналитику.", + "analysis_not_enough_data": "Недостаточно данных для анализа.", + "analysis_not_enough_data_hint": "Для анализа Вейбулла требуется минимум 5 выполненных обслуживаний; сезонные паттерны становятся видны после 6+ точек данных в месяц.", + "analysis_manual_task_hint": "Ручные задачи без интервала не генерируют аналитику.", + "completions": "выполнений", + "current": "Текущий", + "shorter": "Короче", + "longer": "Длиннее", + "normal": "Нормальный", + "disabled": "Отключено", + "compound_logic": "Составная логика", + "compound": "Составной (несколько условий)", + "compound_logic_and": "И — должны сработать все условия", + "compound_logic_or": "ИЛИ — достаточно любого условия", + "compound_help": "Объедините несколько условий датчиков в один триггер.", + "compound_no_conditions": "Условий пока нет — добавьте хотя бы одно.", + "compound_add_condition": "Добавить условие", + "compound_condition": "Условие", + "compound_remove_condition": "Удалить условие", + "card_title": "Заголовок", + "card_show_header": "Показывать заголовок со статистикой", + "card_show_actions": "Показывать кнопки действий", + "card_compact": "Компактный режим", + "card_max_items": "Макс. элементов (0 = все)", + "card_filter_status": "Фильтровать по статусу", + "card_filter_status_help": "Пусто = показать все статусы.", + "card_filter_objects": "Фильтровать по объектам", + "card_filter_objects_help": "Пусто = показать все объекты.", + "card_filter_entities": "Фильтровать по сущностям (entity_ids)", + "card_filter_entities_help": "Выберите сущности sensor / binary_sensor из этой интеграции. Пусто = все.", + "card_loading_objects": "Загрузка объектов…", + "card_load_error": "Не удалось загрузить объекты — проверьте WebSocket-соединение.", + "card_no_tasks_title": "Пока нет задач обслуживания", + "card_no_tasks_cta": "→ Создайте в панели Maintenance", + "no_objects": "Пока нет объектов.", + "action_error": "Не удалось выполнить действие. Попробуйте ещё раз.", + "area_id_optional": "Зона (опционально)", + "installation_date_optional": "Дата установки (опционально)", + "warranty_expiry_optional": "Окончание гарантии (опционально)", + "warranty": "Гарантия", + "warranty_valid_until": "действует до {date}", + "warranty_expires_in": "истекает через {days} дн.", + "warranty_expired": "истекла", + "cal_past_windows": "Прошлые окна", + "cal_forward_windows": "Будущие окна", + "history_edit_title": "Редактировать запись истории", + "history_edit_timestamp": "Метка времени", + "manufacturer": "Производитель", + "model": "Модель", + "area": "Зона", + "actions": "Действия", + "view_mode_label": "Вид", + "view_cards": "Карточки", + "view_table": "Таблица", + "objects_table_columns_label": "Столбцы таблицы объектов", + "objects_table_columns_hint": "Выберите, какие столбцы показывать в табличном виде объектов.", + "custom_icon_optional": "Иконка (опционально, например mdi:wrench)", + "task_enabled": "Задача включена", + "skip_reason_prompt": "Пропустить эту задачу?", + "reason_optional": "Причина (опционально)", + "reset_date_prompt": "Отметить задачу как выполненную?", + "reset_date_optional": "Дата последнего выполнения (опционально, по умолчанию: сегодня)", + "notes_label": "Примечания", + "documentation_label": "Документация", + "no_nfc_tag": "— Нет метки —", + "dashboard": "Панель", + "tab_today": "Сегодня", + "palette_placeholder": "Поиск объектов и задач…", + "palette_no_results": "Нет совпадений", + "palette_hint": "↑↓ выбор · Enter открыть · Esc закрыть", + "today_all_caught_up": "Всё сделано! На этой неделе ничего нет.", + "today_overdue": "Просрочено", + "today_due_today": "Сегодня", + "today_this_week": "На этой неделе", + "settings": "Настройки", + "settings_features": "Расширенные функции", + "settings_features_desc": "Включите или отключите расширенные функции. Отключение скрывает их из интерфейса, но не удаляет данные.", + "feat_adaptive": "Адаптивное планирование", + "feat_adaptive_desc": "Изучать оптимальные интервалы из истории обслуживания", + "feat_predictions": "Предсказания по датчикам", + "feat_predictions_desc": "Предсказывать даты срабатывания по деградации датчика", + "feat_seasonal": "Сезонные корректировки", + "feat_seasonal_desc": "Корректировать интервалы на основе сезонных паттернов", + "feat_environmental": "Экологическая корреляция", + "feat_environmental_desc": "Связывать интервалы с температурой/влажностью", + "feat_budget": "Отслеживание бюджета", + "feat_budget_desc": "Отслеживать месячные и годовые расходы на обслуживание", + "feat_groups": "Группы задач", + "feat_groups_desc": "Организовывать задачи в логические группы", + "feat_checklists": "Контрольные списки", + "feat_checklists_desc": "Многошаговые процедуры для выполнения задачи", + "settings_general": "Основные", + "settings_default_warning": "Дни предупреждения по умолчанию", + "settings_panel_enabled": "Боковая панель", + "settings_panel_title": "Заголовок панели", + "settings_notifications": "Уведомления", + "settings_notify_service": "Сервис уведомлений", + "test_notification": "Тестовое уведомление", + "send_test": "Отправить тест", + "testing": "Отправка…", + "test_notification_success": "Тестовое уведомление отправлено", + "test_notification_failed": "Не удалось отправить тестовое уведомление", + "settings_notify_due_soon": "Уведомлять, когда срок скоро истекает", + "settings_notify_overdue": "Уведомлять при просрочке", + "settings_notify_triggered": "Уведомлять при срабатывании", + "settings_interval_hours": "Интервал повторения (часы, 0 = один раз)", + "settings_quiet_hours": "Часы тишины", + "settings_quiet_start": "Начало", + "settings_quiet_end": "Конец", + "settings_max_per_day": "Макс. уведомлений в день (0 = без ограничений)", + "settings_bundling": "Группировать уведомления", + "settings_bundle_threshold": "Порог группировки", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Кнопки действий в мобильном приложении", + "settings_action_complete": "Показывать кнопку «Выполнить»", + "settings_action_skip": "Показывать кнопку «Пропустить»", + "settings_action_snooze": "Показывать кнопку «Отложить»", + "settings_weekly_digest": "Еженедельная сводка", + "settings_weekly_digest_hint": "Одно сводное уведомление в понедельник утром, когда есть задачи.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Длительность откладывания (часы)", + "settings_budget": "Бюджет", + "settings_currency": "Валюта", + "settings_budget_monthly": "Месячный бюджет", + "settings_budget_yearly": "Годовой бюджет", + "settings_budget_alerts": "Оповещения о бюджете", + "settings_budget_threshold": "Порог оповещения (%)", + "settings_import_export": "Импорт / Экспорт", + "settings_export_json": "Экспорт JSON", + "settings_export_yaml": "Экспорт YAML", + "settings_export_csv": "Экспорт CSV", + "settings_import_csv": "Импорт CSV", + "settings_import_placeholder": "Вставьте содержимое JSON или CSV здесь…", + "settings_import_btn": "Импортировать", + "settings_import_success": "Импортировано объектов: {count}.", + "settings_export_success": "Экспорт загружен.", + "settings_saved": "Настройка сохранена.", + "settings_include_history": "Включать историю", + "sort_alphabetical": "По алфавиту", + "sort_due_soonest": "Ближайший срок", + "sort_task_count": "Количество задач", + "sort_area": "Область", + "sort_assigned_user": "Назначенный пользователь", + "sort_group": "Группа", + "groupby_none": "Без группировки", + "groupby_area": "По области", + "groupby_group": "По группе", + "groupby_user": "По пользователю", + "filter_label": "Фильтр", + "user_label": "Пользователь", + "sort_label": "Сортировка", + "group_by_label": "Группировать по", + "state_value_help": "Используйте значение состояния HA (обычно в нижнем регистре, напр. \"on\"/\"off\"). Регистр нормализуется при сохранении.", + "target_changes_help": "Количество совпадающих переходов, после которых срабатывает триггер (по умолчанию: 1).", + "qr_print_title": "Печать QR-кодов", + "qr_print_desc": "Создай страницу для печати с QR-кодами для вырезания и наклеивания на оборудование.", + "qr_print_load": "Загрузить объекты", + "qr_print_filter": "Фильтр", + "qr_print_objects": "Объекты", + "qr_print_actions": "Действия", + "qr_print_url_mode": "Тип ссылки", + "qr_print_estimate": "Оценка QR-кодов", + "qr_print_over_limit": "лимит 200, сузь фильтр", + "qr_print_generate": "Создать QR-коды", + "qr_print_generating": "Создание…", + "qr_print_ready": "QR-коды готовы", + "qr_print_print_button": "Печать", + "qr_print_empty": "Нечего создавать", + "qr_action_skip": "Пропустить", + "vacation_title": "Режим отпуска", + "vacation_active": "активен", + "vacation_ended": "завершён", + "vacation_desc": "Запланируй отпуск: уведомления приостанавливаются на период плюс несколько буферных дней. Можно задать исключения по задачам.", + "vacation_enable": "Включить режим отпуска", + "vacation_start": "Начало", + "vacation_end": "Конец", + "vacation_buffer": "Буфер (дней)", + "vacation_exempt_title": "Уведомлять несмотря на отпуск", + "vacation_exempt_desc": "Выбери задачи, по которым нужно уведомлять и в отпуске (например, критичная химия бассейна).", + "vacation_load_tasks": "Загрузить задачи", + "vacation_preview_btn": "Показать предпросмотр", + "vacation_preview_affected": "задач затронуто", + "vacation_event_due_soon": "скоро наступит срок", + "vacation_event_overdue": "станет просроченной", + "vacation_event_triggered_est": "возможно срабатывание сенсора", + "vacation_sensor_based": "(сенсорная)", + "vacation_action_notify": "Всё равно уведомлять", + "vacation_action_unsilence": "Снова заглушить", + "vacation_marked_complete": "Отмечено как выполнено", + "vacation_marked_skip": "Пропущено", + "vacation_end_now": "Завершить отпуск сейчас", + "unassigned": "Не назначено", + "no_area": "Без области", + "has_overdue": "Просроченные задачи", + "object": "Объект", + "settings_panel_access": "Доступ к панели", + "settings_panel_access_desc": "Администраторы всегда имеют полный доступ. Чтобы делегировать создание, редактирование и удаление определённым не-админ пользователям, включите это и выберите их ниже — остальные видят только Выполнить и Пропустить.", + "settings_operator_write": "Разрешить выбранным пользователям создавать, редактировать и удалять", + "settings_operator_write_desc": "Выключено: только администраторы могут изменять содержимое. Включено: выбранные пользователи ниже также получают полный доступ.", + "no_non_admin_users": "Не найдено не-админ пользователей. Добавьте их в Настройках → Люди.", + "owner_label": "Владелец", + "feat_completion_actions": "Действия при завершении", + "feat_completion_actions_desc": "HA-действие по задаче при завершении + QR быстрого завершения с предустановленными значениями.", + "on_complete_action_title": "При завершении: запустить HA-действие (опционально)", + "on_complete_action_desc": "Вызывает HA-сервис при завершении задачи — напр., сбросить счётчик на устройстве.", + "on_complete_action_service": "Сервис", + "on_complete_action_target": "Целевая сущность", + "on_complete_action_data": "Данные (JSON, опционально)", + "on_complete_action_test": "Тестировать действие", + "on_complete_action_test_success": "Успешно", + "on_complete_action_test_failed": "Ошибка", + "quick_complete_defaults_title": "Значения по умолчанию для быстрого завершения (для QR-сканов, опционально)", + "quick_complete_defaults_desc": "Предустановленные значения для QR быстрого завершения. Без них QR открывает диалог.", + "quick_complete_defaults_notes": "Заметки", + "quick_complete_defaults_cost": "Стоимость", + "quick_complete_defaults_duration": "Длительность (минут)", + "quick_complete_defaults_feedback_none": "Без отзыва", + "quick_complete_defaults_feedback_needed": "Было необходимо", + "quick_complete_defaults_feedback_not_needed": "Не было необходимо", + "quick_complete_success": "Быстро отмечено как выполнено", + "trigger_replaced": "Триггер заменён", + "add": "Добавить", + "show_stats": "Показать статистику + графики", + "hide_stats": "Скрыть статистику", + "adaptive_no_data": "Пока недостаточно истории выполнений для адаптивного анализа. Выполните эту задачу ещё несколько раз, чтобы разблокировать рекомендации по интервалу и графики надёжности.", + "suggestion_applied": "Предлагаемый интервал применён", + "vacation_mode": "Режим отпуска", + "vacation_status_active": "Активен сейчас", + "vacation_status_scheduled": "Запланирован", + "vacation_status_inactive": "Неактивен", + "vacation_end_now_confirm": "Завершить отпуск немедленно?", + "vacation_exempt_count": "исключено", + "vacation_advanced": "Дополнительно…", + "vacation_open_panel": "Открыть на панели", + "enable": "Включить", + "saved": "Сохранено", + "budget_monthly_set": "Задать месячный", + "budget_yearly_set": "Задать годовой", + "budget_advanced": "Валюта, оповещения…", + "budget_open_panel": "Открыть на панели", + "groups_empty": "Пока нет групп.", + "group_new_placeholder": "Добавить группу…", + "group_delete_confirm": "Удалить группу «{name}»?", + "groups_manage_tasks": "Управление назначениями задач…", + "groups_open_panel": "Открыть на панели", + "on_complete_action_target_hint": "Примечание: домен сущности должен соответствовать сервису — напр. 'button.press' работает только с button.*, 'counter.increment' только с counter.*, 'input_button.press' только с input_button.* и т. д. При несоответствии действие молча не выполнится (HA запишет в журнал 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Показать все объекты", + "show_all_tasks": "Сбросить фильтр — показать все задачи", + "filter_to_overdue": "Фильтровать список только по просроченным", + "filter_to_due_soon": "Фильтровать список только по скоро наступающим", + "filter_to_triggered": "Фильтровать список только по сработавшим", + "open_task": "Открыть задачу", + "show_details": "Показать историю + статистику", + "hide_details": "Скрыть детали", + "history_empty": "Пока нет истории.", + "history_edit_button": "Изменить запись", + "total_cost": "Общая стоимость", + "times_performed": "Выполнено", + "older_entries": "ранее", + "open_in_panel": "Открыть на панели обслуживания", + "skip_reason": "Причина пропуска (необязательно)", + "reset_to_date": "Сбросить последнее выполнение на", + "delete_task_confirm": "Удалить эту задачу и её историю?", + "delete_object_confirm": "Удалить этот объект и все его задачи?", + "loading": "Загрузка…", + "archive": "Архивировать", + "undo": "Отменить", + "task_archived": "Задача архивирована", + "object_archived": "Объект архивирован", + "unarchive": "Разархивировать", + "archived": "В архиве", + "show_archived": "Показать архив", + "hide_archived": "Скрыть архив", + "confirm_archive_object": "Архивировать этот объект и его задачи? История сохранится, позже можно восстановить.", + "settings_archive": "Архив и хранение", + "settings_archive_desc": "Убирайте выполненные разовые задачи в архив, не удаляя их. Архивные элементы скрыты и неактивны, но сохраняют историю и расходы.", + "settings_archive_oneoff_days": "Автоархивация выполненных разовых задач через (дней, 0 = выкл)", + "settings_delete_archived_oneoff_days": "Автоудаление архивных разовых задач через (дней, 0 = никогда)", + "archive_object": "Архивировать объект", + "unarchive_object": "Разархивировать объект", + "documents": "Документы", + "documents_empty": "Пока нет документов.", + "doc_upload": "Загрузить файл", + "doc_uploading": "Загрузка…", + "doc_add_link": "Добавить ссылку", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Название (необязательно)", + "doc_open": "Открыть", + "doc_delete_confirm": "Удалить «{name}»?", + "doc_too_large": "Файл слишком большой (макс. 25 МБ).", + "doc_upload_failed": "Не удалось загрузить.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Уже сохранён в другом месте — общий, без доп. места.", + "doc_dup_in_object": "Этот файл уже прикреплён к этому объекту.", + "doc_link_invalid": "Разрешены только ссылки http/https.", + "doc_cat_manual": "Инструкция", + "doc_cat_warranty": "Гарантия", + "doc_cat_invoice": "Счёт", + "doc_cat_spare_parts": "Запчасти", + "doc_cat_photo": "Фото", + "doc_cat_other": "Другое", + "doc_link_badge": "Ссылка", + "doc_storage_title": "Хранилище документов", + "doc_storage_saved": "Сэкономлено дедупликацией", + "doc_storage_refresh": "Обновить", + "doc_download": "Скачать", + "doc_close": "Закрыть", + "doc_camera": "Сделать фото", + "doc_drop_hint": "Перетащите файлы сюда", + "doc_task_none": "Нет документов, связанных с этой задачей.", + "doc_link_existing": "Связать документ…", + "doc_attach": "Связать", + "doc_unlink": "Отвязать", + "doc_page": "Страница", + "chart_range_7d": "7д", + "chart_range_30d": "30д", + "chart_range_90d": "90д", + "chart_range_1y": "1г", + "chart_since_service": "с последнего обслуживания", + "chart_no_stats": "Нет долгосрочной статистики для этой сущности — показаны только значения из записей обслуживания", + "auto_complete_on_recovery": "Автозавершение при восстановлении датчика", + "auto_complete_on_recovery_help": "Записывает выполнение (обновляет дату последнего обслуживания), когда триггер устраняется сам — например, соль досыпана, фильтр заменён.", + "doc_search": "Поиск документов…", + "doc_search_none": "Нет подходящих документов", + "link_device_optional": "Привязать к существующему устройству (необязательно)", + "parent_object_optional": "Родительский объект (необязательно)", + "parent_none": "(Без родителя)", + "paused": "Приостановлено", + "pause_object": "Приостановить", + "resume_object": "Возобновить", + "pause_until_prompt": "Заморозить расписания этого объекта — ничего не наступает и не уведомляет до возобновления. При желании укажите дату автоматического возобновления.", + "pause_until_label": "Возобновить (необязательно)", + "object_paused": "Объект приостановлен", + "object_resumed": "Объект возобновлён — расписания перезапущены", + "object_paused_badge": "Приостановлено", + "paused_until_label": "до", + "replace_object": "Заменить…", + "replace_object_prompt": "Списать этот объект и создать преемника. История и затраты остаются в архиве старого; задачи и документы переходят к новому, счётчики начинаются заново.", + "replace_name_label": "Имя преемника", + "object_replaced": "Объект заменён — преемник создан", + "reading_unit_label": "Единица показания (напр. кВт·ч, м³)", + "reading_unit_help": "Отображается рядом с записанным значением при выполнении этой задачи.", + "reading_value_label": "Показание", + "reading_label": "Показание", + "settings_templates_label": "Галерея шаблонов", + "settings_templates_hint": "Снимите отметку с шаблонов, которые вам не нужны — они исчезнут из выбора «Из шаблона» (панель и config flow). Больше ничего не меняется; можно включить обратно в любой момент.", + "worksheet": "Рабочий лист", + "worksheet_scan_view": "Сканируйте, чтобы открыть задачу", + "worksheet_scan_complete": "Сканируйте для выполнения", + "worksheet_manual_excerpt": "Выдержка из руководства", + "worksheet_pages": "страницы", + "worksheet_printed": "Напечатано", + "worksheet_never": "Никогда" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/sv.json b/custom_components/maintenance_supporter/frontend-src/locales/sv.json new file mode 100644 index 0000000..6e88345 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/sv.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Underhåll", + "objects": "Objekt", + "tasks": "Uppgifter", + "overdue": "Försenad", + "due_soon": "Snart", + "triggered": "Utlöst", + "ok": "OK", + "all": "Alla", + "new_object": "+ Nytt objekt", + "templates_from": "Från mall", + "templates_title": "Börja från en mall", + "templates_task_count": "{n} uppgifter", + "template_created": "Skapad från mall", + "onboard_hint": "Lägg till ditt första objekt för att spåra underhåll.", + "edit": "Redigera", + "duplicate": "Duplicera", + "task_duplicated": "Uppgift duplicerad", + "object_duplicated": "Objekt duplicerat", + "delete": "Ta bort", + "add_task": "+ Lägg till uppgift", + "complete": "Slutför", + "completed": "Slutförd", + "skip": "Hoppa över", + "skipped": "Hoppade över", + "missed": "Missed", + "reset": "Återställ", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Avbryt", + "bulk_select": "Välj", + "bulk_select_all": "Välj alla", + "bulk_n_selected": "{n} valda", + "bulk_completed": "{n} uppgifter slutförda", + "bulk_archived": "{n} uppgifter arkiverade", + "completing": "Slutför…", + "interval": "Intervall", + "warning": "Varning", + "last_performed": "Senast utförd", + "next_due": "Nästa förfallodatum", + "days_until_due": "Dagar till förfallodatum", + "avg_duration": "Snittlig varaktighet", + "trigger": "Utlösare", + "trigger_type": "Utlösartyp", + "threshold_above": "Övre gräns", + "threshold_below": "Undre gräns", + "threshold": "Tröskel", + "counter": "Räknare", + "state_change": "Tillståndsändring", + "runtime": "Körtid", + "runtime_hours": "Måltid (timmar)", + "target_value": "Målvärde", + "baseline": "Baslinje", + "target_changes": "Antal målförändringar", + "for_minutes": "Under (minuter)", + "time_based": "Tidsbaserad", + "sensor_based": "Sensorbaserad", + "manual": "Manuell", + "one_time": "Engångs", + "weekdays": "Veckodagar", + "nth_weekday": "N:te veckodag i månaden", + "day_of_month": "Dag i månaden", + "recurrence_on_days": "Upprepa på", + "recurrence_occurrence": "Förekomst", + "recurrence_weekday": "Veckodag", + "recurrence_day": "Dag i månaden (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1:a", + "ord_2": "2:a", + "ord_3": "3:e", + "ord_4": "4:e", + "ord_5": "5:e", + "ord_last": "Sista", + "day_word": "Dag", + "interval_value": "Intervall", + "interval_unit": "Enhet", + "unit_days": "Dagar", + "unit_weeks": "Veckor", + "unit_months": "Månader", + "unit_years": "År", + "due_date": "Förfallodatum", + "cleaning": "Rengöring", + "inspection": "Inspektion", + "replacement": "Byte", + "calibration": "Kalibrering", + "service": "Service", + "reading": "Avläsning", + "custom": "Anpassad", + "history": "Historik", + "cost": "Kostnad", + "report_button": "Rapport", + "report_title": "Underhållsrapport", + "report_generated": "Genererad", + "report_times_done": "Utfört", + "report_total_cost": "Total kostnad", + "report_every": "var {n}:e {unit}", + "report_notes": "Anteckningar", + "report_col_type": "Typ", + "report_col_status": "Status", + "report_col_schedule": "Schema", + "duration": "Varaktighet", + "both": "Båda", + "trigger_val": "Utlösarvärde", + "complete_title": "Slutför: ", + "checklist": "Checklista", + "checklist_steps_optional": "Checkliststeg (valfritt)", + "checklist_placeholder": "Rengör filter\nByt tätning\nTesta tryck", + "checklist_help": "Ett steg per rad. Max 100 objekt.", + "err_too_long": "{field}: för lång (max {n} tecken)", + "err_too_short": "{field}: för kort (min {n} tecken)", + "err_value_too_high": "{field}: för stor (max {n})", + "err_value_too_low": "{field}: för liten (min {n})", + "err_required": "{field}: krävs", + "err_wrong_type": "{field}: fel typ (förväntad: {type})", + "err_invalid_choice": "{field}: ej tillåtet värde", + "err_invalid_value": "{field}: ogiltigt värde", + "feat_schedule_time": "Schemaläggning per tid på dygnet", + "feat_schedule_time_desc": "Uppgifter blir försenade vid en specifik tid på dygnet istället för midnatt.", + "schedule_time_optional": "Förfaller kl. (valfritt, HH:MM)", + "schedule_time_help": "Tomt = midnatt (standard). HA-tidszon.", + "at_time": "kl.", + "notes_optional": "Anteckningar (valfritt)", + "cost_optional": "Kostnad (valfritt)", + "duration_minutes": "Varaktighet i minuter (valfritt)", + "days": "dagar", + "day": "dag", + "today": "Idag", + "d_overdue": "d försenad", + "no_tasks": "Inga underhållsuppgifter ännu. Skapa ett objekt för att komma igång.", + "no_tasks_short": "Inga uppgifter", + "no_history": "Inga historikposter ännu.", + "show_all": "Visa alla", + "cost_duration_chart": "Kostnad och varaktighet", + "installed": "Installerad", + "confirm_delete_object": "Ta bort detta objekt och alla dess uppgifter?", + "confirm_delete_task": "Ta bort denna uppgift?", + "min": "Min", + "max": "Max", + "save": "Spara", + "saving": "Sparar…", + "edit_task": "Redigera uppgift", + "new_task": "Ny underhållsuppgift", + "task_name": "Uppgiftsnamn", + "maintenance_type": "Underhållstyp", + "priority": "Prioritet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Låg", + "priority_normal": "Normal", + "priority_high": "Hög", + "schedule_type": "Schematyp", + "interval_days": "Intervall (dagar)", + "warning_days": "Varningsdagar", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Senast utförd (valfritt)", + "interval_anchor": "Intervallankare", + "anchor_completion": "Från slutförandedatum", + "anchor_planned": "Från planerat datum (ingen drift)", + "edit_object": "Redigera objekt", + "name": "Namn", + "manufacturer_optional": "Tillverkare (valfritt)", + "model_optional": "Modell (valfritt)", + "serial_number_optional": "Serienummer (valfritt)", + "serial_number_label": "S/N", + "documentation_url_label": "Manual", + "object_notes_label": "Anteckningar", + "sort_due_date": "Förfallodatum", + "sort_object": "Objektnamn", + "sort_type": "Typ", + "sort_task_name": "Uppgiftsnamn", + "all_objects": "Alla objekt", + "tasks_lower": "uppgifter", + "no_tasks_yet": "Inga uppgifter ännu", + "add_first_task": "Lägg till första uppgift", + "trigger_configuration": "Utlösarkonfiguration", + "entity_id": "Entitets-ID", + "comma_separated": "kommaseparerad", + "entity_logic": "Entitetslogik", + "entity_logic_any": "Vilken entitet som helst utlöser", + "entity_logic_all": "Alla entiteter måste utlösa", + "entities": "entiteter", + "attribute_optional": "Attribut (valfritt, tomt = tillstånd)", + "use_entity_state": "Använd entitetstillstånd (inget attribut)", + "trigger_above": "Utlös över", + "trigger_below": "Utlös under", + "for_at_least_minutes": "Under minst (minuter)", + "safety_interval_days": "Säkerhetsintervall (dagar, valfritt)", + "safety_interval": "Säkerhetsintervall (valfritt)", + "delta_mode": "Delta-läge", + "from_state_optional": "Från tillstånd (valfritt)", + "to_state_optional": "Till tillstånd (valfritt)", + "documentation_url_optional": "Dokumentations-URL (valfritt)", + "object_notes_optional": "Anteckningar (valfritt)", + "nfc_tag_id_optional": "NFC-tagg-ID (valfritt)", + "nfc_tags_empty_help": "Inga NFC-taggar registrerade i Home Assistant än.", + "nfc_tags_open_settings": "Öppna tag-inställningar", + "nfc_tags_refresh": "Uppdatera", + "environmental_entity_optional": "Miljösensor (valfritt)", + "environmental_entity_helper": "t.ex. sensor.outdoor_temperature — justerar intervallet baserat på miljöförhållanden", + "environmental_attribute_optional": "Miljöattribut (valfritt)", + "nfc_tag_id": "NFC-tagg-ID", + "nfc_linked": "NFC-tagg länkad", + "nfc_link_hint": "Klicka för att länka NFC-tagg", + "responsible_user": "Ansvarig användare", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ingen användare tilldelad)", + "all_users": "Alla användare", + "my_tasks": "Mina uppgifter", + "tab_calendar": "Kalender", + "cal_no_events": "Inget underhåll", + "cal_window_7": "7 dagar", + "cal_window_14": "14 dagar", + "cal_window_30": "30 dagar", + "cal_window_365": "1 år", + "cal_every_n_days": "var {n} dag", + "cal_source_time": "Tidsbaserad", + "cal_source_time_adaptive": "Tidsbaserad (adaptiv)", + "cal_source_sensor": "Sensorbaserad", + "cal_predicted": "förutsagt", + "cal_confidence_high": "hög säkerhet", + "cal_confidence_medium": "medelhög säkerhet", + "cal_confidence_low": "låg säkerhet", + "budget_monthly": "Månatlig budget", + "budget_yearly": "Årlig budget", + "groups": "Grupper", + "new_group": "Ny grupp", + "edit_group": "Redigera grupp", + "no_groups": "Inga grupper ännu", + "delete_group": "Ta bort grupp", + "delete_group_confirm": "Ta bort grupp '{name}'?", + "group_select_tasks": "Välj uppgifter", + "group_name_required": "Namn krävs", + "description_optional": "Beskrivning (valfritt)", + "selected": "Valda", + "loading_chart": "Laddar diagramdata...", + "hide_outliers": "Dölj avvikare (sensorfel)", + "was_maintenance_needed": "Behövdes detta underhåll?", + "feedback_needed": "Behövdes", + "feedback_not_needed": "Behövdes inte", + "feedback_not_sure": "Osäker", + "suggested_interval": "Föreslaget intervall", + "apply_suggestion": "Tillämpa", + "reanalyze": "Analysera igen", + "reanalyze_result": "Ny analys", + "reanalyze_insufficient_data": "Otillräckligt med data för rekommendation", + "data_points": "datapunkter", + "dismiss_suggestion": "Avvisa", + "confidence_low": "Låg", + "confidence_medium": "Medel", + "confidence_high": "Hög", + "recommended": "rekommenderad", + "seasonal_awareness": "Säsongsmedvetenhet", + "edit_seasonal_overrides": "Redigera säsongsfaktorer", + "seasonal_overrides_title": "Säsongsfaktorer (åsidosätt)", + "seasonal_overrides_hint": "Faktor per månad (0.1–5.0). Tomt = lärt automatiskt.", + "seasonal_override_invalid": "Ogiltigt värde", + "seasonal_override_range": "Faktor måste vara mellan 0.1 och 5.0", + "clear_all": "Rensa alla", + "seasonal_chart_title": "Säsongsfaktorer", + "seasonal_learned": "Lärt", + "seasonal_manual": "Manuell", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Maj", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensorprediktion", + "degradation_trend": "Trend", + "trend_rising": "Stigande", + "trend_falling": "Fallande", + "trend_stable": "Stabil", + "trend_insufficient_data": "Otillräcklig data", + "days_until_threshold": "Dagar till tröskel", + "threshold_exceeded": "Tröskel överskriden", + "environmental_adjustment": "Miljöfaktor", + "sensor_prediction_urgency": "Sensor förutsäger tröskel om ~{days} dagar", + "day_short": "d", + "weibull_reliability_curve": "Tillförlitlighetskurva", + "weibull_failure_probability": "Felsannolikhet", + "weibull_r_squared": "Anpassning R²", + "beta_early_failures": "Tidiga fel", + "beta_random_failures": "Slumpmässiga fel", + "beta_wear_out": "Slitage", + "beta_highly_predictable": "Mycket förutsägbar", + "confidence_interval": "Konfidensintervall", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisk", + "current_interval_marker": "Aktuellt intervall", + "recommended_marker": "Rekommenderat", + "characteristic_life": "Karakteristisk livslängd", + "chart_mini_sparkline": "Trenddiagram", + "chart_history": "Kostnads- och varaktighetshistorik", + "chart_seasonal": "Säsongsfaktorer, 12 månader", + "chart_weibull": "Weibull tillförlitlighetskurva", + "chart_sparkline": "Sensorutlösarvärdesdiagram", + "days_progress": "Dagsförlopp", + "qr_code": "QR-kod", + "qr_generating": "Genererar QR-kod…", + "qr_error": "Kunde inte generera QR-kod.", + "qr_error_no_url": "Ingen HA-URL konfigurerad. Ange en extern eller intern URL i Inställningar → System → Nätverk.", + "save_error": "Kunde inte spara. Försök igen.", + "qr_print": "Skriv ut", + "qr_download": "Ladda ner SVG", + "qr_action": "Åtgärd vid skanning", + "qr_action_view": "Visa underhållsinformation", + "qr_action_complete": "Markera underhåll som slutfört", + "qr_url_mode": "Länktyp", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Översikt", + "analysis": "Analys", + "recent_activities": "Senaste aktiviteter", + "search_notes": "Sök i anteckningar", + "avg_cost": "Snittlig kostnad", + "no_advanced_features": "Inga avancerade funktioner aktiverade", + "no_advanced_features_hint": "Aktivera „Adaptiva intervall” eller „Säsongsmönster” i integrationsinställningar för att se analysdata här.", + "analysis_not_enough_data": "Inte tillräckligt med data för analys ännu.", + "analysis_not_enough_data_hint": "Weibull-analys kräver minst 5 slutförda underhåll; säsongsmönster blir synliga efter 6+ datapunkter per månad.", + "analysis_manual_task_hint": "Manuella uppgifter utan intervall genererar inte analysdata.", + "completions": "slutföranden", + "current": "Aktuell", + "shorter": "Kortare", + "longer": "Längre", + "normal": "Normal", + "disabled": "Inaktiverad", + "compound_logic": "Sammansatt logik", + "compound": "Sammansatt (flera villkor)", + "compound_logic_and": "OCH — alla villkor måste utlösas", + "compound_logic_or": "ELLER — ett villkor räcker", + "compound_help": "Kombinera flera sensorvillkor till en utlösare.", + "compound_no_conditions": "Inga villkor än — lägg till minst ett.", + "compound_add_condition": "Lägg till villkor", + "compound_condition": "Villkor", + "compound_remove_condition": "Ta bort villkor", + "card_title": "Titel", + "card_show_header": "Visa rubrik med statistik", + "card_show_actions": "Visa åtgärdsknappar", + "card_compact": "Kompakt läge", + "card_max_items": "Max objekt (0 = alla)", + "card_filter_status": "Filtrera efter status", + "card_filter_status_help": "Tomt = visa alla statusar.", + "card_filter_objects": "Filtrera efter objekt", + "card_filter_objects_help": "Tomt = visa alla objekt.", + "card_filter_entities": "Filtrera efter entiteter (entity_ids)", + "card_filter_entities_help": "Välj sensor- / binary_sensor-entiteter från denna integration. Tomt = alla.", + "card_loading_objects": "Laddar objekt…", + "card_load_error": "Kunde inte ladda objekt — kontrollera WebSocket-anslutningen.", + "card_no_tasks_title": "Inga underhållsuppgifter än", + "card_no_tasks_cta": "→ Skapa en i Maintenance-panelen", + "no_objects": "Inga objekt än.", + "action_error": "Åtgärden misslyckades. Försök igen.", + "area_id_optional": "Område (valfritt)", + "installation_date_optional": "Installationsdatum (valfritt)", + "warranty_expiry_optional": "Garantins utgång (valfritt)", + "warranty": "Garanti", + "warranty_valid_until": "giltig till {date}", + "warranty_expires_in": "går ut om {days} dagar", + "warranty_expired": "utgången", + "cal_past_windows": "Tidigare fönster", + "cal_forward_windows": "Kommande fönster", + "history_edit_title": "Redigera historikpost", + "history_edit_timestamp": "Tidsstämpel", + "manufacturer": "Tillverkare", + "model": "Modell", + "area": "Område", + "actions": "Åtgärder", + "view_mode_label": "Vy", + "view_cards": "Kortvy", + "view_table": "Tabellvy", + "objects_table_columns_label": "Kolumner i objekttabell", + "objects_table_columns_hint": "Välj vilka kolumner som visas i objekttabellvyn.", + "custom_icon_optional": "Ikon (valfritt, t.ex. mdi:wrench)", + "task_enabled": "Uppgift aktiverad", + "skip_reason_prompt": "Hoppa över denna uppgift?", + "reason_optional": "Anledning (valfritt)", + "reset_date_prompt": "Markera uppgift som utförd?", + "reset_date_optional": "Datum för senaste utförande (valfritt, standard idag)", + "notes_label": "Anteckningar", + "documentation_label": "Dokumentation", + "no_nfc_tag": "— Ingen tagg —", + "dashboard": "Översikt", + "tab_today": "Idag", + "palette_placeholder": "Sök objekt och uppgifter…", + "palette_no_results": "Inga träffar", + "palette_hint": "↑↓ navigera · Enter öppna · Esc stäng", + "today_all_caught_up": "Allt klart! Inget den här veckan.", + "today_overdue": "Försenade", + "today_due_today": "Förfaller idag", + "today_this_week": "Den här veckan", + "settings": "Inställningar", + "settings_features": "Avancerade funktioner", + "settings_features_desc": "Aktivera eller inaktivera avancerade funktioner. Inaktivering döljer dem från UI men tar inte bort data.", + "feat_adaptive": "Adaptiv schemaläggning", + "feat_adaptive_desc": "Lär dig optimala intervall från underhållshistorik", + "feat_predictions": "Sensorpredictions", + "feat_predictions_desc": "Förutsäg utlösningsdatum från sensordegradering", + "feat_seasonal": "Säsongsjusteringar", + "feat_seasonal_desc": "Justera intervall baserat på säsongsmönster", + "feat_environmental": "Miljökorrelation", + "feat_environmental_desc": "Korrelera intervall med temperatur/luftfuktighet", + "feat_budget": "Budgetuppföljning", + "feat_budget_desc": "Spåra månatliga och årliga underhållsutgifter", + "feat_groups": "Uppgiftsgrupper", + "feat_groups_desc": "Organisera uppgifter i logiska grupper", + "feat_checklists": "Checklistor", + "feat_checklists_desc": "Flerstegs procedurer för uppgiftens slutförande", + "settings_general": "Allmänt", + "settings_default_warning": "Standard varningsdagar", + "settings_panel_enabled": "Sidopanel", + "settings_panel_title": "Sidopanelens titel", + "settings_notifications": "Notifikationer", + "settings_notify_service": "Notifikationstjänst", + "test_notification": "Testnotifikation", + "send_test": "Skicka test", + "testing": "Skickar…", + "test_notification_success": "Testnotifikation skickad", + "test_notification_failed": "Testnotifikation misslyckades", + "settings_notify_due_soon": "Notifiera när snart förfallande", + "settings_notify_overdue": "Notifiera när försenad", + "settings_notify_triggered": "Notifiera när utlöst", + "settings_interval_hours": "Upprepningsintervall (timmar, 0 = en gång)", + "settings_quiet_hours": "Tysta timmar", + "settings_quiet_start": "Start", + "settings_quiet_end": "Slut", + "settings_max_per_day": "Max notifikationer per dag (0 = obegränsat)", + "settings_bundling": "Bunta notifikationer", + "settings_bundle_threshold": "Buntningströskel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobila åtgärdsknappar", + "settings_action_complete": "Visa 'Slutför'-knapp", + "settings_action_skip": "Visa 'Hoppa över'-knapp", + "settings_action_snooze": "Visa 'Snooza'-knapp", + "settings_weekly_digest": "Veckosammanfattning", + "settings_weekly_digest_hint": "En sammanfattningsavisering måndag morgon när uppgifter förfaller.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Snooza-tid (timmar)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Månatlig budget", + "settings_budget_yearly": "Årlig budget", + "settings_budget_alerts": "Budgetvarningar", + "settings_budget_threshold": "Varningströskel (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Exportera JSON", + "settings_export_yaml": "Exportera YAML", + "settings_export_csv": "Exportera CSV", + "settings_import_csv": "Importera CSV", + "settings_import_placeholder": "Klistra in JSON- eller CSV-innehåll här…", + "settings_import_btn": "Importera", + "settings_import_success": "{count} objekt importerade.", + "settings_export_success": "Export nedladdad.", + "settings_saved": "Inställning sparad.", + "settings_include_history": "Inkludera historik", + "sort_alphabetical": "Alfabetisk", + "sort_due_soonest": "Närmaste förfallodatum", + "sort_task_count": "Antal uppgifter", + "sort_area": "Område", + "sort_assigned_user": "Tilldelad användare", + "sort_group": "Grupp", + "groupby_none": "Ingen gruppering", + "groupby_area": "Per område", + "groupby_group": "Per grupp", + "groupby_user": "Per användare", + "filter_label": "Filter", + "user_label": "Användare", + "sort_label": "Sortering", + "group_by_label": "Gruppera efter", + "state_value_help": "Använd HA-tillståndsvärdet (vanligtvis med små bokstäver, t.ex. \"on\"/\"off\"). Versaler normaliseras vid sparande.", + "target_changes_help": "Antal matchande övergångar innan utlösaren aktiveras (standard: 1).", + "qr_print_title": "Skriv ut QR-koder", + "qr_print_desc": "Skapa en utskriftssida med QR-koder att klippa ut och sätta på din utrustning.", + "qr_print_load": "Ladda objekt", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekt", + "qr_print_actions": "Åtgärder", + "qr_print_url_mode": "Länktyp", + "qr_print_estimate": "Uppskattade QR-koder", + "qr_print_over_limit": "gräns 200, begränsa filtret", + "qr_print_generate": "Skapa QR-koder", + "qr_print_generating": "Skapar…", + "qr_print_ready": "QR-koder klara", + "qr_print_print_button": "Skriv ut", + "qr_print_empty": "Inget att skapa", + "qr_action_skip": "Hoppa över", + "vacation_title": "Semesterläge", + "vacation_active": "aktivt", + "vacation_ended": "avslutat", + "vacation_desc": "Planera din semester: aviseringar pausas under perioden plus några buffert-dagar. Du kan göra undantag per uppgift.", + "vacation_enable": "Aktivera semesterläge", + "vacation_start": "Start", + "vacation_end": "Slut", + "vacation_buffer": "Buffert (dagar)", + "vacation_exempt_title": "Avisera ändå under semester", + "vacation_exempt_desc": "Välj uppgifter som ska avisera även under semestern (t.ex. kritisk poolkemi).", + "vacation_load_tasks": "Ladda uppgifter", + "vacation_preview_btn": "Visa förhandsgranskning", + "vacation_preview_affected": "uppgifter berörda", + "vacation_event_due_soon": "blir snart förfallen", + "vacation_event_overdue": "blir försenad", + "vacation_event_triggered_est": "möjlig sensorutlösning", + "vacation_sensor_based": "(sensorbaserad)", + "vacation_action_notify": "Avisera ändå", + "vacation_action_unsilence": "Tysta igen", + "vacation_marked_complete": "Markerad som klar", + "vacation_marked_skip": "Hoppade över", + "vacation_end_now": "Avsluta semester nu", + "unassigned": "Otilldelad", + "no_area": "Inget område", + "has_overdue": "Har försenade uppgifter", + "object": "Objekt", + "settings_panel_access": "Paneltillgång", + "settings_panel_access_desc": "Administratörer har alltid full åtkomst. För att delegera skapa, redigera och ta bort till specifika icke-admin-användare, slå på detta och välj dem nedan — alla andra ser endast Slutför och Hoppa över.", + "settings_operator_write": "Tillåt valda användare att skapa, redigera och ta bort", + "settings_operator_write_desc": "Av: endast administratörer kan ändra innehåll. På: de valda användarna nedan får också full åtkomst.", + "no_non_admin_users": "Inga icke-admin-användare hittades. Lägg till några i Inställningar → Personer.", + "owner_label": "Ägare", + "feat_completion_actions": "Slutförande-åtgärder", + "feat_completion_actions_desc": "HA-åtgärd per uppgift vid slutförande + snabb-slutför-QR med förinställda värden.", + "on_complete_action_title": "Vid slutförande: utlös HA-åtgärd (valfritt)", + "on_complete_action_desc": "Anropar en HA-tjänst när uppgiften slutförs — t.ex. återställ en räknare på enheten.", + "on_complete_action_service": "Tjänst", + "on_complete_action_target": "Målentitet", + "on_complete_action_data": "Data (JSON, valfritt)", + "on_complete_action_test": "Testa åtgärd", + "on_complete_action_test_success": "Lyckades", + "on_complete_action_test_failed": "Misslyckades", + "quick_complete_defaults_title": "Snabb-slutför standardvärden (för QR-skanningar, valfritt)", + "quick_complete_defaults_desc": "Förinställda värden för snabb-slutför-QR. Utan dessa öppnar QR slutför-dialogen.", + "quick_complete_defaults_notes": "Anteckningar", + "quick_complete_defaults_cost": "Kostnad", + "quick_complete_defaults_duration": "Varaktighet (minuter)", + "quick_complete_defaults_feedback_none": "Ingen feedback", + "quick_complete_defaults_feedback_needed": "Var nödvändigt", + "quick_complete_defaults_feedback_not_needed": "Inte nödvändigt", + "quick_complete_success": "Snabbt markerad som klar", + "trigger_replaced": "Utlösare ersatt", + "add": "Lägg till", + "show_stats": "Visa statistik + grafer", + "hide_stats": "Dölj statistik", + "adaptive_no_data": "Ännu inte tillräckligt med slutförandehistorik för adaptiv analys. Slutför denna uppgift några gånger till för att låsa upp intervallrekommendationer och tillförlitlighetsdiagram.", + "suggestion_applied": "Föreslaget intervall tillämpat", + "vacation_mode": "Semesterläge", + "vacation_status_active": "Aktivt nu", + "vacation_status_scheduled": "Schemalagt", + "vacation_status_inactive": "Inaktivt", + "vacation_end_now_confirm": "Avsluta semestern omedelbart?", + "vacation_exempt_count": "undantagna", + "vacation_advanced": "Avancerat…", + "vacation_open_panel": "Öppna i panelen", + "enable": "Aktivera", + "saved": "Sparat", + "budget_monthly_set": "Ange månad", + "budget_yearly_set": "Ange år", + "budget_advanced": "Valuta, varningar…", + "budget_open_panel": "Öppna i panelen", + "groups_empty": "Inga grupper än.", + "group_new_placeholder": "Lägg till grupp…", + "group_delete_confirm": "Ta bort gruppen ”{name}”?", + "groups_manage_tasks": "Hantera uppgiftstilldelningar…", + "groups_open_panel": "Öppna i panelen", + "on_complete_action_target_hint": "Obs: entitetens domän måste matcha tjänsten — t.ex. 'button.press' fungerar bara på button.*, 'counter.increment' bara på counter.*, 'input_button.press' bara på input_button.* osv. Vid en avvikelse misslyckas åtgärden tyst (HA loggar 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Visa alla objekt", + "show_all_tasks": "Rensa filter — visa alla uppgifter", + "filter_to_overdue": "Filtrera listan till endast försenade", + "filter_to_due_soon": "Filtrera listan till endast snart förfallna", + "filter_to_triggered": "Filtrera listan till endast utlösta", + "open_task": "Öppna uppgift", + "show_details": "Visa historik + statistik", + "hide_details": "Dölj detaljer", + "history_empty": "Ingen historik än.", + "history_edit_button": "Redigera post", + "total_cost": "Total kostnad", + "times_performed": "Utförd", + "older_entries": "äldre", + "open_in_panel": "Öppna i Underhållspanelen", + "skip_reason": "Anledning till överhoppning (valfritt)", + "reset_to_date": "Återställ senast utförd till", + "delete_task_confirm": "Ta bort denna uppgift och dess historik?", + "delete_object_confirm": "Ta bort detta objekt och alla dess uppgifter?", + "loading": "Laddar…", + "archive": "Arkivera", + "undo": "Ångra", + "task_archived": "Uppgift arkiverad", + "object_archived": "Objekt arkiverat", + "unarchive": "Återställ", + "archived": "Arkiverad", + "show_archived": "Visa arkiverade", + "hide_archived": "Dölj arkiverade", + "confirm_archive_object": "Arkivera detta objekt och dess uppgifter? Historiken behålls och kan återställas senare.", + "settings_archive": "Arkiv och lagring", + "settings_archive_desc": "Lägg undan slutförda engångsuppgifter utan att radera dem. Arkiverade objekt döljs och är inaktiva men behåller historik och kostnad.", + "settings_archive_oneoff_days": "Arkivera slutförda engångsuppgifter automatiskt efter (dagar, 0 = av)", + "settings_delete_archived_oneoff_days": "Radera arkiverade engångsuppgifter automatiskt efter (dagar, 0 = aldrig)", + "archive_object": "Arkivera objekt", + "unarchive_object": "Återställ objekt", + "documents": "Dokument", + "documents_empty": "Inga dokument ännu.", + "doc_upload": "Ladda upp fil", + "doc_uploading": "Laddar upp…", + "doc_add_link": "Lägg till länk", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (valfritt)", + "doc_open": "Öppna", + "doc_delete_confirm": "Ta bort \"{name}\"?", + "doc_too_large": "Filen är för stor (max 25 MB).", + "doc_upload_failed": "Uppladdningen misslyckades.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Redan lagrad någon annanstans — delad, inget extra utrymme.", + "doc_dup_in_object": "Den här filen är redan kopplad till detta objekt.", + "doc_link_invalid": "Endast http/https-länkar tillåts.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Garanti", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Reservdelar", + "doc_cat_photo": "Foto", + "doc_cat_other": "Övrigt", + "doc_link_badge": "Länk", + "doc_storage_title": "Dokumentlagring", + "doc_storage_saved": "Sparat genom deduplicering", + "doc_storage_refresh": "Uppdatera", + "doc_download": "Ladda ner", + "doc_close": "Stäng", + "doc_camera": "Ta foto", + "doc_drop_hint": "Släpp filer här", + "doc_task_none": "Inga dokument kopplade till denna uppgift.", + "doc_link_existing": "Koppla ett dokument…", + "doc_attach": "Koppla", + "doc_unlink": "Koppla bort", + "doc_page": "Sida", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1å", + "chart_since_service": "sedan senaste underhåll", + "chart_no_stats": "Ingen långtidsstatistik för denna entitet — visar endast värden från underhållsposter", + "auto_complete_on_recovery": "Slutför automatiskt när sensorn återhämtar sig", + "auto_complete_on_recovery_help": "Registrerar ett slutförande (sätter senast utförd) när utlösaren löser sig själv — t.ex. salt påfyllt, filter bytt.", + "doc_search": "Sök dokument…", + "doc_search_none": "Inga matchande dokument", + "link_device_optional": "Länka till befintlig enhet (valfritt)", + "parent_object_optional": "Överordnat objekt (valfritt)", + "parent_none": "(Inget överordnat)", + "paused": "Pausad", + "pause_object": "Pausa", + "resume_object": "Återuppta", + "pause_until_prompt": "Frys objektets scheman — inget förfaller och inget aviserar förrän det återupptas. Ange valfritt ett datum för automatisk återupptagning.", + "pause_until_label": "Återuppta den (valfritt)", + "object_paused": "Objekt pausat", + "object_resumed": "Objekt återupptaget — scheman omstartade", + "object_paused_badge": "Pausad", + "paused_until_label": "till", + "replace_object": "Ersätt…", + "replace_object_prompt": "Pensionera detta objekt och skapa en efterträdare. Historik och kostnader arkiveras på det gamla; uppgifter och dokument följer med till det nya, räknare börjar om.", + "replace_name_label": "Efterträdarens namn", + "object_replaced": "Objekt ersatt — efterträdare skapad", + "reading_unit_label": "Avläsningsenhet (t.ex. kWh, m³)", + "reading_unit_help": "Visas bredvid det registrerade värdet när uppgiften slutförs.", + "reading_value_label": "Avläst värde", + "reading_label": "Avläsning", + "settings_templates_label": "Mallgalleri", + "settings_templates_hint": "Avmarkera mallar du aldrig behöver — de försvinner från \"Från mall\"-väljarna (panel och config flow). Inget annat ändras; kan återaktiveras när som helst.", + "worksheet": "Arbetsblad", + "worksheet_scan_view": "Skanna för att öppna uppgiften", + "worksheet_scan_complete": "Skanna för att slutföra", + "worksheet_manual_excerpt": "Utdrag ur manualen", + "worksheet_pages": "sidor", + "worksheet_printed": "Utskriven", + "worksheet_never": "Aldrig" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/uk.json b/custom_components/maintenance_supporter/frontend-src/locales/uk.json new file mode 100644 index 0000000..ff18efc --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/uk.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Обслуговування", + "objects": "Об'єкти", + "tasks": "Завдання", + "overdue": "Прострочено", + "due_soon": "Незабаром", + "triggered": "Спрацювало", + "ok": "Норма", + "all": "Всі", + "new_object": "+ Новий об'єкт", + "templates_from": "Із шаблону", + "templates_title": "Почати із шаблону", + "templates_task_count": "{n} завдань", + "template_created": "Створено із шаблону", + "onboard_hint": "Додайте перший об'єкт, щоб відстежувати обслуговування.", + "edit": "Редагувати", + "duplicate": "Дублювати", + "task_duplicated": "Завдання продубльовано", + "object_duplicated": "Об'єкт продубльовано", + "delete": "Видалити", + "add_task": "+ Додати завдання", + "complete": "Виконати", + "completed": "Виконано", + "skip": "Пропустити", + "skipped": "Пропущено", + "missed": "Missed", + "reset": "Скинути", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Скасувати", + "bulk_select": "Вибрати", + "bulk_select_all": "Вибрати все", + "bulk_n_selected": "Вибрано: {n}", + "bulk_completed": "Виконано завдань: {n}", + "bulk_archived": "Архівовано завдань: {n}", + "completing": "Виконується…", + "interval": "Інтервал", + "warning": "Попередження", + "last_performed": "Останнє виконання", + "next_due": "Наступний термін", + "days_until_due": "Днів до терміну", + "avg_duration": "Сер. тривалість", + "trigger": "Тригер", + "trigger_type": "Тип тригера", + "threshold_above": "Верхня межа", + "threshold_below": "Нижня межа", + "threshold": "Поріг", + "counter": "Лічильник", + "state_change": "Зміна стану", + "runtime": "Напрацювання", + "runtime_hours": "Цільове напрацювання (години)", + "target_value": "Цільове значення", + "baseline": "Базове значення", + "target_changes": "Цільова кількість змін", + "for_minutes": "Протягом (хвилин)", + "time_based": "За часом", + "sensor_based": "За сенсором", + "manual": "Вручну", + "one_time": "Одноразово", + "weekdays": "Дні тижня", + "nth_weekday": "N-й день тижня в місяці", + "day_of_month": "День місяця", + "recurrence_on_days": "Повторювати у", + "recurrence_occurrence": "Входження", + "recurrence_weekday": "День тижня", + "recurrence_day": "День місяця (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1-й", + "ord_2": "2-й", + "ord_3": "3-й", + "ord_4": "4-й", + "ord_5": "5-й", + "ord_last": "Останній", + "day_word": "День", + "interval_value": "Інтервал", + "interval_unit": "Одиниця", + "unit_days": "Дні", + "unit_weeks": "Тижні", + "unit_months": "Місяці", + "unit_years": "Роки", + "due_date": "Дата виконання", + "cleaning": "Очищення", + "inspection": "Огляд", + "replacement": "Заміна", + "calibration": "Калібрування", + "service": "Сервіс", + "reading": "Показання", + "custom": "Власний", + "history": "Історія", + "cost": "Вартість", + "report_button": "Звіт", + "report_title": "Звіт про обслуговування", + "report_generated": "Створено", + "report_times_done": "Виконано", + "report_total_cost": "Загальна вартість", + "report_every": "кожні {n} {unit}", + "report_notes": "Нотатки", + "report_col_type": "Тип", + "report_col_status": "Статус", + "report_col_schedule": "Розклад", + "duration": "Тривалість", + "both": "Обидва", + "trigger_val": "Значення тригера", + "complete_title": "Виконати: ", + "checklist": "Чекліст", + "checklist_steps_optional": "Кроки чекліста (необов'язково)", + "checklist_placeholder": "Очистити фільтр\nЗамінити ущільнювач\nПеревірити тиск", + "checklist_help": "Один крок на рядок. Макс. 100 елементів.", + "err_too_long": "{field}: задовге (макс. {n} символів)", + "err_too_short": "{field}: закоротке (мін. {n} символів)", + "err_value_too_high": "{field}: завелике (макс. {n})", + "err_value_too_low": "{field}: замале (мін. {n})", + "err_required": "{field}: обов'язкове поле", + "err_wrong_type": "{field}: невірний тип (очікувалось: {type})", + "err_invalid_choice": "{field}: недопустиме значення", + "err_invalid_value": "{field}: невірне значення", + "feat_schedule_time": "Планування за часом доби", + "feat_schedule_time_desc": "Задачі стають простроченими у певний час доби, а не опівночі.", + "schedule_time_optional": "Час прострочення (необов'язково, HH:MM)", + "schedule_time_help": "Порожньо = опівночі (за замовчуванням). Часовий пояс HA.", + "at_time": "о", + "notes_optional": "Примітки (необов'язково)", + "cost_optional": "Вартість (необов'язково)", + "duration_minutes": "Тривалість у хвилинах (необов'язково)", + "days": "днів", + "day": "день", + "today": "Сьогодні", + "d_overdue": "д прострочено", + "no_tasks": "Завдань обслуговування ще немає. Створіть об'єкт, щоб почати.", + "no_tasks_short": "Немає завдань", + "no_history": "Записів в історії ще немає.", + "show_all": "Показати всі", + "cost_duration_chart": "Вартість і тривалість", + "installed": "Встановлено", + "confirm_delete_object": "Видалити цей об'єкт і всі його завдання?", + "confirm_delete_task": "Видалити це завдання?", + "min": "Мін", + "max": "Макс", + "save": "Зберегти", + "saving": "Збереження…", + "edit_task": "Редагувати завдання", + "new_task": "Нове завдання обслуговування", + "task_name": "Назва завдання", + "maintenance_type": "Тип обслуговування", + "priority": "Пріоритет", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Низький", + "priority_normal": "Звичайний", + "priority_high": "Високий", + "schedule_type": "Тип розкладу", + "interval_days": "Інтервал (дні)", + "warning_days": "Днів попередження", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "interval_anchor": "Прив'язка інтервалу", + "anchor_completion": "Від дати виконання", + "anchor_planned": "Від запланованої дати (без зміщення)", + "edit_object": "Редагувати об'єкт", + "name": "Назва", + "manufacturer_optional": "Виробник (необов'язково)", + "model_optional": "Модель (необов'язково)", + "serial_number_optional": "Серійний номер (необов'язково)", + "serial_number_label": "С/Н", + "documentation_url_label": "Посібник", + "object_notes_label": "Примітки", + "last_performed_optional": "Останнé виконання (необов'язково)", + "sort_due_date": "Дата терміну", + "sort_object": "Назва об'єкта", + "sort_type": "Тип", + "sort_task_name": "Назва завдання", + "all_objects": "Всі об'єкти", + "tasks_lower": "завдань", + "no_tasks_yet": "Завдань ще немає", + "add_first_task": "Додати перше завдання", + "trigger_configuration": "Налаштування тригера", + "entity_id": "ID об'єкта", + "comma_separated": "через кому", + "entity_logic": "Логіка об'єктів", + "entity_logic_any": "Будь-який об'єкт спрацьовує", + "entity_logic_all": "Всі об'єкти мають спрацювати", + "entities": "об'єктів", + "attribute_optional": "Атрибут (необов'язково, порожньо = стан)", + "use_entity_state": "Використовувати стан об'єкта (без атрибута)", + "trigger_above": "Спрацювати, коли вище", + "trigger_below": "Спрацювати, коли нижче", + "for_at_least_minutes": "Протягом не менше (хвилин)", + "safety_interval_days": "Страховий інтервал (дні, необов'язково)", + "safety_interval": "Страховий інтервал (необов'язково)", + "delta_mode": "Режим дельти", + "from_state_optional": "З стану (необов'язково)", + "to_state_optional": "До стану (необов'язково)", + "documentation_url_optional": "URL документації (необов'язково)", + "object_notes_optional": "Примітки (необов'язково)", + "nfc_tag_id_optional": "ID NFC-тега (необов'язково)", + "nfc_tags_empty_help": "У Home Assistant ще не зареєстровано NFC-теги.", + "nfc_tags_open_settings": "Відкрити налаштування тегів", + "nfc_tags_refresh": "Оновити", + "environmental_entity_optional": "Датчик навколишнього середовища (необов'язково)", + "environmental_entity_helper": "напр. sensor.outdoor_temperature — коригує інтервал відповідно до умов навколишнього середовища", + "environmental_attribute_optional": "Атрибут середовища (необов'язково)", + "nfc_tag_id": "ID NFC-тега", + "nfc_linked": "NFC-тег прив'язано", + "nfc_link_hint": "Натисніть, щоб прив'язати NFC-тег", + "responsible_user": "Відповідальний користувач", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Користувача не призначено)", + "all_users": "Всі користувачі", + "my_tasks": "Мої завдання", + "tab_calendar": "Календар", + "cal_no_events": "Без обслуговування", + "cal_window_7": "7 днів", + "cal_window_14": "14 днів", + "cal_window_30": "30 днів", + "cal_window_365": "1 рік", + "cal_every_n_days": "кожні {n} днів", + "cal_source_time": "За часом", + "cal_source_time_adaptive": "За часом (адаптивно)", + "cal_source_sensor": "За датчиком", + "cal_predicted": "прогноз", + "cal_confidence_high": "висока надійність", + "cal_confidence_medium": "середня надійність", + "cal_confidence_low": "низька надійність", + "budget_monthly": "Щомісячний бюджет", + "budget_yearly": "Щорічний бюджет", + "groups": "Групи", + "new_group": "Нова група", + "edit_group": "Редагувати групу", + "no_groups": "Груп ще немає", + "delete_group": "Видалити групу", + "delete_group_confirm": "Видалити групу '{name}'?", + "group_select_tasks": "Обрати завдання", + "group_name_required": "Потрібна назва", + "description_optional": "Опис (необов'язково)", + "selected": "Обрано", + "loading_chart": "Завантаження даних графіка...", + "hide_outliers": "Приховати викиди (збої датчика)", + "was_maintenance_needed": "Чи було потрібне це обслуговування?", + "feedback_needed": "Потрібне", + "feedback_not_needed": "Не потрібне", + "feedback_not_sure": "Не впевнений", + "suggested_interval": "Рекомендований інтервал", + "apply_suggestion": "Застосувати", + "reanalyze": "Повторно проаналізувати", + "reanalyze_result": "Новий аналіз", + "reanalyze_insufficient_data": "Недостатньо даних для рекомендації", + "data_points": "точок даних", + "dismiss_suggestion": "Відхилити", + "confidence_low": "Низька", + "confidence_medium": "Середня", + "confidence_high": "Висока", + "recommended": "рекомендовано", + "seasonal_awareness": "Сезонна корекція", + "edit_seasonal_overrides": "Редагувати сезонні коефіцієнти", + "seasonal_overrides_title": "Сезонні коефіцієнти (перевизначення)", + "seasonal_overrides_hint": "Коефіцієнт на місяць (0.1–5.0). Порожньо = автоматично.", + "seasonal_override_invalid": "Недійсне значення", + "seasonal_override_range": "Коефіцієнт має бути між 0.1 та 5.0", + "clear_all": "Очистити все", + "seasonal_chart_title": "Сезонні коефіцієнти", + "seasonal_learned": "Навчена", + "seasonal_manual": "Ручна", + "month_jan": "Січ", + "month_feb": "Лют", + "month_mar": "Бер", + "month_apr": "Кві", + "month_may": "Тра", + "month_jun": "Чер", + "month_jul": "Лип", + "month_aug": "Сер", + "month_sep": "Вер", + "month_oct": "Жов", + "month_nov": "Лис", + "month_dec": "Гру", + "sensor_prediction": "Прогноз сенсора", + "degradation_trend": "Тренд", + "trend_rising": "Зростає", + "trend_falling": "Спадає", + "trend_stable": "Стабільний", + "trend_insufficient_data": "Недостатньо даних", + "days_until_threshold": "Днів до порогу", + "threshold_exceeded": "Поріг перевищено", + "environmental_adjustment": "Екологічний коефіцієнт", + "sensor_prediction_urgency": "Сенсор прогнозує досягнення порогу через ~{days} днів", + "day_short": "день", + "weibull_reliability_curve": "Крива надійності", + "weibull_failure_probability": "Ймовірність відмови", + "weibull_r_squared": "Точність R²", + "beta_early_failures": "Ранні відмови", + "beta_random_failures": "Випадкові відмови", + "beta_wear_out": "Знос", + "beta_highly_predictable": "Дуже передбачуваний", + "confidence_interval": "Довірчий інтервал", + "confidence_conservative": "Консервативний", + "confidence_aggressive": "Оптимістичний", + "current_interval_marker": "Поточний інтервал", + "recommended_marker": "Рекомендовано", + "characteristic_life": "Характеристичний ресурс", + "chart_mini_sparkline": "Мінімальний графік тренду", + "chart_history": "Історія вартості та тривалості", + "chart_seasonal": "Сезонні коефіцієнти, 12 місяців", + "chart_weibull": "Крива надійності Вейбулла", + "chart_sparkline": "Графік значень тригера сенсора", + "days_progress": "Прогрес днів", + "qr_code": "QR-код", + "qr_generating": "Генерація QR-коду…", + "qr_error": "Не вдалося згенерувати QR-код.", + "qr_error_no_url": "URL Home Assistant не налаштовано. Задайте зовнішню або внутрішню URL-адресу в Налаштування → Система → Мережа.", + "save_error": "Не вдалося зберегти. Спробуйте ще раз.", + "qr_print": "Друкувати", + "qr_download": "Завантажити SVG", + "qr_action": "Дія при скануванні", + "qr_action_view": "Переглянути", + "qr_action_complete": "Позначити обслуговування виконаним", + "qr_url_mode": "Тип посилання", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Локальний (mDNS)", + "qr_mode_server": "URL сервера", + "overview": "Огляд", + "analysis": "Аналіз", + "recent_activities": "Остання активність", + "search_notes": "Пошук у примітках", + "avg_cost": "Сер. вартість", + "no_advanced_features": "Розширені функції не увімкнено", + "no_advanced_features_hint": "Увімкніть «Адаптивні інтервали» або «Сезонні закономірності» в налаштуваннях інтеграції, щоб побачити тут дані аналізу.", + "analysis_not_enough_data": "Недостатньо даних для аналізу.", + "analysis_not_enough_data_hint": "Аналіз Вейбулла потребує щонайменше 5 виконаних обслуговувань; сезонні закономірності стають видимими після 6+ записів на місяць.", + "analysis_manual_task_hint": "Ручні завдання без інтервалу не генерують дані аналізу.", + "completions": "виконань", + "current": "Поточний", + "shorter": "Коротший", + "longer": "Довший", + "normal": "Звичайний", + "disabled": "Вимкнено", + "compound_logic": "Складена логіка", + "compound": "Складений (кілька умов)", + "compound_logic_and": "І — усі умови мають спрацювати", + "compound_logic_or": "АБО — достатньо будь-якої умови", + "compound_help": "Об'єднайте кілька умов датчиків в один тригер.", + "compound_no_conditions": "Ще немає умов — додайте принаймні одну.", + "compound_add_condition": "Додати умову", + "compound_condition": "Умова", + "compound_remove_condition": "Видалити умову", + "card_title": "Заголовок", + "card_show_header": "Показувати заголовок зі статистикою", + "card_show_actions": "Показувати кнопки дій", + "card_compact": "Компактний режим", + "card_max_items": "Макс. елементів (0 = всі)", + "card_filter_status": "Фільтрувати за статусом", + "card_filter_status_help": "Порожньо = показати всі статуси.", + "card_filter_objects": "Фільтрувати за об'єктами", + "card_filter_objects_help": "Порожньо = показати всі об'єкти.", + "card_filter_entities": "Фільтрувати за сутностями (entity_ids)", + "card_filter_entities_help": "Виберіть сутності sensor / binary_sensor з цієї інтеграції. Порожньо = всі.", + "card_loading_objects": "Завантаження об'єктів…", + "card_load_error": "Не вдалося завантажити об'єкти — перевірте WebSocket-з'єднання.", + "card_no_tasks_title": "Поки немає завдань обслуговування", + "card_no_tasks_cta": "→ Створіть на панелі Maintenance", + "no_objects": "Поки немає об'єктів.", + "action_error": "Дія не вдалась. Спробуйте ще раз.", + "area_id_optional": "Зона (необов'язково)", + "installation_date_optional": "Дата встановлення (необов'язково)", + "warranty_expiry_optional": "Закінчення гарантії (необов'язково)", + "warranty": "Гарантія", + "warranty_valid_until": "дійсна до {date}", + "warranty_expires_in": "закінчується через {days} дн.", + "warranty_expired": "закінчилася", + "cal_past_windows": "Минулі вікна", + "cal_forward_windows": "Майбутні вікна", + "history_edit_title": "Редагувати запис історії", + "history_edit_timestamp": "Позначка часу", + "manufacturer": "Виробник", + "model": "Модель", + "area": "Зона", + "actions": "Дії", + "view_mode_label": "Вигляд", + "view_cards": "Картки", + "view_table": "Таблиця", + "objects_table_columns_label": "Стовпці таблиці об'єктів", + "objects_table_columns_hint": "Виберіть, які стовпці показувати в табличному вигляді об'єктів.", + "custom_icon_optional": "Іконка (необов'язково, наприклад mdi:wrench)", + "task_enabled": "Завдання увімкнено", + "skip_reason_prompt": "Пропустити це завдання?", + "reason_optional": "Причина (необов'язково)", + "reset_date_prompt": "Позначити як виконане?", + "reset_date_optional": "Дата останнього виконання (необов'язково, типово: сьогодні)", + "notes_label": "Примітки", + "documentation_label": "Документація", + "no_nfc_tag": "— Без тега —", + "dashboard": "Дашборд", + "tab_today": "Сьогодні", + "palette_placeholder": "Пошук об'єктів і завдань…", + "palette_no_results": "Немає збігів", + "palette_hint": "↑↓ навігація · Enter відкрити · Esc закрити", + "today_all_caught_up": "Усе зроблено! Цього тижня нічого немає.", + "today_overdue": "Прострочені", + "today_due_today": "Сьогодні", + "today_this_week": "Цього тижня", + "settings": "Налаштування", + "settings_features": "Розширені функції", + "settings_features_desc": "Увімкніть або вимкніть розширені функції. Вимкнення приховує їх з інтерфейсу, але не видаляє дані.", + "feat_adaptive": "Адаптивне планування", + "feat_adaptive_desc": "Навчатися оптимальним інтервалам з історії обслуговування", + "feat_predictions": "Прогнози за сенсорами", + "feat_predictions_desc": "Прогнозувати дати спрацювання за деградацією сенсора", + "feat_seasonal": "Сезонні корекції", + "feat_seasonal_desc": "Коригувати інтервали на основі сезонних закономірностей", + "feat_environmental": "Кореляція з довкіллям", + "feat_environmental_desc": "Корелювати інтервали з температурою/вологістю", + "feat_budget": "Відстеження бюджету", + "feat_budget_desc": "Відстежувати щомісячні та щорічні витрати на обслуговування", + "feat_groups": "Групи завдань", + "feat_groups_desc": "Організовувати завдання в логічні групи", + "feat_checklists": "Чеклісти", + "feat_checklists_desc": "Багатокрокові процедури для виконання завдань", + "settings_general": "Загальне", + "settings_default_warning": "Днів попередження за замовчуванням", + "settings_panel_enabled": "Панель у бічному меню", + "settings_panel_title": "Заголовок панелі", + "settings_notifications": "Сповіщення", + "settings_notify_service": "Служба сповіщень", + "test_notification": "Тестове сповіщення", + "send_test": "Надіслати тест", + "testing": "Надсилання…", + "test_notification_success": "Тестове сповіщення надіслано", + "test_notification_failed": "Не вдалося надіслати тестове сповіщення", + "settings_notify_due_soon": "Сповіщати, коли термін наближається", + "settings_notify_overdue": "Сповіщати про прострочення", + "settings_notify_triggered": "Сповіщати про спрацювання", + "settings_interval_hours": "Інтервал повторення (години, 0 = одноразово)", + "settings_quiet_hours": "Тихі години", + "settings_quiet_start": "Початок", + "settings_quiet_end": "Кінець", + "settings_max_per_day": "Макс. сповіщень на день (0 = без обмежень)", + "settings_bundling": "Групувати сповіщення", + "settings_bundle_threshold": "Поріг групування", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Кнопки дій у мобільних сповіщеннях", + "settings_action_complete": "Показувати кнопку «Виконати»", + "settings_action_skip": "Показувати кнопку «Пропустити»", + "settings_action_snooze": "Показувати кнопку «Відкласти»", + "settings_weekly_digest": "Щотижневий огляд", + "settings_weekly_digest_hint": "Одне зведене сповіщення в понеділок вранці, коли є завдання.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Тривалість відкладення (години)", + "settings_budget": "Бюджет", + "settings_currency": "Валюта", + "settings_budget_monthly": "Щомісячний бюджет", + "settings_budget_yearly": "Щорічний бюджет", + "settings_budget_alerts": "Сповіщення про бюджет", + "settings_budget_threshold": "Поріг сповіщення (%)", + "settings_import_export": "Імпорт / Експорт", + "settings_export_json": "Експортувати JSON", + "settings_export_yaml": "Експортувати YAML", + "settings_export_csv": "Експортувати CSV", + "settings_import_csv": "Імпортувати CSV", + "settings_import_placeholder": "Вставте вміст JSON або CSV сюди…", + "settings_import_btn": "Імпортувати", + "settings_import_success": "{count} об'єктів успішно імпортовано.", + "settings_export_success": "Експорт завантажено.", + "settings_saved": "Налаштування збережено.", + "settings_include_history": "Включити історію", + "sort_alphabetical": "За алфавітом", + "sort_due_soonest": "Найближчий термін", + "sort_task_count": "Кількість завдань", + "sort_area": "Зона", + "sort_assigned_user": "Призначений користувач", + "sort_group": "Група", + "groupby_none": "Без групування", + "groupby_area": "За зоною", + "groupby_group": "За групою", + "groupby_user": "За користувачем", + "filter_label": "Фільтр", + "user_label": "Користувач", + "sort_label": "Сортування", + "group_by_label": "Групувати за", + "state_value_help": "Використовуйте значення стану HA (зазвичай у нижньому регістрі, напр. \"on\"/\"off\"). Регістр нормалізується при збереженні.", + "target_changes_help": "Кількість відповідних переходів, після яких тригер спрацює (за замовчуванням: 1).", + "qr_print_title": "Друкувати QR-коди", + "qr_print_desc": "Створи сторінку для друку з QR-кодами, які можна вирізати та наклеїти на обладнання.", + "qr_print_load": "Завантажити об'єкти", + "qr_print_filter": "Фільтр", + "qr_print_objects": "Об'єкти", + "qr_print_actions": "Дії", + "qr_print_url_mode": "Тип посилання", + "qr_print_estimate": "Прогноз QR-кодів", + "qr_print_over_limit": "ліміт 200, звузь фільтр", + "qr_print_generate": "Створити QR-коди", + "qr_print_generating": "Створення…", + "qr_print_ready": "QR-коди готові", + "qr_print_print_button": "Друкувати", + "qr_print_empty": "Нічого створювати", + "qr_action_skip": "Пропустити", + "vacation_title": "Режим відпустки", + "vacation_active": "активний", + "vacation_ended": "завершено", + "vacation_desc": "Сплануй відпустку: сповіщення призупиняються на період плюс кілька буферних днів. Можна налаштувати винятки.", + "vacation_enable": "Увімкнути режим відпустки", + "vacation_start": "Початок", + "vacation_end": "Кінець", + "vacation_buffer": "Буфер (днів)", + "vacation_exempt_title": "Сповіщати попри відпустку", + "vacation_exempt_desc": "Обери завдання, які мають сповіщати навіть під час відпустки (наприклад, критична хімія басейну).", + "vacation_load_tasks": "Завантажити завдання", + "vacation_preview_btn": "Показати попередній перегляд", + "vacation_preview_affected": "завдань зачеплено", + "vacation_event_due_soon": "наближається термін", + "vacation_event_overdue": "стане простроченим", + "vacation_event_triggered_est": "можливе спрацювання сенсора", + "vacation_sensor_based": "(сенсорне)", + "vacation_action_notify": "Сповіщати все одно", + "vacation_action_unsilence": "Знову вимкнути сповіщення", + "vacation_marked_complete": "Позначено як виконане", + "vacation_marked_skip": "Пропущено", + "vacation_end_now": "Завершити відпустку зараз", + "unassigned": "Не призначено", + "no_area": "Без зони", + "has_overdue": "Прострочені завдання", + "object": "Об'єкт", + "settings_panel_access": "Доступ до панелі", + "settings_panel_access_desc": "Адміністратори завжди мають повний доступ. Щоб делегувати створення, редагування та видалення певним не-адмін користувачам, увімкніть це та виберіть їх нижче — інші бачать лише Виконати та Пропустити.", + "settings_operator_write": "Дозволити вибраним користувачам створювати, редагувати та видаляти", + "settings_operator_write_desc": "Вимкнено: лише адміністратори можуть змінювати вміст. Увімкнено: вибрані користувачі нижче також отримують повний доступ.", + "no_non_admin_users": "Не знайдено не-адмін користувачів. Додайте їх у Налаштуваннях → Особи.", + "owner_label": "Власник", + "feat_completion_actions": "Дії при завершенні", + "feat_completion_actions_desc": "Дія HA по завданню при завершенні + QR швидкого завершення з попередньо встановленими значеннями.", + "on_complete_action_title": "При завершенні: викликати HA-дію (опційно)", + "on_complete_action_desc": "Викликає HA-сервіс, коли завдання завершено — напр., скинути лічильник на пристрої.", + "on_complete_action_service": "Сервіс", + "on_complete_action_target": "Цільова сутність", + "on_complete_action_data": "Дані (JSON, опційно)", + "on_complete_action_test": "Тестувати дію", + "on_complete_action_test_success": "Успіх", + "on_complete_action_test_failed": "Помилка", + "quick_complete_defaults_title": "Стандартні значення швидкого завершення (для QR-сканів, опційно)", + "quick_complete_defaults_desc": "Попередньо встановлені значення для QR швидкого завершення. Без них QR відкриває діалог.", + "quick_complete_defaults_notes": "Нотатки", + "quick_complete_defaults_cost": "Вартість", + "quick_complete_defaults_duration": "Тривалість (хвилин)", + "quick_complete_defaults_feedback_none": "Без зворотного зв'язку", + "quick_complete_defaults_feedback_needed": "Було необхідно", + "quick_complete_defaults_feedback_not_needed": "Не було необхідно", + "quick_complete_success": "Швидко позначено виконаним", + "trigger_replaced": "Тригер замінено", + "add": "Додати", + "show_stats": "Показати статистику + графіки", + "hide_stats": "Сховати статистику", + "adaptive_no_data": "Поки що недостатньо історії виконань для адаптивного аналізу. Виконайте це завдання ще кілька разів, щоб розблокувати рекомендації щодо інтервалу та графіки надійності.", + "suggestion_applied": "Запропонований інтервал застосовано", + "vacation_mode": "Режим відпустки", + "vacation_status_active": "Активний зараз", + "vacation_status_scheduled": "Заплановано", + "vacation_status_inactive": "Неактивний", + "vacation_end_now_confirm": "Завершити відпустку негайно?", + "vacation_exempt_count": "виключено", + "vacation_advanced": "Додатково…", + "vacation_open_panel": "Відкрити на панелі", + "enable": "Увімкнути", + "saved": "Збережено", + "budget_monthly_set": "Задати місячний", + "budget_yearly_set": "Задати річний", + "budget_advanced": "Валюта, сповіщення…", + "budget_open_panel": "Відкрити на панелі", + "groups_empty": "Поки немає груп.", + "group_new_placeholder": "Додати групу…", + "group_delete_confirm": "Видалити групу «{name}»?", + "groups_manage_tasks": "Керування призначеннями завдань…", + "groups_open_panel": "Відкрити на панелі", + "on_complete_action_target_hint": "Примітка: домен сутності має відповідати сервісу — напр. 'button.press' працює лише з button.*, 'counter.increment' лише з counter.*, 'input_button.press' лише з input_button.* тощо. У разі невідповідності дія мовчки не виконається (HA запише в журнал 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Показати всі об'єкти", + "show_all_tasks": "Скинути фільтр — показати всі завдання", + "filter_to_overdue": "Фільтрувати список лише за простроченими", + "filter_to_due_soon": "Фільтрувати список лише за тими, що скоро настануть", + "filter_to_triggered": "Фільтрувати список лише за спрацьованими", + "open_task": "Відкрити завдання", + "show_details": "Показати історію + статистику", + "hide_details": "Сховати деталі", + "history_empty": "Поки немає історії.", + "history_edit_button": "Редагувати запис", + "total_cost": "Загальна вартість", + "times_performed": "Виконано", + "older_entries": "раніше", + "open_in_panel": "Відкрити на панелі обслуговування", + "skip_reason": "Причина пропуску (необов'язково)", + "reset_to_date": "Скинути останнє виконання на", + "delete_task_confirm": "Видалити це завдання та його історію?", + "delete_object_confirm": "Видалити цей об'єкт і всі його завдання?", + "loading": "Завантаження…", + "archive": "Архівувати", + "undo": "Скасувати", + "task_archived": "Завдання архівовано", + "object_archived": "Об'єкт архівовано", + "unarchive": "Розархівувати", + "archived": "В архіві", + "show_archived": "Показати архів", + "hide_archived": "Сховати архів", + "confirm_archive_object": "Архівувати цей об'єкт та його завдання? Історія збережеться, згодом можна відновити.", + "settings_archive": "Архів і зберігання", + "settings_archive_desc": "Відкладайте виконані одноразові завдання в архів, не видаляючи їх. Архівні елементи приховані й неактивні, але зберігають історію та витрати.", + "settings_archive_oneoff_days": "Автоархівування виконаних одноразових завдань через (днів, 0 = вимк)", + "settings_delete_archived_oneoff_days": "Автовидалення архівних одноразових завдань через (днів, 0 = ніколи)", + "archive_object": "Архівувати об'єкт", + "unarchive_object": "Розархівувати об'єкт", + "documents": "Документи", + "documents_empty": "Ще немає документів.", + "doc_upload": "Завантажити файл", + "doc_uploading": "Завантаження…", + "doc_add_link": "Додати посилання", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Назва (необов'язково)", + "doc_open": "Відкрити", + "doc_delete_confirm": "Видалити «{name}»?", + "doc_too_large": "Файл завеликий (макс. 25 МБ).", + "doc_upload_failed": "Не вдалося завантажити.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Уже збережено в іншому місці — спільний, без додаткового місця.", + "doc_dup_in_object": "Цей файл уже прикріплено до цього об'єкта.", + "doc_link_invalid": "Дозволені лише посилання http/https.", + "doc_cat_manual": "Інструкція", + "doc_cat_warranty": "Гарантія", + "doc_cat_invoice": "Рахунок", + "doc_cat_spare_parts": "Запчастини", + "doc_cat_photo": "Фото", + "doc_cat_other": "Інше", + "doc_link_badge": "Посилання", + "doc_storage_title": "Сховище документів", + "doc_storage_saved": "Заощаджено дедуплікацією", + "doc_storage_refresh": "Оновити", + "doc_download": "Завантажити", + "doc_close": "Закрити", + "doc_camera": "Зробити фото", + "doc_drop_hint": "Перетягніть файли сюди", + "doc_task_none": "Немає документів, пов'язаних із цим завданням.", + "doc_link_existing": "Пов'язати документ…", + "doc_attach": "Пов'язати", + "doc_unlink": "Відв'язати", + "doc_page": "Сторінка", + "chart_range_7d": "7д", + "chart_range_30d": "30д", + "chart_range_90d": "90д", + "chart_range_1y": "1р", + "chart_since_service": "з останнього обслуговування", + "chart_no_stats": "Немає довгострокової статистики для цієї сутності — показано лише значення із записів обслуговування", + "auto_complete_on_recovery": "Автозавершення при відновленні датчика", + "auto_complete_on_recovery_help": "Записує виконання (оновлює дату останнього обслуговування), коли тригер зникає сам — наприклад, сіль досипано, фільтр замінено.", + "doc_search": "Пошук документів…", + "doc_search_none": "Немає відповідних документів", + "link_device_optional": "Прив'язати до наявного пристрою (необов'язково)", + "parent_object_optional": "Батьківський об'єкт (необов'язково)", + "parent_none": "(Без батьківського)", + "paused": "Призупинено", + "pause_object": "Призупинити", + "resume_object": "Відновити", + "pause_until_prompt": "Заморозити розклади цього об'єкта — нічого не настає і не сповіщає до відновлення. За бажанням укажіть дату автоматичного відновлення.", + "pause_until_label": "Відновити (необов'язково)", + "object_paused": "Об'єкт призупинено", + "object_resumed": "Об'єкт відновлено — розклади перезапущено", + "object_paused_badge": "Призупинено", + "paused_until_label": "до", + "replace_object": "Замінити…", + "replace_object_prompt": "Списати цей об'єкт і створити наступника. Історія та витрати лишаються в архіві старого; завдання й документи переходять до нового, лічильники починаються заново.", + "replace_name_label": "Ім'я наступника", + "object_replaced": "Об'єкт замінено — наступника створено", + "reading_unit_label": "Одиниця показання (напр. кВт·год, м³)", + "reading_unit_help": "Показується поруч із записаним значенням під час виконання цієї задачі.", + "reading_value_label": "Показання", + "reading_label": "Показання", + "settings_templates_label": "Галерея шаблонів", + "settings_templates_hint": "Зніміть позначку з шаблонів, які вам не потрібні — вони зникнуть із вибору «З шаблону» (панель і config flow). Більше нічого не змінюється; можна ввімкнути знову будь-коли.", + "worksheet": "Робочий аркуш", + "worksheet_scan_view": "Скануйте, щоб відкрити задачу", + "worksheet_scan_complete": "Скануйте для виконання", + "worksheet_manual_excerpt": "Витяг з посібника", + "worksheet_pages": "сторінки", + "worksheet_printed": "Надруковано", + "worksheet_never": "Ніколи" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/zh.json b/custom_components/maintenance_supporter/frontend-src/locales/zh.json new file mode 100644 index 0000000..c1cee8a --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/zh.json @@ -0,0 +1,696 @@ +{ + "maintenance": "维护", + "objects": "维护项", + "tasks": "任务", + "overdue": "逾期", + "due_soon": "即将到期", + "triggered": "已触发", + "trigger_replaced": "触发器已更换", + "ok": "正常", + "all": "全部", + "new_object": "+ 新建维护项", + "templates_from": "从模板", + "templates_title": "从模板开始", + "templates_task_count": "{n} 个任务", + "template_created": "已从模板创建", + "onboard_hint": "添加第一个对象以开始跟踪维护。", + "edit": "编辑", + "duplicate": "复制", + "task_duplicated": "任务已复制", + "object_duplicated": "对象已复制", + "delete": "删除", + "add_task": "+ 添加任务", + "complete": "完成", + "completed": "已完成", + "skip": "跳过", + "skipped": "已跳过", + "missed": "Missed", + "reset": "重置", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "取消", + "bulk_select": "选择", + "bulk_select_all": "全选", + "bulk_n_selected": "已选 {n} 项", + "bulk_completed": "已完成 {n} 个任务", + "bulk_archived": "已归档 {n} 个任务", + "completing": "正在完成…", + "interval": "间隔", + "warning": "预警", + "last_performed": "上次执行", + "next_due": "下次到期", + "days_until_due": "距离到期天数", + "avg_duration": "平均耗时", + "trigger": "触发器", + "trigger_type": "触发类型", + "threshold_above": "超过阈值", + "threshold_below": "低于阈值", + "threshold": "阈值", + "counter": "计数器", + "state_change": "状态变化", + "runtime": "运行时间", + "runtime_hours": "目标运行时长 (小时)", + "target_value": "目标值", + "baseline": "基准线", + "target_changes": "目标变化次数", + "for_minutes": "持续 (分钟)", + "time_based": "基于时间", + "sensor_based": "基于传感器", + "manual": "手动", + "one_time": "一次性", + "weekdays": "工作日", + "nth_weekday": "每月第 N 个工作日", + "day_of_month": "每月某天", + "recurrence_on_days": "重复日期", + "recurrence_occurrence": "发生次数", + "recurrence_weekday": "星期", + "recurrence_day": "每月日期 (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "第 1", + "ord_2": "第 2", + "ord_3": "第 3", + "ord_4": "第 4", + "ord_5": "第 5", + "ord_last": "最后", + "day_word": "日", + "interval_value": "间隔数值", + "interval_unit": "单位", + "unit_days": "天", + "unit_weeks": "周", + "unit_months": "月", + "unit_years": "年", + "due_date": "到期日期", + "cleaning": "清洁", + "inspection": "检查", + "replacement": "更换", + "calibration": "校准", + "service": "保养", + "reading": "读数", + "custom": "自定义", + "history": "历史记录", + "cost": "成本", + "report_button": "报告", + "report_title": "维护报告", + "report_generated": "生成于", + "report_times_done": "完成次数", + "report_total_cost": "总费用", + "report_every": "每 {n} {unit}", + "report_notes": "备注", + "report_col_type": "类型", + "report_col_status": "状态", + "report_col_schedule": "计划", + "duration": "耗时", + "both": "两者", + "trigger_val": "触发值", + "complete_title": "完成: ", + "checklist": "检查清单", + "checklist_steps_optional": "检查步骤 (可选)", + "checklist_placeholder": "清理过滤器\n更换密封圈\n测试压力", + "checklist_help": "每行一个步骤。最多 100 项。", + "err_too_long": "{field}: 太长 (最多 {n} 个字符)", + "err_too_short": "{field}: 太短 (最少 {n} 个字符)", + "err_value_too_high": "{field}: 太大 (最大 {n})", + "err_value_too_low": "{field}: 太小 (最小 {n})", + "err_required": "{field}: 必填项", + "err_wrong_type": "{field}: 类型错误 (预期: {type})", + "err_invalid_choice": "{field}: 无效选项", + "err_invalid_value": "{field}: 无效数值", + "feat_schedule_time": "具体时间调度", + "feat_schedule_time_desc": "任务将在特定时间点变为逾期,而非默认的午夜。", + "schedule_time_optional": "到期时间 (可选, HH:MM)", + "schedule_time_help": "留空 = 午夜 (默认)。基于 HA 时区。", + "at_time": "于", + "notes_optional": "备注 (可选)", + "cost_optional": "成本 (可选)", + "duration_minutes": "耗时 (分钟, 可选)", + "days": "天", + "day": "天", + "today": "今天", + "d_overdue": "天逾期", + "no_tasks": "尚无维护任务。请创建一个维护项开始使用。", + "no_tasks_short": "无任务", + "no_history": "尚无历史记录。", + "show_all": "全部显示", + "cost_duration_chart": "成本与耗时", + "installed": "已安装", + "confirm_delete_object": "确定删除此维护项及其所有任务吗?", + "confirm_delete_task": "确定删除此任务吗?", + "min": "最小", + "max": "最大", + "save": "保存", + "saving": "正在保存…", + "edit_task": "编辑任务", + "new_task": "新建维护任务", + "task_name": "任务名称", + "maintenance_type": "维护类型", + "priority": "优先级", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "低", + "priority_normal": "普通", + "priority_high": "高", + "schedule_type": "计划类型", + "interval_days": "间隔 (天)", + "warning_days": "预警天数", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "上次执行时间 (可选)", + "interval_anchor": "计划锚点", + "anchor_completion": "从完成日期起算", + "anchor_planned": "从计划日期起算 (无偏差)", + "edit_object": "编辑维护项", + "name": "名称", + "manufacturer_optional": "制造商 (可选)", + "model_optional": "型号 (可选)", + "serial_number_optional": "序列号 (可选)", + "serial_number_label": "序列号", + "documentation_url_label": "手册", + "object_notes_label": "备注", + "sort_due_date": "到期时间", + "sort_object": "维护项名称", + "sort_type": "类型", + "sort_task_name": "任务名称", + "all_objects": "所有维护项", + "tasks_lower": "任务", + "no_tasks_yet": "尚无任务", + "add_first_task": "添加首个任务", + "trigger_configuration": "触发器配置", + "entity_id": "实体 ID", + "comma_separated": "逗号分隔", + "entity_logic": "实体逻辑", + "entity_logic_any": "任一实体触发", + "entity_logic_all": "所有实体均触发", + "entities": "实体", + "attribute_optional": "属性 (可选, 留空 = 状态)", + "use_entity_state": "使用实体状态 (不使用属性)", + "trigger_above": "高于此值触发", + "trigger_below": "低于此值触发", + "for_at_least_minutes": "持续至少 (分钟)", + "safety_interval_days": "安全间隔 (天, 可选)", + "safety_interval": "安全间隔 (可选)", + "delta_mode": "增量模式", + "from_state_optional": "起始状态 (可选)", + "to_state_optional": "目标状态 (可选)", + "documentation_url_optional": "文档链接 (可选)", + "object_notes_optional": "备注 (可选)", + "nfc_tag_id_optional": "NFC 标签 ID (可选)", + "nfc_tags_empty_help": "Home Assistant 中尚未注册任何 NFC 标签。", + "nfc_tags_open_settings": "打开标签设置", + "nfc_tags_refresh": "刷新", + "environmental_entity_optional": "环境传感器 (可选)", + "environmental_entity_helper": "例如:sensor.outdoor_temperature — 根据环境条件自动调整间隔", + "environmental_attribute_optional": "环境属性 (可选)", + "nfc_tag_id": "NFC 标签 ID", + "nfc_linked": "NFC 标签已链接", + "nfc_link_hint": "点击链接 NFC 标签", + "responsible_user": "负责人", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(未分配)", + "all_users": "所有用户", + "my_tasks": "我的任务", + "tab_calendar": "日历", + "cal_no_events": "无维护事项", + "cal_window_7": "7 天", + "cal_window_14": "14 天", + "cal_window_30": "30 天", + "cal_window_365": "1 年", + "cal_every_n_days": "每 {n} 天", + "cal_source_time": "基于时间", + "cal_source_time_adaptive": "基于时间 (自适应)", + "cal_source_sensor": "基于传感器", + "cal_predicted": "预测", + "cal_confidence_high": "高置信度", + "cal_confidence_medium": "中置信度", + "cal_confidence_low": "低置信度", + "budget_monthly": "月度预算", + "budget_yearly": "年度预算", + "groups": "分组", + "new_group": "新建分组", + "edit_group": "编辑分组", + "no_groups": "尚无分组", + "delete_group": "删除分组", + "delete_group_confirm": "确定删除分组 '{name}' 吗?", + "group_select_tasks": "选择任务", + "group_name_required": "名称必填", + "description_optional": "描述 (可选)", + "selected": "已选择", + "loading_chart": "正在加载图表数据...", + "hide_outliers": "隐藏离群值(传感器故障)", + "was_maintenance_needed": "此次维护是否确实需要?", + "feedback_needed": "需要", + "feedback_not_needed": "不需要", + "feedback_not_sure": "不确定", + "suggested_interval": "建议间隔", + "apply_suggestion": "应用建议", + "reanalyze": "重新分析", + "reanalyze_result": "新分析结果", + "reanalyze_insufficient_data": "数据不足,无法生成建议", + "data_points": "个数据点", + "dismiss_suggestion": "忽略", + "confidence_low": "低", + "confidence_medium": "中", + "confidence_high": "高", + "recommended": "推荐", + "seasonal_awareness": "季节性感知", + "edit_seasonal_overrides": "编辑季节性修正系数", + "seasonal_overrides_title": "季节性修正系数 (覆盖)", + "seasonal_overrides_hint": "每月系数 (0.1–5.0)。留空则自动学习。", + "seasonal_override_invalid": "无效数值", + "seasonal_override_range": "系数必须介于 0.1 到 5.0 之间", + "clear_all": "全部清除", + "seasonal_chart_title": "季节性因素", + "seasonal_learned": "自动学习", + "seasonal_manual": "手动设置", + "month_jan": "一月", + "month_feb": "二月", + "month_mar": "三月", + "month_apr": "四月", + "month_may": "五月", + "month_jun": "六月", + "month_jul": "七月", + "month_aug": "八月", + "month_sep": "九月", + "month_oct": "十月", + "month_nov": "十一月", + "month_dec": "十二月", + "sensor_prediction": "传感器预测", + "degradation_trend": "趋势", + "trend_rising": "上升", + "trend_falling": "下降", + "trend_stable": "稳定", + "trend_insufficient_data": "数据不足", + "days_until_threshold": "距离阈值天数", + "threshold_exceeded": "已超过阈值", + "environmental_adjustment": "环境因子", + "sensor_prediction_urgency": "传感器预测约 {days} 天后达到阈值", + "day_short": "天", + "weibull_reliability_curve": "韦伯可靠性曲线", + "weibull_failure_probability": "故障概率", + "weibull_r_squared": "拟合度 R²", + "beta_early_failures": "早期失效", + "beta_random_failures": "随机失效", + "beta_wear_out": "耗损失效", + "beta_highly_predictable": "高度可预测", + "confidence_interval": "置信区间", + "confidence_conservative": "保守", + "confidence_aggressive": "进取", + "current_interval_marker": "当前间隔", + "recommended_marker": "推荐", + "characteristic_life": "特征寿命", + "chart_mini_sparkline": "趋势走势图", + "chart_history": "成本与时长历史", + "chart_seasonal": "12个月季节性因素图表", + "chart_weibull": "韦伯可靠性曲线图表", + "chart_sparkline": "传感器触发值图表", + "days_progress": "天数进度", + "qr_code": "二维码", + "qr_generating": "正在生成二维码…", + "qr_error": "无法生成二维码。", + "qr_error_no_url": "未配置 HA URL。请前往“设置 → 系统 → 网络”设置外部或内部 URL。", + "save_error": "保存失败。请重试。", + "qr_print": "打印", + "qr_download": "下载 SVG", + "qr_action": "扫码后动作", + "qr_action_view": "查看维护信息", + "qr_action_complete": "标记维护为已完成", + "qr_url_mode": "链接类型", + "qr_mode_companion": "Companion App", + "qr_mode_local": "本地 (mDNS)", + "qr_mode_server": "服务器 URL", + "overview": "概览", + "analysis": "分析", + "recent_activities": "近期活动", + "search_notes": "搜索备注", + "avg_cost": "平均成本", + "no_advanced_features": "未启用高级功能", + "no_advanced_features_hint": "请在集成设置中启用“自适应间隔”或“季节性模式”以在此查看分析数据。", + "analysis_not_enough_data": "暂无足够的分析数据。", + "analysis_not_enough_data_hint": "韦伯分析需要至少 5 次完成记录;季节性模式在每月有 6 个以上数据点后可见。", + "analysis_manual_task_hint": "无间隔的手动任务不会生成分析数据。", + "completions": "次完成记录", + "current": "当前", + "shorter": "较短", + "longer": "较长", + "normal": "正常", + "disabled": "已禁用", + "compound_logic": "组合逻辑", + "compound": "组合(多个条件)", + "compound_logic_and": "AND — 所有条件都需满足", + "compound_logic_or": "OR — 任一条件即可", + "compound_help": "将多个传感器条件组合成一个触发器。", + "compound_no_conditions": "还没有条件 — 至少添加一个。", + "compound_add_condition": "添加条件", + "compound_condition": "条件", + "compound_remove_condition": "删除条件", + "card_title": "标题", + "card_show_header": "显示统计信息页眉", + "card_show_actions": "显示操作按钮", + "card_compact": "紧凑模式", + "card_max_items": "最大显示项 (0 = 全部)", + "card_filter_status": "按状态过滤", + "card_filter_status_help": "留空则显示所有状态。", + "card_filter_objects": "按维护项过滤", + "card_filter_objects_help": "留空则显示所有维护项。", + "card_filter_entities": "按实体过滤 (entity_ids)", + "card_filter_entities_help": "选择该集成的传感器或二进制传感器实体。留空则显示全部。", + "card_loading_objects": "正在加载维护项…", + "card_load_error": "无法加载维护项 — 请检查 WebSocket 连接。", + "card_no_tasks_title": "暂无维护任务", + "card_no_tasks_cta": "→ 请前往维护面板创建", + "no_objects": "暂无维护项。", + "action_error": "操作失败。请重试。", + "area_id_optional": "区域 (可选)", + "installation_date_optional": "安装日期 (可选)", + "warranty_expiry_optional": "保修到期 (可选)", + "warranty": "保修", + "warranty_valid_until": "有效期至 {date}", + "warranty_expires_in": "{days} 天后到期", + "warranty_expired": "已过期", + "cal_past_windows": "过去的窗口", + "cal_forward_windows": "未来的窗口", + "history_edit_title": "编辑历史记录", + "history_edit_timestamp": "时间戳", + "manufacturer": "制造商", + "model": "型号", + "area": "区域", + "actions": "操作", + "view_mode_label": "视图", + "view_cards": "卡片视图", + "view_table": "表格视图", + "objects_table_columns_label": "对象表格列", + "objects_table_columns_hint": "选择对象表格视图中显示的列。", + "custom_icon_optional": "图标 (可选,例如 mdi:wrench)", + "task_enabled": "任务已启用", + "skip_reason_prompt": "跳过此任务?", + "reason_optional": "原因 (可选)", + "reset_date_prompt": "标记任务为已执行?", + "reset_date_optional": "最后执行日期 (可选,默认为今天)", + "notes_label": "备注", + "documentation_label": "文档", + "no_nfc_tag": "— 无标签 —", + "dashboard": "仪表盘", + "tab_today": "今天", + "palette_placeholder": "搜索对象和任务…", + "palette_no_results": "无匹配", + "palette_hint": "↑↓ 导航 · Enter 打开 · Esc 关闭", + "today_all_caught_up": "全部完成!本周没有待办。", + "today_overdue": "逾期", + "today_due_today": "今天到期", + "today_this_week": "本周", + "settings": "设置", + "settings_features": "高级功能", + "settings_features_desc": "启用或禁用高级功能。禁用将从界面隐藏相关功能,但不会删除数据。", + "feat_adaptive": "自适应计划", + "feat_adaptive_desc": "根据维护历史学习最佳间隔", + "feat_predictions": "传感器预测", + "feat_predictions_desc": "根据传感器损耗趋势预测触发日期", + "feat_seasonal": "季节性调整", + "feat_seasonal_desc": "根据季节性模式自动调整间隔", + "feat_environmental": "环境关联", + "feat_environmental_desc": "将维护间隔与温度或湿度关联", + "feat_budget": "预算追踪", + "feat_budget_desc": "追踪月度及年度维护支出", + "feat_groups": "任务组", + "feat_groups_desc": "将任务组织进逻辑组", + "feat_checklists": "检查清单", + "feat_checklists_desc": "为任务完成提供多步骤操作流程", + "settings_general": "常规", + "settings_default_warning": "默认预警天数", + "settings_panel_enabled": "侧边栏面板", + "settings_panel_title": "侧边栏面板标题", + "settings_notifications": "通知", + "settings_notify_service": "通知服务", + "test_notification": "测试通知", + "send_test": "发送测试", + "testing": "正在发送…", + "test_notification_success": "测试通知已发送", + "test_notification_failed": "测试通知发送失败", + "settings_notify_due_soon": "到期前通知提醒", + "settings_notify_overdue": "超期后通知提醒", + "settings_notify_triggered": "触发时通知提醒", + "settings_interval_hours": "重复提醒间隔 (小时,0 = 仅一次)", + "settings_quiet_hours": "静默时段", + "settings_quiet_start": "开始时间", + "settings_quiet_end": "结束时间", + "settings_max_per_day": "每日最高通知次数 (0 = 不限)", + "settings_bundling": "合并通知", + "settings_bundle_threshold": "合并阈值", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "移动端操作按钮", + "settings_action_complete": "显示“完成”按钮", + "settings_action_skip": "显示“跳过”按钮", + "settings_action_snooze": "显示“稍后”按钮", + "settings_weekly_digest": "每周摘要", + "settings_weekly_digest_hint": "当有待办任务时,周一早上发送一条汇总通知。", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "稍后提醒间隔 (小时)", + "settings_budget": "预算", + "settings_currency": "货币单位", + "settings_budget_monthly": "月度预算", + "settings_budget_yearly": "年度预算", + "settings_budget_alerts": "预算警报", + "settings_budget_threshold": "警报阈值 (%)", + "settings_import_export": "导入 / 导出", + "settings_export_json": "导出 JSON", + "settings_export_yaml": "导出 YAML", + "settings_export_csv": "导出 CSV", + "settings_import_csv": "导入 CSV", + "settings_import_placeholder": "在此粘贴 JSON 或 CSV 内容…", + "settings_import_btn": "导入", + "settings_import_success": "成功导入 {count} 个维护项。", + "settings_export_success": "导出已下载。", + "settings_saved": "设置已保存。", + "settings_include_history": "包含历史记录", + "sort_alphabetical": "字母排序", + "sort_due_soonest": "最近到期", + "sort_task_count": "任务数量", + "sort_area": "区域", + "sort_assigned_user": "负责人", + "sort_group": "任务组", + "groupby_none": "不分组", + "groupby_area": "按区域分组", + "groupby_group": "按分组", + "groupby_user": "按负责人", + "filter_label": "过滤器", + "user_label": "用户", + "sort_label": "排序", + "group_by_label": "分组依据", + "state_value_help": "使用 HA 状态值(通常为小写,例如 \"on\"/\"off\")。保存时大小写会自动规范化。", + "target_changes_help": "触发前需要满足的状态转换次数(默认为 1)。", + "qr_print_title": "打印二维码", + "qr_print_desc": "生成一张包含多个二维码的可打印页面,方便裁剪并贴在设备上。", + "qr_print_load": "加载设备", + "qr_print_filter": "过滤器", + "qr_print_objects": "设备", + "qr_print_actions": "动作", + "qr_print_url_mode": "链接类型", + "qr_print_estimate": "预计二维码数量", + "qr_print_over_limit": "上限为 200,请缩小过滤范围", + "qr_print_generate": "生成二维码", + "qr_print_generating": "正在生成…", + "qr_print_ready": "二维码已就绪", + "qr_print_print_button": "打印", + "qr_print_empty": "无内容可生成", + "qr_action_skip": "跳过", + "vacation_title": "度假模式", + "vacation_active": "已激活", + "vacation_ended": "已结束", + "vacation_desc": "计划假期:在此时段及设定的缓冲天数内将暂停通知。您可以为特定任务设置例外。", + "vacation_enable": "开启度假模式", + "vacation_start": "开始日期", + "vacation_end": "结束日期", + "vacation_buffer": "缓冲(天)", + "vacation_exempt_title": "度假期间仍保持通知的任务", + "vacation_exempt_desc": "选择在假期中仍需接收通知的任务(例如关键的水池维护)。", + "vacation_load_tasks": "加载任务", + "vacation_preview_btn": "显示预览", + "vacation_preview_affected": "受影响的任务", + "vacation_event_due_soon": "即将到期", + "vacation_event_overdue": "将超期", + "vacation_event_triggered_est": "可能触发传感器", + "vacation_sensor_based": "(基于传感器)", + "vacation_action_notify": "依然通知", + "vacation_action_unsilence": "恢复静默", + "vacation_marked_complete": "已标记为完成", + "vacation_marked_skip": "已跳过", + "vacation_end_now": "立即结束度假", + "add": "添加", + "show_stats": "显示统计与图表", + "hide_stats": "隐藏统计", + "adaptive_no_data": "尚无足够的完成记录用于自适应分析。请完成此任务几次,以解锁间隔建议和可靠性图表。", + "suggestion_applied": "已应用建议间隔", + "vacation_mode": "度假模式", + "vacation_status_active": "当前激活", + "vacation_status_scheduled": "已计划", + "vacation_status_inactive": "未激活", + "vacation_end_now_confirm": "确定立即结束度假吗?", + "vacation_exempt_count": "例外项", + "vacation_advanced": "高级选项…", + "vacation_open_panel": "在面板中打开", + "enable": "启用", + "saved": "已保存", + "budget_monthly_set": "设置月度预算", + "budget_yearly_set": "设置年度预算", + "budget_advanced": "货币、预警…", + "budget_open_panel": "在面板中打开", + "groups_empty": "尚无分组。", + "group_new_placeholder": "添加分组…", + "group_delete_confirm": "确定删除分组 \"{name}\" 吗?", + "groups_manage_tasks": "管理任务分配…", + "groups_open_panel": "在面板中打开", + "unassigned": "未分配", + "no_area": "无区域", + "has_overdue": "有超期任务", + "object": "设备", + "settings_panel_access": "面板访问权限", + "settings_panel_access_desc": "管理员始终拥有完整访问权限。要将创建、编辑和删除权限委派给特定的普通用户,请开启此项并在下方选择他们 — 其他用户将仅能看到“完成”和“跳过”。", + "settings_operator_write": "允许所选用户创建、编辑和删除", + "settings_operator_write_desc": "关闭时:仅管理员可修改内容。开启时:下方所选用户也获得完整访问权限。", + "no_non_admin_users": "未找到普通用户。请在“设置 -> 人员”中添加。", + "owner_label": "所有者", + "feat_completion_actions": "完成动作", + "feat_completion_actions_desc": "为每个任务配置完成时的 HA 动作 + 预设值的快速完成二维码。", + "on_complete_action_title": "完成时:触发 HA 动作(可选)", + "on_complete_action_desc": "任务完成时调用 HA 服务 — 例如重置设备上的物理计数器。", + "on_complete_action_service": "服务", + "on_complete_action_target": "目标实体", + "on_complete_action_target_hint": "注意:实体的域必须与服务匹配 — 例如 'button.press' 仅适用于 button.* 实体,'counter.increment' 仅适用于 counter.* 实体等。如果不匹配,动作将静默失败。", + "on_complete_action_data": "数据(JSON, 可选)", + "on_complete_action_test": "验证配置", + "on_complete_action_test_success": "✓ 配置有效(动作将在任务完成时触发)", + "on_complete_action_test_failed": "失败", + "quick_complete_defaults_title": "快速完成默认值(用于二维码扫描, 可选)", + "quick_complete_defaults_desc": "扫描快速完成二维码时使用的预设值。如果不设置,二维码将打开完成对话框。", + "quick_complete_defaults_notes": "备注", + "quick_complete_defaults_cost": "费用", + "quick_complete_defaults_duration": "耗时(分钟)", + "quick_complete_defaults_feedback_none": "无反馈", + "quick_complete_defaults_feedback_needed": "是必要的", + "quick_complete_defaults_feedback_not_needed": "不必要的", + "quick_complete_success": "已快速标记为完成", + "show_all_objects": "显示所有设备", + "show_all_tasks": "清除过滤 — 显示所有任务", + "filter_to_overdue": "仅显示超期任务", + "filter_to_due_soon": "仅显示即将到期任务", + "filter_to_triggered": "仅显示已触发任务", + "open_task": "打开任务", + "show_details": "显示历史与统计", + "hide_details": "隐藏详情", + "history_empty": "尚无历史记录。", + "history_edit_button": "编辑条目", + "total_cost": "总计费用", + "times_performed": "执行次数", + "older_entries": "更早的条目", + "open_in_panel": "在维护面板中打开", + "skip_reason": "跳过原因(可选)", + "reset_to_date": "重置最后执行日期为", + "delete_task_confirm": "确定删除此任务及其历史记录吗?", + "delete_object_confirm": "确定删除此设备及其所有任务吗?", + "loading": "加载中…", + "archive": "归档", + "undo": "撤销", + "task_archived": "任务已归档", + "object_archived": "对象已归档", + "unarchive": "取消归档", + "archived": "已归档", + "show_archived": "显示已归档", + "hide_archived": "隐藏已归档", + "confirm_archive_object": "归档此对象及其任务?历史记录将保留,以后可恢复。", + "settings_archive": "归档与保留", + "settings_archive_desc": "将已完成的一次性任务归档而不删除。已归档项目会被隐藏且不再活动,但保留其历史记录和费用。", + "settings_archive_oneoff_days": "已完成的一次性任务在多少天后自动归档(0 = 关闭)", + "settings_delete_archived_oneoff_days": "已归档的一次性任务在多少天后自动删除(0 = 永不)", + "archive_object": "归档对象", + "unarchive_object": "取消归档对象", + "documents": "文档", + "documents_empty": "暂无文档。", + "doc_upload": "上传文件", + "doc_uploading": "上传中…", + "doc_add_link": "添加链接", + "doc_link_url": "网址 (https://…)", + "doc_link_title": "标题(可选)", + "doc_open": "打开", + "doc_delete_confirm": "删除“{name}”?", + "doc_too_large": "文件过大(最大 25 MB)。", + "doc_upload_failed": "上传失败。", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "已在别处存储——共享,不占用额外空间。", + "doc_dup_in_object": "此文件已附加到该对象。", + "doc_link_invalid": "仅允许 http/https 链接。", + "doc_cat_manual": "手册", + "doc_cat_warranty": "保修", + "doc_cat_invoice": "发票", + "doc_cat_spare_parts": "备件", + "doc_cat_photo": "照片", + "doc_cat_other": "其他", + "doc_link_badge": "链接", + "doc_storage_title": "文档存储", + "doc_storage_saved": "通过去重节省", + "doc_storage_refresh": "刷新", + "doc_download": "下载", + "doc_close": "关闭", + "doc_camera": "拍照", + "doc_drop_hint": "将文件拖放到此处", + "doc_task_none": "没有与此任务关联的文档。", + "doc_link_existing": "关联文档…", + "doc_attach": "关联", + "doc_unlink": "取消关联", + "doc_page": "页码", + "chart_range_7d": "7天", + "chart_range_30d": "30天", + "chart_range_90d": "90天", + "chart_range_1y": "1年", + "chart_since_service": "自上次维护以来", + "chart_no_stats": "此实体没有长期统计数据 — 仅显示维护记录中的数值", + "auto_complete_on_recovery": "传感器恢复时自动完成", + "auto_complete_on_recovery_help": "当触发条件自行解除时(如已加盐、已更换滤芯),记录一次完成并更新上次执行日期。", + "doc_search": "搜索文档…", + "doc_search_none": "没有匹配的文档", + "link_device_optional": "关联到现有设备(可选)", + "parent_object_optional": "父对象(可选)", + "parent_none": "(无父对象)", + "paused": "已暂停", + "pause_object": "暂停", + "resume_object": "恢复", + "pause_until_prompt": "冻结此对象的日程——在恢复之前不会有任何到期或通知。可选设置自动恢复日期。", + "pause_until_label": "恢复日期(可选)", + "object_paused": "对象已暂停", + "object_resumed": "对象已恢复——日程已重新开始", + "object_paused_badge": "已暂停", + "paused_until_label": "至", + "replace_object": "更换…", + "replace_object_prompt": "让此对象退役并创建继任者。历史和费用留在旧对象的归档中;任务和文档转移到新对象,计数器重新开始。", + "replace_name_label": "继任者名称", + "object_replaced": "对象已更换——已创建继任者", + "reading_unit_label": "读数单位(如 kWh、m³)", + "reading_unit_help": "完成此任务时显示在记录值旁边。", + "reading_value_label": "读数值", + "reading_label": "读数", + "settings_templates_label": "模板库", + "settings_templates_hint": "取消勾选你永远不需要的模板——它们会从\"从模板\"选择器(面板和配置流程)中消失。其他一切不变;可随时重新启用。", + "worksheet": "工作单", + "worksheet_scan_view": "扫码打开任务", + "worksheet_scan_complete": "扫码完成", + "worksheet_manual_excerpt": "手册摘录", + "worksheet_pages": "页", + "worksheet_printed": "打印于", + "worksheet_never": "从未" +} diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-calendar-card.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-calendar-card.ts new file mode 100644 index 0000000..e4ef695 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-calendar-card.ts @@ -0,0 +1,586 @@ +/** Maintenance Supporter — Calendar Card. + * + * Standalone Lovelace card extracted from the panel's Calendar tab (v1.5.0+). + * All the visuals: 7/14/30/365 day window chips, per-event source icons + * (clock vs trending-up), prediction-confidence pill, projected recurrences + * at 55% opacity, today-pill highlight, empty-day collapsing in the year view. + * + * Click on an event fires an ``ll-custom`` event with payload + * ``{type: "maintenance-supporter:open-task", entry_id, task_id}``. The + * dashboard-strategy bundle's document-level handler picks that up and + * either opens the task dialog in-place (preferred) or deep-links into the + * panel as a fallback. + * + * Card config: + * + * type: custom:maintenance-supporter-calendar-card + * title: My maintenance calendar # optional + * window_days: 30 # 7 | 14 | 30 | 365 — default 30 + * show_window_chips: true # default true; hide for embedded use + * show_user_filter: true # default true + * user_filter: "" # "" | "current_user" | "" + */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { + buildCalendarBuckets, + buildPastBuckets, + isoDateLocal, + type CalendarEvent, +} from "./helpers/calendar-bucket"; +import { calendarStyles } from "./calendar-styles"; +import { sharedStyles, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded } from "./styles"; +import type { + HomeAssistant, + MaintenanceObjectResponse, + StatisticsResponse, +} from "./types"; + +type WindowDays = 7 | 14 | 30 | 365; +type PastDays = 30 | 90; + +interface CalendarCardConfig { + type: string; + title?: string; + window_days?: WindowDays; + show_window_chips?: boolean; + show_user_filter?: boolean; + user_filter?: string; + /** v2.2.0 — when set, the card renders past N days from history instead + * of forward N days from next_due. Mutually exclusive with window_days. */ + past_days?: PastDays; +} + +export class MaintenanceCalendarCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _config: CalendarCardConfig = { + type: "custom:maintenance-supporter-calendar-card", + }; + @state() private _objects: MaintenanceObjectResponse[] = []; + @state() private _stats: StatisticsResponse | null = null; + @state() private _windowDays: WindowDays = 30; + @state() private _pastDays: PastDays | 0 = 0; // 0 = forward mode + @state() private _userFilter = ""; + @state() private _unsub: (() => void) | null = null; + + private _dataLoaded = false; + private _lastConnection: unknown = null; + + static getConfigElement() { + return document.createElement("maintenance-supporter-calendar-card-editor"); + } + + static getStubConfig() { + // Opinionated default: 30-day rolling window, both controls visible. + return { + type: "custom:maintenance-supporter-calendar-card", + window_days: 30, + show_window_chips: true, + show_user_filter: true, + }; + } + + setConfig(config: CalendarCardConfig): void { + this._config = { ...config }; + if (config.past_days && [30, 90].includes(config.past_days)) { + this._pastDays = config.past_days as PastDays; + } else if (config.window_days && [7, 14, 30, 365].includes(config.window_days)) { + this._windowDays = config.window_days as WindowDays; + this._pastDays = 0; + } + if (typeof config.user_filter === "string") { + this._userFilter = config.user_filter; + } + } + + getCardSize(): number { + return 6; + } + + private get _lang(): string { + return this.hass?.language || "en"; + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + if (this._unsub) { + try { this._unsub(); } catch { /* ignore */ } + this._unsub = null; + } + this._dataLoaded = false; + this._lastConnection = null; + } + + updated(changedProps: Map): void { + super.updated(changedProps); + const lang = this.hass?.language; + if (lang && !isLocaleLoaded(lang)) { + ensureLocale(lang).then(() => this.requestUpdate()); + } + if (changedProps.has("hass") && this.hass) { + if (!this._dataLoaded) { + this._dataLoaded = true; + this._lastConnection = this.hass.connection; + this._loadData(); + this._subscribe(); + } else if (this.hass.connection !== this._lastConnection) { + this._lastConnection = this.hass.connection; + if (this._unsub) { + try { this._unsub(); } catch { /* ignore */ } + this._unsub = null; + } + this._subscribe(); + this._loadData(); + } + } + } + + private async _loadData(): Promise { + try { + const [objResult, statsResult] = await Promise.all([ + this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/objects", + }), + this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/statistics", + }), + ]); + this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects; + this._stats = statsResult as StatisticsResponse; + } catch { + // WS not available yet + } + } + + private async _subscribe(): Promise { + try { + const unsub = await this.hass.connection.subscribeMessage( + (msg: unknown) => { + const data = msg as { objects: MaintenanceObjectResponse[] }; + this._objects = data.objects; + }, + { type: "maintenance_supporter/subscribe" }, + ); + // Detached mid-subscribe → drop the orphaned subscription. + if (!this.isConnected) { + unsub(); + return; + } + this._unsub = unsub; + } catch { + // Subscription failed + } + } + + private _onEventClick(ev: CalendarEvent): void { + // Past events carry a history_timestamp — those open the history-edit + // dialog instead of the task editor. Future / next_due events open + // the task editor as usual. + if (ev.history_timestamp) { + this.dispatchEvent( + new CustomEvent("ll-custom", { + detail: { + type: "maintenance-supporter:edit-history", + entry_id: ev.entry_id, + task_id: ev.task_id, + original_timestamp: ev.history_timestamp, + }, + bubbles: true, + composed: true, + }), + ); + return; + } + this.dispatchEvent( + new CustomEvent("ll-custom", { + detail: { + type: "maintenance-supporter:open-task", + entry_id: ev.entry_id, + task_id: ev.task_id, + }, + bubbles: true, + composed: true, + }), + ); + } + + render() { + if (!this.hass) return nothing; + + const L = this._lang; + const showChips = this._config.show_window_chips !== false; + const showUserFilter = this._config.show_user_filter !== false; + const title = this._config.title; + + let userFilter: string | null = null; + if (this._userFilter) { + // No UserService here (lives in panel only) — current_user resolves + // via hass.user when the user picks "my tasks". + userFilter = this._userFilter === "current_user" + ? (this.hass?.user?.id ?? null) + : this._userFilter; + } + + const today = new Date(); + today.setHours(0, 0, 0, 0); + const isPast = this._pastDays > 0; + const buckets = isPast + ? buildPastBuckets(this._objects, today, this._pastDays, userFilter) + : buildCalendarBuckets(this._objects, today, this._windowDays, userFilter); + + const todayIso = isoDateLocal(today); + // Year view AND past views collapse empty days — past windows can be + // sparse (most days have no maintenance recorded), so listing "no events" + // for 30 rows is noise. + const hideEmptyDays = this._windowDays === 365 || isPast; + const visibleBuckets = hideEmptyDays + ? buckets.filter((b) => b.events.length > 0) + : buckets; + + const renderEvent = (ev: CalendarEvent) => { + const statusClass = `cal-status-${ev.status}`; + const projClass = ev.projected ? "cal-event-projected" : ""; + const overdueLabel = ev.status === "overdue" && ev.days_until_due != null + ? ` (${Math.abs(ev.days_until_due)}d ${t("overdue", L).toLowerCase()})` + : ""; + const recurEvery = ev.projected && ev.interval_days + ? html`${ + ev.interval_unit && ev.interval_unit !== "days" + ? `${ev.interval_days} ${t("unit_" + ev.interval_unit, L)}` + : t("cal_every_n_days", L).replace("{n}", String(ev.interval_days)) + }` + : nothing; + const isSensor = ev.schedule_type === "sensor_based"; + const sourceIcon = isSensor + ? html`` + : html``; + const predictionSubtitle = isSensor && ev.prediction_confidence + && ev.status !== "triggered" && !ev.projected + ? html` + ${t("cal_predicted", L)} · ${t(`cal_confidence_${ev.prediction_confidence}`, L)} + ` + : nothing; + const currencySymbol = this._stats?.budget?.currency_symbol || DEFAULT_CURRENCY_SYMBOL; + // Past mode: ev.history_type is the actual event ('completed', 'skipped', + // 'reset', 'triggered', 'trigger_replaced'). Showing the derived status + // ('OK' for a completion) was confusing — discussion #49 user-feedback: + // *"im Panel sind die alten Auslösungen drin nicht die completion oder + // skips"* — completion events looked like they hadn't happened because + // the badge said "OK" (current task status) not "Completed" (the + // actual past event). In past mode we now label with the event type. + // Forward mode still uses the projected status (overdue/due_soon/etc). + const badgeLabel = ev.history_type + ? t(ev.history_type, L) + : t(ev.status, L); + return html` +
    this._onEventClick(ev)}> + ${sourceIcon} + ${badgeLabel} +
    +
    ${ev.object_name} · ${ev.task_name}${overdueLabel}
    + ${predictionSubtitle} + ${recurEvery} +
    + ${ev.avg_cost != null && ev.avg_cost > 0 + ? html`${ev.avg_cost.toFixed(0)} ${currencySymbol}` + : nothing} +
    + `; + }; + + const renderDayRow = (bucket: { date: string; events: CalendarEvent[] }) => { + const [y, m, d] = bucket.date.split("-").map(Number); + const date = new Date(y, m - 1, d); + const isToday = bucket.date === todayIso; + const weekday = date.toLocaleDateString(L, { weekday: "short" }); + const monthLabel = date.toLocaleDateString(L, { month: "long" }); + return html` +
    +
    + ${weekday} + ${date.getDate()} +
    +
    +
    + ${monthLabel} + ${isToday ? html`${t("today", L)}` : nothing} +
    + ${bucket.events.length === 0 + ? html`
    ${t("cal_no_events", L)}
    ` + : bucket.events.map(renderEvent)} +
    +
    + `; + }; + + return html` + + ${showChips || showUserFilter + ? html` +
    + ${showChips + ? html` +
    + ${[30, 90].map((p) => html` + + `)} +
    + +
    + ${[7, 14, 30, 365].map((w) => html` + + `)} +
    + ` + : nothing} + ${showUserFilter + ? html` + + ` + : nothing} +
    + ` + : nothing} +
    + ${visibleBuckets.length === 0 && hideEmptyDays + ? html`
    ${t("cal_no_events", L)}
    ` + : visibleBuckets.map(renderDayRow)} +
    +
    + `; + } + + static styles = [ + sharedStyles, + calendarStyles, + css` + :host { display: block; } + ha-card { padding: 0; overflow: hidden; } + `, + ]; +} + +// ── Calendar Card Editor ──────────────────────────────────────────────────── +// +// Visual config: pick window_days from a dropdown, toggle the in-card chips +// and user-filter on/off. Same pattern as MaintenanceSupporterCardEditor — +// LitElement with setConfig + dispatched config-changed. + +const WINDOW_DAY_OPTIONS: Array<{ value: WindowDays; label: string }> = [ + { value: 7, label: "Week (7 days)" }, + { value: 14, label: "Fortnight (14 days)" }, + { value: 30, label: "Month (30 days, default)" }, + { value: 365, label: "Year (365 days, empty days collapsed)" }, +]; + +class MaintenanceCalendarCardEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _config: CalendarCardConfig = { + type: "custom:maintenance-supporter-calendar-card", + }; + + setConfig(config: CalendarCardConfig): void { + this._config = { ...config }; + } + + private _valueChanged(key: keyof CalendarCardConfig, value: unknown): void { + const newConfig = { ...this._config, [key]: value } as CalendarCardConfig; + // Drop default-equivalent values so saved YAML stays minimal + if (key === "show_window_chips" && value === true) { + delete (newConfig as unknown as Record).show_window_chips; + } + if (key === "show_user_filter" && value === true) { + delete (newConfig as unknown as Record).show_user_filter; + } + if (key === "title" && (!value || (typeof value === "string" && value.trim() === ""))) { + delete (newConfig as unknown as Record).title; + } + if (key === "user_filter" && value === "") { + delete (newConfig as unknown as Record).user_filter; + } + this._config = newConfig; + this.dispatchEvent( + new CustomEvent("config-changed", { + detail: { config: newConfig }, + bubbles: true, + composed: true, + }), + ); + } + + render() { + const currentWindow = this._config.window_days ?? 30; + const showChips = this._config.show_window_chips !== false; + const showUserFilter = this._config.show_user_filter !== false; + const userFilter = this._config.user_filter ?? ""; + const title = this._config.title ?? ""; + return html` +
    +
    + + + this._valueChanged("title", (e.target as HTMLInputElement).value)} + /> +
    +
    + + +
    +
    + + + this._valueChanged( + "show_window_chips", + (e.target as HTMLInputElement).checked, + )} + /> +
    +
    + Hide the chips when the card is embedded in a strategy view that + already serves as the window selector. +
    +
    + + + this._valueChanged( + "show_user_filter", + (e.target as HTMLInputElement).checked, + )} + /> +
    +
    + + +
    +
    + `; + } + + static styles = css` + :host { display: block; padding: 8px 0; } + .editor { display: flex; flex-direction: column; gap: 12px; } + .row { display: flex; flex-direction: column; gap: 4px; } + .row.toggle { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 12px; + } + label { font-weight: 500; color: var(--primary-text-color); font-size: 14px; } + input[type="text"], select { + padding: 8px; + font-size: 14px; + background: var(--card-background-color, white); + color: var(--primary-text-color, black); + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 4px; + } + .hint { + margin-top: -4px; + font-size: 12px; + color: var(--secondary-text-color, #666); + } + `; +} + +if (!customElements.get("maintenance-supporter-calendar-card")) { + customElements.define( + "maintenance-supporter-calendar-card", + MaintenanceCalendarCard, + ); +} +if (!customElements.get("maintenance-supporter-calendar-card-editor")) { + customElements.define( + "maintenance-supporter-calendar-card-editor", + MaintenanceCalendarCardEditor, + ); +} + +// Register with HACS / customCards so the picker shows it +const w = window as unknown as { + customCards?: Array<{ + type: string; + name: string; + description: string; + preview?: boolean; + }>; +}; +w.customCards = w.customCards || []; +// HA's custom-card resolver maps ``custom:X`` → element tag ``X``. Our element +// is registered as ``maintenance-supporter-calendar-card``, so the customCards +// type MUST match that exact suffix or the card-picker entry resolves to a +// non-existent element and the strategy's calendar mode throws a config error. +const CALENDAR_CARD_TYPE = "maintenance-supporter-calendar-card"; +const alreadyRegistered = w.customCards.some( + (c) => c.type === CALENDAR_CARD_TYPE, +); +if (!alreadyRegistered) { + w.customCards.push({ + type: CALENDAR_CARD_TYPE, + name: "Maintenance Supporter — Calendar", + description: + "Rolling calendar of maintenance tasks with 7/14/30/365 day windows, source icons, and prediction-confidence pills.", + preview: true, + }); +} + +export {}; diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-card-editor.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-card-editor.ts new file mode 100644 index 0000000..6c9c07d --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-card-editor.ts @@ -0,0 +1,278 @@ +/** Card editor for the Maintenance Supporter Lovelace card. */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { t } from "./styles"; +import type { HomeAssistant, CardConfig, MaintenanceObjectResponse } from "./types"; + +const STATUS_KEYS = ["overdue", "triggered", "due_soon", "ok"] as const; + +export class MaintenanceSupporterCardEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _config: CardConfig = { type: "custom:maintenance-supporter-card" }; + @state() private _objects: MaintenanceObjectResponse[] = []; + @state() private _loadingObjects = true; + @state() private _loadError = false; + + private _objectsLoaded = false; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + setConfig(config: CardConfig): void { + this._config = { ...config }; + } + + updated(changedProps: Map): void { + super.updated(changedProps); + if (changedProps.has("hass") && this.hass && !this._objectsLoaded) { + this._objectsLoaded = true; + this._loadObjects(); + } + } + + private async _loadObjects(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/objects", + }) as { objects: MaintenanceObjectResponse[] }; + this._objects = result.objects || []; + this._loadError = false; + } catch { + this._objects = []; + this._loadError = true; + } + this._loadingObjects = false; + } + + private _valueChanged(key: string, value: unknown): void { + const newConfig = { ...this._config, [key]: value }; + // Drop empty arrays so the saved YAML stays clean + if (Array.isArray(value) && value.length === 0) { + delete (newConfig as Record)[key]; + } + this._config = newConfig; + this.dispatchEvent( + new CustomEvent("config-changed", { detail: { config: newConfig } }) + ); + } + + private _toggleStatus(status: string, on: boolean): void { + const current = new Set(this._config.filter_status || []); + if (on) current.add(status); else current.delete(status); + this._valueChanged("filter_status", [...current]); + } + + private _toggleObject(name: string, on: boolean): void { + const current = new Set(this._config.filter_objects || []); + if (on) current.add(name); else current.delete(name); + this._valueChanged("filter_objects", [...current]); + } + + private _onEntitiesChanged = (e: CustomEvent<{ value: string[] }>): void => { + this._valueChanged("entity_ids", e.detail.value || []); + }; + + render() { + const L = this._lang; + const selectedStatuses = new Set(this._config.filter_status || []); + const selectedObjects = new Set(this._config.filter_objects || []); + const objectNames = [...this._objects] + .map((o) => o.object.name) + .sort((a, b) => a.localeCompare(b)); + // Build the list of OUR sensor + binary_sensor entity_ids so the + // ha-entities-picker only shows maintenance_supporter entities, not + // every sensor in HA. + const ourEntities: string[] = []; + for (const o of this._objects) { + for (const t of o.tasks) { + if (t.sensor_entity_id) ourEntities.push(t.sensor_entity_id); + if (t.binary_sensor_entity_id) ourEntities.push(t.binary_sensor_entity_id); + } + } + + return html` +
    + + this._valueChanged("title", (e.target as HTMLInputElement).value)} + > + + +
    +
    ${t("card_filter_status", L)}
    +
    + ${STATUS_KEYS.map((s) => html` + + `)} +
    +
    ${t("card_filter_status_help", L)}
    +
    + + +
    +
    ${t("card_filter_objects", L)}
    + ${this._loadingObjects + ? html`
    ${t("card_loading_objects", L)}
    ` + : this._loadError + ? html`
    ${t("card_load_error", L)}
    ` + : objectNames.length === 0 + ? html`
    ${t("no_objects", L)}
    ` + : html` +
    + ${objectNames.map((name) => html` + + `)} +
    +
    ${t("card_filter_objects_help", L)}
    + ` + } +
    + + +
    +
    ${t("card_filter_entities", L)}
    + +
    ${t("card_filter_entities_help", L)}
    +
    + + + + this._valueChanged("show_header", (e.target as HTMLInputElement).checked)} + > + + + + + this._valueChanged("show_actions", (e.target as HTMLInputElement).checked)} + > + + + + + this._valueChanged("compact", (e.target as HTMLInputElement).checked)} + > + + + + this._valueChanged("max_items", parseInt((e.target as HTMLInputElement).value, 10) || 0)} + > + ${nothing} +
    + `; + } + + static styles = css` + .editor { + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; + } + ha-textfield { display: block; } + .field { + display: flex; + flex-direction: column; + gap: 6px; + } + .field-label { + font-size: 13px; + color: var(--secondary-text-color); + font-weight: 500; + } + .field-help { + font-size: 12px; + color: var(--secondary-text-color); + font-style: italic; + } + .field-help.error-text { + color: var(--error-color, #f44336); + font-style: normal; + } + .chip-row { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + .chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border: 1px solid var(--divider-color); + border-radius: 14px; + cursor: pointer; + font-size: 13px; + user-select: none; + transition: background 0.15s, border-color 0.15s; + } + .chip:hover { + background: var(--secondary-background-color); + } + .chip.active { + background: var(--primary-color); + color: var(--text-primary-color); + border-color: var(--primary-color); + } + .chip input { + display: none; + } + .object-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 4px 12px; + padding: 6px 0; + } + .object-row { + display: flex; + align-items: center; + gap: 6px; + padding: 3px 0; + font-size: 13px; + cursor: pointer; + } + .object-row input { cursor: pointer; } + `; +} + +// Module-bottom registration so esbuild's tree-shaker doesn't drop the +// element class when the only reference looks "type-only" (the +// @customElement decorator pattern triggered exactly that bug for the +// dialog components — same fix applied here for the card editor). +if (!customElements.get("maintenance-supporter-card-editor")) { + customElements.define( + "maintenance-supporter-card-editor", + MaintenanceSupporterCardEditor, + ); +} diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-card.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-card.ts new file mode 100644 index 0000000..4e49e4e --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-card.ts @@ -0,0 +1,419 @@ +/** Maintenance Supporter Lovelace Card. */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded } from "./styles"; +import type { + HomeAssistant, + MaintenanceObjectResponse, + MaintenanceTask, + StatisticsResponse, + CardConfig, +} from "./types"; +import "./maintenance-card-editor"; +import "./components/complete-dialog"; +import { + openCreateObjectDialog, + openCreateTaskDialog, + openTaskQuickActions, +} from "./dialog-mount"; + +interface FlatTask { + entry_id: string; + object_name: string; + task: MaintenanceTask; +} + +export class MaintenanceSupporterCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _config: CardConfig = { type: "custom:maintenance-supporter-card" }; + @state() private _objects: MaintenanceObjectResponse[] = []; + @state() private _stats: StatisticsResponse | null = null; + @state() private _unsub: (() => void) | null = null; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + static getConfigElement() { + return document.createElement("maintenance-supporter-card-editor"); + } + + static getStubConfig() { + // Opinionated default: when a user picks "Maintenance Supporter" from the + // card picker, show only the actionable tasks (overdue + triggered + due + // soon, max 10) — not the full task list which is overwhelming on first + // add. The user can broaden the filter via the editor afterwards. + return { + type: "custom:maintenance-supporter-card", + show_header: true, + show_actions: true, + filter_status: ["overdue", "triggered", "due_soon"], + max_items: 10, + }; + } + + setConfig(config: CardConfig): void { + this._config = config; + } + + getCardSize(): number { + return 3; + } + + private _dataLoaded = false; + private _lastConnection: unknown = null; + + connectedCallback(): void { + super.connectedCallback(); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + if (this._unsub) { + this._unsub(); + this._unsub = null; + } + this._dataLoaded = false; + this._lastConnection = null; + } + + updated(changedProps: Map): void { + super.updated(changedProps); + const lang = this.hass?.language; + if (lang && !isLocaleLoaded(lang)) { + ensureLocale(lang).then(() => this.requestUpdate()); + } + if (changedProps.has("hass") && this.hass) { + if (!this._dataLoaded) { + this._dataLoaded = true; + this._lastConnection = this.hass.connection; + this._loadData(); + this._subscribe(); + } else if (this.hass.connection !== this._lastConnection) { + this._lastConnection = this.hass.connection; + if (this._unsub) { + try { this._unsub(); } catch { /* ignore */ } + this._unsub = null; + } + this._subscribe(); + this._loadData(); + } + } + } + + private async _loadData(): Promise { + try { + const [objResult, statsResult] = await Promise.all([ + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/objects" }), + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/statistics" }), + ]); + this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects; + this._stats = statsResult as StatisticsResponse; + } catch { + // WS not available yet + } + } + + private async _subscribe(): Promise { + try { + const unsub = await this.hass.connection.subscribeMessage( + (msg: unknown) => { + const data = msg as { objects: MaintenanceObjectResponse[] }; + this._objects = data.objects; + }, + { type: "maintenance_supporter/subscribe" } + ); + // Detached mid-subscribe → drop the orphaned subscription. + if (!this.isConnected) { + unsub(); + return; + } + this._unsub = unsub; + } catch { + // Subscription failed + } + } + + private get _flatTasks(): FlatTask[] { + const tasks: FlatTask[] = []; + const { + filter_status, + filter_objects, + entity_ids, + filter_due_min_days, + filter_due_max_days, + max_items, + } = this._config; + const entityFilter = entity_ids?.length ? new Set(entity_ids) : null; + const hasDueRange = + filter_due_min_days !== undefined || filter_due_max_days !== undefined; + + for (const obj of this._objects) { + if (filter_objects?.length && !filter_objects.includes(obj.object.name)) continue; + for (const task of obj.tasks) { + // Completed one-time tasks ("done") are hidden from the active list. + if (task.is_done) continue; + // v2.10.0: archived tasks (and tasks of an archived object) are inert — + // never shown on the Lovelace card. + if (task.archived || obj.object.archived) continue; + if (filter_status?.length && !filter_status.includes(task.status)) continue; + // entity_ids: HA-native filter — match the task's sensor or + // binary_sensor entity_id. Both fields come pre-resolved from the + // backend WS response (see _build_task_summary in websocket/__init__.py). + if (entityFilter) { + const matches = (task.sensor_entity_id && entityFilter.has(task.sensor_entity_id)) + || (task.binary_sensor_entity_id && entityFilter.has(task.binary_sensor_entity_id)); + if (!matches) continue; + } + // due-days range — used by group_by=due_date strategy buckets. + // Tasks without a numeric days_until_due are excluded from any + // ranged view; they show up in unfiltered or status-only views. + if (hasDueRange) { + const days = task.days_until_due; + if (days === null || days === undefined) continue; + if (filter_due_min_days !== undefined && days < filter_due_min_days) continue; + if (filter_due_max_days !== undefined && days > filter_due_max_days) continue; + } + tasks.push({ entry_id: obj.entry_id, object_name: obj.object.name, task }); + } + } + + const order: Record = { overdue: 0, triggered: 1, due_soon: 2, ok: 3 }; + tasks.sort((a, b) => { + const byStatus = (order[a.task.status] ?? 9) - (order[b.task.status] ?? 9); + if (byStatus !== 0) return byStatus; + // Within a status, soonest-due first; tasks without a due date go last. + return (a.task.days_until_due ?? Infinity) - (b.task.days_until_due ?? Infinity); + }); + + if (max_items && max_items > 0) { + return tasks.slice(0, max_items); + } + return tasks; + } + + private _onCompleted = async (): Promise => { + await this._loadData(); + }; + + /** Open the per-task quick-actions dialog (Complete / Skip / Reset / Edit / + * QR / Delete) — full per-task panel parity. Mounted on document.body via + * the shared dialog-mount helper, so the card works on any dashboard + * without depending on the strategy bundle's ll-custom handler. */ + private _openTaskDetail(entryId: string, taskId: string): void { + openTaskQuickActions(entryId, taskId); + } + + render() { + const L = this._lang; + const title = this._config.title || t("maintenance", L); + const showHeader = this._config.show_header !== false; + const showActions = this._config.show_actions !== false; + const compact = this._config.compact || false; + const tasks = this._flatTasks; + const s = this._stats; + + return html` + +
    +

    ${title}

    +
    + ${showHeader && s + ? html` +
    + ${s.overdue > 0 ? html`${s.overdue}` : nothing} + ${s.due_soon > 0 ? html`${s.due_soon}` : nothing} + ${s.triggered > 0 ? html`${s.triggered}` : nothing} +
    + ` + : nothing} + ${showActions + ? html` + openCreateObjectDialog()} + > + + + openCreateTaskDialog()} + > + + + ` + : nothing} +
    +
    + ${tasks.length === 0 + ? html`
    +
    ${t("card_no_tasks_title", L)}
    + ${t("card_no_tasks_cta", L)} +
    ` + : html` +
    + ${tasks.map( + ({ entry_id, object_name, task }) => html` +
    this._openTaskDetail(entry_id, task.id)} + title="${t("open_task", L) || "Open task"}"> +
    +
    +
    ${task.name}
    + ${!compact ? html`
    ${object_name} · ${t(task.type, L)}
    ` : nothing} +
    +
    + ${task.days_until_due !== null && task.days_until_due !== undefined + ? task.days_until_due < 0 + ? html`${Math.abs(task.days_until_due)}${L.startsWith("de") ? "T" : "d"}` + : task.days_until_due === 0 + ? t("today", L) + : `${task.days_until_due}${L.startsWith("de") ? "T" : "d"}` + : task.trigger_active + ? "⚡" + : "—"} +
    + ${showActions + ? html` + { + // Stop the row's open-task handler from also firing + e.stopPropagation(); + const dlg = this.shadowRoot!.querySelector("maintenance-complete-dialog") as any; + dlg.entryId = entry_id; + dlg.taskId = task.id; + dlg.taskName = task.name; + dlg.checklist = task.checklist || []; + dlg.adaptiveEnabled = !!task.adaptive_config?.enabled; + dlg.taskType = task.type || ""; + dlg.readingUnit = (task as any).reading_unit || ""; + dlg.lang = L; + dlg.open(); + }} + > + + + ` + : nothing} +
    + ` + )} +
    + `} +
    + + `; + } + + static styles = [ + sharedStyles, + css` + ha-card { overflow: hidden; } + + .card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 16px 8px; + } + + .card-header h1 { margin: 0; font-size: 18px; font-weight: 500; } + .header-right { display: flex; align-items: center; gap: 6px; } + .header-stats { display: flex; gap: 6px; } + .hdr-add { + --mdc-icon-button-size: 32px; + --mdc-icon-size: 20px; + color: var(--primary-color); + } + + .badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 22px; + border-radius: 11px; + font-size: 12px; + font-weight: 600; + color: white; + padding: 0 6px; + } + + .badge.overdue { background: var(--error-color, #f44336); } + .badge.due_soon { background: var(--warning-color, #ff9800); } + .badge.triggered { background: #ff5722; } + + .empty-card { + padding: 24px 16px; + text-align: center; + color: var(--secondary-text-color); + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + } + .empty-link { + color: var(--primary-color); + text-decoration: none; + font-size: 13px; + } + .empty-link:hover { text-decoration: underline; } + .task-list { padding: 0 16px 16px; } + + .task-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid var(--divider-color); + } + .task-item:last-child { border-bottom: none; } + .task-list.compact .task-item { padding: 4px 0; } + /* Row click opens the task editor (in-place via the strategy bundle's + ll-custom handler). Hover state hints that the row is interactive. */ + .task-item.clickable { cursor: pointer; transition: background 0.12s; } + .task-item.clickable:hover { + background: var(--state-icon-color, rgba(255,255,255,0.04)); + } + + .status-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } + .task-info { flex: 1; min-width: 0; } + .task-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .task-meta { font-size: 12px; color: var(--secondary-text-color); } + + .task-due { font-size: 13px; color: var(--secondary-text-color); min-width: 40px; text-align: right; } + .overdue-text { color: var(--error-color); font-weight: 500; } + + .complete-btn { + --mdc-icon-button-size: 32px; + --mdc-icon-size: 18px; + color: var(--primary-color); + } + `, + ]; +} + +// Module-bottom registration so esbuild's tree-shaker doesn't drop the +// element class when the only reference is the @customElement decorator +// (which triggered this exact bug for the dialog components — issue #32: +// "card not showing in the card selector, busy spinner only"). +if (!customElements.get("maintenance-supporter-card")) { + customElements.define("maintenance-supporter-card", MaintenanceSupporterCard); +} + +// Register as custom card so the Lovelace card picker lists it. +(window as any).customCards = (window as any).customCards || []; +(window as any).customCards.push({ + type: "maintenance-supporter-card", + name: "Maintenance Supporter", + description: "Overview of your maintenance tasks with quick actions.", + preview: true, +}); diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-dashboard-strategy.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-dashboard-strategy.ts new file mode 100644 index 0000000..44273fb --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-dashboard-strategy.ts @@ -0,0 +1,1174 @@ +/** Maintenance Supporter — custom dashboard strategy. + * + * Generates a complete Lovelace dashboard from the integration's WS feed. + * Honors a ``group_by`` config so YAML users can pick the layout: + * + * strategy: + * type: custom:maintenance-supporter + * group_by: area # default — one view per area + Unassigned + * + * strategy: + * type: custom:maintenance-supporter + * group_by: status # one view per status (Overdue / Triggered / Due Soon / OK) + * + * strategy: + * type: custom:maintenance-supporter + * group_by: floor # one view per floor with area sub-headings (#155 follow-up) + * + * strategy: + * type: custom:maintenance-supporter + * group_by: due_date # 5 buckets — Overdue / Today / This Week / This Month / Later + * + * Layout pattern follows HA's own ``maintenance-view-strategy``, + * ``areas-dashboard-strategy`` and ``home-overview-view-strategy``: a leading + * "Overview" view of actionable tasks, empty groups skipped, and + * STATE_NOT_RUNNING / recovery_mode handled with the same starting / + * recovery-mode card placeholders HA core uses. + * + * Requires Home Assistant 2026.5+ to appear in the "Add Dashboard" picker + * (that's when ``window.customStrategies`` got picked up by the frontend). + * On older HA versions the registration is a silent no-op. + */ + +interface MaintenanceObjectResp { + entry_id: string; + object: { + id: string; + name: string; + area_id: string | null; + }; + tasks: Array<{ status?: string; days_until_due?: number | null }>; +} + +interface AreaEntry { + area_id: string; + name: string; + icon?: string | null; + floor_id?: string | null; +} + +interface FloorEntry { + floor_id: string; + name: string; + icon?: string | null; + level?: number | null; +} + +interface HassLike { + language?: string; + config?: { + state?: string; + recovery_mode?: boolean; + }; + connection: { + sendMessagePromise(msg: Record): Promise; + }; + areas?: Record; + floors?: Record; +} + +type GroupBy = "area" | "status" | "floor" | "due_date" | "calendar"; + +interface MaintenanceDashboardStrategyConfig { + type: "custom:maintenance-supporter" | "maintenance-supporter"; + group_by?: GroupBy; +} + +interface DashboardConfig { + title?: string; + views: ViewConfig[]; +} + +interface ViewConfig { + title?: string; + path?: string; + icon?: string; + type?: string; + subview?: boolean; + cards?: CardConfig[]; + sections?: SectionConfig[]; + // HA 2026.5+ — banner card above the view (mobile + desktop), and a + // narrow column on the right (large screens only). See + // home-overview-view-strategy.ts for the canonical usage. + header?: { layout?: string; card: CardConfig }; + sidebar?: { + sections: SectionConfig[]; + visibility?: ViewColumnsCondition[]; + content_label?: string; + sidebar_label?: string; + }; + max_columns?: number; +} + +interface ViewColumnsCondition { + condition: "view_columns"; + min?: number; + max?: number; +} + +interface SectionConfig { + type?: string; + cards: CardConfig[]; +} + +interface CardConfig { + type: string; + [key: string]: unknown; +} + +const STRATEGY_TYPE = "maintenance-supporter"; +const STRATEGY_TAG = `ll-strategy-dashboard-${STRATEGY_TYPE}`; +const EDITOR_TAG = "hui-maintenance-supporter-strategy-editor"; + +const STATE_NOT_RUNNING = "NOT_RUNNING"; + +const STATUS_VIEWS: Array<{ + status: string; + title: string; + icon: string; + path: string; +}> = [ + { status: "overdue", title: "Overdue", icon: "mdi:alert-circle", path: "overdue" }, + { status: "triggered", title: "Triggered", icon: "mdi:flash", path: "triggered" }, + { status: "due_soon", title: "Due Soon", icon: "mdi:clock-alert-outline", path: "due-soon" }, + { status: "ok", title: "OK", icon: "mdi:check-circle-outline", path: "ok" }, +]; + +const DUE_DATE_VIEWS: Array<{ + title: string; + icon: string; + path: string; + filter: { filter_due_min_days?: number; filter_due_max_days?: number }; + // Pre-check predicate so we can skip empty buckets without a card render. + matches: (days: number) => boolean; +}> = [ + { + title: "Overdue", + icon: "mdi:alert-circle", + path: "overdue", + filter: { filter_due_max_days: -1 }, + matches: (d) => d <= -1, + }, + { + title: "Today", + icon: "mdi:calendar-today", + path: "today", + filter: { filter_due_min_days: 0, filter_due_max_days: 0 }, + matches: (d) => d === 0, + }, + { + title: "This Week", + icon: "mdi:calendar-week", + path: "this-week", + filter: { filter_due_min_days: 1, filter_due_max_days: 7 }, + matches: (d) => d >= 1 && d <= 7, + }, + { + title: "This Month", + icon: "mdi:calendar-month", + path: "this-month", + filter: { filter_due_min_days: 8, filter_due_max_days: 30 }, + matches: (d) => d >= 8 && d <= 30, + }, + { + title: "Later", + icon: "mdi:calendar-clock", + path: "later", + filter: { filter_due_min_days: 31 }, + matches: (d) => d >= 31, + }, +]; + +function makeCardSection(card: CardConfig): SectionConfig { + return { type: "grid", cards: [card] }; +} + +// Replicate HA core's responsive conditions (see panels/lovelace/strategies/ +// helpers/view-columns-conditions.ts). They use the "view_columns" condition +// rather than raw media queries so they follow whatever breakpoint the user's +// view actually has. +const LARGE_SCREEN_CONDITION: ViewColumnsCondition = { + condition: "view_columns", + min: 2, +}; +const SMALL_SCREEN_CONDITION: ViewColumnsCondition = { + condition: "view_columns", + max: 1, +}; + +// Headline counts come from the global summary sensors — the single source of +// truth shared with the panel KPI chips and the statistics WS endpoint — so the +// numbers can't drift. Rendered live via a markdown template instead of being +// counted client-side at generation time (which froze them until reload). +function kpiMarkdownCard(): CardConfig { + return { + type: "markdown", + text_only: true, + content: [ + "🔴 **{{ states('sensor.maintenance_supporter_overdue') }}** overdue", + "⚡ **{{ states('sensor.maintenance_supporter_triggered') }}** triggered", + "🟡 **{{ states('sensor.maintenance_supporter_due_soon') }}** due soon", + "🟢 **{{ states('sensor.maintenance_supporter_ok') }}** ok", + ].join(" · "), + }; +} + +// Onboarding card shown when there are zero maintenance objects. Pattern +// follows HA core's home-overview-view-strategy empty-state branch. +// +// Two buttons: +// • "Open Maintenance panel" — plain navigate, the dependable path. +// • "Add object" — fire-dom-event with a custom ll-custom payload that +// our document-level handler (see registerLlCustomHandler below) catches +// and turns into a deep-link navigation. This demonstrates the fire-dom +// pattern HA core uses for in-place actions, without requiring us to +// rip our object-add dialog out of the panel into a custom element. +function emptyStateView(): ViewConfig { + return { + title: "Maintenance", + type: "panel", + cards: [ + { + type: "empty-state", + icon: "mdi:wrench-clock", + icon_color: "primary", + content_only: true, + title: "No maintenance objects yet", + content: + "Open the Maintenance panel to add your first object — pool pump, HVAC filter, vehicle, anything that needs scheduled care.", + buttons: [ + { + icon: "mdi:wrench", + text: "Open Maintenance panel", + appearance: "filled", + variant: "brand", + tap_action: { + action: "navigate", + navigation_path: "/maintenance-supporter", + }, + }, + { + icon: "mdi:plus", + text: "Add object", + appearance: "outlined", + variant: "brand", + tap_action: { + action: "fire-dom-event", + ll_custom: { type: "maintenance-supporter:add-object" }, + }, + }, + ], + }, + ], + }; +} + +function overviewView(): ViewConfig { + const kpiCard = kpiMarkdownCard(); + return { + title: "Overview", + icon: "mdi:wrench-clock", + path: "overview", + type: "sections", + // Header runs above the main content — present on every screen size + // so the user always sees the headline counts. Layout "responsive" + // matches HA core's home strategy. + header: { layout: "responsive", card: kpiCard }, + // Sidebar appears only on screens wide enough for ≥2 columns. Mirrors + // the same KPI card so power users with a wide dashboard never lose + // sight of it as they scroll long lists. + sidebar: { + sections: [ + { + type: "grid", + cards: [ + { + type: "heading", + heading: "Status", + heading_style: "title", + }, + kpiCard, + ], + }, + ], + visibility: [LARGE_SCREEN_CONDITION], + content_label: "Tasks", + sidebar_label: "Status", + }, + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_status: ["overdue", "triggered", "due_soon"], + }), + ], + }; +} + +function viewsByArea( + objects: MaintenanceObjectResp[], + areas: Record, +): ViewConfig[] { + const byArea = new Map(); + for (const obj of objects) { + const aid = obj.object.area_id || null; + if (!byArea.has(aid)) byArea.set(aid, []); + byArea.get(aid)!.push(obj); + } + + const areaIds = Array.from(byArea.keys()).filter( + (a): a is string => a !== null, + ); + areaIds.sort((a, b) => { + const na = areas[a]?.name || a; + const nb = areas[b]?.name || b; + return na.localeCompare(nb); + }); + + const views: ViewConfig[] = []; + for (const areaId of areaIds) { + const objs = byArea.get(areaId)!; + if (objs.length === 0) continue; + const areaInfo = areas[areaId]; + views.push({ + title: areaInfo?.name || areaId, + icon: areaInfo?.icon || "mdi:floor-plan", + path: `area-${areaId}`, + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_objects: objs.map((o) => o.object.name), + }), + ], + }); + } + + const unassigned = byArea.get(null); + if (unassigned && unassigned.length > 0) { + views.push({ + title: "Unassigned", + icon: "mdi:help-circle-outline", + path: "unassigned", + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_objects: unassigned.map((o) => o.object.name), + }), + ], + }); + } + + return views; +} + +function viewsByStatus(objects: MaintenanceObjectResp[]): ViewConfig[] { + const present = new Set(); + for (const obj of objects) { + for (const task of obj.tasks || []) { + if (task.status) present.add(task.status); + } + } + + const views: ViewConfig[] = []; + for (const v of STATUS_VIEWS) { + if (!present.has(v.status)) continue; + views.push({ + title: v.title, + icon: v.icon, + path: v.path, + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_status: [v.status], + }), + ], + }); + } + return views; +} + +function viewsByFloor( + objects: MaintenanceObjectResp[], + areas: Record, + floors: Record, +): ViewConfig[] { + // Build {floor_id → [object…]} via area.floor_id lookup; null bucket for + // objects whose area has no floor OR whose object has no area at all. + const byFloor = new Map(); + for (const obj of objects) { + const aid = obj.object.area_id; + const fid = aid ? areas[aid]?.floor_id || null : null; + if (!byFloor.has(fid)) byFloor.set(fid, []); + byFloor.get(fid)!.push(obj); + } + + // Floor sort: by `level` then name (matches HA's own floor sorting). + const floorIds = Array.from(byFloor.keys()).filter( + (f): f is string => f !== null, + ); + floorIds.sort((a, b) => { + const fa = floors[a]; + const fb = floors[b]; + const la = fa?.level ?? 0; + const lb = fb?.level ?? 0; + if (la !== lb) return la - lb; + return (fa?.name || a).localeCompare(fb?.name || b); + }); + + const views: ViewConfig[] = []; + for (const floorId of floorIds) { + const objs = byFloor.get(floorId)!; + if (objs.length === 0) continue; + const floor = floors[floorId]; + views.push({ + title: floor?.name || floorId, + icon: floor?.icon || "mdi:home-floor-1", + path: `floor-${floorId}`, + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_objects: objs.map((o) => o.object.name), + }), + ], + }); + } + + const unassigned = byFloor.get(null); + if (unassigned && unassigned.length > 0) { + views.push({ + title: "Other", + icon: "mdi:help-circle-outline", + path: "other", + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_objects: unassigned.map((o) => o.object.name), + }), + ], + }); + } + + return views; +} + +function viewsByCalendar(_objects: MaintenanceObjectResp[]): ViewConfig[] { + // One view per window. Each is a panel-typed view with a single + // ``maintenance-supporter-calendar`` card pinned to the matching + // window_days. Window-chips inside the card are hidden because the + // tab-bar already serves as the window selector. + const WINDOWS: Array<{ + title: string; + icon: string; + path: string; + window_days: 7 | 14 | 30 | 365; + }> = [ + { title: "Week", icon: "mdi:calendar-week", path: "cal-7", window_days: 7 }, + { title: "Fortnight", icon: "mdi:calendar-week-begin", path: "cal-14", window_days: 14 }, + { title: "Month", icon: "mdi:calendar-month", path: "cal-30", window_days: 30 }, + { title: "Year", icon: "mdi:calendar-clock", path: "cal-365", window_days: 365 }, + ]; + + return WINDOWS.map((w) => ({ + title: w.title, + icon: w.icon, + path: w.path, + type: "panel", + cards: [ + { + type: "custom:maintenance-supporter-calendar-card", + window_days: w.window_days, + show_window_chips: false, + show_user_filter: true, + }, + ], + })); +} + +function viewsByDueDate(objects: MaintenanceObjectResp[]): ViewConfig[] { + // Bucket-presence pre-check so we don't render an empty "Today" tab. + const presentBuckets = new Set(); + for (const obj of objects) { + for (const task of obj.tasks || []) { + const d = task.days_until_due; + if (d === null || d === undefined) continue; + DUE_DATE_VIEWS.forEach((v, i) => { + if (v.matches(d)) presentBuckets.add(i); + }); + } + } + + const views: ViewConfig[] = []; + DUE_DATE_VIEWS.forEach((v, i) => { + if (!presentBuckets.has(i)) return; + views.push({ + title: v.title, + icon: v.icon, + path: v.path, + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + ...v.filter, + }), + ], + }); + }); + return views; +} + +export class MaintenanceDashboardStrategy extends HTMLElement { + static getCreateSuggestions(_hass: HassLike) { + return { + title: "Maintenance Supporter", + icon: "mdi:wrench-clock", + }; + } + + // Lazy-load editor so picker users who never hit "Edit Dashboard" don't + // pay for the LitElement bundle. HA's areas-dashboard-strategy uses the + // same pattern. + static async getConfigElement() { + // The editor element is registered in the same bundle below — just + // construct it. The dynamic import would work too, but with esbuild + // bundling everything into one file there's nothing extra to load. + return document.createElement(EDITOR_TAG); + } + + static async generate( + config: MaintenanceDashboardStrategyConfig | undefined, + hass: HassLike, + ): Promise { + if (hass.config?.state === STATE_NOT_RUNNING) { + return { + views: [ + { type: "sections", sections: [{ cards: [{ type: "starting" }] }] }, + ], + }; + } + if (hass.config?.recovery_mode) { + return { + views: [ + { + type: "sections", + sections: [{ cards: [{ type: "recovery-mode" }] }], + }, + ], + }; + } + + let response: { objects: MaintenanceObjectResp[] }; + try { + response = await hass.connection.sendMessagePromise<{ + objects: MaintenanceObjectResp[]; + }>({ type: "maintenance_supporter/objects" }); + } catch { + return { + title: "Maintenance", + views: [ + { + title: "Maintenance", + cards: [ + { + type: "markdown", + content: + "**Maintenance Supporter** is not loaded. Install/enable the integration first.", + }, + ], + }, + ], + }; + } + + const objects = response.objects || []; + + // Onboarding short-circuit. With zero objects the area / status / floor / + // due_date branches all produce just the Overview view (which shows + // "no tasks") — that's a useless first impression. Show an actionable + // empty-state instead so the user knows where to click next. + if (objects.length === 0) { + return { title: "Maintenance", views: [emptyStateView()] }; + } + + const groupBy: GroupBy = config?.group_by ?? "area"; + + // Record-of-builders pattern from HA core's home-overview-view-strategy + // — each entry is a no-arg lambda producing the per-mode views. Extending + // with a new group_by mode means adding one entry instead of growing an + // if/else chain. + const viewBuilders: Record ViewConfig[]> = { + area: () => viewsByArea(objects, hass.areas || {}), + status: () => viewsByStatus(objects), + floor: () => viewsByFloor(objects, hass.areas || {}, hass.floors || {}), + due_date: () => viewsByDueDate(objects), + calendar: () => viewsByCalendar(objects), + }; + + return { + title: "Maintenance", + views: [ + overviewView(), + ...(viewBuilders[groupBy] ?? viewBuilders.area)(), + ], + }; + } +} + +// ── Editor ────────────────────────────────────────────────────────────────── +// +// Minimal LovelaceStrategyEditor: a single dropdown for ``group_by``. Pattern +// follows HA core's ``hui-areas-dashboard-strategy-editor`` — LitElement that +// exposes setConfig(), holds the current config in @state, and dispatches +// "config-changed" with the new config on user input. +// +// We keep the editor as a plain HTMLElement (no Lit dependency) because the +// strategy file otherwise stays Lit-free. It's enough HTML for one ${options} +
    + The "Overview" view is always first. Empty groups are skipped. +
    + + `; + const select = this.querySelector("#group-by") as HTMLSelectElement | null; + select?.addEventListener("change", () => { + const newConfig: MaintenanceDashboardStrategyConfig = { + ...this._config, + group_by: select.value as GroupBy, + }; + this._config = newConfig; + this.dispatchEvent( + new CustomEvent("config-changed", { + detail: { config: newConfig }, + bubbles: true, + composed: true, + }), + ); + }); + } +} + +// ── Section Strategy ──────────────────────────────────────────────────────── +// +// A standalone section users can drop into ANY view (HA's home dashboard, the +// areas dashboard's per-area view, a custom dashboard) to surface a slice of +// maintenance tasks in context. Mirrors HA core's ``common-controls`` section +// strategy pattern. YAML usage: +// +// sections: +// - strategy: +// type: custom:maintenance-supporter-section +// area_id: kitchen # optional — restrict to one area +// filter_status: # optional — restrict to certain statuses +// - overdue +// - triggered +// title: Kitchen upkeep # optional heading card +// max_items: 5 # optional row limit +// +// At least one filter is recommended; with no filters it shows everything, +// which is what the dashboard strategy already does. + +const SECTION_STRATEGY_TYPE = "maintenance-supporter-section"; +const SECTION_STRATEGY_TAG = `ll-strategy-section-${SECTION_STRATEGY_TYPE}`; + +interface MaintenanceSectionStrategyConfig { + type: "custom:maintenance-supporter-section" | "maintenance-supporter-section"; + area_id?: string; + filter_status?: string[]; + filter_objects?: string[]; + filter_due_min_days?: number; + filter_due_max_days?: number; + title?: string; + max_items?: number; +} + +class MaintenanceSectionStrategy extends HTMLElement { + static async generate( + config: MaintenanceSectionStrategyConfig | undefined, + hass: HassLike, + ): Promise { + const card: CardConfig = { + type: "custom:maintenance-supporter-card", + show_header: false, + }; + + // Pass through optional filters + if (config?.filter_status?.length) card.filter_status = config.filter_status; + if (config?.max_items) card.max_items = config.max_items; + if (config?.filter_due_min_days !== undefined) { + card.filter_due_min_days = config.filter_due_min_days; + } + if (config?.filter_due_max_days !== undefined) { + card.filter_due_max_days = config.filter_due_max_days; + } + + // Resolve area_id → object names via WS (the card filters by name). + // Falls back gracefully if the WS call fails — empty filter means + // "show everything", which still renders something useful. + let names: string[] | undefined = config?.filter_objects; + if (config?.area_id && !names) { + try { + const r = await hass.connection.sendMessagePromise<{ + objects: MaintenanceObjectResp[]; + }>({ type: "maintenance_supporter/objects" }); + names = (r.objects || []) + .filter((o) => o.object.area_id === config.area_id) + .map((o) => o.object.name); + } catch { + // ignore — card will show all + } + } + if (names && names.length > 0) card.filter_objects = names; + + const cards: CardConfig[] = []; + if (config?.title) { + cards.push({ + type: "heading", + heading: config.title, + heading_style: "title", + }); + } + cards.push(card); + + return { type: "grid", cards }; + } +} + +// v2.3.5 (issue #52 follow-up): wrap registration in try/catch + emit a +// banner-style console.log so that when ANY user hits a strategy timeout +// we can see in the browser console exactly which step failed instead of +// guessing. The non-error console.log also serves as a positive load +// signal — if the user doesn't see "[maintenance-supporter] strategy +// bundle loaded" at all, the script never ran (cached index.html / CSP / +// MIME-type problem). If they see the log but still get the timeout, +// something else (Lovelace resource conflict, HACS plugin clash) is +// holding the registry. +const STRATEGY_VERSION = "2.3.6"; +const LOG_PREFIX = "[maintenance-supporter]"; + +function safeDefine(tag: string, ctor: CustomElementConstructor): "ok" | "skipped" | string { + try { + if (customElements.get(tag)) { + return "skipped"; + } + customElements.define(tag, ctor); + return "ok"; + } catch (err) { + return `ERROR: ${String(err)}`; + } +} + +// NOTE: the dashboard tag (STRATEGY_TAG) is intentionally NOT defined here. +// It is owned by the tiny zero-import shim (maintenance-strategy-shim.ts), +// which is what HA loads via frontend_extra_module_url so registration wins +// HA's 5 s whenDefined race even under heavy plugin load. This heavy bundle is +// lazy-loaded by the shim on first generate()/getConfigElement(); the shim +// calls our exported MaintenanceDashboardStrategy.generate(). We still define +// the editor + section strategies here (not on HA's timeout-critical path). +const _defineResults = { + editor: safeDefine(EDITOR_TAG, MaintenanceStrategyEditor), + section: safeDefine(SECTION_STRATEGY_TAG, MaintenanceSectionStrategy), +}; + +console.log( + `${LOG_PREFIX} strategy bundle v${STRATEGY_VERSION} loaded — registrations:`, + _defineResults, + "— run maintenanceSupporterDiagnose() in console for full state", +); + +// Global devtools helper — typing maintenanceSupporterDiagnose() in the +// browser console returns a snapshot of registration state + module URLs +// + Lovelace resources. Easier than asking users to paste a multi-line +// snippet they then can't run for whatever reason. +(window as unknown as { maintenanceSupporterDiagnose?: () => Promise }).maintenanceSupporterDiagnose = + async () => { + const tag = STRATEGY_TAG; + let resources: Array<{ url?: string }> = []; + try { + const ha = document.querySelector("home-assistant") as + | (HTMLElement & { hass?: { connection: { sendMessagePromise(m: Record): Promise } } }) + | null; + if (ha?.hass) { + const r = await ha.hass.connection.sendMessagePromise<{ resources?: Array<{ url?: string }> }>( + { type: "lovelace/resources" }, + ); + resources = r.resources || []; + } + } catch (e) { resources = [{ url: `` }]; } + return { + version: STRATEGY_VERSION, + el_registered: !!customElements.get(tag), + el_class_excerpt: customElements.get(tag)?.toString().slice(0, 100) || null, + customStrategies_dashboard: (window as unknown as { customStrategies?: Array<{ type: string; strategyType: string }> }) + .customStrategies?.filter((s) => s.strategyType === "dashboard").map((s) => s.type) || [], + scriptTagsInPage: Array.from(document.querySelectorAll("script[type=module]")) + .map((s) => (s as HTMLScriptElement).src) + .filter((s) => /maint|strateg/i.test(s)), + lovelaceResources_matching: resources.filter((r) => /maint|strateg/i.test(r.url || "")).map((r) => r.url), + registrations: _defineResults, + }; + }; + +// ── Phase 5: Status Section-Strategies ────────────────────────────────────── +// +// Three small status sections so users with a Lovelace-only setup +// can monitor vacation / budget / groups state without leaving the dashboard. +// v2.4.0 update: each strategy now delegates to a live interactive custom +// card (vacation-section-card / budget-section-card / groups-section-card) +// that lets admins edit values inline. +// +// v2.3.4 (issue #52): the section-card modules are LAZY-loaded inside +// generate() — bundling them at module top blew the strategy file up to +// 509 KB, and HA's 5 s strategy-load timeout was firing on slow connections +// before customElements.define for ll-strategy-dashboard-* could run. Now +// the strategy bundle is tiny (~10 KB) and registers instantly; the heavy +// section-card code only loads when a section actually renders. + +class MaintenanceVacationSectionStrategy extends HTMLElement { + static async generate( + _config: { type: string; title?: string }, + _hass: HassLike, + ): Promise { + await import("./components/vacation-section-card"); + return { + type: "grid", + cards: [ + { + type: "custom:maintenance-vacation-section-card", + title: _config?.title, + }, + ], + }; + } +} + +class MaintenanceBudgetSectionStrategy extends HTMLElement { + static async generate( + _config: { type: string; title?: string }, + _hass: HassLike, + ): Promise { + await import("./components/budget-section-card"); + return { + type: "grid", + cards: [ + { + type: "custom:maintenance-budget-section-card", + title: _config?.title, + }, + ], + }; + } +} + +class MaintenanceGroupsSectionStrategy extends HTMLElement { + static async generate( + _config: { type: string; title?: string }, + _hass: HassLike, + ): Promise { + await import("./components/groups-section-card"); + return { + type: "grid", + cards: [ + { + type: "custom:maintenance-groups-section-card", + title: _config?.title, + }, + ], + }; + } +} + +// Register the three section strategies +const VACATION_TAG = "ll-strategy-section-maintenance-supporter-vacation"; +const BUDGET_TAG = "ll-strategy-section-maintenance-supporter-budget"; +const GROUPS_TAG = "ll-strategy-section-maintenance-supporter-groups"; +if (!customElements.get(VACATION_TAG)) customElements.define(VACATION_TAG, MaintenanceVacationSectionStrategy); +if (!customElements.get(BUDGET_TAG)) customElements.define(BUDGET_TAG, MaintenanceBudgetSectionStrategy); +if (!customElements.get(GROUPS_TAG)) customElements.define(GROUPS_TAG, MaintenanceGroupsSectionStrategy); + +// ── fire-dom-event handler ────────────────────────────────────────────────── +// +// HA's standard ``tap_action: { action: "fire-dom-event", ll_custom: {...} }`` +// dispatches an ``ll-custom`` CustomEvent on the tapped element. We register +// one bubble-phase listener at document level that handles every payload +// whose ``type`` is namespaced with ``maintenance-supporter:`` — currently: +// +// maintenance-supporter:add-object → in-place open of MaintenanceObjectDialog +// maintenance-supporter:open-task → in-place open of MaintenanceTaskDialog +// (with entry_id + task_id in detail) +// +// The dialog components are imported via dialog-mount.ts and mounted lazily +// onto document.body the first time they are needed. If the in-place mount +// fails (e.g. not yet ready), we fall back to deep-linking +// the panel via ?ms_action=… so the user still ends up where they want. + +// v2.3.4 (issue #52): dialog-mount is heavy (pulls in 7 dialog components + +// their Lit runtime). We only need it when the user actually triggers a +// fire-dom-event from a strategy-generated card, so the import is dynamic +// and runs the first time a maintenance-supporter:* event fires. +type DialogMountModule = typeof import("./dialog-mount"); +let _dialogMountPromise: Promise | null = null; +function getDialogMount(): Promise { + if (!_dialogMountPromise) { + // Don't cache a REJECTED import: after a version bump the browser may hold a + // stale strategy bundle whose hashed dialog-mount chunk 404s once; caching + // that rejection would dead-end every dialog action until a full reload. + // Clear the cache on failure so a later attempt (or fixed chunk) can retry. + _dialogMountPromise = import("./dialog-mount").catch((err) => { + _dialogMountPromise = null; + throw err; + }); + } + return _dialogMountPromise; +} + +interface LlCustomEventDetail { + type?: string; + entry_id?: unknown; + task_id?: unknown; + [key: string]: unknown; +} + +const LL_CUSTOM_HANDLER_FLAG = "_msSupporterLlCustomBound"; + +function deepLink(path: string): void { + history.pushState(null, "", path); + window.dispatchEvent(new CustomEvent("location-changed")); +} + +function registerLlCustomHandler(): void { + const w = window as unknown as Record; + if (w[LL_CUSTOM_HANDLER_FLAG]) return; // idempotent — strategy file may load twice + w[LL_CUSTOM_HANDLER_FLAG] = true; + + document.addEventListener("ll-custom", (event: Event) => { + const raw = (event as CustomEvent).detail; + if (!raw || typeof raw !== "object") return; + // HA's `tap_action: { action: "fire-dom-event", ll_custom: {…} }` dispatches + // `ll-custom` with the ENTIRE action config as `detail`, nesting our payload + // under `.ll_custom` (verified live on HA 2026.6 — issue #69: "Add object" + // did nothing because we read `detail.type`, which was undefined). Direct + // dispatchers (the calendar card, the panel) put the payload at the top + // level. Accept both: prefer the nested `ll_custom` object when present. + const nested = (raw as { ll_custom?: unknown }).ll_custom; + const detail = ( + nested && typeof nested === "object" ? nested : raw + ) as LlCustomEventDetail; + if (!detail || typeof detail.type !== "string") return; + if (!detail.type.startsWith("maintenance-supporter:")) return; + const action = detail.type.slice("maintenance-supporter:".length); + + if (action === "deep-link" && typeof detail.path === "string") { + deepLink(detail.path); + return; + } + + // Everything else needs the heavy dialog-mount module — load it lazily. + void (async () => { + let dm: DialogMountModule; + try { + dm = await getDialogMount(); + } catch { + // dialog-mount chunk unavailable (e.g. stale bundle after an update) — + // degrade to opening the panel instead of a silent, unhandled failure. + deepLink("/maintenance-supporter"); + return; + } + + if (action === "add-object") { + if (dm.openCreateObjectDialog()) return; + deepLink("/maintenance-supporter?ms_action=add_object"); + return; + } + + if ( + action === "open-task" && + typeof detail.entry_id === "string" && + typeof detail.task_id === "string" + ) { + if (dm.openTaskQuickActions(detail.entry_id, detail.task_id)) return; + if (dm.openEditTaskDialog(detail.entry_id, detail.task_id)) return; + deepLink( + `/maintenance-supporter?entry_id=${encodeURIComponent(detail.entry_id)}` + + `&task_id=${encodeURIComponent(detail.task_id)}`, + ); + return; + } + + if (action === "open-object" && typeof detail.entry_id === "string") { + if (dm.openObjectQuickActions(detail.entry_id)) return; + deepLink(`/maintenance-supporter?entry_id=${encodeURIComponent(detail.entry_id)}`); + return; + } + + if ( + action === "edit-history" && + typeof detail.entry_id === "string" && + typeof detail.task_id === "string" && + typeof detail.original_timestamp === "string" + ) { + const haRoot = document.querySelector( + "home-assistant", + ) as (HTMLElement & { hass?: { connection: { sendMessagePromise(m: Record): Promise } } }) | null; + const hass = haRoot?.hass; + if (!hass) return; + try { + const r = await hass.connection.sendMessagePromise<{ + tasks?: Array<{ id?: string; history?: Array> }>; + }>({ + type: "maintenance_supporter/object", + entry_id: detail.entry_id, + }); + const task = r.tasks?.find((t) => t.id === detail.task_id); + const histEntry = task?.history?.find( + (h) => h.timestamp === detail.original_timestamp, + ); + if (!histEntry) return; + dm.openHistoryEditDialog({ + entry_id: detail.entry_id as string, + task_id: detail.task_id as string, + original_timestamp: detail.original_timestamp as string, + type: (histEntry.type as string) || "completed", + timestamp: (histEntry.timestamp as string) || (detail.original_timestamp as string), + notes: (histEntry.notes as string) ?? null, + cost: (histEntry.cost as number | undefined) ?? null, + duration: (histEntry.duration as number | undefined) ?? null, + completed_by: (histEntry.completed_by as string) ?? null, + }); + } catch { + deepLink("/maintenance-supporter"); + } + } + })(); + }); +} + +registerLlCustomHandler(); + +// Discovery — picked up by HA 2026.5+. Older HA ignores it silently. +const w = window as unknown as { + customStrategies?: Array<{ + type: string; + strategyType: string; + name: string; + description?: string; + documentationURL?: string; + }>; +}; +w.customStrategies = w.customStrategies || []; + +function registerStrategy(entry: { + type: string; + strategyType: string; + name: string; + description?: string; + documentationURL?: string; +}): void { + const exists = w.customStrategies!.some( + (s) => s.type === entry.type && s.strategyType === entry.strategyType, + ); + if (!exists) w.customStrategies!.push(entry); +} + +registerStrategy({ + type: STRATEGY_TYPE, + strategyType: "dashboard", + name: "Maintenance Supporter", + description: + "Auto-generated dashboard. Group views by area, status, floor, or due date — picked from the strategy editor or YAML.", + documentationURL: + "https://github.com/iluebbe/maintenance_supporter#dashboard-strategy", +}); + +registerStrategy({ + type: SECTION_STRATEGY_TYPE, + strategyType: "section", + name: "Maintenance Supporter — Section", + description: + "Embed maintenance tasks (filterable by area, status, due date) as a section in any dashboard view.", + documentationURL: + "https://github.com/iluebbe/maintenance_supporter#section-strategy", +}); + +// Phase 5 status sections +registerStrategy({ + type: "maintenance-supporter-vacation", + strategyType: "section", + name: "Maintenance Supporter — Vacation Status", + description: "Compact vacation-mode status banner. Tap to open settings in the panel.", +}); +registerStrategy({ + type: "maintenance-supporter-budget", + strategyType: "section", + name: "Maintenance Supporter — Budget Status", + description: "Monthly + yearly maintenance budget overview. Tap for details.", +}); +registerStrategy({ + type: "maintenance-supporter-groups", + strategyType: "section", + name: "Maintenance Supporter — Groups", + description: "List of configured task groups with member counts.", +}); + +export {}; diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts new file mode 100644 index 0000000..688d340 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts @@ -0,0 +1,2903 @@ +/** Maintenance Supporter Sidebar Panel. */ + +import { LitElement, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence } from "./styles"; +import { daysProgress } from "./helpers/interval"; +import { buildObjectReportHtml, type ReportLabels } from "./helpers/report"; +import { warrantyStatus } from "./helpers/warranty"; +import { OBJECT_COLUMNS, DEFAULT_OBJECTS_TABLE_COLUMNS, sanitizeColumns } from "./helpers/object-columns"; +import { downloadTextFile } from "./helpers/download"; +import { buildTaskWorksheetHtml, type WorksheetExcerpt, type WorksheetLabels } from "./helpers/worksheet"; +import { describeWsError } from "./ws-errors"; +import { panelStyles } from "./panel-styles"; +import type { + HomeAssistant, + MaintenanceObjectResponse, + MaintenanceTask, + MaintenanceGroup, + StatisticsResponse, + BudgetStatus, + AdvancedFeatures, + TaskRow, + HistoryEntry, + TriggerConfig, + StatisticsPoint, +} from "./types"; +import { StatisticsService } from "./statistics-service"; +import { UserService } from "./user-service"; +import "./components/object-dialog"; +import type { MaintenanceObjectDialog } from "./components/object-dialog"; +import "./components/documents-section"; +import "./components/task-documents"; +import "./components/task-dialog"; +import type { MaintenanceTaskDialog } from "./components/task-dialog"; +import "./components/complete-dialog"; +import type { MaintenanceCompleteDialog } from "./components/complete-dialog"; +import "./components/qr-dialog"; +import type { MaintenanceQrDialog } from "./components/qr-dialog"; +// v2.0.0: panel uses the extracted Calendar Card instead of its own +// _renderCalendar() method — single source of truth for the calendar view. +import "./maintenance-calendar-card"; +// v2.2.0: in-place edit dialog for past history entries +import "./components/history-edit-dialog"; +import type { + MaintenanceHistoryEditDialog, + HistoryEntryDraft, +} from "./components/history-edit-dialog"; +import "./components/confirm-dialog"; +import type { MaintenanceConfirmDialog } from "./components/confirm-dialog"; +import "./components/settings-view"; +import "./components/storage-section-card"; +import "./components/seasonal-overrides-dialog"; +import type { SeasonalOverridesDialog } from "./components/seasonal-overrides-dialog"; +import "./components/group-dialog"; +import type { MaintenanceGroupDialog } from "./components/group-dialog"; +import { type SparklineContext } from "./renderers/sparkline"; +import { buildCalendarBuckets, isoDateLocal, type CalendarEvent } from "./helpers/calendar-bucket"; +import { renderTriggerProgress, renderMiniSparkline } from "./renderers/progress"; +import { type HistoryContext } from "./renderers/history"; +import { renderTaskDetail, renderUserBadge, type TaskDetailContext } from "./renderers/task-detail"; +import { computeWindow, VIRTUAL_MIN_ROWS } from "./helpers/virtual-window"; + +type View = "overview" | "object" | "task" | "all_objects"; +type SortMode = "due_date" | "object" | "type" | "task_name" | "area" | "assigned_user" | "group"; +type ObjectSortMode = "alphabetical" | "due_soonest" | "task_count"; +type GroupByMode = "none" | "area" | "group" | "user"; + +// Chart dimension constants for mini sparklines (overview) + +@customElement("maintenance-supporter-panel") +export class MaintenanceSupporterPanel extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean, reflect: true }) public narrow = false; + @property({ attribute: false }) public panel: Record = {}; + + @state() private _objects: MaintenanceObjectResponse[] = []; + @state() private _stats: StatisticsResponse | null = null; + @state() private _view: View = "overview"; + @state() private _selectedEntryId: string | null = null; + @state() private _selectedTaskId: string | null = null; + @state() private _filterStatus = ""; + @state() private _filterUser: string | null = null; + @state() private _unsub: (() => void) | null = null; + @state() private _chartRangeDays = (() => { + try { + const v = parseInt(localStorage.getItem("msp-chart-range") || "", 10); + return [7, 30, 90, 365].includes(v) ? v : 30; + } catch { + return 30; + } + })(); + @state() private _hideOutliers = (() => { + try { return localStorage.getItem("msp-chart-hide-outliers") === "1"; } catch { return false; } + })(); + @state() private _historyFilter: string | null = null; + @state() private _budget: BudgetStatus | null = null; + @state() private _groups: Record = {}; + @state() private _detailStatsData: Map = new Map(); + @state() private _miniStatsData: Map = new Map(); + @state() private _features: AdvancedFeatures = { adaptive: false, predictions: false, seasonal: false, environmental: false, budget: false, groups: false, checklists: false, schedule_time: false, completion_actions: false }; + // HA user IDs (UUIDs) granted full panel access despite not being HA admins. + @state() private _adminPanelUserIds: string[] = []; + // v2.8.4: master switch — the allowlist only grants the full panel when this + // is on (default off → every non-admin is read-only, admins unaffected). + @state() private _operatorWriteEnabled = false; + // Default warning_days from the global config entry — used as the initial + // value in the task-create dialog so the Settings → General → "Default + // warning days" choice actually flows through to new tasks. + @state() private _defaultWarningDays = 7; + @state() private _actionLoading = false; + @state() private _moreMenuOpen = false; + @state() private _toastMessage = ""; + @state() private _toastUndo: (() => void) | null = null; + private _toastTimer: ReturnType | null = null; + private _dismissedSuggestions = new Set(); + + // Dashboard redesign state + @state() private _overviewTab: "today" | "dashboard" | "calendar" | "settings" = (() => { + try { + const v = localStorage.getItem("msp-overview-tab"); + return v === "today" || v === "calendar" ? v : "dashboard"; + } catch { return "dashboard"; } + })(); + @state() private _activeTab: "overview" | "history" = "overview"; + @state() private _costDurationToggle: "cost" | "duration" | "both" = "both"; + @state() private _historySearch = ""; + @state() private _sortMode: SortMode = "due_date"; + @state() private _objectSortMode: ObjectSortMode = "alphabetical"; + @state() private _groupByMode: GroupByMode = "none"; + // (#67): All-Objects view mode (cards|table) + the configurable table + // columns sourced from the global setting (sanitised; defaults until loaded). + @state() private _objectViewMode: "cards" | "table" = "cards"; + @state() private _objectsTableColumns: string[] = DEFAULT_OBJECTS_TABLE_COLUMNS; + // v2.10.0: archived tasks/objects are hidden until this toggle is on. + @state() private _showArchived = false; + // v2.15.0: bulk selection on the dashboard task list (multi-select + + // complete/archive). Keys are `${entry_id}:${task_id}`. + @state() private _bulkMode = false; + @state() private _bulkSelected = new Set(); + // Virtualized dashboard task table (large installs): only rows in the + // scroll window are in the DOM; spacers keep the scrollbar honest. The + // window is recomputed from `.content` scroll/resize (rAF-throttled). + @state() private _virtStart = 0; + @state() private _virtEnd = 0; // 0 = uninitialized → initial slice + private _virtRowHeight = 53; + private _virtTotalRows = 0; + private _virtScrollAttached = false; + private _virtRaf = 0; + // v2.15.0: collapsed analysis sections on the task-detail overview tab, + // remembered per section across visits. + @state() private _collapsedSections: Set = (() => { + try { return new Set(JSON.parse(localStorage.getItem("msp-collapsed-sections") || "[]")); } + catch { return new Set(); } + })(); + // v2.15.0: command palette ("/" since 2.18.1 — Ctrl+K clashed with HA's own + // global search) — global fuzzy search over objects + tasks. + @state() private _paletteOpen = false; + @state() private _paletteQuery = ""; + @state() private _paletteActive = 0; + // v2.15.0: template gallery (surfaces the config-flow object templates). + @state() private _templateGalleryOpen = false; + @state() private _templates: Array<{ id: string; name: string; category: string; tasks: unknown[]; disabled?: boolean }> = []; + @state() private _templateCategories: Record = {}; + @state() private _templateBusy = false; + // v1.5.0: Calendar tab state + // v2.0.0: window-days + user-filter state moved into the + // custom element — the panel just + // renders the card and intercepts its ll-custom open-task events. + + private _statsService: StatisticsService | null = null; + private _userService: UserService | null = null; + private _dataLoaded = false; + private _lastConnection: unknown = null; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + /** + * Operator mode = read-only end-user mode (hides every create/edit/delete action). + * + * Gating (full panel only when an exception applies): + * - admins (incl. the owner) always see the full panel + * - a non-admin sees the full panel only when operator-write delegation is + * enabled AND their ID is in the `admin_panel_user_ids` allowlist + * - everyone else sees operator mode (Complete / Skip only) + * + * Delegation defaults OFF, so out of the box every non-admin is read-only. + * The switch + allowlist are managed by an admin under Settings → Panel + * Access (either the panel's Settings tab or the HA config flow), and the + * server mirrors this exact rule in helpers/permissions.user_may_write. + */ + private get _isOperator(): boolean { + const u = this.hass?.user; + if (!u) return true; // pre-hass state: render safe default + if (u.is_admin) return false; + return !(this._operatorWriteEnabled && this._adminPanelUserIds.includes(u.id)); + } + + private _popstateHandler = (e: PopStateEvent) => this._onPopState(e); + + connectedCallback(): void { + super.connectedCallback(); + window.addEventListener("popstate", this._popstateHandler); + window.addEventListener("keydown", this._paletteKeydown); + window.addEventListener("resize", this._onVirtualScroll, { passive: true }); + const saved = localStorage.getItem("maintenance_supporter_sort"); + if (saved && ["due_date", "object", "type", "task_name", "area", "assigned_user", "group"].includes(saved)) { + this._sortMode = saved as SortMode; + } + const savedObj = localStorage.getItem("maintenance_supporter_object_sort"); + if (savedObj && ["alphabetical", "due_soonest", "task_count"].includes(savedObj)) { + this._objectSortMode = savedObj as ObjectSortMode; + } + const savedGroup = localStorage.getItem("maintenance_supporter_groupby"); + if (savedGroup && ["none", "area", "group", "user"].includes(savedGroup)) { + this._groupByMode = savedGroup as GroupByMode; + } + const savedView = localStorage.getItem("maintenance_supporter_object_view"); + if (savedView === "cards" || savedView === "table") { + this._objectViewMode = savedView; + } + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener("popstate", this._popstateHandler); + window.removeEventListener("keydown", this._paletteKeydown); + window.removeEventListener("resize", this._onVirtualScroll); + this.shadowRoot?.querySelector(".content")?.removeEventListener("scroll", this._onVirtualScroll); + this._virtScrollAttached = false; + if (this._virtRaf) cancelAnimationFrame(this._virtRaf); + if (this._unsub) { + this._unsub(); + this._unsub = null; + } + this._dataLoaded = false; + this._lastConnection = null; + this._deepLinkHandled = false; + this._statsService?.clearCache(); + this._statsService = null; + } + + updated(changedProps: Map): void { + super.updated(changedProps); + // Lazy-load the user's UI language (non-EN tables aren't bundled) and + // re-render once it arrives. EN is bundled, so strings read in English + // until then rather than as raw keys. + const lang = this.hass?.language; + if (lang && !isLocaleLoaded(lang)) { + ensureLocale(lang).then(() => this.requestUpdate()); + } + if (changedProps.has("hass") && this.hass) { + if (!this._dataLoaded) { + this._dataLoaded = true; + this._lastConnection = this.hass.connection; + // Seed initial history state so back button returns to overview + history.replaceState({ msp_view: "overview", msp_entry: null, msp_task: null }, ""); + this._loadData(); + this._subscribe(); + } else if (this.hass.connection !== this._lastConnection) { + this._lastConnection = this.hass.connection; + if (this._unsub) { + try { this._unsub(); } catch { /* ignore */ } + this._unsub = null; + } + this._subscribe(); + this._loadData(); + } + + if (!this._statsService) { + this._statsService = new StatisticsService(this.hass); + this._fetchMiniStatsForOverview(); + } else { + this._statsService.updateHass(this.hass); + } + + if (!this._userService) { + this._userService = new UserService(this.hass); + this._userService.getUsers(); + } else { + this._userService.updateHass(this.hass); + } + } + + // Virtualized task table: attach the scroll listener once `.content` + // exists (lit keeps the node stable across renders), then reconcile the + // window with the just-rendered DOM (row-height probe + clamps). Only + // sets state when the window actually moved, so this can't loop. + const content = this.shadowRoot?.querySelector(".content"); + if (content && !this._virtScrollAttached) { + content.addEventListener("scroll", this._onVirtualScroll, { passive: true }); + this._virtScrollAttached = true; + } + this._updateVirtualWindow(); + } + + /** rAF-throttled scroll/resize handler for the virtualized task table. */ + private _onVirtualScroll = (): void => { + if (this._virtRaf) return; + this._virtRaf = requestAnimationFrame(() => { + this._virtRaf = 0; + this._updateVirtualWindow(); + }); + }; + + /** Recompute the rendered window of the virtualized task table from the + * live scroll position. No-op unless the virtual table is in the DOM. */ + private _updateVirtualWindow(): void { + const content = this.shadowRoot?.querySelector(".content"); + const table = this.shadowRoot?.querySelector(".task-table.virtual"); + if (!content || !table) return; + // Probe the real row height from a rendered row (uniform rows; the + // hidden sizer row is height 0 and excluded). + const probe = table.querySelector(".task-row:not(.virt-sizer)"); + if (probe && probe.offsetHeight > 20) this._virtRowHeight = probe.offsetHeight; + const listTop = + table.getBoundingClientRect().top - content.getBoundingClientRect().top + content.scrollTop; + const w = computeWindow({ + scrollTop: content.scrollTop, + viewportHeight: content.clientHeight, + listTop, + rowHeight: this._virtRowHeight, + total: this._virtTotalRows, + }); + if (w.start !== this._virtStart || w.end !== this._virtEnd) { + this._virtStart = w.start; + this._virtEnd = w.end; + } + } + + private async _loadData(): Promise { + const [objResult, statsResult, budgetResult, groupsResult, settingsResult] = await Promise.all([ + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/objects" }).catch(() => null), + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/statistics" }).catch(() => null), + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/budget_status" }).catch(() => null), + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/groups" }).catch(() => null), + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/settings" }).catch(() => null), + ]); + if (objResult) this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects; + if (statsResult) this._stats = statsResult as StatisticsResponse; + if (budgetResult) this._budget = budgetResult as BudgetStatus; + if (groupsResult) this._groups = (groupsResult as { groups: Record }).groups || {}; + if (settingsResult) { + const sr = settingsResult as { + features: AdvancedFeatures; + admin_panel_user_ids?: string[]; + operator_write_enabled?: boolean; + general?: { default_warning_days?: number }; + objects_table_columns?: string[]; + }; + this._features = sr.features; + this._adminPanelUserIds = sr.admin_panel_user_ids || []; + this._operatorWriteEnabled = sr.operator_write_enabled ?? false; + const dwd = sr.general?.default_warning_days; + if (typeof dwd === "number" && dwd >= 0 && dwd <= 365) { + this._defaultWarningDays = dwd; + } + this._objectsTableColumns = sanitizeColumns(sr.objects_table_columns); + } + + // Fetch mini-sparkline data for overview (non-blocking) + this._fetchMiniStatsForOverview(); + + // Handle deep-link URL parameters (from QR code scan) + this._handleDeepLink(); + } + + private _deepLinkHandled = false; + + private _handleDeepLink(): void { + if (this._deepLinkHandled) return; + const params = new URLSearchParams(window.location.search); + + // v1.8.1: ms_action deep links from the dashboard strategy's fire-dom-event + // Empty-state buttons. Cleared from the URL after one fire so refreshing + // the page doesn't re-open it. + // + // v2.3.0 Phase 5/6: also handles open_vacation / open_budget / open_groups + // / open_settings from the section strategies' status banners. Each + // routes to the right tab + scroll target inside the Settings view. + const msAction = params.get("ms_action"); + const cleanMsActionUrl = () => { + const cleanUrl = window.location.pathname + window.location.hash; + history.replaceState(history.state, "", cleanUrl); + }; + if (msAction === "add_object") { + this._deepLinkHandled = true; + cleanMsActionUrl(); + requestAnimationFrame(() => { + this.shadowRoot + ?.querySelector("maintenance-object-dialog") + ?.openCreate(); + }); + return; + } + if (msAction === "open_vacation" + || msAction === "open_budget" + || msAction === "open_groups" + || msAction === "open_settings") { + this._deepLinkHandled = true; + cleanMsActionUrl(); + // Switch to the Settings tab — that's where Vacation/Budget/Groups live. + this._overviewTab = "settings"; + // Hint the settings-view which sub-section to scroll to (its own + // settings-view component reads this on attribute change). + requestAnimationFrame(() => { + const settingsView = this.shadowRoot?.querySelector( + "maintenance-settings-view", + ) as (HTMLElement & { scrollToSection?: (s: string) => void }) | null; + const target = msAction.replace("open_", ""); // "vacation" / "budget" / "groups" / "settings" + settingsView?.scrollToSection?.(target); + }); + return; + } + + const entryId = params.get("entry_id"); + if (!entryId) return; + this._deepLinkHandled = true; + + const taskId = params.get("task_id"); + const action = params.get("action"); + + // Always clean URL params — they are consumed once + const cleanUrl = window.location.pathname + window.location.hash; + history.replaceState(history.state, "", cleanUrl); + + // Validate that the referenced object exists + const obj = this._getObject(entryId); + if (!obj) { + this._showOverview(); + return; + } + + // Navigate to the right view + if (taskId) { + const task = obj.tasks.find((t) => t.id === taskId); + if (!task) { + this._showObject(entryId); + return; + } + this._showTask(entryId, taskId); + if (action === "complete") { + requestAnimationFrame(() => { + this._openCompleteDialog(entryId, taskId, task.name, this._features.checklists ? task.checklist : undefined, this._features.adaptive && !!task.adaptive_config?.enabled); + }); + } else if (action === "quick_complete") { + // v1.3.0: silent complete using pre-configured defaults; falls back + // to the normal dialog if the task has none. + requestAnimationFrame(() => { + this._handleQuickComplete(entryId, taskId, task); + }); + } + } else { + this._showObject(entryId); + } + } + + private _isCounterEntity(tc: TriggerConfig | null | undefined): boolean { + if (!tc) return false; + const type = tc.type || "threshold"; + return type === "counter" || type === "state_change"; + } + + private async _fetchDetailStats(entityId: string, isCounter: boolean): Promise { + if (!this._statsService) return; + const points = await this._statsService.getDetailStats(entityId, isCounter, this._chartRangeDays); + const updated = new Map(this._detailStatsData); + updated.set(entityId, points); + this._detailStatsData = updated; + } + + /** Chart range chips (7/30/90/365 d): persist the choice and refetch the + * open task's stats at the new window. */ + private _setChartRange(days: number): void { + if (days === this._chartRangeDays) return; + this._chartRangeDays = days; + try { localStorage.setItem("msp-chart-range", String(days)); } catch { /* private mode */ } + const task = this._selectedEntryId && this._selectedTaskId + ? this._getTask(this._selectedEntryId, this._selectedTaskId) + : null; + const entityId = task?.trigger_config?.entity_id; + if (entityId) { + // Drop the stale-range series so the chart shows its loading state. + const updated = new Map(this._detailStatsData); + updated.delete(entityId); + this._detailStatsData = updated; + void this._fetchDetailStats(entityId, this._isCounterEntity(task!.trigger_config)); + } + } + + private _setHideOutliers(hide: boolean): void { + if (hide === this._hideOutliers) return; + // Outlier filtering is client-side on the already-fetched series, so just + // flip the flag and let renderChart re-filter — no re-fetch needed. + this._hideOutliers = hide; + try { localStorage.setItem("msp-chart-hide-outliers", hide ? "1" : "0"); } catch { /* private mode */ } + } + + private async _fetchMiniStatsForOverview(): Promise { + if (!this._statsService) return; + + const entities: Array<{ entityId: string; isCounter: boolean }> = []; + for (const obj of this._objects) { + for (const task of obj.tasks) { + const entityId = task.trigger_config?.entity_id; + if (!entityId) continue; + entities.push({ entityId, isCounter: this._isCounterEntity(task.trigger_config) }); + } + } + + if (entities.length === 0) return; + const batchResult = await this._statsService.getBatchMiniStats(entities); + this._miniStatsData = new Map([...this._miniStatsData, ...batchResult]); + } + + private async _subscribe(): Promise { + try { + const unsub = await this.hass.connection.subscribeMessage( + (msg: unknown) => { + const data = msg as { objects: MaintenanceObjectResponse[] }; + this._objects = data.objects; + }, + { type: "maintenance_supporter/subscribe" } + ); + // If the element was detached while the subscribe was in flight, drop the + // now-orphaned subscription instead of storing it on a dead component. + if (!this.isConnected) { + unsub(); + return; + } + this._unsub = unsub; + } catch { + // Subscription failed; fall back to polling + } + } + + // --- Data accessors --- + + private get _taskRows(): TaskRow[] { + const rows: TaskRow[] = []; + for (const obj of this._objects) { + for (const task of obj.tasks) { + // v2.10.0: archived tasks are hidden unless the toggle reveals them. + if (!this._showArchived && task.archived) continue; + if (this._filterStatus && task.status !== this._filterStatus) continue; + + // User filter + if (this._filterUser) { + const userId = this._filterUser === "current_user" + ? this._userService?.getCurrentUserId() + : this._filterUser; + if (task.responsible_user_id !== userId) continue; + } + + // Collect groups that contain this task + const groupNames: string[] = []; + for (const group of Object.values(this._groups)) { + if (group.task_refs?.some((r) => r.entry_id === obj.entry_id && r.task_id === task.id)) { + groupNames.push(group.name); + } + } + + rows.push({ + entry_id: obj.entry_id, + task_id: task.id, + object_name: obj.object.name, + task_name: task.name, + type: task.type, + schedule_type: task.schedule_type, + status: task.status, + days_until_due: task.days_until_due ?? null, + next_due: task.next_due ?? null, + trigger_active: task.trigger_active, + trigger_current_value: task.trigger_current_value ?? null, + trigger_current_delta: task.trigger_current_delta ?? null, + trigger_config: task.trigger_config ?? null, + trigger_entity_info: task.trigger_entity_info ?? null, + times_performed: task.times_performed, + total_cost: task.total_cost, + interval_days: task.interval_days ?? null, + interval_unit: task.interval_unit ?? null, + interval_anchor: task.interval_anchor ?? null, + is_done: task.is_done ?? false, + archived: task.archived ?? false, + history: task.history || [], + enabled: task.enabled, + nfc_tag_id: task.nfc_tag_id ?? null, + priority: task.priority ?? "normal", + labels: task.labels ?? [], + area_id: obj.object.area_id ?? null, + responsible_user_id: task.responsible_user_id ?? null, + group_names: groupNames, + }); + } + } + const statusOrder: Record = { overdue: 0, triggered: 1, due_soon: 2, ok: 3 }; + const byStatus = (a: TaskRow, b: TaskRow) => (statusOrder[a.status] ?? 9) - (statusOrder[b.status] ?? 9); + const byDays = (a: TaskRow, b: TaskRow) => (a.days_until_due ?? 99999) - (b.days_until_due ?? 99999); + const byDue = (a: TaskRow, b: TaskRow) => byStatus(a, b) || byDays(a, b); + const areaName = (r: TaskRow) => + r.area_id ? this.hass?.areas?.[r.area_id]?.name || "" : ""; + const userName = (r: TaskRow) => + r.responsible_user_id ? this._userService?.getUserName(r.responsible_user_id) || "" : ""; + const groupName = (r: TaskRow) => r.group_names[0] || ""; + const sorts: Record number> = { + due_date: byDue, + object: (a, b) => a.object_name.localeCompare(b.object_name) || byDue(a, b), + type: (a, b) => a.type.localeCompare(b.type) || byDue(a, b), + task_name: (a, b) => a.task_name.localeCompare(b.task_name), + area: (a, b) => { + const an = areaName(a), bn = areaName(b); + // Empty areas at end + if (!an && bn) return 1; + if (an && !bn) return -1; + return an.localeCompare(bn) || byDue(a, b); + }, + assigned_user: (a, b) => { + const an = userName(a), bn = userName(b); + if (!an && bn) return 1; + if (an && !bn) return -1; + return an.localeCompare(bn) || byDue(a, b); + }, + group: (a, b) => { + const an = groupName(a), bn = groupName(b); + if (!an && bn) return 1; + if (an && !bn) return -1; + return an.localeCompare(bn) || byDue(a, b); + }, + }; + rows.sort(sorts[this._sortMode]); + return rows; + } + + private _getObject(entryId: string): MaintenanceObjectResponse | undefined { + return this._objects.find((o) => o.entry_id === entryId); + } + + private _getTask(entryId: string, taskId: string): MaintenanceTask | undefined { + const obj = this._getObject(entryId); + return obj?.tasks.find((t) => t.id === taskId); + } + + // --- Navigation --- + + /** Push a browser history entry so the back button navigates within the panel. */ + private _pushPanelState(view: View, entryId?: string | null, taskId?: string | null): void { + const state = { msp_view: view, msp_entry: entryId || null, msp_task: taskId || null }; + history.pushState(state, ""); + } + + /** Handle browser back/forward button. */ + private _onPopState(e: PopStateEvent): void { + const s = e.state as { msp_view?: View; msp_entry?: string; msp_task?: string } | null; + if (!s?.msp_view) { + // No panel state → we're leaving the panel, let HA handle it + return; + } + // Restore view without pushing another history entry + this._view = s.msp_view; + this._selectedEntryId = s.msp_entry || null; + this._selectedTaskId = s.msp_task || null; + this._moreMenuOpen = false; + if (s.msp_view === "task" && s.msp_entry && s.msp_task) { + this._historyFilter = null; + const task = this._getTask(s.msp_entry, s.msp_task); + if (task?.trigger_config?.entity_id) { + this._fetchDetailStats(task.trigger_config.entity_id, this._isCounterEntity(task.trigger_config)); + } + } + } + + private _showOverview(): void { + this._pushPanelState("overview"); + this._view = "overview"; + this._selectedEntryId = null; + this._selectedTaskId = null; + this._moreMenuOpen = false; + this._scrollContentToTop(); + } + + private _showAllObjects(): void { + this._pushPanelState("all_objects"); + this._view = "all_objects"; + this._selectedEntryId = null; + this._selectedTaskId = null; + this._scrollContentToTop(); + } + + /** v2.1.0 (Discussion #49 — @byoung79): tap a KPI value to auto-filter + * the task list. Empty string clears the filter (used by "Tasks" KPI). */ + private _filterByStatus(status: string): void { + this._filterStatus = status; + // Make sure we're on the dashboard tab so the task list is actually + // visible — pointless to filter on the calendar/settings tabs. + if (this._overviewTab !== "dashboard") { + this._overviewTab = "dashboard"; + } + this._scrollContentToTop(); + } + + /** Reset scroll on the panel's .content container. Used by every + * navigation action so the user always lands at the top of the new + * view, never at a stale scroll position from the previous view. + * + * History: an earlier version targeted .tab-bar via scrollIntoView, + * which worked for the 4 filter KPIs (stay in overview, .tab-bar + * exists) but silently no-op'd for the Objects KPI (switches to the + * all_objects view where .tab-bar isn't rendered). Targeting .content + * directly works regardless of which view is now showing. Plus same + * helper is now called from every navigation entry-point so e.g. + * clicking on a deep object in a long list lands you at the top of + * the object-detail view, not partway through it. */ + private _scrollContentToTop(): void { + requestAnimationFrame(() => { + const content = this.shadowRoot?.querySelector(".content"); + if (content) content.scrollTo({ top: 0, behavior: "smooth" }); + }); + } + + private _showObject(entryId: string): void { + this._pushPanelState("object", entryId); + this._view = "object"; + this._selectedEntryId = entryId; + this._selectedTaskId = null; + this._scrollContentToTop(); + } + + private _showTask(entryId: string, taskId: string): void { + this._pushPanelState("task", entryId, taskId); + this._view = "task"; + this._selectedEntryId = entryId; + this._selectedTaskId = taskId; + this._activeTab = "overview"; + this._historyFilter = null; + this._scrollContentToTop(); + + // Lazy-load statistics for the task's trigger entity + const task = this._getTask(entryId, taskId); + if (task?.trigger_config?.entity_id) { + const entityId = task.trigger_config.entity_id; + const isCounter = this._isCounterEntity(task.trigger_config); + this._fetchDetailStats(entityId, isCounter); + } + } + + // --- Toast --- + + private _showToast(msg: string): void { + if (this._toastTimer) clearTimeout(this._toastTimer); + this._toastUndo = null; + this._toastMessage = msg; + this._toastTimer = setTimeout(() => { this._toastMessage = ""; this._toastTimer = null; }, 4000); + } + + /** A toast with an Undo action — used for reversible actions (archive) that + * run immediately instead of behind a confirm dialog. Longer-lived so the + * user has time to react; the undo callback dismisses it. */ + private _showUndoToast(msg: string, undo: () => void): void { + if (this._toastTimer) clearTimeout(this._toastTimer); + this._toastMessage = msg; + this._toastUndo = undo; + this._toastTimer = setTimeout(() => { + this._toastMessage = ""; this._toastUndo = null; this._toastTimer = null; + }, 7000); + } + + private _runToastUndo(): void { + const undo = this._toastUndo; + if (this._toastTimer) clearTimeout(this._toastTimer); + this._toastMessage = ""; this._toastUndo = null; this._toastTimer = null; + undo?.(); + } + + // --- Command palette ("/") --- + + private _paletteKeydown = (e: KeyboardEvent): void => { + // "/" opens the palette (GitHub/Discourse convention; Shift+7 on a German + // layout still yields key === "/"). Deliberately NOT Ctrl/Cmd+K: that is + // HA's own global-search hotkey, and this window-level preventDefault + // would shadow it whenever the panel is open. + if ( + e.key === "/" + && !e.ctrlKey && !e.metaKey && !e.altKey + && !this._paletteOpen + ) { + const target = e.composedPath()[0]; + const typing = target instanceof HTMLElement + && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" + || target.tagName === "SELECT" || target.isContentEditable); + if (typing) return; + e.preventDefault(); + this._openPalette(); + return; + } + if (!this._paletteOpen) return; + const results = this._paletteResults; + if (e.key === "Escape") { e.preventDefault(); this._closePalette(); } + else if (e.key === "ArrowDown") { e.preventDefault(); this._paletteActive = Math.min(this._paletteActive + 1, results.length - 1); } + else if (e.key === "ArrowUp") { e.preventDefault(); this._paletteActive = Math.max(this._paletteActive - 1, 0); } + else if (e.key === "Enter") { e.preventDefault(); const r = results[this._paletteActive]; if (r) this._selectPaletteResult(r); } + }; + + private _openPalette(): void { + this._paletteQuery = ""; + this._paletteActive = 0; + this._paletteOpen = true; + this.updateComplete.then(() => { + this.shadowRoot?.querySelector(".palette-input")?.focus(); + }); + } + + private _closePalette(): void { + this._paletteOpen = false; + this._paletteQuery = ""; + } + + private get _paletteResults(): Array<{ kind: "object" | "task"; entryId: string; taskId?: string; label: string; sub: string }> { + const q = this._paletteQuery.trim().toLowerCase(); + const out: Array<{ kind: "object" | "task"; entryId: string; taskId?: string; label: string; sub: string }> = []; + for (const obj of this._objects) { + const oname = obj.object.name || ""; + if (!q || oname.toLowerCase().includes(q)) { + out.push({ kind: "object", entryId: obj.entry_id, label: oname, sub: t("object", this._lang) }); + } + for (const task of obj.tasks) { + if (task.archived) continue; + const tname = task.name || ""; + const labelHit = (task.labels || []).some((lb) => lb.toLowerCase().includes(q)); + if (!q || tname.toLowerCase().includes(q) || oname.toLowerCase().includes(q) || labelHit) { + const labelSub = (task.labels || []).length ? ` #${(task.labels || []).join(" #")}` : ""; + out.push({ kind: "task", entryId: obj.entry_id, taskId: task.id, label: tname, sub: oname + labelSub }); + } + } + if (out.length > 60) break; // cap the working set; sliced below + } + return out.slice(0, 40); + } + + private _selectPaletteResult(r: { kind: "object" | "task"; entryId: string; taskId?: string }): void { + this._closePalette(); + if (r.kind === "task" && r.taskId) this._showTask(r.entryId, r.taskId); + else this._showObject(r.entryId); + } + + private _renderPalette() { + if (!this._paletteOpen) return nothing; + const L = this._lang; + const results = this._paletteResults; + return html` +
    this._closePalette()}> +
    e.stopPropagation()}> + { this._paletteQuery = (e.target as HTMLInputElement).value; this._paletteActive = 0; }} + /> +
    + ${results.length === 0 + ? html`
    ${t("palette_no_results", L)}
    ` + : results.map((r, i) => html` +
    { this._paletteActive = i; }} + @click=${() => this._selectPaletteResult(r)}> + + ${r.label} + ${r.sub} +
    + `)} +
    +
    ${t("palette_hint", L)}
    +
    +
    + `; + } + + // --- Template gallery --- + + private async _openTemplateGallery(): Promise { + this._templateGalleryOpen = true; + if (this._templates.length > 0) return; + try { + const res = await this.hass.connection.sendMessagePromise<{ + categories: Record; + templates: Array<{ id: string; name: string; category: string; tasks: unknown[]; disabled?: boolean }>; + }>({ type: "maintenance_supporter/templates", language: this._lang }); + this._templateCategories = res.categories || {}; + // v2.21: admin-hidden templates stay out of the gallery. + this._templates = (res.templates || []).filter((tpl) => !tpl.disabled); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private async _createFromTemplate(templateId: string): Promise { + this._templateBusy = true; + try { + const res = await this.hass.connection.sendMessagePromise<{ entry_id?: string }>({ + type: "maintenance_supporter/object/from_template", + language: this._lang, + template_id: templateId, + }); + this._templateGalleryOpen = false; + await this._loadData(); + this._showToast(t("template_created", this._lang)); + if (res?.entry_id) this._showObject(res.entry_id); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._templateBusy = false; + } + } + + private _categoryName(catId: string): string { + const cat = this._templateCategories[catId] as Record | undefined; + if (!cat) return catId; + return cat[`name_${this._lang}`] || cat["name_en"] || catId; + } + + private _renderTemplateGallery() { + if (!this._templateGalleryOpen) return nothing; + const L = this._lang; + // Group templates by category, preserving category declaration order. + const byCat = new Map(); + for (const tpl of this._templates) { + if (!byCat.has(tpl.category)) byCat.set(tpl.category, []); + byCat.get(tpl.category)!.push(tpl); + } + return html` +
    { this._templateGalleryOpen = false; }}> + +
    + `; + } + + // --- Bulk selection (dashboard task list) --- + + private _bulkKey(row: TaskRow): string { + return `${row.entry_id}:${row.task_id}`; + } + + private _toggleBulkMode(): void { + this._bulkMode = !this._bulkMode; + if (!this._bulkMode) this._bulkSelected = new Set(); + } + + private _toggleBulkRow(row: TaskRow): void { + const key = this._bulkKey(row); + const next = new Set(this._bulkSelected); + if (next.has(key)) next.delete(key); else next.add(key); + this._bulkSelected = next; + } + + private _bulkSelectAll(rows: TaskRow[]): void { + const keys = rows.map((r) => this._bulkKey(r)); + const allSelected = keys.every((k) => this._bulkSelected.has(k)); + this._bulkSelected = allSelected ? new Set() : new Set(keys); + } + + /** Run one WS message per selected row (the endpoints are per-task); reports + * how many succeeded and refreshes once at the end. */ + private async _runBulk( + rows: TaskRow[], + build: (row: TaskRow) => Record, + doneMsg: (n: number) => string, + undo?: () => void, + ): Promise { + const selected = rows.filter((r) => this._bulkSelected.has(this._bulkKey(r))); + if (selected.length === 0) return; + this._actionLoading = true; + let ok = 0; + for (const row of selected) { + try { + await this.hass.connection.sendMessagePromise(build(row)); + ok++; + } catch { /* keep going; report the successful count */ } + } + this._actionLoading = false; + this._bulkSelected = new Set(); + this._bulkMode = false; + await this._loadData(); + if (undo && ok > 0) this._showUndoToast(doneMsg(ok), undo); + else this._showToast(doneMsg(ok)); + } + + private _bulkComplete(rows: TaskRow[]): void { + void this._runBulk( + rows, + (row) => ({ type: "maintenance_supporter/task/complete", entry_id: row.entry_id, task_id: row.task_id }), + (n) => t("bulk_completed", this._lang).replace("{n}", String(n)), + ); + } + + private _bulkArchive(rows: TaskRow[]): void { + // Capture the selection so the undo can unarchive exactly those. + const keys = rows.filter((r) => this._bulkSelected.has(this._bulkKey(r))) + .map((r) => ({ entry_id: r.entry_id, task_id: r.task_id })); + void this._runBulk( + rows, + (row) => ({ type: "maintenance_supporter/task/archive", entry_id: row.entry_id, task_id: row.task_id }), + (n) => t("bulk_archived", this._lang).replace("{n}", String(n)), + async () => { + for (const k of keys) { + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/unarchive", entry_id: k.entry_id, task_id: k.task_id, + }); + } catch { /* best effort */ } + } + await this._loadData(); + }, + ); + } + + // --- Actions --- + + private async _deleteObject(entryId: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + const ok = await dlg?.confirm({ + title: t("delete", this._lang), + message: t("confirm_delete_object", this._lang), + confirmText: t("delete", this._lang), + danger: true, + }); + if (!ok) return; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/object/delete", + entry_id: entryId, + }); + this._showOverview(); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + /** Open a printable maintenance report for the object in a new tab (the user + * prints / saves as PDF from there). Self-contained HTML, no dependency. */ + private _printObjectReport(entryId: string): void { + const resp = this._getObject(entryId); + if (!resp) return; + const L = this._lang; + const labels: ReportLabels = { + title: t("report_title", L), + generated: t("report_generated", L), + manufacturer: t("manufacturer", L), + model: t("model", L), + serial: t("serial_number_label", L), + installed: t("installed", L), + warranty: t("warranty", L), + area: t("area", L), + notes: t("report_notes", L), + tasksHeading: t("tasks", L), + colTask: t("task_name", L), + colType: t("report_col_type", L), + colStatus: t("report_col_status", L), + colSchedule: t("report_col_schedule", L), + colLastDone: t("last_performed", L), + colNextDue: t("next_due", L), + colCost: t("cost", L), + colTimes: t("report_times_done", L), + totalCost: t("report_total_cost", L), + scheduleLabel: (task) => formatRecurrence(task, L), + none: "—", + statusLabel: (s: string) => t(s, L), + typeLabel: (ty: string) => t(ty, L), + }; + const html = buildObjectReportHtml( + resp.object, resp.tasks, labels, + (iso) => (iso ? formatDate(iso, L) : ""), + this._budget?.currency_symbol || DEFAULT_CURRENCY_SYMBOL, + new Date().toISOString(), + ); + const url = URL.createObjectURL(new Blob([html], { type: "text/html" })); + window.open(url, "_blank"); + setTimeout(() => URL.revokeObjectURL(url), 60000); + } + + private async _duplicateObject(entryId: string): Promise { + this._actionLoading = true; + try { + const res = await this.hass.connection.sendMessagePromise<{ entry_id?: string }>({ + type: "maintenance_supporter/object/duplicate", + entry_id: entryId, + }); + await this._loadData(); + this._showToast(t("object_duplicated", this._lang)); + if (res?.entry_id) this._showObject(res.entry_id); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + private async _deleteTask(entryId: string, taskId: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + const ok = await dlg?.confirm({ + title: t("delete", this._lang), + message: t("confirm_delete_task", this._lang), + confirmText: t("delete", this._lang), + danger: true, + }); + if (!ok) return; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/delete", + entry_id: entryId, + task_id: taskId, + }); + this._showObject(entryId); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + // v2.10.0: archive / unarchive a single task (reversible — no confirm). + private async _duplicateTask(entryId: string, taskId: string): Promise { + this._moreMenuOpen = false; + this._actionLoading = true; + try { + const res = await this.hass.connection.sendMessagePromise<{ task_id?: string }>({ + type: "maintenance_supporter/task/duplicate", + entry_id: entryId, + task_id: taskId, + }); + await this._loadData(); + this._showToast(t("task_duplicated", this._lang)); + // Jump straight to the copy so the user can rename/adjust it. + if (res?.task_id) this._showTask(entryId, res.task_id); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + private async _toggleArchiveTask(entryId: string, taskId: string, archived: boolean): Promise { + this._actionLoading = true; + try { + await this.hass.connection.sendMessagePromise({ + type: archived + ? "maintenance_supporter/task/unarchive" + : "maintenance_supporter/task/archive", + entry_id: entryId, + task_id: taskId, + }); + await this._loadData(); + // Just archived → offer a one-tap undo (unarchive) instead of a confirm. + if (!archived) { + this._showUndoToast(t("task_archived", this._lang), + () => this._toggleArchiveTask(entryId, taskId, true)); + } + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + // v2.10.0: archive / unarchive an object. Archiving cascades to its tasks but + // is fully reversible, so instead of a blocking confirm we run it immediately + // and offer an Undo toast (v2.14.0). + private async _toggleArchiveObject(entryId: string, archived: boolean): Promise { + try { + await this.hass.connection.sendMessagePromise({ + type: archived + ? "maintenance_supporter/object/unarchive" + : "maintenance_supporter/object/archive", + entry_id: entryId, + }); + await this._loadData(); + if (!archived) { + this._showUndoToast(t("object_archived", this._lang), + () => this._toggleArchiveObject(entryId, true)); + } + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + // v2.20 (N3): seasonal pause / resume. Pausing asks for an optional + // auto-resume date; resuming re-anchors recurring tasks to a fresh cycle. + private async _togglePauseObject(entryId: string, paused: boolean): Promise { + if (!paused) { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + const result = await dlg?.prompt({ + title: t("pause_object", this._lang), + message: t("pause_until_prompt", this._lang), + confirmText: t("pause_object", this._lang), + inputLabel: t("pause_until_label", this._lang), + inputType: "date", + }); + if (!result?.confirmed) return; + try { + const msg: Record = { + type: "maintenance_supporter/object/pause", + entry_id: entryId, + }; + if (result.value) msg.until = result.value; + await this.hass.connection.sendMessagePromise(msg); + await this._loadData(); + this._showUndoToast(t("object_paused", this._lang), + () => this._togglePauseObject(entryId, true)); + } catch (e) { + this._showToast(describeWsError(e, this._lang)); + } + return; + } + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/object/resume", + entry_id: entryId, + }); + await this._loadData(); + this._showToast(t("object_resumed", this._lang)); + } catch (e) { + this._showToast(describeWsError(e, this._lang)); + } + } + + // v2.20 (N1): replace a worn-out object with a successor — the old one is + // archived in place (history/costs stay browsable), the new one starts as a + // pre-filled fresh unit with tasks and documents carried over. + private async _replaceObject(entryId: string, currentName: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + const result = await dlg?.prompt({ + title: t("replace_object", this._lang), + message: t("replace_object_prompt", this._lang), + confirmText: t("replace_object", this._lang), + inputLabel: t("replace_name_label", this._lang), + inputType: "text", + inputValue: currentName, + }); + if (!result?.confirmed) return; + this._actionLoading = true; + try { + const res = await this.hass.connection.sendMessagePromise<{ entry_id?: string }>({ + type: "maintenance_supporter/object/replace", + entry_id: entryId, + name: result.value || currentName, + }); + await this._loadData(); + this._showToast(t("object_replaced", this._lang)); + if (res?.entry_id) this._showObject(res.entry_id); + } catch (e) { + this._showToast(describeWsError(e, this._lang)); + } finally { + this._actionLoading = false; + } + } + + private async _skipTask(entryId: string, taskId: string, reason?: string): Promise { + this._actionLoading = true; + try { + const msg: Record = { + type: "maintenance_supporter/task/skip", + entry_id: entryId, + task_id: taskId, + }; + if (reason) msg.reason = reason; + await this.hass.connection.sendMessagePromise(msg); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + private async _resetTask(entryId: string, taskId: string, resetDate?: string): Promise { + this._actionLoading = true; + try { + const msg: Record = { + type: "maintenance_supporter/task/reset", + entry_id: entryId, + task_id: taskId, + }; + if (resetDate) msg.date = resetDate; + await this.hass.connection.sendMessagePromise(msg); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + private async _applySuggestion(entryId: string, taskId: string, interval: number): Promise { + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/apply_suggestion", + entry_id: entryId, + task_id: taskId, + interval: interval, + }); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private _openSeasonalOverrides(task: MaintenanceTask): void { + const dlg = this.shadowRoot!.querySelector("maintenance-seasonal-overrides-dialog"); + if (!dlg || !this._selectedEntryId) return; + const overrides = task.adaptive_config?.seasonal_overrides as Record | null | undefined; + dlg.open(this._selectedEntryId, task.id, overrides); + } + + private async _reanalyzeInterval(entryId: string, taskId: string): Promise { + try { + const res = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/analyze_interval", + entry_id: entryId, + task_id: taskId, + }) as { + recommended_interval: number | null; + confidence: string; + data_points: number; + recommendation_reason: string | null; + }; + if (res.recommended_interval) { + this._showToast( + `${t("reanalyze_result", this._lang)}: ${res.recommended_interval} ${t("days", this._lang)} ` + + `(${t(`confidence_${res.confidence}`, this._lang)}, ${res.data_points} ${t("data_points", this._lang)})`, + ); + } else { + this._showToast(t("reanalyze_insufficient_data", this._lang)); + } + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private async _promptSkipTask(entryId: string, taskId: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + if (!dlg) return; + const result = await dlg.prompt({ + title: t("skip", this._lang), + message: t("skip_reason_prompt", this._lang), + confirmText: t("skip", this._lang), + inputLabel: t("reason_optional", this._lang), + inputType: "text", + }); + if (!result.confirmed) return; + this._skipTask(entryId, taskId, result.value || undefined); + } + + private async _promptResetTask(entryId: string, taskId: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + if (!dlg) return; + const result = await dlg.prompt({ + title: t("reset", this._lang), + message: t("reset_date_prompt", this._lang), + confirmText: t("reset", this._lang), + inputLabel: t("reset_date_optional", this._lang), + inputType: "date", + }); + if (!result.confirmed) return; + this._resetTask(entryId, taskId, result.value || undefined); + } + + private async _snoozeTask(entryId: string, taskId: string): Promise { + this._actionLoading = true; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/snooze", + entry_id: entryId, + task_id: taskId, + }); + this._showToast(t("snoozed", this._lang)); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + private _dismissSuggestion(entryId?: string, taskId?: string): void { + if (entryId && taskId) { + this._dismissedSuggestions.add(`${entryId}_${taskId}`); + } + this.requestUpdate(); + } + + // v1.3.0: silent complete-via-QR. Tries the quick endpoint first; if the + // task has no quick_complete_defaults configured, falls back to the + // normal complete dialog so the user is never stuck after a scan. + private async _handleQuickComplete( + entryId: string, taskId: string, task: MaintenanceTask, + ): Promise { + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/quick_complete", + entry_id: entryId, task_id: taskId, + }); + this._showToast(t("quick_complete_success", this._lang)); + } catch (e: unknown) { + const code = (e as { code?: string })?.code || ""; + if (code === "no_defaults") { + // No defaults set — open the dialog so the user can fill them in. + this._openCompleteDialog( + entryId, taskId, task.name, + this._features.checklists ? task.checklist : undefined, + this._features.adaptive && !!task.adaptive_config?.enabled, + ); + } else { + this._showToast(t("action_error", this._lang)); + } + } + } + + // v2.21: printable one-pager for a task — details, checklist tick boxes, + // QR pair, and (when a linked PDF manual has a page hint for this task) a + // link to the server-cut manual excerpt. Opens in a new tab for printing. + private async _printTaskWorksheet(entryId: string, taskId: string): Promise { + const obj = this._getObject(entryId); + const task = obj?.tasks.find((tk) => tk.id === taskId); + if (!obj || !task) return; + this._actionLoading = true; + try { + const qrBase: Record = { + type: "maintenance_supporter/qr/generate", + entry_id: entryId, task_id: taskId, url_mode: "server", + }; + const [qrView, qrComplete] = await Promise.all([ + this.hass.connection.sendMessagePromise<{ svg_data_uri?: string }>({ ...qrBase, action: "view" }).catch(() => null), + this.hass.connection.sendMessagePromise<{ svg_data_uri?: string }>({ ...qrBase, action: "complete" }).catch(() => null), + ]); + + // Manual excerpt: first linked PDF with a page hint for this task. + let excerpt: WorksheetExcerpt | null = null; + try { + const docs = await this.hass.connection.sendMessagePromise<{ + documents: Array<{ id: string; kind: string; mime?: string; title?: string; + filename?: string; task_ids?: string[]; task_pages?: Record }>; + }>({ type: "maintenance_supporter/documents/list", entry_id: entryId }); + const manual = (docs.documents || []).find( + (d) => d.kind === "file" && d.mime === "application/pdf" + && (d.task_ids || []).includes(taskId) && d.task_pages?.[taskId], + ); + if (manual) { + const start = manual.task_pages![taskId]; + const count = 4; + const signed = await this.hass.connection.sendMessagePromise<{ path: string }>({ + type: "auth/sign_path", + path: `/api/maintenance_supporter/document/${manual.id}/excerpt?start=${start}&count=${count}`, + expires: 3600, + }); + excerpt = { + title: manual.title || manual.filename || "Manual", + startPage: start, endPage: start + count - 1, + // Absolute URL: the sheet lives on a blob: page, where a + // relative /api/... href cannot resolve (caught live). + url: new URL(signed.path, window.location.origin).toString(), + vendorBase: new URL("/maintenance_supporter_vendor", window.location.origin).toString(), + }; + } + } catch { /* worksheet works without the excerpt */ } + + const L = this._lang; + const labels: WorksheetLabels = { + title: t("worksheet", L), + object: t("object", L), + type: t("maintenance_type", L), + interval: t("interval", L), + nextDue: t("next_due", L), + lastDone: t("last_performed", L), + priority: t("priority", L), + checklist: t("checklist", L), + notes: t("notes_label", L), + scanView: t("worksheet_scan_view", L), + scanComplete: t("worksheet_scan_complete", L), + manualExcerpt: t("worksheet_manual_excerpt", L), + pages: t("worksheet_pages", L), + printedOn: t("worksheet_printed", L), + never: t("worksheet_never", L), + typeLabel: (ty: string) => t(ty, L), + statusLabel: (st: string) => t(st, L), + }; + const html = buildTaskWorksheetHtml( + task, obj.object.name, labels, + (iso) => formatDate(iso, L), + (tk) => formatRecurrence(tk, L), + qrView?.svg_data_uri || null, + qrComplete?.svg_data_uri || null, + excerpt, + new Date().toISOString(), + ); + const url = URL.createObjectURL(new Blob([html], { type: "text/html" })); + window.open(url, "_blank"); + setTimeout(() => URL.revokeObjectURL(url), 60000); + } finally { + this._actionLoading = false; + } + } + + private _openCompleteDialog(entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void { + const dlg = this.shadowRoot!.querySelector("maintenance-complete-dialog"); + if (!dlg) return; + dlg.entryId = entryId; + dlg.taskId = taskId; + dlg.taskName = taskName; + dlg.lang = this._lang; + dlg.checklist = checklist || []; + dlg.adaptiveEnabled = !!adaptiveEnabled; + // v2.20 (#83): reading tasks get a value field — resolve type + unit here + // so none of the many call sites need to thread them through. + const tk = this._objects + .find((o) => o.entry_id === entryId) + ?.tasks.find((tsk) => tsk.id === taskId); + dlg.taskType = tk?.type || ""; + dlg.readingUnit = tk?.reading_unit || ""; + dlg.open(); + } + + private _openQrForObject(entryId: string, objectName: string): void { + const dlg = this.shadowRoot!.querySelector("maintenance-qr-dialog"); + dlg?.openForObject(entryId, objectName); + } + + private _openQrForTask(entryId: string, taskId: string, objectName: string, taskName: string): void { + const dlg = this.shadowRoot!.querySelector("maintenance-qr-dialog"); + dlg?.openForTask(entryId, taskId, objectName, taskName); + } + + private _onDialogEvent = async (): Promise => { + try { await this._loadData(); } catch { /* subscription will sync */ } + }; + + // --- Render --- + + render() { + return html` +
    + ${this.narrow || this._view !== "overview" ? this._renderHeader() : nothing} +
    + ${this._view === "overview" + ? this._renderOverview() + : this._view === "all_objects" + ? this._renderAllObjects() + : this._view === "object" + ? this._renderObjectDetail() + : this._renderTaskDetail()} +
    +
    + + + + + + + + + ${this._toastMessage ? html`
    + ${this._toastMessage} + ${this._toastUndo ? html`` : nothing} +
    ` : nothing} + ${this._renderPalette()} + ${this._renderTemplateGallery()} + `; + } + + private _renderHeader() { + const crumbs: { label: string; action?: () => void }[] = [ + { label: t("maintenance", this._lang), action: () => this._showOverview() }, + ]; + if (this._view === "object" && this._selectedEntryId) { + const obj = this._getObject(this._selectedEntryId); + crumbs.push({ label: obj?.object.name || "Object" }); + } + if (this._view === "task" && this._selectedEntryId && this._selectedTaskId) { + const obj = this._getObject(this._selectedEntryId); + crumbs.push({ + label: obj?.object.name || "Object", + action: () => this._showObject(this._selectedEntryId!), + }); + const task = this._getTask(this._selectedEntryId, this._selectedTaskId); + crumbs.push({ label: task?.name || "Task" }); + } + + return html` +
    + ${this.narrow ? html`` : nothing} + ${this._view !== "overview" + ? html` { + if (this._view === "task") this._showObject(this._selectedEntryId!); + else this._showOverview(); + }} + >` + : nothing} + +
    + `; + } + + private _renderOverview() { + const L = this._lang; + const isAdmin = !!this.hass?.user?.is_admin; + const s = this._stats; + // Only true HA admins get the Settings tab: global config, feature toggles, + // vacation and import live there and stay admin-only server-side. Operators + // (incl. allowlisted ones with full content CRUD) and plain users can't open + // it. The Calendar tab IS visible to everyone (read-only view). + if (!isAdmin && this._overviewTab === "settings") { + this._overviewTab = "dashboard"; + } + return html` + ${s + ? html` +
    +
    this._showAllObjects()} + title=${t("show_all_objects", L)}> + ${s.total_objects} + ${t("objects", L)} +
    +
    this._filterByStatus("")} + title=${t("show_all_tasks", L)}> + ${s.total_tasks} + ${t("tasks", L)} +
    +
    this._filterByStatus("overdue")} + title=${t("filter_to_overdue", L)}> + ${s.overdue} + ${t("overdue", L)} +
    +
    this._filterByStatus("due_soon")} + title=${t("filter_to_due_soon", L)}> + ${s.due_soon} + ${t("due_soon", L)} +
    +
    this._filterByStatus("triggered")} + title=${t("filter_to_triggered", L)}> + ${s.triggered} + ${t("triggered", L)} +
    +
    + ` + : nothing} +
    +
    this._setOverviewTab("today")}> + ${t("tab_today", L)} +
    +
    this._setOverviewTab("dashboard")}> + ${t("dashboard", L)} +
    +
    this._setOverviewTab("calendar")}> + ${t("tab_calendar", L)} +
    + ${isAdmin ? html` +
    this._setOverviewTab("settings")}> + ${t("settings", L)} +
    + ` : nothing} +
    + ${this._overviewTab === "today" + ? this._renderToday() + : this._overviewTab === "dashboard" + ? this._renderDashboard() + : this._overviewTab === "calendar" + ? html` +
    + +
    + ` + : html``} + `; + } + + /** v2.0.0: intercept ll-custom open-task events from the embedded calendar + * card so we navigate within the panel (using _showTask) instead of + * letting the document-level dialog-mount handler open a duplicate + * dialog on top. The handler stops propagation so the document listener + * (registered by the dashboard-strategy bundle) doesn't also fire. */ + private _onCalendarLlCustom = (e: Event): void => { + const detail = (e as CustomEvent<{ type?: string; entry_id?: string; task_id?: string }>).detail; + if ( + detail?.type === "maintenance-supporter:open-task" && + detail.entry_id && + detail.task_id + ) { + e.stopPropagation(); + this._showTask(detail.entry_id, detail.task_id); + } + }; + + // v2.0.0: _renderCalendar() removed — replaced by an embedded + // . The card holds all the same + // logic (window chips, source icons, prediction pills, projected events) + // and is the single source of truth for calendar rendering across the + // panel and stand-alone Lovelace use. Kept for reference: the old + // implementation rendered ~120 lines of Lit template literals here, + // duplicating what the card now does. + + /** Status badge with a shape icon so status isn't conveyed by colour alone + * (colour-blind accessibility) — icon + text + colour together. */ + private _statusBadge(archived: boolean, isDone: boolean, status: string) { + const L = this._lang; + const cls = archived ? "archived" : (isDone ? "done" : status); + const iconKey = archived ? "archived" : (isDone ? "completed" : status); + const label = archived ? t("archived", L) : (isDone ? t("completed", L) : t(status, L)); + return html`${label}`; + } + + private _setOverviewTab(tab: "today" | "dashboard" | "calendar" | "settings"): void { + this._overviewTab = tab; + try { localStorage.setItem("msp-overview-tab", tab); } catch { /* private mode */ } + this._scrollContentToTop(); + } + + /** Mobile-first focus list: what needs attention now, grouped by urgency. */ + private _renderToday() { + const L = this._lang; + const rows = this._taskRows; // already excludes archived + disabled + const key = (r: TaskRow) => `${r.entry_id}:${r.task_id}`; + const overdue = rows.filter((r) => r.status === "overdue" || r.trigger_active); + const claimed = new Set(overdue.map(key)); + const dueToday = rows.filter((r) => !claimed.has(key(r)) && r.days_until_due === 0); + dueToday.forEach((r) => claimed.add(key(r))); + const thisWeek = rows.filter((r) => !claimed.has(key(r)) + && r.days_until_due != null && r.days_until_due > 0 && r.days_until_due <= 7); + + if (overdue.length + dueToday.length + thisWeek.length === 0) { + return html` +
    + +

    ${t("today_all_caught_up", L)}

    +
    + `; + } + return html` +
    + ${this._renderTodaySection("today_overdue", overdue, "overdue")} + ${this._renderTodaySection("today_due_today", dueToday, "due_soon")} + ${this._renderTodaySection("today_this_week", thisWeek, "")} +
    + `; + } + + private _renderTodaySection(titleKey: string, rows: TaskRow[], cls: string) { + if (rows.length === 0) return nothing; + const L = this._lang; + return html` +
    +
    + ${t(titleKey, L)}${rows.length} +
    + ${rows.map((row) => html` +
    this._showTask(row.entry_id, row.task_id)}> + +
    +
    ${row.task_name}
    +
    ${row.object_name} · ${formatDueDays(row.days_until_due, L)}
    +
    + { e.stopPropagation(); this._openCompleteDialogForRow(row); }}> + + +
    + `)} +
    + `; + } + + private _renderDashboard() { + const s = this._stats; + const rows = this._taskRows; + const L = this._lang; + const isOperator = this._isOperator; + // v2.10.0: count archived tasks so the reveal toggle can show how many are + // hidden (and hide itself entirely when there is nothing archived). + const archivedCount = this._objects.reduce( + (n, o) => n + o.tasks.filter((tk) => tk.archived).length, 0, + ); + + return html` + ${this._features.budget ? this._renderBudgetBar() : nothing} + +
    + + + + + ${archivedCount > 0 ? html` + { this._showArchived = !this._showArchived; }} + > + + ${this._showArchived ? t("hide_archived", L) : `${t("show_archived", L)} (${archivedCount})`} + + ` : nothing} + ${!isOperator && rows.length > 0 ? html` + this._toggleBulkMode()} + > + + ${this._bulkMode ? t("cancel", L) : t("bulk_select", L)} + + ` : nothing} + ${!isOperator ? html` + this.shadowRoot!.querySelector("maintenance-object-dialog")?.openCreate()} + > + ${t("new_object", L)} + + this._openTemplateGallery()}> + ${t("templates_from", L)} + + this.shadowRoot!.querySelector("maintenance-task-dialog")?.openCreate("", this._objects)} + > + ${t("new_task", L)} + + ` : nothing} +
    + + ${rows.length === 0 + ? html` +
    + +

    ${t("no_tasks", L)}

    + ${!isOperator && this._objects.length === 0 ? html` +

    ${t("onboard_hint", L)}

    +
    + this._openTemplateGallery()}> + ${t("templates_from", L)} + + this.shadowRoot!.querySelector("maintenance-object-dialog")?.openCreate()}> + ${t("new_object", L)} + +
    + ` : nothing} +
    + ` + : html` + ${this._bulkMode ? this._renderBulkBar(rows, L) : nothing} + ${this._groupByMode === "none" + ? this._renderTaskTable(rows) + : this._renderGroupedTasks(rows, L)} + `} + + ${this._features.groups && !isOperator ? this._renderGroupsSection() : nothing} + ${!isOperator + ? html`) => { + const eid = e.detail?.entry_id; + if (eid) this._showObject(eid); + }} + >` + : nothing} + `; + } + + /** The flat dashboard task table. Below VIRTUAL_MIN_ROWS (or on narrow + * layouts, where rows aren't uniform-height) every row renders directly. + * Above it, only the scroll window is in the DOM: two grid-spanning + * spacers keep the scrollbar honest, and a hidden "sizer" row pins the + * content-sized badge column to the widest badge set across ALL rows so + * the shared subgrid tracks can't jitter as rows enter/leave the window. */ + private _renderTaskTable(rows: TaskRow[]) { + const bulkCls = this._bulkMode ? " bulk" : ""; + this._virtTotalRows = rows.length; + if (this.narrow || rows.length < VIRTUAL_MIN_ROWS) { + return html` +
    + ${rows.map((row) => this._renderOverviewRow(row))} +
    + `; + } + const total = rows.length; + const rh = this._virtRowHeight; + let start = Math.max(0, Math.min(this._virtStart, total)); + let end = this._virtEnd > 0 ? Math.min(this._virtEnd, total) : Math.min(total, 40); + if (end < start) { start = 0; end = Math.min(total, 40); } + const padTop = start * rh; + const padBottom = (total - end) * rh; + return html` +
    + ${this._renderVirtSizerRow(rows)} + ${padTop > 0 ? html`
    ` : nothing} + ${rows.slice(start, end).map((row) => this._renderOverviewRow(row))} + ${padBottom > 0 ? html`
    ` : nothing} +
    + `; + } + + /** Invisible zero-height row whose badge cell carries the union of the + * widest badges across ALL rows. Because the subgrid's `auto` badge track + * sizes to the max over rendered rows, including this row makes the track + * width independent of which data rows happen to be in the window. */ + private _renderVirtSizerRow(rows: TaskRow[]) { + const L = this._lang; + let widest = ""; + let anyDisabled = false; + let anyNfc = false; + let anyPriority = false; + for (const r of rows) { + const label = r.archived ? t("archived", L) : (r.is_done ? t("completed", L) : t(r.status, L)); + if (label.length > widest.length) widest = label; + if (!r.enabled) anyDisabled = true; + if (r.nfc_tag_id) anyNfc = true; + if (r.priority === "high" || r.priority === "low") anyPriority = true; + } + return html` + + `; + } + + private _renderBulkBar(rows: TaskRow[], L: string) { + const n = this._bulkSelected.size; + const allSelected = rows.length > 0 && rows.every((r) => this._bulkSelected.has(this._bulkKey(r))); + return html` +
    + + ${t("bulk_n_selected", L).replace("{n}", String(n))} + + this._bulkComplete(rows)}> + ${t("complete", L)} + + this._bulkArchive(rows)}> + ${t("archive", L)} + + +
    + `; + } + + private _renderGroupedTasks(rows: TaskRow[], L: string) { + const groups = new Map(); + const noneLabel = t("unassigned", L); + for (const row of rows) { + let keys: string[] = []; + if (this._groupByMode === "area") { + const a = row.area_id ? this.hass?.areas?.[row.area_id]?.name : null; + keys = [a || noneLabel]; + } else if (this._groupByMode === "user") { + const u = row.responsible_user_id ? this._userService?.getUserName(row.responsible_user_id) : null; + keys = [u || noneLabel]; + } else if (this._groupByMode === "group") { + keys = row.group_names.length > 0 ? row.group_names : [noneLabel]; + } + for (const k of keys) { + if (!groups.has(k)) groups.set(k, []); + groups.get(k)!.push(row); + } + } + const sorted = [...groups.entries()].sort(([a], [b]) => { + // Push "unassigned" / "no area" to bottom + if (a === noneLabel && b !== noneLabel) return 1; + if (b === noneLabel && a !== noneLabel) return -1; + return a.localeCompare(b); + }); + const icon = this._groupByMode === "area" ? "mdi:map-marker-outline" + : this._groupByMode === "group" ? "mdi:folder-outline" + : "mdi:account-outline"; + return html` + ${sorted.map(([key, taskRows]) => html` +
    + + + ${key} + (${taskRows.length}) + +
    + ${taskRows.map((row) => this._renderOverviewRow(row))} +
    +
    + `)} + `; + } + + // (#67) Localised warranty label, shared by the detail meta + table cell. + private _warrantyLabel(ws: ReturnType, iso: string, L: string): string { + if (ws.kind === "expired") return t("warranty_expired", L); + if (ws.kind === "expiring") { + return t("warranty_expires_in", L).replace("{days}", String(ws.days ?? 0)); + } + return t("warranty_valid_until", L).replace("{date}", formatDate(iso, L)); + } + + // (#67) Warranty status as a coloured chip in the object detail meta. + private _renderWarrantyMeta(iso: string, L: string) { + const ws = warrantyStatus(iso); + return html`

    ${t("warranty", L)}: + ${this._warrantyLabel(ws, iso, L)}

    `; + } + + private _renderAllObjects() { + const L = this._lang; + const isOperator = this._isOperator; + // (#67) Table mode is desktop-only; narrow viewports always fall back to + // cards (the table's many columns don't fit a phone). + const tableMode = this._objectViewMode === "table" && !this.narrow; + // v2.10.0: how many objects are archived (drives the reveal toggle below). + const archivedObjCount = this._objects.filter((o) => o.object.archived).length; + + // Sort + group helpers + const minDays = (obj: MaintenanceObjectResponse): number => { + let m = Infinity; + for (const t of obj.tasks) { + const d = t.days_until_due; + if (d !== null && d !== undefined && d < m) m = d; + } + return m; + }; + const sorted = this._objects.filter((o) => this._showArchived || !o.object.archived); + if (this._objectSortMode === "alphabetical") { + sorted.sort((a, b) => a.object.name.localeCompare(b.object.name)); + } else if (this._objectSortMode === "task_count") { + sorted.sort((a, b) => b.tasks.length - a.tasks.length || a.object.name.localeCompare(b.object.name)); + } else { + // due_soonest + sorted.sort((a, b) => minDays(a) - minDays(b) || a.object.name.localeCompare(b.object.name)); + } + + // Group by area for objects view + const groupedByArea = (): Map => { + const map = new Map(); + for (const obj of sorted) { + const areaId = obj.object.area_id; + const key = areaId ? this.hass?.areas?.[areaId]?.name || t("unassigned", L) : t("no_area", L); + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(obj); + } + return new Map([...map.entries()].sort(([a], [b]) => a.localeCompare(b))); + }; + + const renderObject = (obj: MaintenanceObjectResponse) => { + const overdue = obj.tasks.some(t => t.status === "overdue" || t.status === "triggered"); + return html` +
    this._showObject(obj.entry_id)}> + ${overdue ? html`` : nothing} +
    + ${obj.object.name} + ${obj.object.paused + ? html` + + ` + : nothing} + ${obj.object.document_count + ? html` + ${obj.object.document_count} + ` + : nothing} + ${obj.tasks.length} ${t("tasks_lower", L)} +
    + ${obj.object.manufacturer || obj.object.model + ? html`
    ${[obj.object.manufacturer, obj.object.model].filter(Boolean).join(" ")}
    ` + : nothing} + ${obj.tasks.length === 0 + ? html`
    ${t("no_tasks_yet", L)}
    ` + : nothing} +
    + `; + }; + + return html` + +
    + + ${!this.narrow ? html` +
    + + +
    + ` : nothing} + ${!tableMode ? html` + + ` : nothing} + ${!isOperator ? html` + this.shadowRoot!.querySelector("maintenance-object-dialog")?.openCreate()} + > + ${t("new_object", L)} + + ` : nothing} + this._exportObjectsCsv()}> + ${t("settings_export_csv", L)} + + ${archivedObjCount > 0 ? html` + { this._showArchived = !this._showArchived; }} + > + + ${this._showArchived ? t("hide_archived", L) : `${t("show_archived", L)} (${archivedObjCount})`} + + ` : nothing} +
    + ${tableMode + ? this._renderObjectsTable(sorted) + : this._groupByMode === "area" + ? html` + ${[...groupedByArea().entries()].map(([area, objs]) => html` +
    + + + ${area} + (${objs.length}) + +
    ${objs.map(renderObject)}
    +
    + `)} + ` + : html`
    ${sorted.map(renderObject)}
    `} + `; + } + + private _setObjectViewMode(mode: "cards" | "table"): void { + this._objectViewMode = mode; + localStorage.setItem("maintenance_supporter_object_view", mode); + } + + // (#67 / Phase 3) Download all objects as a one-row-per-object CSV. + private async _exportObjectsCsv(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/objects/csv", + }) as { csv: string }; + const ts = new Date().toISOString().slice(0, 10); + downloadTextFile(result.csv, `maintenance_objects_${ts}.csv`, "text/csv;charset=utf-8"); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + // (#67) Tabular All-Objects view honouring the configured columns. Desktop + // only — the caller falls back to cards on narrow viewports. + private _renderObjectsTable(objs: MaintenanceObjectResponse[]) { + const L = this._lang; + const cols = this._objectsTableColumns; + return html` +
    + + + + ${cols.map((key) => { + const def = OBJECT_COLUMNS.find((c) => c.key === key); + // The actions column header stays blank (icon-only cells). + const label = def && def.key !== "actions" ? t(def.labelKey, L) : ""; + return html``; + })} + + + + ${objs.map((obj) => html` + this._showObject(obj.entry_id)}> + ${cols.map((key) => this._renderObjectCell(key, obj, L))} + + `)} + +
    ${label}
    +
    + `; + } + + private _renderObjectCell(key: string, obj: MaintenanceObjectResponse, L: string) { + const o = obj.object; + switch (key) { + case "name": + return html` + ${o.name} + ${o.document_count + ? html` + ${o.document_count} + ` + : nothing} + `; + case "manufacturer": + return html`${o.manufacturer || "—"}`; + case "model": + return html`${o.model || "—"}`; + case "serial_number": + return html`${o.serial_number || "—"}`; + case "installation_date": + return html`${o.installation_date ? formatDate(o.installation_date, L) : "—"}`; + case "warranty_expiry": + return html`${this._renderWarrantyCell(o.warranty_expiry, L)}`; + case "area_id": { + const area = o.area_id ? (this.hass?.areas?.[o.area_id]?.name || o.area_id) : "—"; + return html`${area}`; + } + case "documentation_url": + return html`${ + o.documentation_url && /^https?:\/\//i.test(o.documentation_url) + ? html` e.stopPropagation()}>` + : "—" + }`; + case "notes": + return html`${o.notes || "—"}`; + case "task_count": + return html`${obj.tasks.length}`; + case "actions": + return html` + { e.stopPropagation(); this._openQrForObject(obj.entry_id, o.name); }}> + + + `; + default: + return html``; + } + } + + // Warranty chip for a table cell — like _renderWarrantyMeta but renders an + // em-dash placeholder when the object has no warranty date. + private _renderWarrantyCell(iso: string | null | undefined, L: string) { + const ws = warrantyStatus(iso); + if (ws.kind === "none") return html``; + return html`${this._warrantyLabel(ws, iso as string, L)}`; + } + + private async _onSettingsChanged(): Promise { + await this._loadData(); + } + + private _renderGroupsSection() { + if (!this._features.groups) return nothing; + const entries = Object.entries(this._groups); + const L = this._lang; + + return html` +
    +
    +

    ${t("groups", L)}

    + this._openGroupCreate()}> + ${t("new_group", L)} + +
    + ${entries.length === 0 + ? html`
    ${t("no_groups", L)}
    ` + : html` +
    + ${entries.map(([gid, group]) => { + const taskNames = group.task_refs + .map((ref) => this._getTask(ref.entry_id, ref.task_id)?.name) + .filter(Boolean); + return html` +
    +
    +
    ${group.name}
    +
    + this._openGroupEdit(gid)}> + + + this._deleteGroup(gid, group.name)}> + + +
    +
    + ${group.description ? html`
    ${group.description}
    ` : nothing} +
    + ${taskNames.length > 0 + ? taskNames.map((n) => html`${n}`) + : html`${t("no_tasks_short", L)}`} +
    +
    + `; + })} +
    + `} +
    + `; + } + + private _openGroupCreate(): void { + this.shadowRoot!.querySelector("maintenance-group-dialog")?.openCreate(); + } + + private _openGroupEdit(groupId: string): void { + const group = this._groups[groupId]; + if (!group) return; + this.shadowRoot!.querySelector("maintenance-group-dialog")?.openEdit(groupId, group); + } + + private async _deleteGroup(groupId: string, name: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + const ok = dlg + ? await dlg.confirm({ + title: t("delete_group", this._lang), + message: t("delete_group_confirm", this._lang).replace("{name}", name), + confirmText: t("delete", this._lang), + }) + : confirm(`${t("delete_group_confirm", this._lang).replace("{name}", name)}`); + if (!ok) return; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/group/delete", + group_id: groupId, + }); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private _renderBudgetBar() { + const b = this._budget; + if (!b) return nothing; + const L = this._lang; + const cs = b.currency_symbol || DEFAULT_CURRENCY_SYMBOL; + const bars: { label: string; spent: number; budget: number }[] = []; + if (b.monthly_budget > 0) bars.push({ label: t("budget_monthly", L), spent: b.monthly_spent, budget: b.monthly_budget }); + if (b.yearly_budget > 0) bars.push({ label: t("budget_yearly", L), spent: b.yearly_spent, budget: b.yearly_budget }); + if (bars.length === 0) return nothing; + + return html` +
    + ${bars.map((bar) => { + const pct = Math.min(100, Math.max(0, (bar.spent / bar.budget) * 100)); + const color = pct >= 100 ? "var(--error-color, #f44336)" : pct >= b.alert_threshold_pct ? "var(--warning-color, #ff9800)" : "var(--success-color, #4caf50)"; + return html` +
    +
    + ${bar.label} + ${bar.spent.toFixed(2)} / ${bar.budget.toFixed(2)} ${cs} +
    +
    +
    +
    +
    + `; + })} +
    + `; + } + + private _renderOverviewRow(row: TaskRow) { + const L = this._lang; + // Compute days progress bar + const hasDaysBar = row.schedule_type === "time_based" && row.interval_days && row.interval_days > 0; + let pct = 0; + let barColor = STATUS_COLORS.ok; + let daysOverflow = false; + if (hasDaysBar && row.days_until_due !== null) { + const p = daysProgress(row.interval_days, row.days_until_due, row.interval_unit); + pct = p.pct; + daysOverflow = p.overflow; + if (row.status === "overdue") barColor = STATUS_COLORS.overdue; + else if (row.status === "due_soon") barColor = STATUS_COLORS.due_soon; + } + + const areaName = row.area_id ? this.hass?.areas?.[row.area_id]?.name : null; + const userName = row.responsible_user_id ? this._userService?.getUserName(row.responsible_user_id) : null; + const hasSub = row.group_names.length > 0 || areaName || userName; + + const bulkSelected = this._bulkMode && this._bulkSelected.has(this._bulkKey(row)); + return html` +
    + ${this._bulkMode ? html` + + ` : nothing} + + ${this._statusBadge(!!row.archived, row.is_done, row.status)} + ${!row.enabled ? html`${t("disabled", L)}` : nothing} + ${row.nfc_tag_id ? html`` : nothing} + ${row.priority === "high" ? html`` : nothing} + ${row.priority === "low" ? html`` : nothing} + + { e.stopPropagation(); this._showObject(row.entry_id); }}>${row.object_name} + this._showTask(row.entry_id, row.task_id)}>${row.task_name} + + ${row.group_names.length > 0 ? html` + + ${row.group_names.join(", ")} + ` : nothing} + ${areaName ? html` + + ${areaName} + ` : nothing} + ${userName ? html` + + ${userName} + ` : nothing} + ${(row.labels || []).map((label) => html` + + ${label} + `)} + + ${t(row.type, L)} + this._showTask(row.entry_id, row.task_id)}> + ${formatDueDays(row.days_until_due, L)} + ${hasDaysBar + ? html`
    ` + : nothing} + ${row.trigger_config + ? renderTriggerProgress(row) + : !hasDaysBar && row.trigger_active + ? html`` + : nothing} + ${renderMiniSparkline(row, this._miniStatsData, this._lang)} +
    + + { e.stopPropagation(); this._openCompleteDialogForRow(row); }}> + + + { e.stopPropagation(); this._promptSkipTask(row.entry_id, row.task_id); }}> + + + +
    + `; + } + + private _openCompleteDialogForRow(row: TaskRow): void { + const obj = this._objects.find(o => o.entry_id === row.entry_id); + const task = obj?.tasks.find(t => t.id === row.task_id); + this._openCompleteDialog( + row.entry_id, row.task_id, row.task_name, + this._features.checklists ? task?.checklist : undefined, + this._features.adaptive && !!task?.adaptive_config?.enabled + ); + } + + /** + * Render a compact trigger progress bar for overview rows. + * Works for threshold (value vs limit), counter (value/delta vs target), state_change (count vs target). + */ + private _renderObjectDetail() { + if (!this._selectedEntryId) return nothing; + const obj = this._getObject(this._selectedEntryId); + if (!obj) return html`

    Object not found.

    `; + const o = obj.object; + const L = this._lang; + + const isOperator = this._isOperator; + + // v2.10.0: archived tasks are hidden in the object detail too, unless the + // shared show-archived toggle is on; a per-detail toggle reveals them when + // this object has any (e.g. a task archived individually on an active object). + const archivedInObj = obj.tasks.filter((tk) => tk.archived).length; + const visibleTasks = obj.tasks.filter((tk) => this._showArchived || !tk.archived); + + return html` +
    +
    +

    ${o.name}

    +
    + ${!isOperator ? html` + { + const dlg = this.shadowRoot!.querySelector("maintenance-object-dialog"); + dlg?.openEdit(obj.entry_id, o); + }}>${t("edit", L)} + { + const dlg = this.shadowRoot!.querySelector("maintenance-task-dialog"); + dlg?.openCreate(obj.entry_id); + }}>${t("add_task", L)} + this._duplicateObject(obj.entry_id)}> + ${t("duplicate", L)} + + this._toggleArchiveObject(obj.entry_id, !!o.archived)}> + + ${o.archived ? t("unarchive_object", L) : t("archive_object", L)} + + ${!o.archived ? html` + this._togglePauseObject(obj.entry_id, !!o.paused)}> + + ${o.paused ? t("resume_object", L) : t("pause_object", L)} + + this._replaceObject(obj.entry_id, o.name)}> + ${t("replace_object", L)} + + ` : nothing} + this._deleteObject(obj.entry_id)}>${t("delete", L)} + ` : nothing} + this._openQrForObject(obj.entry_id, o.name)}> ${t("qr_code", L)} + this._printObjectReport(obj.entry_id)}> ${t("report_button", L)} +
    +
    + ${o.paused + ? html`

    + + ${t("object_paused_badge", L)}${o.paused_until + ? html` — ${t("paused_until_label", L)} ${formatDate(o.paused_until, L)}` + : nothing} +

    ` + : nothing} + ${o.manufacturer || o.model + ? html`

    ${[o.manufacturer, o.model].filter(Boolean).join(" ")}

    ` + : nothing} + ${o.serial_number ? html`

    ${t("serial_number_label", L)}: ${o.serial_number}

    ` : nothing} + ${o.documentation_url && /^https?:\/\//i.test(o.documentation_url) + ? html`

    ${t("documentation_url_label", L)}: + ${o.documentation_url} +

    ` + : nothing} + ${o.installation_date ? html`

    ${t("installed", L)}: ${formatDate(o.installation_date, L)}

    ` : nothing} + ${o.warranty_expiry ? this._renderWarrantyMeta(o.warranty_expiry, L) : nothing} + ${o.notes + ? html`
    +
    ${t("object_notes_label", L)}
    +
    ${o.notes}
    +
    ` + : nothing} + + + +

    ${t("tasks", L)} (${visibleTasks.length})${archivedInObj > 0 ? html` + { this._showArchived = !this._showArchived; }} + > + + ${this._showArchived ? t("hide_archived", L) : `${t("show_archived", L)} (${archivedInObj})`} + ` : nothing}

    + ${obj.tasks.length === 0 + ? html`
    +

    ${t("no_tasks_yet", L)}

    + { + const dlg = this.shadowRoot!.querySelector("maintenance-task-dialog"); + dlg?.openCreate(obj.entry_id); + }}>${t("add_first_task", L)} +
    ` + : html`
    ${[...visibleTasks].sort((a, b) => { + const so: Record = { overdue: 0, triggered: 1, due_soon: 2, ok: 3 }; + return (so[a.status] ?? 9) - (so[b.status] ?? 9) || (a.days_until_due ?? 99999) - (b.days_until_due ?? 99999); + }).map((task) => html` +
    + + ${this._statusBadge(!!task.archived, !!task.is_done, task.status)} + ${!task.enabled ? html`${t("disabled", L)}` : nothing} + ${task.nfc_tag_id ? html`` : nothing} + + this._showTask(obj.entry_id, task.id)}>${task.name} + ${renderUserBadge(task, (id) => this._userService?.getUserName(id) ?? null)} + ${t(task.type, L)} + this._showTask(obj.entry_id, task.id)}> + ${formatDueDays(task.days_until_due, L)} + ${task.trigger_config + ? renderTriggerProgress(task) + : nothing} + ${renderMiniSparkline(task, this._miniStatsData, this._lang)} + + + { e.stopPropagation(); this._openCompleteDialog(obj.entry_id, task.id, task.name, this._features.checklists ? task.checklist : undefined, this._features.adaptive && !!task.adaptive_config?.enabled); }}> + + + { e.stopPropagation(); this._promptSkipTask(obj.entry_id, task.id); }}> + + + +
    + `)}
    `} +
    + `; + } + + /** + * Render compact task header with status chip and action buttons. + */ + private _toggleMoreMenu(): void { + this._moreMenuOpen = !this._moreMenuOpen; + if (this._moreMenuOpen) { + // Close menu on next outside click + const handler = () => { this._moreMenuOpen = false; document.removeEventListener("click", handler); }; + setTimeout(() => document.addEventListener("click", handler, { once: true }), 0); + } + } + + private _closeMoreMenu(): void { + this._moreMenuOpen = false; + } + + private get _sparklineCtx(): SparklineContext { + return { + lang: this._lang, + detailStatsData: this._detailStatsData, + hasStatsService: !!this._statsService, + isCounterEntity: (tc) => this._isCounterEntity(tc), + rangeDays: this._chartRangeDays, + setRangeDays: (days) => this._setChartRange(days), + hideOutliers: this._hideOutliers, + setHideOutliers: (hide) => this._setHideOutliers(hide), + }; + } + + private _toggleSection(key: string): void { + const next = new Set(this._collapsedSections); + if (next.has(key)) next.delete(key); else next.add(key); + this._collapsedSections = next; + try { localStorage.setItem("msp-collapsed-sections", JSON.stringify([...next])); } catch { /* private mode */ } + } + + /** Build the context the history renderers need from panel state. */ + private _historyCtx(): HistoryContext { + // v2.20 (#83): reading tasks show value + delta per entry. The delta is + // against the chronologically PREVIOUS entry that carries a reading. + const task = this._selectedEntryId && this._selectedTaskId + ? this._getObject(this._selectedEntryId)?.tasks.find((tk) => tk.id === this._selectedTaskId) + : undefined; + const readings = (task?.history || []) + .filter((h) => h.reading_value != null) + .sort((a, b) => a.timestamp.localeCompare(b.timestamp)); + return { + lang: this._lang, + hass: this.hass, + filter: this._historyFilter, + search: this._historySearch, + currencySymbol: this._budget?.currency_symbol || DEFAULT_CURRENCY_SYMBOL, + setFilter: (f) => { this._historyFilter = f; }, + setSearch: (s) => { this._historySearch = s; }, + openEdit: (entry) => this._openHistoryEdit(entry), + readingUnit: task?.reading_unit ?? null, + readingDelta: (entry) => { + const idx = readings.findIndex((h) => h.timestamp === entry.timestamp); + if (idx <= 0) return null; + return (entry.reading_value as number) - (readings[idx - 1].reading_value as number); + }, + }; + } + + /** Build the context the task-detail renderers need from panel state. + * Dialog-opening callbacks stay panel-side (the dialogs live in THIS + * shadow root), so the extracted module never touches a dialog itself. */ + private _taskDetailCtx(): TaskDetailContext { + const entryId = this._selectedEntryId!; + const taskId = this._selectedTaskId!; + const obj = this._getObject(entryId); + return { + lang: this._lang, + hass: this.hass, + entryId, + taskId, + objectName: obj?.object.name || "", + objectDocUrl: obj?.object?.documentation_url ?? null, + isOperator: this._isOperator, + actionLoading: this._actionLoading, + moreMenuOpen: this._moreMenuOpen, + activeTab: this._activeTab, + features: this._features, + currencySymbol: this._budget?.currency_symbol || DEFAULT_CURRENCY_SYMBOL, + collapsedSections: this._collapsedSections, + costDurationToggle: this._costDurationToggle, + suggestionDismissed: this._dismissedSuggestions.has(`${entryId}_${taskId}`), + sparkline: this._sparklineCtx, + history: this._historyCtx(), + getUserName: (id) => this._userService?.getUserName(id) ?? null, + setActiveTab: (tab) => { this._activeTab = tab; }, + toggleSection: (key) => this._toggleSection(key), + setCostDurationToggle: (v) => { this._costDurationToggle = v; }, + showTaskView: () => { this._view = "task"; }, + showObject: () => this._showObject(entryId), + toggleMoreMenu: () => this._toggleMoreMenu(), + closeMoreMenu: () => this._closeMoreMenu(), + openEdit: (tk) => { this.shadowRoot!.querySelector("maintenance-task-dialog")?.openEdit(entryId, tk); }, + openComplete: (tk) => this._openCompleteDialog(entryId, taskId, tk.name, this._features.checklists ? tk.checklist : undefined, this._features.adaptive && !!tk.adaptive_config?.enabled), + promptSkip: () => this._promptSkipTask(entryId, taskId), + toggleArchive: (archived) => this._toggleArchiveTask(entryId, taskId, archived), + openQr: (taskName) => this._openQrForTask(entryId, taskId, obj?.object.name || "", taskName), + duplicateTask: () => this._duplicateTask(entryId, taskId), + promptReset: () => this._promptResetTask(entryId, taskId), + snoozeTask: () => this._snoozeTask(entryId, taskId), + printWorksheet: () => this._printTaskWorksheet(entryId, taskId), + deleteTask: () => this._deleteTask(entryId, taskId), + applySuggestion: (interval) => this._applySuggestion(entryId, taskId, interval), + reanalyze: () => this._reanalyzeInterval(entryId, taskId), + dismissSuggestion: () => this._dismissSuggestion(entryId, taskId), + openSeasonalOverrides: (tk) => this._openSeasonalOverrides(tk), + }; + } + + private _renderTaskDetail() { + if (!this._selectedEntryId || !this._selectedTaskId) return nothing; + const task = this._getTask(this._selectedEntryId, this._selectedTaskId); + if (!task) return html`

    Task not found.

    `; + return renderTaskDetail(task, this._taskDetailCtx()); + } + + /** v2.2.0: open the in-place history-edit dialog for the given entry. + * After save, the dialog fires `history-entry-saved` which we listen + * for in the template to trigger a data refresh. */ + private _openHistoryEdit(entry: HistoryEntry): void { + if (!this._selectedEntryId || !this._selectedTaskId) return; + const draft: HistoryEntryDraft = { + entry_id: this._selectedEntryId, + task_id: this._selectedTaskId, + original_timestamp: entry.timestamp, + type: entry.type, + timestamp: entry.timestamp, + notes: entry.notes ?? null, + cost: entry.cost ?? null, + duration: entry.duration ?? null, + completed_by: entry.completed_by ?? null, + }; + this.shadowRoot + ?.querySelector("maintenance-history-edit-dialog") + ?.openEdit(draft); + } + + /** Triggered by the dialog after a successful WS save. Refresh the loaded + * data so the user sees the updated entry. */ + private _onHistoryEntrySaved = async (): Promise => { + await this._loadData(); + }; + + static styles = [sharedStyles, panelStyles]; +} diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-strategy-shim.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-strategy-shim.ts new file mode 100644 index 0000000..0225090 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-strategy-shim.ts @@ -0,0 +1,348 @@ +/** Maintenance Supporter — dashboard-strategy registration shim. + * + * WHY THIS FILE EXISTS + * -------------------- + * HA serves custom frontend modules via `frontend_extra_module_url`, which the + * frontend loads as fire-and-forget `import("")` calls in index.html — no + * ordering, no await, no completion guarantee. For a `custom:` dashboard + * strategy, HA's renderer then only waits ~5 s via + * `customElements.whenDefined("ll-strategy-dashboard-…")` for the element to + * appear (unlike built-in strategies, which HA imports and awaits itself). + * + * On systems with many heavy HACS frontend plugins (card-mod, layout-card, …) + * our full strategy bundle (~14 KB + a transitive graph) can lose that race: + * the module is downloaded but its top-level `customElements.define` hasn't + * finished executing within the 5 s window → "Timeout waiting for strategy + * element ll-strategy-dashboard-maintenance-supporter to be registered". + * Reproduced ~30–40 % of cold loads on a plugin-heavy live system. + * + * THE FIX + * ------- + * This shim is the `extra_module_url` instead of the full bundle. It has ZERO + * static imports and does ONE thing synchronously: define a tiny wrapper + * element for the dashboard strategy (and push its picker entry). Parsing a + * few hundred bytes with no import graph reaches `define()` in well under a + * microtask, so it wins HA's 5 s race essentially every time. The heavy + * bundle is `import()`-ed lazily on first `generate()` / `getConfigElement()` + * — exactly how HA's own built-in strategies defer their code. + * + * The heavy bundle keeps registering the editor, the section strategies, the + * `ll-custom` tap handler, and the section picker entries at its own top level + * (all of which only matter once a dashboard is actually being generated). + */ + +const STRATEGY_TYPE = "maintenance-supporter"; +const STRATEGY_TAG = `ll-strategy-dashboard-${STRATEGY_TYPE}`; +const EDITOR_TAG = "hui-maintenance-supporter-strategy-editor"; + +// Absolute URL of the full strategy bundle (served at STRATEGY_URL by the +// integration). The shim's only dependency, loaded on demand. +const BUNDLE_URL = + "/maintenance_supporter_strategy/maintenance-dashboard-strategy.js"; + +let _bundle: Promise | null = null; +function loadBundle(): Promise { + if (!_bundle) _bundle = import(/* @vite-ignore */ BUNDLE_URL); + return _bundle; +} + +// The real dashboard class, exported by the heavy bundle once it loads. We +// resolve it from the registered editor's sibling export at call time. +type DashboardStrategyClass = { + generate(config: unknown, hass: unknown): Promise; +}; + +async function realDashboard(): Promise { + const mod = (await loadBundle()) as { + MaintenanceDashboardStrategy?: DashboardStrategyClass; + }; + if (!mod.MaintenanceDashboardStrategy) { + throw new Error( + "[maintenance-supporter] strategy bundle loaded but did not export " + + "MaintenanceDashboardStrategy", + ); + } + return mod.MaintenanceDashboardStrategy; +} + +class MaintenanceDashboardStrategyShim extends HTMLElement { + static getCreateSuggestions(_hass: unknown) { + return { title: "Maintenance Supporter", icon: "mdi:wrench-clock" }; + } + + static async getConfigElement(): Promise { + // Loading the bundle defines the editor element as a side effect. + await loadBundle(); + return document.createElement(EDITOR_TAG); + } + + static async generate(config: unknown, hass: unknown): Promise { + const real = await realDashboard(); + return real.generate(config, hass); + } +} + +function defineShim(): void { + // Attempt unconditionally: customElements.get() is unreliable across HA's + // scoped registries (it can read true while the active dashboard registry has + // no registration), so we always try and swallow the "already defined" throw. + // On a fresh (cache-busted) re-import this re-registers on the now-active + // registry, which is what the self-heal relies on. + try { + customElements.define(STRATEGY_TAG, MaintenanceDashboardStrategyShim); + } catch { + /* already defined in this registry — fine */ + } +} +defineShim(); + +// Picker discovery (HA 2026.5+ reads window.customStrategies when the +// "Add Dashboard" dialog opens). Idempotent — the heavy bundle pushes the +// same entry too once it loads; whichever runs first wins, the other skips. +const w = window as unknown as { + customStrategies?: Array<{ + type: string; + strategyType: string; + name: string; + description?: string; + documentationURL?: string; + }>; +}; +w.customStrategies = w.customStrategies || []; +if ( + !w.customStrategies.some( + (s) => s.type === STRATEGY_TYPE && s.strategyType === "dashboard", + ) +) { + w.customStrategies.push({ + type: STRATEGY_TYPE, + strategyType: "dashboard", + name: "Maintenance Supporter", + description: + "Auto-generated dashboard. Group views by area, status, floor, or due date — picked from the strategy editor or YAML.", + documentationURL: + "https://github.com/iluebbe/maintenance_supporter#dashboard-strategy", + }); +} + +// ── Self-heal ─────────────────────────────────────────────────────────────── +// Root cause (diagnosed live): HA's frontend uses a SCOPED custom-element +// registry that is swapped in during boot. Our element, defined at module load, +// can end up registered on a registry that the dashboard renderer no longer +// uses — so `customElements.get(STRATEGY_TAG)` reads false in the active +// registry and HA's 5 s whenDefined times out (console-only error, empty view). +// +// Verified recovery (9/9 on a live plugin-heavy system): RE-DEFINE on the +// now-current registry, then bounce the view (navigate away + back) so HA's +// reconnects and re-runs _fetchConfig — generate() then +// finds the element and the real dashboard renders. +// +// Fully defensive: only acts on a Lovelace path when our element is missing +// from the active registry; on a healthy load it never fires. +(() => { + const heal = window as unknown as { __msStrategyHealActive?: boolean }; + if (heal.__msStrategyHealActive) return; // one heal loop per page session + heal.__msStrategyHealActive = true; + + const NON_LOVELACE = + /^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/; + + // NOTE: customElements.get(STRATEGY_TAG) is NOT a reliable health signal — + // HA's scoped registry makes it read `true` here even when the dashboard's + // renderer (a different registry) failed. So we gate on the RENDERED OUTCOME: + // a strategy dashboard that failed shows an essentially empty view. + const ERROR_RE = + /Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i; + + // The dashboard/view strategy type our generated dashboards carry. + const OUR_STRATEGY = `custom:${STRATEGY_TYPE}`; + + // Deep querySelector through open shadow roots (HA nests the panel deeply). + function findDeep(tag: string): Element | null { + const stack: Array = [document.documentElement]; + let scanned = 0; + while (stack.length && scanned < 9000) { + const node = stack.pop(); + scanned++; + if (!node) continue; + const el = node as Element & { shadowRoot?: ShadowRoot | null }; + if (el.nodeType === 1 && el.tagName && el.tagName.toLowerCase() === tag) { + return el; + } + if (el.shadowRoot) stack.push(el.shadowRoot); + const kids = (node as ParentNode).children; + if (kids) for (const k of Array.from(kids)) stack.push(k); + } + return null; + } + + // The view config for the path we are on (by view `path`, else numeric index, + // else the first view). + function currentViewCfg( + cfg: { views?: Array> }, + ): Record | null { + const views = cfg?.views; + if (!Array.isArray(views) || !views.length) return null; + const seg = window.location.pathname.split("/").filter(Boolean).pop() || ""; + const byPath = views.find((v) => (v as { path?: string })?.path === seg); + if (byPath) return byPath; + const idx = Number(seg); + if (Number.isInteger(idx) && views[idx]) return views[idx]; + return views[0]; + } + + // True ONLY when the active dashboard/view is generated by our strategy. + // Reads the Lovelace config off (both the processed + // `config` and the original `rawConfig`, at dashboard level and current-view + // level). This scopes the self-heal so it never touches a dashboard that is + // not ours — the fix for issue #68, where the blank-view heuristic bounced + // unrelated dashboards that happened to have < 3 cards. Defensive: any read + // failure → false (never heal a view we cannot confirm is ours). + function isOurStrategyView(): boolean { + try { + const panel = findDeep("ha-panel-lovelace") as unknown as { + lovelace?: { + config?: Record; + rawConfig?: Record; + }; + } | null; + const lov = panel?.lovelace; + if (!lov) return false; + const stratType = (s: unknown): string | undefined => + (s as { type?: string } | undefined)?.type; + for (const cfg of [lov.config, lov.rawConfig]) { + if (!cfg) continue; + if (stratType((cfg as { strategy?: unknown }).strategy) === OUR_STRATEGY) { + return true; + } + const v = currentViewCfg( + cfg as { views?: Array> }, + ); + if (v && stratType((v as { strategy?: unknown }).strategy) === OUR_STRATEGY) { + return true; + } + } + return false; + } catch { + return false; + } + } + + // Heuristic for "we are on a broken strategy view": the lovelace view exists + // but is essentially empty (HA rendered the failed strategy as a blank view), + // OR HA surfaced our strategy error card. Walks light + shadow DOM. + function viewIsBroken(): boolean { + let hasView = false; + let cards = 0; + let ourError = false; + let hasEmptyState = false; + const stack: Array = [document.documentElement]; + let scanned = 0; + while (stack.length && scanned < 9000) { + const node = stack.pop(); + scanned++; + if (!node) continue; + const el = node as Element & { shadowRoot?: ShadowRoot | null }; + if (el.nodeType === 1 && el.tagName) { + const tag = el.tagName.toLowerCase(); + if (tag === "hui-view" || tag === "hui-sections-view") hasView = true; + if (tag === "ha-card" || tag === "hui-card") cards++; + // The zero-objects onboarding view renders exactly ONE hui-empty-state- + // card. That is a SUCCESSFUL strategy render, but it trips the "< 3 + // cards" blank-view heuristic below — issue #69, where the empty state + // was mistaken for a broken render and the page reloaded every ~5 s. + if (tag === "hui-empty-state-card") hasEmptyState = true; + if (tag === "hui-error-card" && ERROR_RE.test(el.textContent || "")) { + ourError = true; + } + } + if (el.shadowRoot) stack.push(el.shadowRoot); + const kids = (node as ParentNode).children; + if (kids) for (const k of Array.from(kids)) stack.push(k); + } + // ourError (HA's error card naming OUR strategy element) is unambiguous — + // act on it directly. The "blank view" signal (< 3 cards) is ambiguous on + // its own (a small or still-loading dashboard looks identical), so gate it + // on the active dashboard actually being our strategy. Without that gate the + // self-heal bounced unrelated dashboards with few cards (issue #68). + if (ourError) return true; + // A rendered empty-state card means generate() ran and produced our + // onboarding view — that is healthy by definition, so never bounce it. A + // genuine strategy timeout shows hui-error-card or zero cards, never this + // card (issue #69). + if (hasEmptyState) return false; + return hasView && cards < 3 && isOurStrategyView(); + } + + // URL of THIS module, so recovery can force a fresh re-import (a cache-busted + // import re-evaluates the module and re-runs customElements.define against the + // registry that is active NOW — the boot-time define may have landed on a + // scoped registry HA has since discarded, and a plain defineShim() call is a + // no-op because customElements.get() reads stale-true across scoped registries). + const SHIM_URL = "/maintenance_supporter_strategy_shim.js"; + + let bounces = 0; + let lastBounce = 0; + function recover(): void { + const now = Date.now(); + if (now - lastBounce < 5000 || bounces >= 3) return; // never thrash / loop + lastBounce = now; + bounces += 1; + // 1) Force a fresh re-import so define() re-runs on the now-active registry. + // (Verified live: this is what makes the element resolvable to HA's + // dashboard renderer; a guarded re-define is not enough.) + void import(/* @vite-ignore */ `${SHIM_URL}?heal=${now}`) + .catch(() => undefined) + .finally(() => { + // 2) Bounce so HA re-fetches the strategy config; generate() now + // resolves the element and the real dashboard renders. + const back = window.location.pathname + window.location.search; + history.pushState(null, "", "/lovelace"); + window.dispatchEvent(new CustomEvent("location-changed")); + window.setTimeout(() => { + history.pushState(null, "", back); + window.dispatchEvent(new CustomEvent("location-changed")); + }, 200); + }); + } + + function watch(): void { + if (NON_LOVELACE.test(window.location.pathname)) return; + let ticks = 0; + const startedAt = Date.now(); + const id = window.setInterval(() => { + ticks++; + try { + // Give HA past its ~5 s strategy timeout before judging the outcome. + if (Date.now() - startedAt < 6000) return; + if (NON_LOVELACE.test(window.location.pathname)) { + window.clearInterval(id); + return; + } + if (viewIsBroken()) { + recover(); + } else { + window.clearInterval(id); // healthy / recovered + } + if (ticks >= 30) window.clearInterval(id); // ~15 s safety cap + } catch { + window.clearInterval(id); + } + }, 500); + } + + try { + if (document.readyState === "loading") { + window.addEventListener("DOMContentLoaded", watch); + } else { + watch(); + } + window.addEventListener("location-changed", () => { + if (!NON_LOVELACE.test(window.location.pathname)) watch(); + }); + } catch { + /* best-effort; never block registration */ + } +})(); + +export {}; diff --git a/custom_components/maintenance_supporter/frontend-src/package-lock.json b/custom_components/maintenance_supporter/frontend-src/package-lock.json new file mode 100644 index 0000000..cd407e3 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/package-lock.json @@ -0,0 +1,5611 @@ +{ + "name": "maintenance-supporter-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "maintenance-supporter-frontend", + "version": "0.1.0", + "dependencies": { + "pdfjs-dist": "^4.10.38", + "playwright": "1.61" + }, + "devDependencies": { + "@open-wc/testing": "^4.0.0", + "@web/dev-server-esbuild": "^1.0.0", + "@web/test-runner": "^0.20.0", + "@web/test-runner-playwright": "^0.11.0", + "esbuild": "^0.28.1", + "lit": "^3.3.3", + "typescript": "^6.0.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esm-bundle/chai": { + "version": "4.3.4-fix.0", + "resolved": "https://registry.npmjs.org/@esm-bundle/chai/-/chai-4.3.4-fix.0.tgz", + "integrity": "sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^4.2.12" + } + }, + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "dev": true, + "license": "BSD-3-Clause" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@mdn/browser-compat-data": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz", + "integrity": "sha512-EWUguj2kd7ldmrF9F+vI5hUOralPd+sdsUnYbRy33vZTuZkduC1shE9TtEMEjAQwyfyMb4ole5KtjF8MsnQOlA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-wc/dedupe-mixin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-2.0.1.tgz", + "integrity": "sha512-+R4VxvceUxHAUJXJQipkkoV9fy10vNo+OnUnGKZnVmcwxMl460KLzytnUM4S35SI073R0yZQp9ra0MbPUwVcEA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-wc/scoped-elements": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@open-wc/scoped-elements/-/scoped-elements-3.0.6.tgz", + "integrity": "sha512-w1ayJaUUmBw8tALtqQ6cBueld+op+bufujzbrOdH0uCTXnSQkONYZzOH+9jyQ8auVgKLqcxZ8oU6SzfqQhQkPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-wc/dedupe-mixin": "^2.0.0", + "lit": "^3.0.0" + } + }, + "node_modules/@open-wc/semantic-dom-diff": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.20.1.tgz", + "integrity": "sha512-mPF/RPT2TU7Dw41LEDdaeP6eyTOWBD4z0+AHP4/d0SbgcfJZVRymlIB6DQmtz0fd2CImIS9kszaMmwMt92HBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^4.3.1", + "@web/test-runner-commands": "^0.9.0" + } + }, + "node_modules/@open-wc/testing": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@open-wc/testing/-/testing-4.0.0.tgz", + "integrity": "sha512-KI70O0CJEpBWs3jrTju4BFCy7V/d4tFfYWkg8pMzncsDhD7TYNHLw5cy+s1FHXIgVFetnMDhPpwlKIPvtTQW7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@esm-bundle/chai": "^4.3.4-fix.0", + "@open-wc/semantic-dom-diff": "^0.20.0", + "@open-wc/testing-helpers": "^3.0.0", + "@types/chai-dom": "^1.11.0", + "@types/sinon-chai": "^3.2.3", + "chai-a11y-axe": "^1.5.0" + } + }, + "node_modules/@open-wc/testing-helpers": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@open-wc/testing-helpers/-/testing-helpers-3.0.1.tgz", + "integrity": "sha512-hyNysSatbgT2FNxHJsS3rGKcLEo6+HwDFu1UQL6jcSQUabp/tj3PyX7UnXL3H5YGv0lJArdYLSnvjLnjn3O2fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-wc/scoped-elements": "^3.0.2", + "lit": "^2.0.0 || ^3.0.0", + "lit-html": "^2.0.0 || ^3.0.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/babel__code-frame": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.27.0.tgz", + "integrity": "sha512-Dwlo+LrxDx/0SpfmJ/BKveHf7QXWvLBLc+x03l5sbzykj3oB9nHygCpSECF1a+s+QIxbghe+KHqC90vGtxLRAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai-dom": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@types/chai-dom/-/chai-dom-1.11.3.tgz", + "integrity": "sha512-EUEZI7uID4ewzxnU7DJXtyvykhQuwe+etJ1wwOiJyQRTH/ifMWKX+ghiXkxCUvNJ6IQDodf0JXhuP6zZcy2qXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/co-body": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-6.1.3.tgz", + "integrity": "sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*" + } + }, + "node_modules/@types/command-line-args": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", + "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-disposition": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/convert-source-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/convert-source-map/-/convert-source-map-2.0.3.tgz", + "integrity": "sha512-ag0BfJLZf6CQz8VIuRIEYQ5Ggwk/82uvTQf27RcpyDNbY0Vw49LIPqAxk5tqYfrCs9xDaIMvl4aj7ZopnYL8bA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cookies": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.2.tgz", + "integrity": "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "node_modules/@types/debounce": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.4.tgz", + "integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-assert": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/koa": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.9.tgz", + "integrity": "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sinon": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.1.tgz", + "integrity": "sha512-5yoJSqLbjH8T9V2bksgRayuhpZy+723/z6wBOR+Soe4ZlXC0eW8Na71TeaZPUWDQvM7LYKa9UGFc6LRqxiR5fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinon-chai": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.12.tgz", + "integrity": "sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@web/browser-logs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.1.tgz", + "integrity": "sha512-ypmMG+72ERm+LvP+loj9A64MTXvWMXHUOu773cPO4L1SV/VWg6xA9Pv7vkvkXQX+ItJtCJt+KQ+U6ui2HhSFUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "errorstacks": "^2.4.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/config-loader": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@web/config-loader/-/config-loader-0.3.3.tgz", + "integrity": "sha512-ilzeQzrPpPLWZhzFCV+4doxKDGm7oKVfdKpW9wiUNVgive34NSzCw+WzXTvjE4Jgr5CkyTDIObEmMrqQEjhT0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@web/dev-server/-/dev-server-0.4.6.tgz", + "integrity": "sha512-jj/1bcElAy5EZet8m2CcUdzxT+CRvUjIXGh8Lt7vxtthkN9PzY9wlhWx/9WOs5iwlnG1oj0VGo6f/zvbPO0s9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.11", + "@types/command-line-args": "^5.0.0", + "@web/config-loader": "^0.3.0", + "@web/dev-server-core": "^0.7.2", + "@web/dev-server-rollup": "^0.6.1", + "camelcase": "^6.2.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^7.0.1", + "debounce": "^1.2.0", + "deepmerge": "^4.2.2", + "internal-ip": "^6.2.0", + "nanocolors": "^0.2.1", + "open": "^8.0.2", + "portfinder": "^1.0.32" + }, + "bin": { + "wds": "dist/bin.js", + "web-dev-server": "dist/bin.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server-core": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.5.tgz", + "integrity": "sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/koa": "^2.11.6", + "@types/ws": "^7.4.0", + "@web/parse5-utils": "^2.1.0", + "chokidar": "^4.0.1", + "clone": "^2.1.2", + "es-module-lexer": "^1.0.0", + "get-stream": "^6.0.0", + "is-stream": "^2.0.0", + "isbinaryfile": "^5.0.0", + "koa": "^2.13.0", + "koa-etag": "^4.0.0", + "koa-send": "^5.0.1", + "koa-static": "^5.0.0", + "lru-cache": "^8.0.4", + "mime-types": "^2.1.27", + "parse5": "^6.0.1", + "picomatch": "^2.2.2", + "ws": "^7.5.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server-esbuild": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@web/dev-server-esbuild/-/dev-server-esbuild-1.0.5.tgz", + "integrity": "sha512-QkKfnBuo+ogcMXLE9kKhw8+UwfbuIgiKKATY26ssgSwtMmAhgq+/SSG+QM55VMkVVzDDLi97aMDEcdr3V2sYvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdn/browser-compat-data": "^4.0.0", + "@web/dev-server-core": "^0.7.4", + "esbuild": "^0.27.0", + "parse5": "^6.0.1", + "ua-parser-js": "^1.0.33" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server-rollup": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@web/dev-server-rollup/-/dev-server-rollup-0.6.4.tgz", + "integrity": "sha512-sJZfTGCCrdku5xYnQQG51odGI092hKY9YFM0X3Z0tRY3iXKXcYRaLZrErw5KfCxr6g0JRuhe4BBhqXTA5Q2I3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-node-resolve": "^15.0.1", + "@web/dev-server-core": "^0.7.2", + "nanocolors": "^0.2.1", + "parse5": "^6.0.1", + "rollup": "^4.4.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/parse5-utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.1.tgz", + "integrity": "sha512-7rBVZEMGfrq2iPcAEwJ0KSNSvmA2a6jT2CK8/gyIOHgn4reg7bSSRbzyWIEYWyIkeRoYEukX/aW+nAeCgSSqhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse5": "^6.0.1", + "parse5": "^6.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@web/test-runner/-/test-runner-0.20.2.tgz", + "integrity": "sha512-zfEGYEDnS0EI8qgoWFjmtkIXhqP15W40NW3dCaKtbxj5eU0a7E53f3GV/tZGD0GlZKF8d4Fyw+AFrwOJU9Z4GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/browser-logs": "^0.4.0", + "@web/config-loader": "^0.3.0", + "@web/dev-server": "^0.4.0", + "@web/test-runner-chrome": "^0.18.1", + "@web/test-runner-commands": "^0.9.0", + "@web/test-runner-core": "^0.13.0", + "@web/test-runner-mocha": "^0.9.0", + "camelcase": "^6.2.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^7.0.1", + "convert-source-map": "^2.0.0", + "diff": "^5.0.0", + "globby": "^11.0.1", + "nanocolors": "^0.2.1", + "portfinder": "^1.0.32", + "source-map": "^0.7.3" + }, + "bin": { + "web-test-runner": "dist/bin.js", + "wtr": "dist/bin.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-chrome": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@web/test-runner-chrome/-/test-runner-chrome-0.18.1.tgz", + "integrity": "sha512-eO6ctCaqSguGM6G3cFobGHnrEs9wlv9Juj/Akyr4XLjeEMTheNULdvOXw9Bygi+QC/ir/0snMmt+/YKnfy8rYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "@web/test-runner-coverage-v8": "^0.8.0", + "chrome-launcher": "^0.15.0", + "puppeteer-core": "^24.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-commands": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-commands/-/test-runner-commands-0.9.0.tgz", + "integrity": "sha512-zeLI6QdH0jzzJMDV5O42Pd8WLJtYqovgdt0JdytgHc0d1EpzXDsc7NTCJSImboc2NcayIsWAvvGGeRF69SMMYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "mkdirp": "^1.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-core": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.4.tgz", + "integrity": "sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.11", + "@types/babel__code-frame": "^7.0.2", + "@types/co-body": "^6.1.0", + "@types/convert-source-map": "^2.0.0", + "@types/debounce": "^1.2.0", + "@types/istanbul-lib-coverage": "^2.0.3", + "@types/istanbul-reports": "^3.0.0", + "@web/browser-logs": "^0.4.0", + "@web/dev-server-core": "^0.7.3", + "chokidar": "^4.0.1", + "cli-cursor": "^3.1.0", + "co-body": "^6.1.0", + "convert-source-map": "^2.0.0", + "debounce": "^1.2.0", + "dependency-graph": "^0.11.0", + "globby": "^11.0.1", + "internal-ip": "^6.2.0", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.0.2", + "log-update": "^4.0.0", + "nanocolors": "^0.2.1", + "nanoid": "^3.1.25", + "open": "^8.0.2", + "picomatch": "^2.2.2", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-coverage-v8": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.8.0.tgz", + "integrity": "sha512-PskiucYpjUtgNfR2zF2AWqWwjXL7H3WW/SnCAYmzUrtob7X9o/+BjdyZ4wKbOxWWSbJO4lEdGIDLu+8X2Xw+lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "istanbul-lib-coverage": "^3.0.0", + "lru-cache": "^8.0.4", + "picomatch": "^2.2.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-mocha": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-mocha/-/test-runner-mocha-0.9.0.tgz", + "integrity": "sha512-ZL9F6FXd0DBQvo/h/+mSfzFTSRVxzV9st/AHhpgABtUtV/AIpVE9to6+xdkpu6827kwjezdpuadPfg+PlrBWqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-playwright": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@web/test-runner-playwright/-/test-runner-playwright-0.11.1.tgz", + "integrity": "sha512-l9tmX0LtBqMaKAApS4WshpB87A/M8sOHZyfCobSGuYqnREgz5rqQpX314yx+4fwHXLLTa5N64mTrawsYkLjliw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "@web/test-runner-coverage-v8": "^0.8.0", + "playwright": "^1.53.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/axe-core": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.3.tgz", + "integrity": "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz", + "integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz", + "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", + "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai-a11y-axe": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/chai-a11y-axe/-/chai-a11y-axe-1.5.0.tgz", + "integrity": "sha512-V/Vg/zJDr9aIkaHJ2KQu7lGTQQm5ZOH4u1k5iTMvIXuSVlSuUo0jcSpSqf9wUn9zl6oQXa4e4E0cqH18KOgKlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "axe-core": "^4.3.3" + } + }, + "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-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/cliui/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/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/co-body": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz", + "integrity": "sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hapi/bourne": "^3.0.0", + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + }, + "engines": { + "node": ">=8.0.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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.4.tgz", + "integrity": "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.1", + "typical": "^7.3.0" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.3.tgz", + "integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1595872", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1595872.tgz", + "integrity": "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "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/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/errorstacks": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/errorstacks/-/errorstacks-2.4.1.tgz", + "integrity": "sha512-jE4i0SMYevwu/xxAuzhly/KTwtj0xDhbzB6m1xPImxTkw8wcCbgarOQPfCVMi5JKVyW7in29pNJCCJrry3Ynnw==", + "dev": true, + "license": "MIT" + }, + "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-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "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/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "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/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflation": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz", + "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-ip": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", + "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-gateway": "^6.0.0", + "ipaddr.js": "^1.9.1", + "is-ip": "^3.1.0", + "p-event": "^4.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/internal-ip?sponsor=1" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-regex": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.4.tgz", + "integrity": "sha512-3An0GCLDSR34tsCO4H8Tef8Pp2ngtaZDAZnsWJYelqXUK5wyiHvGItgK/xcSkmHLSTn1Jcho1mRQs2ehRzvKKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.9.0", + "debug": "^4.3.2", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^2.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "engines": { + "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" + } + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/koa-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "dev": true, + "license": "MIT", + "dependencies": { + "co": "^4.6.0", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/koa-etag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-etag/-/koa-etag-4.0.0.tgz", + "integrity": "sha512-1cSdezCkBWlyuB9l6c/IFoe1ANCDdPBxkDkRiaIup40xpUub6U/wwRXoKBZw/O5BifX9OlqAjYnDyzM6+l+TAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "etag": "^1.8.1" + } + }, + "node_modules/koa-send": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "resolve-path": "^1.4.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/koa-static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "koa-send": "^5.0.0" + }, + "engines": { + "node": ">= 7.6.0" + } + }, + "node_modules/koa-static/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lit": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", + "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "dev": true, + "license": "Apache-2.0" + }, + "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/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanocolors": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.13.tgz", + "integrity": "sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==", + "dev": true, + "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==", + "dev": true, + "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/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", + "dev": true + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "4.10.38", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz", + "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.65" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/portfinder": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer-core": { + "version": "24.42.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.42.0.tgz", + "integrity": "sha512-T4zXokk/izH01fYPhyyev1A4piWiOKrYq7CUFpdoYQxmOnXoV6YjUabmfIjCYkNspSoAXIxRid3Tw+Vg0fthYg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1595872", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "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/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-path": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-errors": "~1.6.2", + "path-is-absolute": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-path/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/resolve-path/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/resolve-path/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/resolve-path/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "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.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "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/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/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/table-layout": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.3.tgz", + "integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrapjs": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz", + "integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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/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/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/ylru": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/custom_components/maintenance_supporter/frontend-src/package.json b/custom_components/maintenance_supporter/frontend-src/package.json new file mode 100644 index 0000000..6564b46 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/package.json @@ -0,0 +1,27 @@ +{ + "name": "maintenance-supporter-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "node esbuild.mjs", + "test": "web-test-runner --config web-test-runner.config.mjs", + "test:watch": "web-test-runner --config web-test-runner.config.mjs --watch" + }, + "devDependencies": { + "@open-wc/testing": "^4.0.0", + "@web/dev-server-esbuild": "^1.0.0", + "@web/test-runner": "^0.20.0", + "@web/test-runner-playwright": "^0.11.0", + "esbuild": "^0.28.1", + "lit": "^3.3.3", + "typescript": "^6.0.3" + }, + "dependencies": { + "pdfjs-dist": "^4.10.38", + "playwright": "1.61" + }, + "overrides": { + "esbuild": "$esbuild" + } +} diff --git a/custom_components/maintenance_supporter/frontend-src/panel-styles.ts b/custom_components/maintenance_supporter/frontend-src/panel-styles.ts new file mode 100644 index 0000000..8f9eadc --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/panel-styles.ts @@ -0,0 +1,1393 @@ +/** Component-specific styles for the maintenance-supporter-panel. */ + +import { css } from "lit"; + +export const panelStyles = css` + :host { + display: block; + height: 100%; + background: var(--primary-background-color); + } + + .panel { + height: 100%; + display: flex; + flex-direction: column; + } + + .header { + display: flex; + align-items: center; + gap: 4px; + background: var(--app-header-background-color, var(--primary-color)); + color: var(--app-header-text-color, white); + padding: 12px 16px; + font-size: 16px; + } + + .header ha-menu-button { + margin-right: 4px; + color: var(--app-header-text-color, white); + } + .header ha-icon-button { + --mdc-icon-button-size: 36px; + --mdc-icon-size: 20px; + color: var(--app-header-text-color, white); + } + + .breadcrumbs { display: flex; align-items: center; gap: 4px; } + .breadcrumbs a { color: inherit; opacity: 0.8; cursor: pointer; text-decoration: none; } + .breadcrumbs a:hover { opacity: 1; text-decoration: underline; } + .breadcrumbs .sep { opacity: 0.5; margin: 0 4px; } + .breadcrumbs .current { font-weight: 500; } + + .content { flex: 1; overflow-y: auto; padding: 0 16px 16px; } + + .filter-bar { + display: flex; + justify-content: space-between; + align-items: flex-end; + padding: 8px 0; + gap: 8px; + } + + .filter-field { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + } + .filter-label { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--secondary-text-color); + padding-left: 2px; + } + + .filter-bar select { + padding: 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + } + + /* Desktop: the LIST owns the 7-column grid and every row is a subgrid + spanning all columns. Sharing the tracks across rows is what actually + keeps the title (and every other column) aligned regardless of which + optional badges/chips a given row carries. A per-row grid can't: each + row would size its auto badges column independently, so a row with an + NFC badge pushed its title right of the others (issue #66). */ + .task-table { + display: grid; + grid-template-columns: + auto /* badges */ + minmax(100px, 180px) /* object-name */ + minmax(120px, 1fr) /* task-name */ + minmax(0, 220px) /* task-sub (chips) */ + 100px /* type */ + 150px /* due-cell */ + auto; /* row-actions */ + column-gap: 12px; + } + + .task-row { + display: grid; + grid-template-columns: subgrid; + grid-column: 1 / -1; + align-items: center; + column-gap: 12px; + padding: 10px 12px; + border-bottom: 1px solid var(--divider-color); + cursor: pointer; + transition: background 0.15s; + } + + /* Virtualized task table (large installs): only the scroll window of rows + is in the DOM. The spacers span all columns and carry the off-window + height so the scrollbar stays honest. The sizer row is invisible and + zero-height but its badge cell still participates in subgrid track + sizing — pinning the content-sized badge column to the widest badge set + across ALL rows, so columns can't jitter while scrolling. */ + .virt-spacer { grid-column: 1 / -1; } + .task-row.virt-sizer { + height: 0; + min-height: 0; + padding-top: 0; + padding-bottom: 0; + border: none; + overflow: hidden; + visibility: hidden; + pointer-events: none; + } + + /* Bulk selection: a leading checkbox column while selecting. */ + .task-table.bulk { grid-template-columns: auto auto minmax(100px, 180px) minmax(120px, 1fr) minmax(0, 220px) 100px 150px auto; } + .bulk-check { display: flex; align-items: center; justify-content: center; cursor: pointer; } + .bulk-check input, .bulk-selectall input { width: 17px; height: 17px; cursor: pointer; accent-color: var(--primary-color); } + .task-row.bulk-selected { background: color-mix(in srgb, var(--primary-color) 12%, transparent); } + .bulk-bar { + display: flex; align-items: center; gap: 16px; flex-wrap: wrap; + padding: 8px 12px; margin-bottom: 8px; border-radius: 8px; + background: var(--secondary-background-color); border: 1px solid var(--divider-color); + position: sticky; top: 0; z-index: 5; + } + .bulk-selectall { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; } + .bulk-count { color: var(--secondary-text-color); font-size: 13px; } + .bulk-actions { margin-left: auto; display: inline-flex; gap: 8px; } + .bulk-toggle.active { --mdc-theme-primary: var(--primary-color); } + + /* Collapsible analysis sections on the task-detail overview tab. The header + owns the title, so the wrapped card's own title row is hidden to avoid + showing it twice. */ + .collapsible { margin: 8px 0; border: 1px solid var(--divider-color); border-radius: 10px; overflow: hidden; } + .collapsible-head { + display: flex; align-items: center; gap: 8px; width: 100%; + font: inherit; font-weight: 600; font-size: 14px; text-align: left; + padding: 10px 12px; cursor: pointer; background: var(--secondary-background-color); + border: none; color: var(--primary-text-color); + } + .collapsible-head:hover { background: var(--table-row-alternative-background-color, rgba(0,0,0,.04)); } + .collapsible-head ha-icon { --mdc-icon-size: 20px; color: var(--secondary-text-color); } + .collapsible-body { padding: 4px 12px 12px; } + .collapsible-body > .weibull-section > .weibull-title, + .collapsible-body > .seasonal-chart > .seasonal-chart-title { display: none; } + + /* "Today" focus view — mobile-first list grouped by urgency. */ + .today-view { display: flex; flex-direction: column; gap: 16px; padding: 4px 0 12px; } + .today-section { border: 1px solid var(--divider-color); border-radius: 12px; overflow: hidden; } + .today-section-header { + display: flex; align-items: center; justify-content: space-between; + padding: 10px 14px; font-weight: 600; font-size: 14px; + background: var(--secondary-background-color); + border-left: 4px solid var(--divider-color); + } + .today-section-header.overdue { border-left-color: var(--error-color, #f44336); } + .today-section-header.due_soon { border-left-color: var(--warning-color, #ff9800); } + .today-badge { + min-width: 22px; text-align: center; padding: 1px 8px; border-radius: 11px; + background: var(--primary-color); color: var(--text-primary-color, #fff); font-size: 12.5px; + } + .today-row { + display: flex; align-items: center; gap: 12px; padding: 11px 14px; + border-top: 1px solid var(--divider-color); cursor: pointer; + content-visibility: auto; contain-intrinsic-size: auto 46px; + } + .today-row:hover { background: var(--table-row-alternative-background-color, rgba(0,0,0,.04)); } + .today-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; background: var(--success-color, #4caf50); } + .today-dot.overdue { background: var(--error-color, #f44336); } + .today-dot.due_soon { background: var(--warning-color, #ff9800); } + .today-dot.triggered { background: #ff5722; } + .today-main { flex: 1; min-width: 0; } + .today-task { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .today-object { color: var(--secondary-text-color); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .today-row .btn-complete { color: var(--success-color, #4caf50); flex: none; } + .today-empty { + display: flex; flex-direction: column; align-items: center; gap: 10px; + padding: 48px 16px; color: var(--secondary-text-color); text-align: center; + } + .today-empty ha-icon { --mdc-icon-size: 56px; color: var(--success-color, #4caf50); } + + /* Command palette (Ctrl/Cmd+K). */ + .palette-backdrop { + position: fixed; inset: 0; z-index: 1100; background: rgba(0,0,0,.4); + display: flex; align-items: flex-start; justify-content: center; padding-top: 12vh; + animation: toast-in .15s ease; + } + .palette { + width: min(620px, 92vw); max-height: 66vh; display: flex; flex-direction: column; + background: var(--card-background-color, #fff); border-radius: 12px; overflow: hidden; + box-shadow: 0 12px 48px rgba(0,0,0,.4); + } + .palette-input { + font: inherit; font-size: 16px; padding: 16px 18px; border: none; outline: none; + background: transparent; color: var(--primary-text-color); + border-bottom: 1px solid var(--divider-color); + } + .palette-results { overflow-y: auto; } + .palette-item { + display: flex; align-items: center; gap: 10px; padding: 10px 16px; cursor: pointer; + } + .palette-item.active { background: color-mix(in srgb, var(--primary-color) 14%, transparent); } + .palette-item ha-icon { --mdc-icon-size: 20px; color: var(--secondary-text-color); flex: none; } + .palette-label { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .palette-sub { margin-left: auto; color: var(--secondary-text-color); font-size: 12.5px; flex: none; padding-left: 10px; } + .palette-empty { padding: 20px 16px; color: var(--secondary-text-color); text-align: center; } + .palette-hint { padding: 8px 16px; font-size: 12px; color: var(--secondary-text-color); border-top: 1px solid var(--divider-color); } + + /* Template gallery. */ + .template-gallery { + width: min(720px, 94vw); max-height: 80vh; display: flex; flex-direction: column; + background: var(--card-background-color, #fff); border-radius: 12px; overflow: hidden; + box-shadow: 0 12px 48px rgba(0,0,0,.4); + } + .template-gallery-head { + display: flex; align-items: center; justify-content: space-between; + padding: 12px 8px 12px 18px; font-weight: 600; font-size: 16px; + border-bottom: 1px solid var(--divider-color); + } + .template-gallery-body { overflow-y: auto; padding: 8px 16px 16px; } + .template-cat { margin-top: 12px; } + .template-cat-head { + display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 13px; + color: var(--secondary-text-color); margin-bottom: 8px; + } + .template-cat-head ha-icon { --mdc-icon-size: 20px; } + .template-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; } + .template-card { + display: flex; flex-direction: column; gap: 4px; text-align: left; cursor: pointer; + padding: 12px 14px; border: 1px solid var(--divider-color); border-radius: 10px; + background: var(--card-background-color); font: inherit; color: var(--primary-text-color); + } + .template-card:hover { border-color: var(--primary-color); background: color-mix(in srgb, var(--primary-color) 6%, transparent); } + .template-card[disabled] { opacity: .5; pointer-events: none; } + .template-card-name { font-weight: 600; font-size: 14px; } + .template-card-count { font-size: 12px; color: var(--secondary-text-color); } + .empty-onboard-hint { color: var(--secondary-text-color); font-size: 13px; margin: 4px 0 12px; } + .empty-onboard-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; } + + .task-row:hover { + background: var(--table-row-alternative-background-color, rgba(0, 0, 0, 0.04)); + } + + /* Wrapper for status + optional disabled/NFC badges so they share one grid column */ + .cell-badges { + display: flex; + align-items: center; + gap: 6px; + } + + .cell { font-size: 14px; } + .cell.object-name { color: var(--primary-color); cursor: pointer; } + .cell.task-name { font-weight: 500; } + .cell.type { color: var(--secondary-text-color); } + + /* Task subline chips (group / area / assigned user) — desktop shows inline, mobile wraps below */ + .task-sub { + display: flex; + gap: 6px; + align-items: center; + font-size: 12px; + color: var(--secondary-text-color); + flex-wrap: wrap; + justify-content: flex-end; + } + /* Empty subline still occupies its grid slot so neighbouring columns line up */ + .task-sub-empty { min-height: 1px; } + .sub-chip { + display: inline-flex; + align-items: center; + gap: 4px; + white-space: nowrap; + padding: 2px 8px; + border-radius: 10px; + background: var(--secondary-background-color, rgba(127, 127, 127, 0.1)); + line-height: 1.4; + } + .sub-chip ha-icon { + --mdc-icon-size: 14px; + opacity: 0.75; + } + + /* Row action buttons (Complete / Skip): right-aligned in their column and a + bit larger — the default mwc glyph reads small inside its padded button. */ + .row-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 2px; + } + .row-actions mwc-icon-button { + --mdc-icon-button-size: 44px; + --mdc-icon-size: 26px; + } + + .detail-section { padding: 16px 0; } + + .detail-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 8px; + } + .detail-header h2 { margin: 0; font-size: 22px; } + h3 { margin: 16px 0 8px; font-size: 16px; font-weight: 500; } + .meta { color: var(--secondary-text-color); margin: 4px 0; } + /* v1.4.10 (#46): per-object free-form notes block */ + .object-notes { + margin: 12px 0 4px; + padding: 12px 14px; + background: var(--card-background-color, var(--ha-card-background, #1c1c1c)); + border-left: 3px solid var(--primary-color, #03a9f4); + border-radius: 4px; + } + .object-notes-label { + font-size: 12px; + font-weight: 500; + color: var(--secondary-text-color); + text-transform: uppercase; + letter-spacing: 0.4px; + margin-bottom: 6px; + } + .object-notes-body { + color: var(--primary-text-color); + white-space: pre-wrap; + word-break: break-word; + line-height: 1.45; + } + .empty { color: var(--secondary-text-color); font-style: italic; } + .analysis-empty-state { text-align: center; padding: 24px 16px; } + .analysis-empty-state .empty { font-size: 15px; margin-bottom: 8px; } + .analysis-empty-state .empty-icon { + --mdc-icon-size: 48px; + color: var(--secondary-text-color); + opacity: 0.4; + display: block; + margin: 0 auto 12px; + } + .empty-hint { color: var(--secondary-text-color); font-size: 13px; margin: 4px 0; } + .analysis-progress { + width: 120px; margin: 12px auto 4px; height: 6px; + background: var(--divider-color, #e0e0e0); border-radius: 3px; overflow: hidden; + } + .analysis-progress-bar { + height: 100%; background: var(--primary-color); border-radius: 3px; + } + + .info-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 8px; + } + + .info-item { + display: flex; + flex-direction: column; + padding: 8px; + background: var(--card-background-color, #fff); + border-radius: 8px; + } + + .info-item .label { + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 2px; + } + + /* Dashboard redesign styles */ + + .task-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 8px; + margin-bottom: 16px; + gap: 12px; + flex-wrap: wrap; + } + + .task-header-title { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + } + + .task-name-breadcrumb, + .object-name-breadcrumb { + cursor: pointer; + color: var(--primary-text-color); + text-decoration: none; + } + + .task-name-breadcrumb:hover, + .object-name-breadcrumb:hover { + text-decoration: underline; + } + + .breadcrumb-separator { + color: var(--secondary-text-color); + margin: 0 4px; + } + + .status-chip { + display: inline-block; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + } + + .status-chip.ok { + background: #4caf50; + color: white; + } + + .status-chip.warning { + background: #ff9800; + color: white; + } + + .status-chip.overdue { + background: #f44336; + color: white; + } + + .status-chip.done { + background: #78909c; + color: white; + } + + /* (#67) Warranty status chip — object detail meta + objects table */ + .warranty-chip { + display: inline-block; + padding: 2px 8px; + border-radius: 10px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; + } + .warranty-valid { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #2e7d32); + } + .warranty-expiring { + background: rgba(255, 152, 0, 0.18); + color: var(--warning-color, #e65100); + } + .warranty-expired { + background: rgba(244, 67, 54, 0.16); + color: var(--error-color, #c62828); + } + .warranty-none { + color: var(--secondary-text-color); + } + + /* (#67) All-Objects view-mode toggle (cards / table) */ + .view-toggle { + display: inline-flex; + border: 1px solid var(--divider-color); + border-radius: 8px; + overflow: hidden; + align-self: end; + } + .view-toggle-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 10px; + background: var(--card-background-color, #fff); + color: var(--secondary-text-color); + border: none; + cursor: pointer; + } + .view-toggle-btn + .view-toggle-btn { border-left: 1px solid var(--divider-color); } + .view-toggle-btn.active { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + } + .view-toggle-btn ha-icon { --mdc-icon-size: 18px; } + + /* (#67) Objects table (desktop All-Objects view) */ + .objects-table-wrap { + overflow-x: auto; + border: 1px solid var(--divider-color); + border-radius: 8px; + } + .objects-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + } + .objects-table th, + .objects-table td { + text-align: left; + padding: 8px 12px; + border-bottom: 1px solid var(--divider-color); + white-space: nowrap; + } + .objects-table thead th { + font-weight: 600; + color: var(--secondary-text-color); + background: var(--secondary-background-color, rgba(0, 0, 0, 0.03)); + position: sticky; + top: 0; + } + .objects-table tbody tr { cursor: pointer; } + .objects-table tbody tr:hover { + background: var(--secondary-background-color, rgba(0, 0, 0, 0.04)); + } + .objects-table tbody tr:last-child td { border-bottom: none; } + .objects-table-name { font-weight: 500; color: var(--primary-text-color); } + .doc-badge { + display: inline-flex; align-items: center; gap: 2px; vertical-align: middle; + margin-left: 8px; padding: 1px 7px 1px 5px; border-radius: 10px; + font-size: 12px; font-weight: 600; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); + color: var(--secondary-text-color, #888); + } + .doc-badge ha-icon { --mdc-icon-size: 14px; } + /* v2.20 (N3): paused marker on object cards + the detail meta line. */ + .paused-badge { + display: inline-flex; align-items: center; vertical-align: middle; + margin-left: 8px; color: var(--info-color, #2196f3); + } + .paused-badge ha-icon { --mdc-icon-size: 16px; } + .paused-meta { + display: flex; align-items: center; gap: 6px; + color: var(--info-color, #2196f3); font-weight: 500; + } + .paused-meta ha-icon { --mdc-icon-size: 16px; } + .objects-table .oc-notes { + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + } + .objects-table .oc-task_count, + .objects-table .oc-actions { text-align: center; } + + .user-badge { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 2px 8px; + margin-left: 8px; + background: var(--primary-color); + color: var(--text-primary-color); + border-radius: 10px; + font-size: 11px; + font-weight: 500; + line-height: 1.4; + } + + .user-badge ha-icon { + --mdc-icon-size: 12px; + } + + .nfc-badge { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 3px 8px; + margin-left: 6px; + background: var(--secondary-background-color, #e8e8e8); + color: var(--primary-text-color); + border-radius: 12px; + font-size: 11px; + font-weight: 500; + } + .priority-badge { + display: inline-flex; + align-items: center; + margin-left: 6px; + border-radius: 12px; + padding: 2px; + } + .priority-badge ha-icon { + --mdc-icon-size: 16px; + } + .priority-high { + color: var(--error-color, #db4437); + } + .priority-low { + color: var(--secondary-text-color, #888); + } + .label-chip { + background: var(--primary-color, #03a9f4); + color: var(--text-primary-color, #fff); + opacity: 0.85; + } + .label-chip ha-icon { + --mdc-icon-size: 13px; + } + .nfc-badge ha-icon { + --mdc-icon-size: 14px; + } + .nfc-badge.unlinked { + opacity: 0.4; + cursor: pointer; + border: 1px dashed var(--divider-color); + background: transparent; + } + .nfc-badge.unlinked:hover { + opacity: 0.7; + } + + .task-header-actions { + display: flex; + gap: 8px; + } + + .more-menu-wrapper { + position: relative; + } + + .popup-menu { + position: absolute; + top: 100%; + right: 0; + background: var(--card-background-color, #fff); + border: 1px solid var(--divider-color); + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 100; + min-width: 180px; + overflow: hidden; + } + + .popup-menu-item { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + cursor: pointer; + font-size: 14px; + color: var(--primary-text-color); + } + + .popup-menu-item:hover { + background: var(--table-row-alternative-background-color, rgba(0, 0, 0, 0.04)); + } + + .popup-menu-item.danger { + color: var(--error-color, #f44336); + } + + .popup-menu-item ha-icon { + --mdc-icon-size: 18px; + } + + .popup-menu-divider { + height: 1px; + background: var(--divider-color); + margin: 4px 0; + } + + .tab-bar { + display: flex; + gap: 4px; + border-bottom: 2px solid var(--divider-color); + margin-bottom: 16px; + } + + .tab { + padding: 12px 24px; + cursor: pointer; + font-weight: 500; + color: var(--secondary-text-color); + border-bottom: 2px solid transparent; + margin-bottom: -2px; + transition: all 0.2s; + } + + .tab:hover { + color: var(--primary-text-color); + } + + .tab.active { + color: var(--primary-color); + border-bottom-color: var(--primary-color); + } + + .tab-content { + padding: 16px 0; + } + + .kpi-bar { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 12px; + margin-bottom: 24px; + } + + .kpi-card { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 16px 12px; + text-align: center; + border: 1px solid var(--divider-color); + } + + .kpi-card.warning { + border-color: #ff9800; + background: rgba(255, 152, 0, 0.1); + } + + .kpi-card.overdue { + border-color: #f44336; + background: rgba(244, 67, 54, 0.1); + } + + .kpi-label { + font-size: 11px; + color: var(--secondary-text-color); + margin-bottom: 6px; + text-transform: uppercase; + font-weight: 500; + } + + .kpi-value { + font-size: 16px; + font-weight: 500; + color: var(--primary-text-color); + } + + .kpi-value-large { + font-size: 22px; + font-weight: 600; + color: var(--primary-text-color); + } + + .kpi-subtext { + font-size: 10px; + color: var(--secondary-text-color); + margin-top: 4px; + } + + .two-column-layout { + display: grid; + grid-template-columns: 40% 60%; + gap: 16px; + margin-bottom: 24px; + } + + .two-column-layout.single-column { + grid-template-columns: 1fr; + } + + .left-column, + .right-column { + display: flex; + flex-direction: column; + gap: 16px; + } + + .recent-activities { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 16px; + border: 1px solid var(--divider-color); + } + + .recent-activities h3 { + margin: 0 0 12px 0; + } + + .activity-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 0; + border-bottom: 1px solid var(--divider-color); + } + + .activity-item:last-of-type { + border-bottom: none; + } + + .activity-icon { + font-size: 18px; + width: 24px; + text-align: center; + } + + .activity-date { + font-size: 12px; + color: var(--secondary-text-color); + min-width: 120px; + } + + .activity-note { + flex: 1; + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .activity-badge { + font-size: 12px; + padding: 2px 8px; + background: var(--primary-color); + color: white; + border-radius: 12px; + } + + .activity-show-all { + margin-top: 12px; + text-align: center; + } + + .history-filters-new { + display: flex; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; + flex-wrap: wrap; + } + + .filter-chips { + display: flex; + gap: 8px; + flex-wrap: wrap; + } + + .filter-controls { + display: flex; + gap: 8px; + } + + .search-input { + padding: 8px 12px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 14px; + min-width: 200px; + } + + .search-input:focus { + outline: none; + border-color: var(--primary-color); + } + + /* Checklist preview card (read-only display in task overview) */ + .checklist-preview-card { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 12px 16px; + border: 1px solid var(--divider-color); + margin-top: 8px; + } + .checklist-preview-header { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 8px; + } + .checklist-preview-header ha-icon { + --mdc-icon-size: 18px; + } + .checklist-preview-list { + margin: 0; + padding-left: 20px; + color: var(--primary-text-color); + font-size: 14px; + line-height: 1.6; + } + .checklist-preview-list li { + padding: 1px 0; + } + + /* Recommendation Card */ + .recommendation-card { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 16px; + border: 1px solid var(--divider-color); + } + + .recommendation-card h4 { + margin: 0 0 12px 0; + font-size: 14px; + } + + .interval-comparison { + margin-bottom: 16px; + } + + .interval-bar { + margin-bottom: 12px; + } + + .interval-label { + font-size: 12px; + margin-bottom: 4px; + display: flex; + align-items: center; + gap: 8px; + } + + .interval-visual { + height: 24px; + border-radius: 4px; + transition: width 0.3s; + } + + .interval-visual.current { + background: var(--secondary-text-color); + opacity: 0.5; + } + + .interval-visual.suggested { + background: var(--primary-color); + } + + .confidence-badge { + font-size: 10px; + padding: 2px 6px; + border-radius: 3px; + background: var(--divider-color); + } + + .confidence-badge.high { + background: #4caf50; + color: white; + } + + .confidence-badge.medium { + background: #ff9800; + color: white; + } + + .confidence-badge.low { + background: var(--secondary-text-color); + color: white; + } + + .recommendation-actions { + display: flex; + gap: 8px; + } + + /* Seasonal Card Compact */ + .seasonal-card-compact { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 16px; + border: 1px solid var(--divider-color); + } + + .seasonal-card-compact h4 { + margin: 0 0 12px 0; + font-size: 14px; + } + + .seasonal-mini-chart { + display: flex; + align-items: flex-end; + gap: 4px; + height: 60px; + margin-bottom: 12px; + } + + .seasonal-bar { + flex: 1; + border-radius: 2px 2px 0 0; + transition: all 0.2s; + cursor: pointer; + } + + .seasonal-bar.low { + background: #2196f3; + } + + .seasonal-bar.normal { + background: var(--secondary-text-color); + opacity: 0.5; + } + + .seasonal-bar.high { + background: #ff9800; + } + + .seasonal-bar.current { + border: 2px solid var(--primary-color); + box-sizing: border-box; + } + + .seasonal-legend { + display: flex; + gap: 12px; + font-size: 11px; + } + + .legend-item { + display: flex; + align-items: center; + gap: 4px; + } + + .legend-item .dot { + width: 8px; + height: 8px; + border-radius: 50%; + } + + .legend-item .dot.low { + background: #2196f3; + } + + .legend-item .dot.normal { + background: var(--secondary-text-color); + opacity: 0.5; + } + + .legend-item .dot.high { + background: #ff9800; + } + + /* Task meta card (notes + documentation URL) */ + .task-meta-card { + background: var(--card-background-color, #fff); + border: 1px solid var(--divider-color); + border-radius: 12px; + padding: 12px 16px; + margin-bottom: 16px; + display: flex; + flex-direction: column; + gap: 10px; + } + + .task-meta-row { + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 14px; + color: var(--primary-text-color); + } + + .task-meta-row ha-icon { + --mdc-icon-size: 18px; + color: var(--secondary-text-color); + flex-shrink: 0; + margin-top: 2px; + } + + .task-meta-notes { + white-space: pre-wrap; + word-break: break-word; + } + + .task-meta-link a { + color: var(--primary-color); + text-decoration: none; + } + + .task-meta-link a:hover { + text-decoration: underline; + } + + /* ── Responsive: :host([narrow]) (HA sets narrow on mobile/companion) ── */ + + :host([narrow]) .content { + padding: 0 8px 8px; + } + + :host([narrow]) .header { + padding: 8px 12px; + font-size: 14px; + } + + :host([narrow]) .kpi-bar { + grid-template-columns: repeat(2, 1fr); + gap: 8px; + margin-bottom: 16px; + } + + :host([narrow]) .kpi-card { + padding: 12px 8px; + } + + :host([narrow]) .kpi-label { + font-size: 10px; + } + + :host([narrow]) .kpi-value { + font-size: 14px; + } + + :host([narrow]) .kpi-value-large { + font-size: 18px; + } + + :host([narrow]) .two-column-layout { + grid-template-columns: 1fr; + } + + :host([narrow]) .tab { + padding: 12px 16px; + font-size: 14px; + } + + :host([narrow]) .task-header { + flex-direction: column; + align-items: flex-start; + } + + :host([narrow]) .task-header-actions { + width: 100%; + justify-content: flex-start; + } + + :host([narrow]) .filter-bar { + flex-wrap: wrap; + } + + :host([narrow]) .filter-field { + flex: 1; + min-width: 48%; + } + + :host([narrow]) .filter-bar select { + flex: 1; + min-width: 0; + width: 100%; + } + + :host([narrow]) .task-table { display: block; } + + :host([narrow]) .task-row { + /* Mobile: 4-column grid keeps due-cell + actions at deterministic + X-positions across rows regardless of content (sparkline, bar, %). + Earlier flex-wrap-based layouts let the row wrap unpredictably so + "X days" sometimes sat near the middle, sometimes at the right edge. + Grid template: + [badges auto | task-name 1fr | due-cell 100px | actions auto] + Task-name spans the full top row (own row above), chips span the + full bottom row. */ + display: grid; + grid-column: auto; + grid-template-columns: auto minmax(0, 1fr) 100px auto; + grid-template-rows: auto auto auto; + column-gap: 8px; + row-gap: 4px; + padding: 12px; + } + + :host([narrow]) .cell.type { display: none; } + :host([narrow]) .cell.task-name { + grid-column: 1 / -1; + grid-row: 1; + min-width: 0; + } + :host([narrow]) .cell-badges { + grid-column: 1; + grid-row: 2; + } + :host([narrow]) .cell.object-name { + grid-column: 2; + grid-row: 2; + min-width: 0; + /* Cap long object names at 2 lines with ellipsis instead of growing + unbounded vertically. The full name is still readable via the panel + object-detail view (one tap on the object). */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.2; + } + :host([narrow]) .due-cell { + grid-column: 3; + grid-row: 2; + align-items: flex-end; + min-width: 0; + } + :host([narrow]) .row-actions { + grid-column: 4; + grid-row: 2; + } + :host([narrow]) .task-sub { + grid-column: 1 / -1; + grid-row: 3; + font-size: 11px; + gap: 6px; + justify-content: flex-start; + flex-wrap: wrap; + } + :host([narrow]) .task-sub-empty { display: none; } + :host([narrow]) .mini-sparkline { width: 50px; } + + :host([narrow]) .detail-header { + flex-direction: column; + align-items: flex-start; + } + + :host([narrow]) .info-grid { + grid-template-columns: 1fr; + } + + :host([narrow]) .history-filters-new { + flex-direction: column; + } + + :host([narrow]) .search-input { + min-width: 0; + width: 100%; + } + + :host([narrow]) .cost-duration-card { + padding: 12px; + } + + :host([narrow]) .card-header { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + :host([narrow]) .toggle-buttons { + width: 100%; + } + + :host([narrow]) .toggle-btn { + flex: 1; + padding: 8px; + font-size: 12px; + } + + :host([narrow]) .activity-item { + flex-wrap: wrap; + } + + :host([narrow]) .activity-date { + min-width: auto; + } + + :host([narrow]) .activity-note { + flex-basis: 100%; + white-space: normal; + } + + :host([narrow]) .popup-menu { + right: auto; + left: 0; + min-width: 160px; + } + + /* Cost/Duration Card with Toggle */ + .cost-duration-card { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 16px; + border: 1px solid var(--divider-color); + } + + .card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + } + + .card-header h3 { + margin: 0; + font-size: 16px; + } + + .toggle-buttons { + display: flex; + gap: 4px; + background: var(--divider-color); + border-radius: 4px; + padding: 2px; + } + + .toggle-btn { + padding: 6px 12px; + border: none; + background: transparent; + color: var(--primary-text-color); + cursor: pointer; + border-radius: 3px; + font-size: 13px; + transition: all 0.2s; + } + + .toggle-btn:hover { + background: rgba(0, 0, 0, 0.05); + } + + .toggle-btn.active { + background: var(--primary-color); + color: white; + } + + /* ── Responsive: @media fallback (when narrow attr not set) ── */ + @media (max-width: 768px) { + .content { padding: 0 8px 8px; } + .header { padding: 8px 12px; font-size: 14px; } + .kpi-bar { grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 16px; } + .kpi-card { padding: 12px 8px; } + .kpi-label { font-size: 10px; } + .kpi-value { font-size: 14px; } + .kpi-value-large { font-size: 18px; } + .two-column-layout { grid-template-columns: 1fr; } + .tab { padding: 12px 16px; font-size: 14px; } + .task-header { flex-direction: column; align-items: flex-start; } + .task-header-actions { width: 100%; justify-content: flex-start; } + .filter-bar { flex-wrap: wrap; } + .filter-bar select { flex: 1; min-width: 0; } + /* Mirror the :host([narrow]) grid layout for narrow desktop windows */ + .task-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) 100px auto; + grid-template-rows: auto auto auto; + column-gap: 8px; + row-gap: 4px; + padding: 12px; + } + .cell.type { display: none; } + .cell.task-name { grid-column: 1 / -1; grid-row: 1; min-width: 0; } + .cell-badges { grid-column: 1; grid-row: 2; } + .cell.object-name { grid-column: 2; grid-row: 2; min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.2; } + .due-cell { grid-column: 3; grid-row: 2; align-items: flex-end; min-width: 0; } + .row-actions { grid-column: 4; grid-row: 2; } + .task-sub { grid-column: 1 / -1; grid-row: 3; font-size: 11px; gap: 6px; justify-content: flex-start; flex-wrap: wrap; } + .task-sub-empty { display: none; } + .mini-sparkline { width: 50px; } + .detail-header { flex-direction: column; align-items: flex-start; } + .info-grid { grid-template-columns: 1fr; } + .history-filters-new { flex-direction: column; } + .search-input { min-width: 0; width: 100%; } + .cost-duration-card { padding: 12px; } + .card-header { flex-direction: column; align-items: flex-start; gap: 8px; } + .toggle-buttons { width: 100%; } + .toggle-btn { flex: 1; padding: 8px; font-size: 12px; } + .activity-item { flex-wrap: wrap; } + .activity-date { min-width: auto; } + .activity-note { flex-basis: 100%; white-space: normal; } + .popup-menu { right: auto; left: 0; min-width: 160px; } + } + + /* ha-button handles variant="danger" natively */ + + .toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: var(--error-color, #f44336); + color: #fff; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0,0,0,.3); + animation: toast-in .3s ease; + display: flex; + align-items: center; + gap: 16px; + } + .toast-undo { + font: inherit; font-weight: 600; color: #fff; cursor: pointer; + background: transparent; border: 1px solid rgba(255,255,255,.6); + border-radius: 6px; padding: 4px 12px; text-transform: uppercase; font-size: 12.5px; + } + .toast-undo:hover { background: rgba(255,255,255,.15); } + @keyframes toast-in { + from { opacity: 0; transform: translateX(-50%) translateY(16px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } + } +`; diff --git a/custom_components/maintenance_supporter/frontend-src/qr_test.mjs b/custom_components/maintenance_supporter/frontend-src/qr_test.mjs new file mode 100644 index 0000000..73399ec --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/qr_test.mjs @@ -0,0 +1,106 @@ +import { chromium } from "playwright"; + +const HA = "http://homeassistant-dev:8123"; +const REFRESH = process.env.HA_REFRESH_TOKEN; +if (!REFRESH) { console.error("Set HA_REFRESH_TOKEN env var"); process.exit(1); } + +const browser = await chromium.connect("ws://localhost:3000"); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 }, locale: "de" }); +const page = await ctx.newPage(); + +await page.goto(HA); +await page.waitForTimeout(1000); +await page.evaluate((refresh) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: "http://homeassistant-dev:8123", + clientId: "http://homeassistant-dev:8123/", + refresh_token: refresh, + access_token: "", + token_type: "Bearer", + expires_in: 1800, + expires: 0, + })); +}, REFRESH); +await page.goto(HA + "/maintenance-supporter"); +await page.waitForTimeout(6000); + +function getPanel() { + return "var ha = document.querySelector('home-assistant'); " + + "var main = ha && ha.shadowRoot && ha.shadowRoot.querySelector('home-assistant-main'); " + + "var drawer = main && main.shadowRoot && main.shadowRoot.querySelector('ha-drawer'); " + + "var resolver = drawer && drawer.querySelector('partial-panel-resolver'); " + + "var custom = resolver && resolver.querySelector('ha-panel-custom'); " + + "var p = custom && custom.querySelector('maintenance-supporter-panel'); "; +} + +var info = await page.evaluate("{ " + getPanel() + + "var result = []; " + + "if (p && p._objects) { " + + " for (var i = 0; i < p._objects.length; i++) { " + + " var obj = p._objects[i]; " + + " var tasks = []; " + + " if (obj.tasks) for (var j = 0; j < obj.tasks.length; j++) tasks.push({ id: obj.tasks[j].id, name: obj.tasks[j].name }); " + + " result.push({ entry_id: obj.entry_id, name: obj.object.name, tasks: tasks }); " + + " } " + + "} " + + "JSON.stringify(result); " + + "}"); +var data = JSON.parse(info); +var entryId = data[0].entry_id; +var taskId = data[0].tasks[0].id; +var objName = data[0].name; +var taskName = data[0].tasks[0].name; + +await page.evaluate("{ " + getPanel() + "if (p) p._showTask('" + entryId + "', '" + taskId + "'); }"); +await page.waitForTimeout(2000); + +await page.evaluate("{ " + getPanel() + + "if (p) { var dlg = p.shadowRoot.querySelector('maintenance-qr-dialog'); " + + "dlg.openForTask('" + entryId + "', '" + taskId + "', '" + objName + "', '" + taskName + "'); } }"); +await page.waitForTimeout(3000); + +await page.screenshot({ path: "screenshots/qr_dialog_view.png" }); +console.log("QR dialog (view) screenshot done"); + +var s1 = await page.evaluate("{ " + getPanel() + + "var dlg = p && p.shadowRoot && p.shadowRoot.querySelector('maintenance-qr-dialog'); " + + "var sr = dlg && dlg.shadowRoot; " + + "var result = { toggleButtons: [], url: '', error: '' }; " + + "if (sr) { " + + " var btns = sr.querySelectorAll('.toggle-btn'); " + + " for (var i = 0; i < btns.length; i++) result.toggleButtons.push({ text: btns[i].textContent.trim(), active: btns[i].classList.contains('active') }); " + + " var urlEl = sr.querySelector('.url-display'); " + + " if (urlEl) result.url = urlEl.textContent.trim(); " + + " var errEl = sr.querySelector('.error'); " + + " if (errEl) result.error = errEl.textContent.trim(); " + + "} " + + "JSON.stringify(result); }"); +console.log("State (view):", s1); + +await page.evaluate("{ " + getPanel() + + "var dlg = p && p.shadowRoot && p.shadowRoot.querySelector('maintenance-qr-dialog'); " + + "var sr = dlg && dlg.shadowRoot; " + + "var btns = sr && sr.querySelectorAll('.toggle-btn'); " + + "if (btns && btns.length > 1) btns[1].click(); }"); +await page.waitForTimeout(3000); + +await page.screenshot({ path: "screenshots/qr_dialog_complete.png" }); + +var s2 = await page.evaluate("{ " + getPanel() + + "var dlg = p && p.shadowRoot && p.shadowRoot.querySelector('maintenance-qr-dialog'); " + + "var sr = dlg && dlg.shadowRoot; " + + "var result = { toggleButtons: [], url: '', error: '' }; " + + "if (sr) { " + + " var btns = sr.querySelectorAll('.toggle-btn'); " + + " for (var i = 0; i < btns.length; i++) result.toggleButtons.push({ text: btns[i].textContent.trim(), active: btns[i].classList.contains('active') }); " + + " var urlEl = sr.querySelector('.url-display'); " + + " if (urlEl) result.url = urlEl.textContent.trim(); " + + " var errEl = sr.querySelector('.error'); " + + " if (errEl) result.error = errEl.textContent.trim(); " + + "} " + + "JSON.stringify(result); }"); +console.log("State (complete):", s2); + +await ctx.close(); +await browser.close(); +console.log("Done!"); diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/chart-utils.ts b/custom_components/maintenance_supporter/frontend-src/renderers/chart-utils.ts new file mode 100644 index 0000000..b5223f7 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/chart-utils.ts @@ -0,0 +1,90 @@ +/** Shared chart helpers: nice axis ticks and consistent number/date formats. */ + +/** Round axis ticks covering [min, max] — the classic "nice numbers" loop. + * Returns the tick values plus the (possibly widened) nice domain, so the + * chart can plot against the same bounds the labels describe. */ +export function niceTicks( + min: number, + max: number, + targetCount = 4, +): { ticks: number[]; niceMin: number; niceMax: number } { + if (!isFinite(min) || !isFinite(max)) return { ticks: [], niceMin: 0, niceMax: 1 }; + if (min === max) { + // Degenerate domain: pad around the value so the line isn't glued to an edge. + const pad = Math.abs(min) * 0.1 || 1; + min -= pad; + max += pad; + } + const span = max - min; + const step0 = Math.pow(10, Math.floor(Math.log10(span / Math.max(1, targetCount)))); + let step = step0; + for (const m of [1, 2, 5, 10]) { + step = step0 * m; + if (span / step <= targetCount + 0.5) break; + } + const niceMin = Math.floor(min / step) * step; + const niceMax = Math.ceil(max / step) * step; + const ticks: number[] = []; + // Epsilon guards float drift (0.30000000000000004 style) at tick boundaries. + for (let v = niceMin; v <= niceMax + step * 1e-6; v += step) { + ticks.push(Math.abs(v) < step * 1e-9 ? 0 : v); + } + return { ticks, niceMin, niceMax }; +} + +/** Compact, consistent number format: 1.2M / 88k / 730 / 7.5 / 0.42 */ +export function fmtNum(v: number): string { + const a = Math.abs(v); + if (a >= 1_000_000) return trimZero((v / 1_000_000).toFixed(a >= 10_000_000 ? 0 : 1)) + "M"; + if (a >= 10_000) return trimZero((v / 1000).toFixed(0)) + "k"; + if (a >= 1000) return trimZero((v / 1000).toFixed(1)) + "k"; + if (a >= 100) return v.toFixed(0); + if (a >= 10) return trimZero(v.toFixed(1)); + if (a >= 1) return trimZero(v.toFixed(1)); + if (a === 0) return "0"; + return trimZero(v.toFixed(2)); +} + +function trimZero(s: string): string { + return s.replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1"); +} + +/** Full-precision value + unit for tooltips (locale-aware grouping). */ +export function fmtVal(v: number, unit: string, lang: string): string { + const s = v.toLocaleString(lang, { maximumFractionDigits: Math.abs(v) >= 100 ? 0 : 1 }); + return unit ? `${s} ${unit}` : s; +} + +/** Short date tick: "3. Juli" / "Jul 3", with year appended when the plotted + * range spans more than ~10 months (disambiguates "Jun … Feb" sequences). */ +export function fmtDateTick(ts: number, lang: string, withYear: boolean): string { + const d = new Date(ts); + const opts: Intl.DateTimeFormatOptions = withYear + ? { month: "short", day: "numeric", year: "2-digit" } + : { month: "short", day: "numeric" }; + return d.toLocaleDateString(lang, opts); +} + +/** Date+time for crosshair/tooltip labels. */ +export function fmtDateTime(ts: number, lang: string): string { + return new Date(ts).toLocaleDateString(lang, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +/** Whether date ticks need the year: the range crosses a calendar-year + * boundary (a "Jun … Feb" sequence would otherwise read backwards). */ +export function needsYear(tsMin: number, tsMax: number): boolean { + return new Date(tsMin).getFullYear() !== new Date(tsMax).getFullYear(); +} + +/** Evenly-spaced x-axis tick timestamps (first/last inclusive). */ +export function timeTicks(tsMin: number, tsMax: number, count: number): number[] { + if (count < 2 || tsMax <= tsMin) return [tsMin, tsMax]; + const out: number[] = []; + for (let i = 0; i < count; i++) out.push(tsMin + ((tsMax - tsMin) * i) / (count - 1)); + return out; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/charts.ts b/custom_components/maintenance_supporter/frontend-src/renderers/charts.ts new file mode 100644 index 0000000..cbffe7a --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/charts.ts @@ -0,0 +1,149 @@ +/** Cost/duration chart renderers (task detail). + * + * Completions are plotted on a true time axis (a completion 8 months ago sits + * visibly further away than one from last week), with round y-ticks per axis + * and year-aware date labels once the span crosses years — "Jun 3 '25" cannot + * be misread as coming after "Feb 21 '26". + */ + +import { html, svg, nothing } from "lit"; +import { t } from "../styles"; +import { niceTicks, fmtNum, fmtDateTick, timeTicks, needsYear } from "./chart-utils"; +import type { MaintenanceTask } from "../types"; + +const COST_CHART_H = 200; +const PAD_T = 10; +const PAD_B = 22; + +export function renderCostDurationCard( + task: MaintenanceTask, + lang: string, + toggle: "cost" | "duration" | "both", + setToggle: (val: "cost" | "duration" | "both") => void, +) { + const completedEntries = task.history.filter((h) => h.type === "completed" && (h.cost != null || h.duration != null)); + if (completedEntries.length < 2) return nothing; + + const anyCost = completedEntries.some((h) => (h.cost ?? 0) > 0); + const anyDuration = completedEntries.some((h) => (h.duration ?? 0) > 0); + if (!anyCost && !anyDuration) return nothing; + + return html` +
    +
    +

    ${t("cost_duration_chart", lang)}

    +
    + ${anyCost ? html`` : nothing} + ${anyCost && anyDuration ? html`` : nothing} + ${anyDuration ? html`` : nothing} +
    +
    + ${renderHistoryChart(task, lang, toggle)} +
    + `; +} + +function renderHistoryChart(task: MaintenanceTask, lang: string, toggle: "cost" | "duration" | "both") { + const entries = task.history + .filter((h) => h.type === "completed" && (h.cost != null || h.duration != null)) + .map((h) => ({ ts: new Date(h.timestamp).getTime(), cost: h.cost ?? 0, duration: h.duration ?? 0 })) + .sort((a, b) => a.ts - b.ts); + + if (entries.length < 2) return nothing; + + const dataCost = entries.some((e) => e.cost > 0); + const dataDuration = entries.some((e) => e.duration > 0); + if (!dataCost && !dataDuration) return nothing; + + const hasCost = toggle !== "duration" && dataCost; + const hasDuration = toggle !== "cost" && dataDuration; + const showCost = hasCost || (!hasDuration && dataCost); + const showDuration = hasDuration || (!hasCost && dataDuration); + + const W = 640; // wide viewBox; the container scales it to full card width + const H = COST_CHART_H; + const PAD_L = showCost ? 44 : 12; + const PAD_R = showDuration ? 44 : 12; + const plotW = W - PAD_L - PAD_R; + const plotB = H - PAD_B; + const plotH = plotB - PAD_T; + + // True time axis with a padded domain so edge bars don't clip. + const tsMin = entries[0].ts; + const tsMax = entries[entries.length - 1].ts; + const tsPad = (tsMax - tsMin || 86400000) * 0.05; + const t0 = tsMin - tsPad; + const t1 = tsMax + tsPad; + const withYear = needsYear(tsMin, tsMax); + const toX = (ts: number) => PAD_L + ((ts - t0) / (t1 - t0)) * plotW; + + const costAxis = niceTicks(0, Math.max(...entries.map((e) => e.cost)) || 1, 3); + const durAxis = niceTicks(0, Math.max(...entries.map((e) => e.duration)) || 1, 3); + const costY = (v: number) => PAD_T + (1 - v / (costAxis.niceMax || 1)) * plotH; + const durY = (v: number) => PAD_T + (1 - v / (durAxis.niceMax || 1)) * plotH; + + // Bars keep a readable width even when completions crowd together. + const minGap = entries.length > 1 + ? Math.min(...entries.slice(1).map((e, i) => toX(e.ts) - toX(entries[i].ts))) + : plotW; + const barW = Math.max(6, Math.min(22, minGap * 0.55)); + + const xTicks = timeTicks(tsMin, tsMax, Math.max(2, Math.min(4, entries.length))); + + return html` +
    + + ${showCost ? costAxis.ticks.map((v) => { + const y = costY(v); + if (y < PAD_T - 1 || y > plotB + 1) return nothing; + return svg` + + ${fmtNum(v)}€`; + }) : nothing} + ${showDuration ? durAxis.ticks.map((v) => { + const y = durY(v); + if (y < PAD_T - 1 || y > plotB + 1) return nothing; + return svg`${fmtNum(v)}m`; + }) : nothing} + + ${showCost ? entries.filter((e) => e.cost > 0).map((e) => svg` + + ${fmtDateTick(e.ts, lang, true)}: ${e.cost.toLocaleString(lang)}€${e.duration ? ` · ${e.duration}m` : ""} + + `) : nothing} + ${showDuration ? svg` + + ${entries.map((e) => svg` + + ${fmtDateTick(e.ts, lang, true)}: ${e.duration}m${e.cost ? ` · ${e.cost.toLocaleString(lang)}€` : ""} + + `)} + ` : nothing} + + + ${xTicks.map((ts, i) => { + const anchor = i === 0 ? "start" : i === xTicks.length - 1 ? "end" : "middle"; + return svg`${fmtDateTick(ts, lang, withYear)}`; + })} + +
    +
    + ${showCost ? html`${t("cost", lang)}` : nothing} + ${showDuration ? html`${t("duration", lang)}` : nothing} +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/history.ts b/custom_components/maintenance_supporter/frontend-src/renderers/history.ts new file mode 100644 index 0000000..c65eb22 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/history.ts @@ -0,0 +1,122 @@ +/** Task history sub-view: filter chips + search + the timeline of entries. + * + * Extracted from the panel (renderers/ pattern). State the panel owns — + * the active filter, the search text, the currency symbol — plus the + * callbacks that mutate it or open the edit dialog are passed in via + * HistoryContext, keeping these functions pure of component internals. + */ + +import { html, nothing } from "lit"; +import { t, formatDateTime, STATUS_ICONS } from "../styles"; +import type { MaintenanceTask, HistoryEntry, HomeAssistant } from "../types"; +import "../components/history-photo"; + +export interface HistoryContext { + lang: string; + /** Home Assistant object — used to sign completion-photo URLs. */ + hass: HomeAssistant; + /** Active type filter, or null for "all". */ + filter: string | null; + /** Free-text search over entry notes. */ + search: string; + /** Currency symbol for cost display (defaults to €). */ + currencySymbol: string; + setFilter: (filter: string | null) => void; + setSearch: (search: string) => void; + openEdit: (entry: HistoryEntry) => void; + /** v2.20 (#83): unit + delta-vs-previous for reading-task entries. */ + readingUnit?: string | null; + readingDelta?: (entry: HistoryEntry) => number | null; +} + +const _FILTER_TYPES = ["completed", "skipped", "missed", "reset", "triggered"] as const; + +export function renderHistoryFilters(task: MaintenanceTask, ctx: HistoryContext) { + const L = ctx.lang; + return html` +
    +
    + ${_FILTER_TYPES.map((type) => { + const count = task.history.filter((h) => h.type === type).length; + if (count === 0) return nothing; + return html` + ctx.setFilter(ctx.filter === type ? null : type)}> + ${t(type, L)} (${count}) + + `; + })} + ${ctx.filter ? html` ctx.setFilter(null)}>${t("show_all", L)}` : nothing} +
    +
    + ctx.setSearch((e.target as HTMLInputElement).value)} /> +
    +
    + `; +} + +export function renderHistoryList(task: MaintenanceTask, ctx: HistoryContext) { + const L = ctx.lang; + let filtered = ctx.filter + ? task.history.filter((h) => h.type === ctx.filter) + : task.history; + + // Apply search filter + if (ctx.search) { + const search = ctx.search.toLowerCase(); + filtered = filtered.filter((h) => h.notes?.toLowerCase().includes(search)); + } + + if (filtered.length === 0) { + return html`

    ${t("no_history", L)}

    `; + } + + return html` +
    + ${[...filtered].reverse().map((entry: HistoryEntry) => renderHistoryEntry(entry, ctx))} +
    + `; +} + +export function renderHistoryEntry(entry: HistoryEntry, ctx: HistoryContext) { + const L = ctx.lang; + // v2.2.0: only "lifecycle + cost-bearing" entries are user-editable. + // Triggers are auto-generated by sensors and shouldn't be retroactively + // rewritten. Allow edits for completed / reset / skipped entries. + const editable = ["completed", "reset", "skipped"].includes(entry.type); + return html` +
    +
    + +
    +
    +
    + ${t(entry.type, L)} + ${editable + ? html`` + : nothing} +
    +
    ${formatDateTime(entry.timestamp, L)}
    + ${entry.notes ? html`
    ${entry.notes}
    ` : nothing} + ${entry.photo_doc_id + ? html`` + : nothing} +
    + ${entry.cost != null ? html`${t("cost", L)}: ${entry.cost.toFixed(2)} ${ctx.currencySymbol}` : nothing} + ${entry.duration != null ? html`${t("duration", L)}: ${entry.duration} min` : nothing} + ${entry.trigger_value != null ? html`${t("trigger_val", L)}: ${entry.trigger_value}` : nothing} + ${entry.reading_value != null + ? html`${t("reading_label", L)}: ${entry.reading_value}${ctx.readingUnit ? ` ${ctx.readingUnit}` : ""}${(() => { + const d = ctx.readingDelta?.(entry); + return d == null ? "" : ` (${d >= 0 ? "+" : ""}${Number(d.toFixed(3))})`; + })()}` + : nothing} +
    +
    +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/prediction.ts b/custom_components/maintenance_supporter/frontend-src/renderers/prediction.ts new file mode 100644 index 0000000..3a5b2d7 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/prediction.ts @@ -0,0 +1,60 @@ +/** Sensor prediction section renderer. */ + +import { html, nothing } from "lit"; +import { t, formatDate, fireMoreInfo } from "../styles"; +import type { MaintenanceTask, AdvancedFeatures } from "../types"; + +export function renderPredictionSection(task: MaintenanceTask, lang: string, features: AdvancedFeatures) { + const hasDegradation = task.degradation_trend != null && task.degradation_trend !== "insufficient_data"; + const hasThreshold = task.days_until_threshold != null; + const hasEnv = task.environmental_factor != null && task.environmental_factor !== 1.0; + if (!hasDegradation && !hasThreshold && !hasEnv) return nothing; + + const trendIcon = task.degradation_trend === "rising" + ? "M16,6L18.29,8.29L13.41,13.17L9.41,9.17L2,16.59L3.41,18L9.41,12L13.41,16L19.71,9.71L22,12V6H16Z" + : task.degradation_trend === "falling" + ? "M16,18L18.29,15.71L13.41,10.83L9.41,14.83L2,7.41L3.41,6L9.41,12L13.41,8L19.71,14.29L22,12V18H16Z" + : "M22,12L18,8V11H3V13H18V16L22,12Z"; + + return html` +
    + ${task.sensor_prediction_urgency ? html` +
    + + ${t("sensor_prediction_urgency", lang).replace("{days}", String(Math.round(task.days_until_threshold || 0)))} +
    + ` : nothing} +
    + + ${t("sensor_prediction", lang)} +
    +
    + ${hasDegradation ? html` +
    + + ${t("degradation_trend", lang)} + ${t("trend_" + task.degradation_trend, lang)} + ${task.degradation_rate != null ? html`${task.degradation_rate > 0 ? "+" : ""}${Math.abs(task.degradation_rate) >= 10 ? Math.round(task.degradation_rate).toLocaleString() : task.degradation_rate.toFixed(1)} ${task.trigger_entity_info?.unit_of_measurement || ""}/${t("day_short", lang)}` : nothing} +
    + ` : nothing} + ${hasThreshold ? html` +
    + + ${t("days_until_threshold", lang)} + ${task.days_until_threshold === 0 ? t("threshold_exceeded", lang) : "~" + Math.round(task.days_until_threshold!) + " " + t("days", lang)} + ${task.threshold_prediction_date ? html`${formatDate(task.threshold_prediction_date, lang)}` : nothing} + ${task.threshold_prediction_confidence ? html`` : nothing} +
    + ` : nothing} + ${hasEnv && features.environmental ? html` +
    + + ${t("environmental_adjustment", lang)} + ${task.environmental_factor!.toFixed(2)}x + ${task.environmental_entity ? html` fireMoreInfo(ev, task.environmental_entity!)}>${task.environmental_entity}` : nothing} +
    + ` : nothing} +
    +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/progress.ts b/custom_components/maintenance_supporter/frontend-src/renderers/progress.ts new file mode 100644 index 0000000..4f0676a --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/progress.ts @@ -0,0 +1,189 @@ +/** Progress renderers shared by the dashboard rows, the object-detail view and + * the task-detail overview tab. + * + * These are pure of component state — everything they need is passed in — so + * they live outside the panel (which keeps the 2.5k-line panel focused on + * routing/data) yet still render into its shadow root, where the CSS lives. + * + * - renderTriggerProgress: a "current / target" bar for a sensor trigger + * (threshold / counter / state_change / runtime / compound). + * - renderMiniSparkline: a tiny 60x20 trend line for overview rows. + * - renderDaysProgress: the detailed last→next due bar for the detail view. + */ + +import { html, nothing } from "lit"; +import { t, formatDate, formatDueDays } from "../styles"; +import { daysProgress } from "../helpers/interval"; +import type { MaintenanceTask, TaskRow, StatisticsPoint } from "../types"; + +const MINI_SPARKLINE_W = 60; +const MINI_SPARKLINE_H = 20; +const MAX_MINI_POINTS = 30; + +export function renderTriggerProgress(row: TaskRow | MaintenanceTask) { + const tc = row.trigger_config ?? null; + if (!tc) return nothing; + + const triggerType = tc.type || "threshold"; + const unit = row.trigger_entity_info?.unit_of_measurement ?? ""; + + let pct = 0; + let label = ""; + + if (triggerType === "threshold") { + const val = row.trigger_current_value ?? null; + if (val == null) return nothing; + const above = tc.trigger_above; + const below = tc.trigger_below; + if (above != null) { + // Progress toward upper limit + const low = below ?? 0; + const range = above - low || 1; + pct = Math.min(100, Math.max(0, ((val - low) / range) * 100)); + label = `${val.toFixed(1)} / ${above} ${unit}`; + } else if (below != null) { + // Progress toward lower limit (inverted: lower is worse) + // Use entity max, or 2x the threshold as a stable "safe" reference. + // Using val*2 caused a dynamic ceiling that distorted the bar. + const entityMax = row.trigger_entity_info?.max; + const high = entityMax ?? ((below * 2) || 100); + const range = high - below || 1; + pct = Math.min(100, Math.max(0, ((high - val) / range) * 100)); + label = `${val.toFixed(1)} / ${below} ${unit}`; + } else { + return nothing; + } + } else if (triggerType === "counter") { + const target = tc.trigger_target_value || 1; + // Use delta if available, otherwise current value + const delta = row.trigger_current_delta ?? null; + const val = delta ?? (row.trigger_current_value ?? null); + if (val == null) return nothing; + pct = Math.min(100, Math.max(0, (val / target) * 100)); + label = `${val.toFixed(1)} / ${target} ${unit}`; + } else if (triggerType === "state_change") { + const target = tc.trigger_target_changes || 1; + const val = row.trigger_current_value ?? null; + if (val == null) return nothing; + pct = Math.min(100, Math.max(0, (val / target) * 100)); + label = `${Math.round(val)} / ${target}`; + } else if (triggerType === "runtime") { + const target = tc.trigger_runtime_hours || 100; + const val = row.trigger_current_value ?? null; + if (val == null) return nothing; + pct = Math.min(100, Math.max(0, (val / target) * 100)); + label = `${val.toFixed(1)}h / ${target}h`; + } else if (triggerType === "compound") { + const logic = tc.compound_logic || (tc as any).operator || "AND"; + const condCount = tc.conditions?.length || 0; + label = `${logic} (${condCount})`; + pct = row.trigger_active ? 100 : 0; + } else { + return nothing; + } + + const triggerOverflow = pct >= 100; + const barColor = pct > 90 ? "var(--error-color, #f44336)" + : pct > 70 ? "var(--warning-color, #ff9800)" + : "var(--primary-color)"; + + return html` +
    +
    +
    +
    + ${label} +
    + `; +} + +/** A mini sparkline for overview rows (tiny trend line). */ +export function renderMiniSparkline( + row: TaskRow | MaintenanceTask, + miniStatsData: Map, + lang: string, +) { + if (!row.trigger_config?.entity_id) return nothing; + const entityId = row.trigger_config.entity_id; + + // PRIMARY: HA recorder statistics (daily, last 14 days) + const statsPoints = miniStatsData.get(entityId) || []; + + let points: { ts: number; val: number }[] = []; + + if (statsPoints.length >= 2) { + points = statsPoints.map((p) => ({ ts: p.ts, val: p.val })); + } else { + // FALLBACK: original behavior from task history + if (!row.history) return nothing; + for (const h of row.history) { + if (h.trigger_value != null) { + points.push({ ts: new Date(h.timestamp).getTime(), val: h.trigger_value }); + } + } + } + + if (row.trigger_current_value != null) { + points.push({ ts: Date.now(), val: row.trigger_current_value }); + } + if (points.length < 2) return nothing; + points.sort((a, b) => a.ts - b.ts); + + const W = MINI_SPARKLINE_W, H = MINI_SPARKLINE_H; + const vals = points.map((p) => p.val); + let minV = Math.min(...vals), maxV = Math.max(...vals); + const range = maxV - minV || 1; + minV -= range * 0.1; maxV += range * 0.1; + const tsMin = points[0].ts, tsMax = points[points.length - 1].ts; + const tsR = tsMax - tsMin || 1; + + const toX = (ts: number) => ((ts - tsMin) / tsR) * W; + const toY = (v: number) => 2 + (1 - (v - minV) / (maxV - minV)) * (H - 4); + + // Downsample for tiny SVG + let renderPoints = points; + if (renderPoints.length > MAX_MINI_POINTS) { + const step = Math.ceil(renderPoints.length / MAX_MINI_POINTS); + renderPoints = renderPoints.filter((_, i) => i % step === 0 || i === renderPoints.length - 1); + } + + const pts = renderPoints.map((p) => `${toX(p.ts).toFixed(1)},${toY(p.val).toFixed(1)}`).join(" "); + // Match the detail chart's semantics: an actively-triggered sensor tints red. + const stroke = row.trigger_active + ? "var(--error-color, #f44336)" + : "var(--primary-color)"; + + return html` + + + + `; +} + +/** A detailed days-progress bar (last performed → next due) for the detail view. */ +export function renderDaysProgress(task: MaintenanceTask, lang: string) { + const L = lang; + if (task.days_until_due == null || !task.interval_days || task.interval_days <= 0) return nothing; + + // Unit-aware (issue #59): a "1 year" task has span ≈365 d, not 1 d. + const { pct, overflow: daysOverflow } = daysProgress( + task.interval_days, task.days_until_due, task.interval_unit, + ); + + let barColor = "var(--success-color, #4caf50)"; + if (task.status === "overdue") barColor = "var(--error-color, #f44336)"; + else if (task.status === "due_soon") barColor = "var(--warning-color, #ff9800)"; + + return html` +
    +
    + ${task.last_performed ? `${t("last_performed", L)}: ${formatDate(task.last_performed, L)}` : ""} + ${task.next_due ? `${t("next_due", L)}: ${formatDate(task.next_due, L)}` : ""} +
    +
    +
    +
    +
    ${formatDueDays(task.days_until_due, L)}
    +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/recommendation.ts b/custom_components/maintenance_supporter/frontend-src/renderers/recommendation.ts new file mode 100644 index 0000000..3e03f52 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/recommendation.ts @@ -0,0 +1,49 @@ +/** Shared recommendation-card visuals (bars + confidence badge). + * + * The visual layout is shared between the panel's task-detail page + * (maintenance-panel._renderRecommendationCard) and the Lovelace + * task-quick-actions dialog (task-quick-actions-dialog._renderRecommendation). + * + * The two contexts disagree on: + * - Whether ha-button or native + ${collapsed ? nothing : html`
    ${body}
    `} + + `; +} + +/** Read-only preview of the configured checklist steps so users can see + * the steps without having to open the Edit or Complete dialog. Only + * rendered when the Checklists feature is enabled and steps are set. */ +function renderChecklistCard(task: MaintenanceTask, ctx: TaskDetailContext) { + if (!ctx.features.checklists) return nothing; + const items = task.checklist || []; + if (items.length === 0) return nothing; + const L = ctx.lang; + return html` +
    +
    + + ${t("checklist", L)} (${items.length}) +
    +
      + ${items.map((item) => html`
    1. ${item}
    2. `)} +
    +
    + `; +} + +/** Task notes, task documentation URL, and (since v1.4.1) the parent object's + * documentation_url for quick access to the device manual without having to + * navigate back to the object detail. */ +function renderTaskMeta(task: MaintenanceTask, ctx: TaskDetailContext) { + const safeTaskUrl = task.documentation_url && /^https?:\/\//i.test(task.documentation_url) + ? task.documentation_url : null; + const safeObjUrl = ctx.objectDocUrl && /^https?:\/\//i.test(ctx.objectDocUrl) ? ctx.objectDocUrl : null; + if (!task.notes && !safeTaskUrl && !safeObjUrl) return nothing; + const L = ctx.lang; + return html` +
    + ${task.notes ? html` +
    + + ${task.notes} +
    + ` : nothing} + ${safeTaskUrl ? html` + + ` : nothing} + ${safeObjUrl ? html` + + ` : nothing} +
    + `; +} + +/** KPI bar with 7 cards. */ +function renderKPIBar(task: MaintenanceTask, ctx: TaskDetailContext) { + const L = ctx.lang; + const avgCost = task.times_performed > 0 ? task.total_cost / task.times_performed : 0; + const daysClass = task.days_until_due !== null && task.days_until_due !== undefined + ? (task.days_until_due < 0 ? "overdue" : (task.days_until_due <= task.warning_days ? "warning" : "")) + : ""; + + return html` +
    +
    +
    ${t("next_due", L)}
    +
    ${task.next_due ? formatDate(task.next_due, L) : "—"}
    + ${ctx.features.schedule_time && task.schedule_time + ? html`
    ${t("at_time", L)} ${task.schedule_time}
    ` + : nothing} +
    +
    +
    ${t("days_until_due", L)}
    +
    ${task.days_until_due !== null && task.days_until_due !== undefined ? task.days_until_due : "—"}
    +
    +
    +
    ${t("interval", L)}
    +
    ${formatRecurrence(task, L)}
    + ${ctx.features.adaptive && task.suggested_interval && task.suggested_interval !== task.interval_days ? html` +
    ${t("recommended", L)}: ${task.suggested_interval}${task.interval_analysis?.confidence_interval_low != null ? ` (${task.interval_analysis.confidence_interval_low}–${task.interval_analysis.confidence_interval_high})` : ""}
    + ` : nothing} +
    +
    +
    ${t("warning", L)}
    +
    ${task.warning_days} ${t("days", L)}
    +
    +
    +
    ${t("last_performed", L)}
    +
    ${task.last_performed ? formatDate(task.last_performed, L) : "—"}
    +
    +
    +
    ${t("avg_cost", L)}
    +
    ${avgCost.toFixed(0)} ${ctx.currencySymbol}
    +
    +
    +
    ${t("avg_duration", L)}
    +
    ${task.average_duration ? task.average_duration.toFixed(0) : "—"} min
    +
    +
    + `; +} + +function renderRecommendationCard(task: MaintenanceTask, ctx: TaskDetailContext) { + const L = ctx.lang; + + if (!ctx.features.adaptive || !task.suggested_interval + || task.suggested_interval === task.interval_days) { + return nothing; + } + if (ctx.suggestionDismissed) return nothing; + + const suggested = task.suggested_interval; + return html` +
    +

    ${t("suggested_interval", L)}

    + ${renderRecommendationBars( + task.interval_days, suggested, + task.interval_confidence || "medium", L, + )} +
    + ctx.applySuggestion(suggested)}> + ${t("apply_suggestion", L)} + + ctx.reanalyze()}> + ${t("reanalyze", L)} + + ctx.dismissSuggestion()}> + ${t("dismiss_suggestion", L)} + +
    +
    + `; +} + +function renderRecentActivities(task: MaintenanceTask, ctx: TaskDetailContext) { + const L = ctx.lang; + const recent = task.history.slice(-3).reverse(); + + if (recent.length === 0) { + return nothing; + } + + const getIcon = (type: string) => { + switch (type) { + case "completed": return "✓"; + case "triggered": return "⊗"; + case "skipped": return "↷"; + case "reset": return "↺"; + default: return "·"; + } + }; + + return html` +
    +

    ${t("recent_activities", L)}

    + ${recent.map(entry => html` +
    + ${getIcon(entry.type)} + ${formatDateTime(entry.timestamp, L)} + ${entry.notes || "—"} + ${entry.cost ? html`${entry.cost.toFixed(0)}${ctx.currencySymbol}` : nothing} + ${entry.duration ? html`${entry.duration}min` : nothing} +
    + `)} +
    + ctx.setActiveTab("history")}>${t("show_all", L)} → +
    +
    + `; +} + +export function renderOverviewTab(task: MaintenanceTask, ctx: TaskDetailContext) { + const L = ctx.lang; + + // Check if we have recommendation / seasonal content + const hasRecommendation = ctx.features.adaptive && task.suggested_interval && task.suggested_interval !== task.interval_days; + const hasSeasonal = ctx.features.seasonal && task.seasonal_factor && task.seasonal_factor !== 1.0; + const hasLeftColumn = hasRecommendation || hasSeasonal; + + // Analysis content: Weibull/Seasonal expanded (only when data is available) + const hasWeibullData = ctx.features.adaptive + && task.interval_analysis?.weibull_beta != null + && task.interval_analysis?.weibull_eta != null; + const hasSeasonalData = ctx.features.seasonal + && (task.seasonal_factors?.length === 12 + || task.interval_analysis?.seasonal_factors?.length === 12); + + return html` +
    + ${renderKPIBar(task, ctx)} + ${renderTaskMeta(task, ctx)} + ${renderDaysProgress(task, ctx.lang)} + ${renderTriggerSection(task, ctx.sparkline)} + ${renderPredictionSection(task, L, ctx.features)} +
    + ${hasLeftColumn ? html` +
    + ${renderRecommendationCard(task, ctx)} + ${renderSeasonalCardCompact(task, L, ctx.features)} +
    + ` : nothing} +
    + ${renderCostDurationCard(task, L, ctx.costDurationToggle, (v) => ctx.setCostDurationToggle(v))} +
    +
    + ${hasWeibullData + ? collapsible("weibull", "weibull_reliability_curve", renderWeibullSection(task, L), ctx) + : nothing} + ${hasSeasonalData + ? collapsible("seasonal", "seasonal_chart_title", html` + ${renderSeasonalCardExpanded(task, L)} +
    + ctx.openSeasonalOverrides(task)}> + ${t("edit_seasonal_overrides", L)} + +
    + `, ctx) + : nothing} + ${renderChecklistCard(task, ctx)} + ${renderRecentActivities(task, ctx)} +
    + `; +} + +function renderHistoryTab(task: MaintenanceTask, ctx: TaskDetailContext) { + return html` +
    + ${renderHistoryFilters(task, ctx.history)} + ${renderHistoryList(task, ctx.history)} +
    + `; +} + +function renderTabContent(task: MaintenanceTask, ctx: TaskDetailContext) { + switch (ctx.activeTab) { + case "overview": + return renderOverviewTab(task, ctx); + case "history": + return renderHistoryTab(task, ctx); + default: + return nothing; + } +} + +/** The complete task-detail view. Renders into the PANEL's shadow root (this + * is a render function, not a component) so the panel's dialogs and styles + * keep working unchanged. */ +export function renderTaskDetail(task: MaintenanceTask, ctx: TaskDetailContext) { + return html` +
    + ${renderTaskHeader(task, ctx)} + ${renderTabBar(ctx)} + ${renderTabContent(task, ctx)} + +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/weibull.ts b/custom_components/maintenance_supporter/frontend-src/renderers/weibull.ts new file mode 100644 index 0000000..f10330b --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/weibull.ts @@ -0,0 +1,187 @@ +/** Weibull reliability analysis renderers. */ + +import { html, svg, nothing } from "lit"; +import { t } from "../styles"; +import type { MaintenanceTask } from "../types"; + +export function renderWeibullSection(task: MaintenanceTask, lang: string) { + const analysis = task.interval_analysis; + const beta = analysis?.weibull_beta; + const eta = analysis?.weibull_eta; + if (beta == null || eta == null || eta <= 0) return nothing; + + const currentInterval = task.interval_days ?? 0; + const rec = task.suggested_interval ?? currentInterval; + + return html` +
    +
    + + ${t("weibull_reliability_curve", lang)} + ${renderBetaBadge(beta, lang)} +
    + ${renderWeibullChart(beta, eta, currentInterval, rec, lang)} + ${renderWeibullInfo(analysis!, lang)} + ${analysis?.confidence_interval_low != null ? renderConfidenceInterval(analysis!, task, lang) : nothing} +
    + `; +} + +function renderBetaBadge(beta: number, lang: string) { + let cls: string; + let icon: string; + let key: string; + if (beta < 0.8) { + cls = "early_failures"; + icon = "M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"; + key = "beta_early_failures"; + } else if (beta <= 1.2) { + cls = "random_failures"; + icon = "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,17H11V15H13V17M13,13H11V7H13V13Z"; + key = "beta_random_failures"; + } else if (beta <= 3.5) { + cls = "wear_out"; + icon = "M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M12,6A6,6 0 0,1 18,12H12V6Z"; + key = "beta_wear_out"; + } else { + cls = "highly_predictable"; + icon = "M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"; + key = "beta_highly_predictable"; + } + return html` + + + ${t(key, lang)} (\u03B2=${beta.toFixed(2)}) + + `; +} + +function renderWeibullChart(beta: number, eta: number, currentInterval: number, recommended: number, lang: string) { + const W = 300, H = 160; + const PAD_L = 32, PAD_R = 8, PAD_T = 8, PAD_B = 24; + const chartW = W - PAD_L - PAD_R; + const chartH = H - PAD_T - PAD_B; + + const maxT = Math.max(currentInterval, recommended, eta, 1) * 1.3; + const N = 50; + + const points: Array<[number, number]> = []; + for (let i = 0; i <= N; i++) { + const t_val = (i / N) * maxT; + const cdf = 1.0 - Math.exp(-Math.pow(t_val / eta, beta)); + const x = PAD_L + (t_val / maxT) * chartW; + const y = PAD_T + chartH - cdf * chartH; + points.push([x, y]); + } + + const polyline = points.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(" "); + const areaPath = `M${PAD_L},${PAD_T + chartH} ` + + points.map(([x, y]) => `L${x.toFixed(1)},${y.toFixed(1)}`).join(" ") + + ` L${points[N][0].toFixed(1)},${PAD_T + chartH} Z`; + + const curX = PAD_L + (currentInterval / maxT) * chartW; + const curCdf = 1.0 - Math.exp(-Math.pow(currentInterval / eta, beta)); + const curY = PAD_T + chartH - curCdf * chartH; + const reliability = ((1.0 - curCdf) * 100).toFixed(0); + + const recX = PAD_L + (recommended / maxT) * chartW; + const yTicks = [0, 0.25, 0.5, 0.75, 1.0]; + + return html` +
    + + ${yTicks.map(tick => { + const y = PAD_T + chartH - tick * chartH; + return svg` + + ${(tick * 100).toFixed(0)}% + `; + })} + + 0 + ${Math.round(maxT / 2)} + ${Math.round(maxT)} + + + + + ${currentInterval > 0 ? svg` + + + R=${reliability}% + ` : nothing} + + ${recommended > 0 && recommended !== currentInterval ? svg` + + ` : nothing} + + + + +
    +
    + ${t("weibull_failure_probability", lang)} + ${currentInterval > 0 ? html` ${t("current_interval_marker", lang)}` : nothing} + ${recommended > 0 && recommended !== currentInterval ? html` ${t("recommended_marker", lang)}` : nothing} +
    + `; +} + +function renderWeibullInfo(analysis: NonNullable, lang: string) { + return html` +
    +
    + ${t("characteristic_life", lang)} + ${Math.round(analysis.weibull_eta!)} ${t("days", lang)} +
    + ${analysis.weibull_r_squared != null ? html` +
    + ${t("weibull_r_squared", lang)} + ${analysis.weibull_r_squared!.toFixed(3)} +
    + ` : nothing} +
    + `; +} + +function renderConfidenceInterval(analysis: NonNullable, task: MaintenanceTask, lang: string) { + const low = analysis.confidence_interval_low!; + const high = analysis.confidence_interval_high!; + const rec = task.suggested_interval ?? task.interval_days ?? 0; + const current = task.interval_days ?? 0; + + const barMin = Math.max(0, low - 5); + const barMax = high + 5; + const range = barMax - barMin; + + const fillLeft = ((low - barMin) / range) * 100; + const fillWidth = ((high - low) / range) * 100; + const recPos = ((rec - barMin) / range) * 100; + const curPos = current > 0 ? ((current - barMin) / range) * 100 : -1; + + return html` +
    +
    + ${t("confidence_interval", lang)}: ${rec} ${t("days", lang)} (${low}\u2013${high}) +
    +
    +
    + ${curPos >= 0 ? html`
    ` : nothing} + +
    +
    + ${t("confidence_conservative", lang)} (${low}${t("days", lang).charAt(0)}) + ${t("confidence_aggressive", lang)} (${high}${t("days", lang).charAt(0)}) +
    +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/screenshot-section-cards.mjs b/custom_components/maintenance_supporter/frontend-src/screenshot-section-cards.mjs new file mode 100644 index 0000000..e3b6773 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/screenshot-section-cards.mjs @@ -0,0 +1,251 @@ +/** + * Screenshot the new interactive section cards (Vacation/Budget/Groups) + * + the existing task quick-actions dialog (after the button-row fix) + * + the complete-dialog (to investigate the user's "fields are missing" report). + * + * Creates a temporary Lovelace dashboard with all three cards, navigates, + * captures shots in light + dark mode at desktop width. + */ +import { chromium } from "playwright"; +import { promises as fs } from "fs"; +import http from "http"; +import path from "path"; + +const HA = "http://localhost:8125"; +const OUT = path.resolve("../../../screenshots-section-cards"); + +async function getRefreshToken() { + function post(p, body, ct = "application/json") { + return new Promise((resolve, reject) => { + const data = typeof body === "string" ? body : JSON.stringify(body); + const opts = { + hostname: "localhost", port: 8125, path: p, method: "POST", + headers: { "Content-Type": ct, "Content-Length": Buffer.byteLength(data) }, + }; + const req = http.request(opts, (r) => { + let d = ""; + r.on("data", (c) => (d += c)); + r.on("end", () => resolve(JSON.parse(d))); + }); + req.on("error", reject); + req.write(data); + req.end(); + }); + } + const flow = await post("/auth/login_flow", { + client_id: HA + "/", handler: ["homeassistant", null], redirect_uri: HA + "/", + }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { + client_id: HA + "/", username: "dev", password: "dev", + }); + const tok = await post( + "/auth/token", + `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, + "application/x-www-form-urlencoded", + ); + return tok.refresh_token; +} + +async function ws(page, cmd) { + return page.evaluate(async (c) => { + const ha = document.querySelector("home-assistant"); + if (!ha?.hass?.connection) throw new Error("no hass connection"); + return await ha.hass.connection.sendMessagePromise(c); + }, cmd); +} + +async function ensureDashboard(page) { + const dashboards = await ws(page, { type: "lovelace/dashboards/list" }); + const slug = "section-cards-test"; + const existing = dashboards.find((d) => d.url_path === slug); + if (!existing) { + await ws(page, { + type: "lovelace/dashboards/create", + url_path: slug, + title: "Section Cards Test", + icon: "mdi:test-tube", + mode: "storage", + show_in_sidebar: false, + require_admin: false, + }); + } + // Save the lovelace config with our 3 cards + await ws(page, { + type: "lovelace/config/save", + url_path: slug, + config: { + views: [ + { + title: "Sections", + path: "default", + icon: "mdi:view-dashboard", + cards: [ + { type: "custom:maintenance-vacation-section-card" }, + { type: "custom:maintenance-budget-section-card" }, + { type: "custom:maintenance-groups-section-card" }, + ], + }, + { + title: "Card", + path: "card", + icon: "mdi:card-text", + cards: [ + { type: "custom:maintenance-supporter-card", show_actions: true, max_items: 6 }, + ], + }, + ], + }, + }); + return slug; +} + +async function shot(page, name) { + await page.waitForTimeout(800); + const file = path.join(OUT, name + ".png"); + await page.screenshot({ path: file, fullPage: false }); + console.log(` 📸 ${name}.png`); +} + +async function setColorScheme(page, scheme) { + // Force HA's theme by injecting localStorage entry + reloading + await page.evaluate((s) => { + localStorage.setItem("selectedTheme", JSON.stringify({ dark: s === "dark" })); + }, scheme); +} + +async function main() { + await fs.mkdir(OUT, { recursive: true }); + console.log("Getting refresh token…"); + const refreshToken = await getRefreshToken(); + + const browser = await chromium.launch({ headless: true }); + + for (const colorScheme of ["dark", "light"]) { + console.log(`\n=== ${colorScheme} mode ===`); + const ctx = await browser.newContext({ + viewport: { width: 1280, height: 900 }, + colorScheme, + ignoreHTTPSErrors: true, + }); + const page = await ctx.newPage(); + page.on("pageerror", (e) => console.log("PAGE ERROR:", e.message)); + page.on("console", (m) => { + if (m.type() === "error") console.log("CONSOLE:", m.text()); + }); + + await page.goto(HA); + await page.waitForTimeout(800); + await page.evaluate((args) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: args.ha, clientId: args.ha + "/", + refresh_token: args.r, access_token: "", + token_type: "Bearer", expires_in: 1800, expires: 0, + })); + localStorage.setItem("selectedTheme", JSON.stringify({ dark: args.cs === "dark" })); + }, { ha: HA, r: refreshToken, cs: colorScheme }); + + await page.goto(HA); + await page.waitForTimeout(3500); + + const slug = await ensureDashboard(page); + await page.goto(`${HA}/${slug}/default`); + await page.waitForTimeout(4000); + + await shot(page, `sections-${colorScheme}`); + + // Card view — click a task row to open quick-actions dialog + await page.goto(`${HA}/${slug}/card`); + await page.waitForTimeout(3500); + await shot(page, `card-${colorScheme}`); + + // Find a task row that's likely to have adaptive data (Oil Change has the + // longest history in the demo dataset). Falls back to first row. + const taskClicked = await page.evaluate(() => { + function findInAllShadows(root, sel) { + if (!root) return null; + if (root.querySelector?.(sel)) return root.querySelector(sel); + for (const el of root.querySelectorAll?.("*") || []) { + if (el.shadowRoot) { + const found = findInAllShadows(el.shadowRoot, sel); + if (found) return found; + } + } + return null; + } + const card = findInAllShadows(document, "maintenance-supporter-card"); + if (!card?.shadowRoot) return "no card"; + const rows = Array.from(card.shadowRoot.querySelectorAll(".task-item.clickable")); + if (rows.length === 0) return "no rows"; + // Prefer "Filter Replacement" (HVAC) — has interval recommendation, + // then "Oil Change" (Family Car) — has sensor + long history. + const filterReplace = rows.find((r) => /filter replacement|filter wechsel/i.test(r.textContent || "")); + const oilChange = rows.find((r) => /oil change|ölwechsel/i.test(r.textContent || "")); + const target = filterReplace || oilChange || rows[0]; + target.click(); + return "clicked: " + (target.querySelector(".task-name")?.textContent || "?"); + }); + + console.log(` Task row click: ${taskClicked}`); + if (taskClicked.startsWith("clicked")) { + // Wait for both /object load AND /settings (features) load — the stats + // toggle only renders after features arrive + await page.waitForTimeout(4000); + await shot(page, `quick-actions-${colorScheme}`); + + // Click the 2nd link (= "Show stats + graphs") + const statsClick = await page.evaluate(() => { + const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog"); + const links = dlg?.shadowRoot?.querySelectorAll(".details-toggle button.link") || []; + if (links.length < 2) return `only ${links.length} link(s)`; + links[1].click(); + return "clicked: " + (links[1].textContent || "").trim(); + }); + console.log(` Stats toggle: ${statsClick}`); + await page.waitForTimeout(1500); + await shot(page, `quick-actions-stats-${colorScheme}`); + + // Capture the graphs section — scroll dialog and shoot again + await page.evaluate(() => { + const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog"); + const inner = dlg?.shadowRoot?.querySelector(".dialog"); + if (inner) inner.scrollTop = Math.max(0, inner.scrollHeight - 700); + }).catch(() => {}); + await page.waitForTimeout(400); + await shot(page, `quick-actions-stats-bottom-${colorScheme}`); + + // Restore scroll for complete dialog test + await page.evaluate(() => { + const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog"); + const inner = dlg?.shadowRoot?.querySelector(".dialog"); + if (inner) inner.scrollTop = 0; + }).catch(() => {}); + await page.waitForTimeout(300); + const completeClicked = await page.evaluate(() => { + const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog"); + if (!dlg?.shadowRoot) return "no quick-actions dialog on body"; + const btns = dlg.shadowRoot.querySelectorAll(".actions.primary-row button"); + // The first button in primary-row IS Complete + if (btns.length === 0) return "no buttons in primary-row"; + btns[0].click(); + return "clicked: " + (btns[0].textContent || "").trim(); + }); + console.log(` Complete click: ${completeClicked}`); + await page.waitForTimeout(2000); + await shot(page, `complete-dialog-${colorScheme}`); + + // Close everything + await page.keyboard.press("Escape"); + await page.waitForTimeout(500); + } + + await ctx.close(); + } + + await browser.close(); + console.log(`\nDone. Screenshots in ${OUT}`); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/custom_components/maintenance_supporter/frontend-src/screenshot.mjs b/custom_components/maintenance_supporter/frontend-src/screenshot.mjs new file mode 100644 index 0000000..c18f020 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/screenshot.mjs @@ -0,0 +1,88 @@ +import { chromium } from "playwright"; + +const HA = "http://homeassistant-dev:8123"; +const REFRESH = process.env.HA_REFRESH_TOKEN; +if (!REFRESH) { console.error("Set HA_REFRESH_TOKEN env var"); process.exit(1); } + +const browser = await chromium.connect("ws://localhost:3000"); + +function getPanel() { + return "var ha = document.querySelector('home-assistant'); " + + "var main = ha && ha.shadowRoot && ha.shadowRoot.querySelector('home-assistant-main'); " + + "var drawer = main && main.shadowRoot && main.shadowRoot.querySelector('ha-drawer'); " + + "var resolver = drawer && drawer.querySelector('partial-panel-resolver'); " + + "var custom = resolver && resolver.querySelector('ha-panel-custom'); " + + "var p = custom && custom.querySelector('maintenance-supporter-panel'); "; +} + +async function login(page) { + await page.goto(HA); + await page.waitForTimeout(1000); + await page.evaluate((refresh) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: "http://homeassistant-dev:8123", + clientId: "http://homeassistant-dev:8123/", + refresh_token: refresh, + access_token: "", + token_type: "Bearer", + expires_in: 1800, + expires: 0, + })); + }, REFRESH); + await page.goto(HA + "/maintenance-supporter"); + await page.waitForTimeout(6000); +} + +// Desktop screenshot (1280x900) +const desktopCtx = await browser.newContext({ viewport: { width: 1280, height: 900 }, locale: "de" }); +const desktopPage = await desktopCtx.newPage(); +await login(desktopPage); + +var info = await desktopPage.evaluate("{ " + getPanel() + + "var result = []; " + + "if (p && p._objects) { " + + " for (var i = 0; i < p._objects.length; i++) { " + + " var obj = p._objects[i]; " + + " var tasks = []; " + + " if (obj.tasks) for (var j = 0; j < obj.tasks.length; j++) tasks.push({ id: obj.tasks[j].id }); " + + " result.push({ entry_id: obj.entry_id, tasks: tasks }); " + + " } " + + "} " + + "JSON.stringify(result); " + + "}"); +var data = JSON.parse(info); +var entryId = data[0].entry_id; +var taskId = data[0].tasks[0].id; + +// Desktop: task detail +await desktopPage.evaluate("{ " + getPanel() + "if (p) p._showTask('" + entryId + "', '" + taskId + "'); }"); +await desktopPage.waitForTimeout(2000); +await desktopPage.screenshot({ path: "screenshots/desktop_task.png" }); +console.log("Desktop task detail done"); + +// Desktop: overview +await desktopPage.evaluate("{ " + getPanel() + "if (p) p._showOverview(); }"); +await desktopPage.waitForTimeout(2000); +await desktopPage.screenshot({ path: "screenshots/desktop_overview.png" }); +console.log("Desktop overview done"); + +await desktopCtx.close(); + +// Mobile screenshot (375x812, iPhone-like) +const mobileCtx = await browser.newContext({ viewport: { width: 375, height: 812 }, locale: "de", isMobile: true }); +const mobilePage = await mobileCtx.newPage(); +await login(mobilePage); + +// Mobile: overview +await mobilePage.screenshot({ path: "screenshots/mobile_overview.png" }); +console.log("Mobile overview done"); + +// Mobile: task detail +await mobilePage.evaluate("{ " + getPanel() + "if (p) p._showTask('" + entryId + "', '" + taskId + "'); }"); +await mobilePage.waitForTimeout(2000); +await mobilePage.screenshot({ path: "screenshots/mobile_task.png" }); +console.log("Mobile task detail done"); + +await mobileCtx.close(); +await browser.close(); +console.log("All screenshots done!"); diff --git a/custom_components/maintenance_supporter/frontend-src/statistics-service.ts b/custom_components/maintenance_supporter/frontend-src/statistics-service.ts new file mode 100644 index 0000000..f555514 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/statistics-service.ts @@ -0,0 +1,223 @@ +/** + * Statistics service for fetching and caching HA recorder statistics. + * Used by sparkline charts to display dense time-series data. + */ +import type { HomeAssistant, StatisticsPoint, EntityStatisticsCache, HAStatisticsRow } from "./types"; + +const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes +const DETAIL_DAYS = 30; +const MINI_DAYS = 14; + +export class StatisticsService { + private _hass: HomeAssistant; + private _cache = new Map(); + private _pending = new Map>(); + + constructor(hass: HomeAssistant) { + this._hass = hass; + } + + updateHass(hass: HomeAssistant): void { + this._hass = hass; + } + + /** + * Statistics for the detail chart. Hourly resolution for short ranges, + * daily beyond ~5 weeks (keeps a 1-year window at ~365 points). + */ + async getDetailStats( + entityId: string, + isCounter: boolean, + days: number = DETAIL_DAYS, + ): Promise { + return this._getStats(entityId, days <= 35 ? "hour" : "day", days, isCounter); + } + + /** + * Get 14 days of daily statistics for the mini-sparkline (60x20 chart). + */ + async getMiniStats(entityId: string, isCounter: boolean): Promise { + return this._getStats(entityId, "day", MINI_DAYS, isCounter); + } + + /** + * Batch-fetch mini stats for multiple entities in at most 2 WS calls + * (one for counter-type, one for non-counter-type entities). + */ + async getBatchMiniStats( + entities: Array<{ entityId: string; isCounter: boolean }>, + ): Promise> { + const result = new Map(); + const toFetch: Array<{ entityId: string; isCounter: boolean }> = []; + + // Check cache first, collect cache misses (same key shape as _getStats + // so the single-entity mini path and the batch path share the cache). + for (const e of entities) { + const cacheKey = `${e.entityId}:day:${MINI_DAYS}`; + const cached = this._cache.get(cacheKey); + if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) { + result.set(e.entityId, cached.points); + } else { + toFetch.push(e); + } + } + + if (toFetch.length === 0) return result; + + // Group by type (counter vs non-counter need different stat types) + const counterIds = toFetch.filter((e) => e.isCounter).map((e) => e.entityId); + const nonCounterIds = toFetch.filter((e) => !e.isCounter).map((e) => e.entityId); + const startTime = new Date(Date.now() - MINI_DAYS * 24 * 60 * 60 * 1000).toISOString(); + + const promises: Promise[] = []; + + if (counterIds.length > 0) { + promises.push( + this._fetchBatch(counterIds, "day", startTime, ["state", "sum", "change"], true, result), + ); + } + if (nonCounterIds.length > 0) { + promises.push( + this._fetchBatch(nonCounterIds, "day", startTime, ["mean", "min", "max"], false, result), + ); + } + + await Promise.all(promises); + return result; + } + + clearCache(): void { + this._cache.clear(); + this._pending.clear(); + } + + private async _getStats( + entityId: string, + period: "hour" | "day", + days: number, + isCounter: boolean, + ): Promise { + // days must be part of the key: the same entity/period at a different + // range (7d vs 30d) is a different dataset, not a cache hit. + const cacheKey = `${entityId}:${period}:${days}`; + + const cached = this._cache.get(cacheKey); + if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) { + return cached.points; + } + + // Deduplicate in-flight requests + if (this._pending.has(cacheKey)) { + return this._pending.get(cacheKey)!; + } + + const promise = this._fetchAndNormalize(entityId, period, days, isCounter, cacheKey); + this._pending.set(cacheKey, promise); + + try { + return await promise; + } finally { + this._pending.delete(cacheKey); + } + } + + private async _fetchAndNormalize( + entityId: string, + period: "hour" | "day", + days: number, + isCounter: boolean, + cacheKey: string, + ): Promise { + const startTime = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); + const types = isCounter ? ["state", "sum", "change"] : ["mean", "min", "max"]; + + try { + const result = (await this._hass.connection.sendMessagePromise({ + type: "recorder/statistics_during_period", + start_time: startTime, + statistic_ids: [entityId], + period, + types, + })) as Record; + + const rows = result[entityId] || []; + const points = this._normalizeRows(rows, isCounter); + + this._cache.set(cacheKey, { + entityId, + fetchedAt: Date.now(), + period, + points, + }); + + return points; + } catch (err) { + console.warn(`[maintenance-supporter] Failed to fetch statistics for ${entityId}:`, err); + return []; + } + } + + private async _fetchBatch( + entityIds: string[], + period: "hour" | "day", + startTime: string, + types: string[], + isCounter: boolean, + out: Map, + ): Promise { + try { + const response = (await this._hass.connection.sendMessagePromise({ + type: "recorder/statistics_during_period", + start_time: startTime, + statistic_ids: entityIds, + period, + types, + })) as Record; + + for (const entityId of entityIds) { + const rows = response[entityId] || []; + const points = this._normalizeRows(rows, isCounter); + out.set(entityId, points); + this._cache.set(`${entityId}:${period}:${MINI_DAYS}`, { + entityId, + fetchedAt: Date.now(), + period, + points, + }); + } + } catch (err) { + console.warn("[maintenance-supporter] Batch statistics fetch failed:", err); + } + } + + private _normalizeRows(rows: HAStatisticsRow[], isCounter: boolean): StatisticsPoint[] { + const points: StatisticsPoint[] = []; + + for (const row of rows) { + let val: number | null = null; + + if (isCounter) { + val = row.state ?? null; + } else { + val = row.mean ?? null; + } + + if (val === null) continue; + + const point: StatisticsPoint = { + ts: row.start, // HA returns epoch milliseconds + val, + }; + + if (!isCounter) { + if (row.min != null) point.min = row.min; + if (row.max != null) point.max = row.max; + } + + points.push(point); + } + + points.sort((a, b) => a.ts - b.ts); + return points; + } +} diff --git a/custom_components/maintenance_supporter/frontend-src/styles.ts b/custom_components/maintenance_supporter/frontend-src/styles.ts new file mode 100644 index 0000000..be83280 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/styles.ts @@ -0,0 +1,1393 @@ +/** Shared CSS styles and i18n for the Maintenance Supporter frontend. */ + +import { css } from "lit"; +import EN from "./locales/en.json"; + +// Display fallback when the backend hasn't sent a currency symbol. The backend +// derives the real symbol from const.BUDGET_CURRENCIES[DEFAULT_BUDGET_CURRENCY]. +export const DEFAULT_CURRENCY_SYMBOL = "€"; + +export const STATUS_COLORS: Record = { + ok: "var(--success-color, #4caf50)", + due_soon: "var(--warning-color, #ff9800)", + overdue: "var(--error-color, #f44336)", + // Theme-token first so it adapts to dark/custom themes (was a bare #ff5722). + triggered: "var(--deep-orange-color, #ff5722)", + // v2.10.0: archived is a neutral, greyed-out state (retired but retained). + archived: "var(--disabled-color, #9e9e9e)", + // v2.20 (N3): paused is frozen-but-present — info blue, clearly not urgent. + paused: "var(--info-color, #2196f3)", +}; + +export const STATUS_ICONS: Record = { + ok: "mdi:check-circle", + due_soon: "mdi:alert-circle", + overdue: "mdi:alert-octagon", + triggered: "mdi:bell-alert", + archived: "mdi:archive-outline", + paused: "mdi:pause-circle-outline", + completed: "mdi:check-circle", + skipped: "mdi:skip-next", + missed: "mdi:calendar-remove", + reset: "mdi:refresh", +}; + +/* ─── i18n ─── */ + +interface Translations { + [key: string]: string; +} + +// English is bundled (imported above) as the always-available fallback so +// the panel renders instantly with a complete table — never an +// untranslated-key flash, even if a locale fetch fails. The other 17 +// languages live in served JSON (frontend/locales/.json) and are +// fetched on demand by ensureLocale(), so a translation edit needs no bundle +// rebuild — the fix for the recurring "stale bundle ships English to non-EN +// users" pitfall. Steady-state t() is the same sync lookup as before. +const DEFAULT_LANG = "en"; + +// The locale store MUST be shared across bundle copies of this module. +// maintenance-card.js is loaded globally (extra_module_url) and defines the +// dialog custom elements first-wins — so a inside +// the panel runs the CARD bundle's copy of this module. With a module-scoped +// store, the panel's ensureLocale() fills only the PANEL copy and the dialog +// stays English while the rest of the panel is localized (v2.17.0 regression, +// invisible before the runtime-locale split because every bundle inlined all +// languages). One window-scoped store + inflight map keeps every copy reading +// and writing the same tables. +interface LocaleGlobals { + store: Record; + inflight: Record>; +} +const _localeGlobals: LocaleGlobals = (() => { + const w = window as unknown as { __msLocales?: LocaleGlobals }; + if (!w.__msLocales) w.__msLocales = { store: {}, inflight: {} }; + return w.__msLocales; +})(); + +const STORE = _localeGlobals.store; +// Each copy guarantees the bundled English fallback is present. +if (!STORE.en) STORE.en = EN as Translations; + +/** Languages available as runtime-loaded JSON. Keep in sync with locales/. */ +const SUPPORTED_LANGS = new Set([ + "de", "nl", "fr", "it", "es", "pt", "ru", "uk", "pl", "cs", "sv", "zh", + "da", "fi", "nb", "ja", "hi", +]); + +/** Served base for the runtime locale files (mirrors LOCALES_URL in const.py). */ +const LOCALES_BASE = "/maintenance_supporter_locales"; + +const _localeInflight = _localeGlobals.inflight; + +/** Normalize an HA language code to our 2-letter table key. */ +function normLang(lang?: string): string { + return (lang || DEFAULT_LANG).substring(0, 2).toLowerCase(); +} + +/** Get a localized string. Falls back to English, then to the key itself. */ +export function t(key: string, lang?: string): string { + const l = normLang(lang); + return STORE[l]?.[key] ?? STORE.en[key] ?? key; +} + +/** True when *lang*'s table is in memory (English is always bundled). */ +export function isLocaleLoaded(lang?: string): boolean { + const l = normLang(lang); + return l === DEFAULT_LANG || l in STORE; +} + +/** + * Fetch *lang*'s table once and cache it. Resolves immediately for English, + * an already-loaded language, or an unsupported one (which keeps the English + * fallback). Never rejects: a failed fetch silently leaves English in place, + * matching t()'s fall-through. Callers re-render on resolution. + */ +export function ensureLocale(lang?: string): Promise { + const l = normLang(lang); + if (l === DEFAULT_LANG || l in STORE || !SUPPORTED_LANGS.has(l)) { + return Promise.resolve(); + } + if (!(l in _localeInflight)) { + _localeInflight[l] = fetch(`${LOCALES_BASE}/${l}.json`) + .then((r) => (r.ok ? r.json() : null)) + .then((data) => { + if (data) { + STORE[l] = data as Translations; + } else { + // Don't make one failed fetch sticky for the whole page session — + // the next ensureLocale() call retries. + delete _localeInflight[l]; + } + }) + .catch(() => { + delete _localeInflight[l]; + }); + } + return _localeInflight[l]; +} + +/** + * Synchronously seed a locale table, bypassing the fetch. For tests and for + * callers that already hold a table (preload/SSR); the loader and t() share the + * same STORE, so a seeded language reads immediately. + */ +export function setLocale(lang: string, table: Translations): void { + STORE[normLang(lang)] = table; +} + +/** Map language prefix to BCP-47 locale for date formatting. */ +function langToLocale(lang?: string): string { + const l = (lang || "en").substring(0, 2).toLowerCase(); + const map: Record = { + de: "de-DE", en: "en-US", nl: "nl-NL", fr: "fr-FR", it: "it-IT", es: "es-ES", pt: "pt-PT", ru: "ru-RU", uk: "uk-UA", zh: "zh-CN", + da: "da-DK", fi: "fi-FI", nb: "nb-NO", ja: "ja-JP", hi: "hi-IN" + }; + return map[l] ?? "en-US"; +} + +/** Format a date string (ISO) in the user's locale. + * Appends T00:00:00 to date-only strings so JS parses them as local time, not UTC. */ +export function formatDate(iso: string | null | undefined, lang?: string): string { + if (!iso) return "—"; + try { + const local = iso.includes("T") ? iso : iso + "T00:00:00"; + return new Date(local).toLocaleDateString(langToLocale(lang), { day: "2-digit", month: "2-digit", year: "numeric" }); + } catch { + return iso; + } +} + +/** Format a datetime string (ISO) in the user's locale. */ +export function formatDateTime(iso: string | null | undefined, lang?: string): string { + if (!iso) return "—"; + try { + const locale = langToLocale(lang); + const d = new Date(iso); + return ( + d.toLocaleDateString(locale, { day: "2-digit", month: "2-digit", year: "numeric" }) + + " " + + d.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" }) + ); + } catch { + return iso; + } +} + +/** Format "days until due" in localized manner. */ +export function formatDueDays(days: number | null | undefined, lang?: string): string { + if (days === null || days === undefined) return "—"; + const l = lang || "en"; + if (days < 0) return `${Math.abs(days)} ${t("d_overdue", l)}`; + if (days === 0) return t("today", l); + return `${days} ${days === 1 ? t("day", l) : t("days", l)}`; +} + +/** Localized interval label, e.g. "3 Months" / "7 Days" (issue #59 — was + * hard-coded to days). Uses the unit_* keys from the schedule dialog. */ +export function formatInterval( + intervalDays: number | null | undefined, + unit?: string | null, + lang?: string, +): string { + if (intervalDays === null || intervalDays === undefined) return "—"; + return `${intervalDays} ${t("unit_" + (unit || "days"), lang)}`; +} + +/** Localized weekday name (0=Mon … 6=Sun) via Intl — 2024-01-01 is a Monday. + * Single source for the dialog selectors AND formatRecurrence (DRY). */ +export function weekdayName(i: number, lang?: string, style: "long" | "short" = "long"): string { + const locale = (lang || "en").substring(0, 2); + return new Date(Date.UTC(2024, 0, 1 + i)).toLocaleDateString(locale, { weekday: style, timeZone: "UTC" }); +} + +/** Recurrence shape carried on the WS payload (see types.TaskSchedule). */ +interface RecurrenceLike { + schedule?: { + kind?: string; every?: number | null; unit?: string; + weekdays?: number[]; nth?: number; weekday?: number; day?: number; + business?: boolean; offset?: number; + } | null; + interval_days?: number | null; + interval_unit?: string | null; + due_date?: string | null; + schedule_type?: string; +} + +/** A task's recurrence as a localized human label, for ANY schedule kind. + * The single recurrence formatter (DRY) — interval / weekdays / nth_weekday / + * day_of_month / one_time / manual. Used by the panel, card, and quick-actions. */ +export function formatRecurrence(task: RecurrenceLike, lang?: string): string { + const s = task.schedule; + // (#83) ±N-day shift suffix shared by the calendar kinds. + const off = s?.offset ? ` ${s.offset > 0 ? "+" : "−"}${Math.abs(s.offset)}d` : ""; + switch (s?.kind) { + case "weekdays": + return ((s.weekdays || []).map((d) => weekdayName(d, lang, "short")).join(" & ") || "—") + off; + case "nth_weekday": { + if (s.weekday == null || s.nth == null) return "—"; + const ord = s.nth === -1 ? t("ord_last", lang) : t("ord_" + s.nth, lang); + return `${ord} ${weekdayName(s.weekday, lang, "long")}${off}`; + } + case "day_of_month": { + if (s.day == null) return "—"; + // (#83) -1 = last day of the month; business rolls weekends back to Friday. + const base = s.day === -1 + ? t(s.business ? "last_business_day_month" : "last_day_month", lang) + : `${t("day_word", lang)} ${s.day}`; + return base + off; + } + case "one_time": + return task.due_date ? formatDate(task.due_date, lang) : t("one_time", lang); + case "manual": + return t("manual", lang); + case "interval": + return formatInterval(s.every, s.unit, lang); + } + // legacy / no nested schedule + if (task.schedule_type === "one_time") return task.due_date ? formatDate(task.due_date, lang) : t("one_time", lang); + if (task.schedule_type === "manual") return t("manual", lang); + // Sensor-driven tasks have a trigger, not a calendar/interval — label them as + // such instead of "—" (regression when the report moved to formatRecurrence). + if (task.schedule_type === "sensor_based") return t("sensor_based", lang); + return task.interval_days != null ? formatInterval(task.interval_days, task.interval_unit, lang) : "—"; +} + +/** Dispatch HA native "More Info" dialog for an entity. */ +export function fireMoreInfo(ev: Event, entityId: string) { + (ev.currentTarget as HTMLElement).dispatchEvent( + new CustomEvent("hass-more-info", { + detail: { entityId }, + bubbles: true, + composed: true, + }) + ); +} + +export const sharedStyles = css` + :host { + --maint-ok-color: var(--success-color, #4caf50); + --maint-due-soon-color: var(--warning-color, #ff9800); + --maint-overdue-color: var(--error-color, #f44336); + --maint-triggered-color: #ff5722; + } + + .status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + color: white; + white-space: nowrap; + /* Fixed minimum so OK / Due Soon / Overdue / Triggered pills are uniform + width in the task table — keeps the object-name column aligned. */ + min-width: 70px; + box-sizing: border-box; + } + /* Shape icon so status is not conveyed by colour alone (accessibility). */ + .status-badge ha-icon { --mdc-icon-size: 14px; margin-left: -1px; } + + .status-badge.ok { background-color: var(--maint-ok-color); } + .status-badge.due_soon { background-color: var(--maint-due-soon-color); } + .status-badge.overdue { background-color: var(--maint-overdue-color); } + .status-badge.triggered { background-color: var(--maint-triggered-color); } + /* Completed one-time task ("done") — muted blue-grey. */ + .status-badge.done { background-color: var(--maint-done-color, #78909c); } + /* v2.10.0: archived (retire-but-retain) — neutral grey, clearly inert. */ + .status-badge.archived { background-color: var(--disabled-color, #9e9e9e); } + /* v2.20 (N3): paused — frozen but present, info blue. */ + .status-badge.paused { background-color: var(--info-color, #2196f3); } + + /* v1.4.7: 5-column grid so all 5 KPIs (Objects/Tasks/Overdue/Due Soon/ + Triggered) always stay in one row. The previous flex-wrap layout was + wrapping the 5th item (Triggered, the widest label) onto its own row + on narrow viewports because the natural width of the items pushed past + the container width. Grid forces equal 1/5 distribution regardless of + label length. */ + .stats-bar { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 16px; + padding: 16px; + } + + .stat-item { + display: flex; + flex-direction: column; + align-items: center; + min-width: 0; + } + .stat-item .stat-label { + /* Allow long labels to ellipsis rather than overflow the grid cell. */ + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .stat-item.clickable { cursor: pointer; border-radius: 8px; padding: 4px 8px; transition: background 0.15s, box-shadow 0.15s; } + .stat-item.clickable:hover { background: var(--secondary-background-color); } + /* v2.1.0 — KPIs that map to a status filter highlight when active so the + user can see at a glance which filter is on, even after scrolling away. */ + .stat-item.clickable.active { + background: var(--secondary-background-color); + box-shadow: inset 0 -3px 0 var(--primary-color); + } + + .objects-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 16px; + padding: 16px 0; + } + .object-card { + padding: 16px; + background: var(--card-background-color); + border-radius: 8px; + cursor: pointer; + border: 1px solid var(--divider-color); + transition: transform 0.15s, box-shadow 0.15s; + /* Large installs (100+ objects): skip rendering off-screen cards. The + intrinsic size keeps the scrollbar stable while they're skipped. */ + content-visibility: auto; + contain-intrinsic-size: auto 120px; + } + .object-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } + .object-card-header { display: flex; justify-content: space-between; align-items: center; } + .object-card-name { font-weight: 500; font-size: 16px; } + .object-card-count { color: var(--secondary-text-color); font-size: 13px; } + .object-card-meta { color: var(--secondary-text-color); font-size: 13px; margin-top: 4px; } + .object-card-empty { color: var(--warning-color); font-size: 13px; margin-top: 8px; font-style: italic; } + + /* Overdue indicator dot on object cards (#35) */ + .object-card { position: relative; } + .object-card-overdue { border-left: 3px solid var(--error-color); } + .overdue-dot { + position: absolute; + top: 12px; + right: 12px; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--error-color); + box-shadow: 0 0 0 2px var(--card-background-color); + } + + /* Group-by collapsible sections (#35 + #36) */ + .group-section { + margin: 12px 0; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--card-background-color); + } + .group-section[open] { padding-bottom: 8px; } + .group-section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + cursor: pointer; + font-weight: 500; + list-style: none; + user-select: none; + } + .group-section-header::-webkit-details-marker { display: none; } + .group-section-header::before { + content: "▶"; + font-size: 10px; + color: var(--secondary-text-color); + transition: transform 0.15s; + } + .group-section[open] .group-section-header::before { transform: rotate(90deg); } + .group-section-count { + color: var(--secondary-text-color); + font-size: 13px; + font-weight: 400; + } + .group-section .objects-grid, + .group-section .task-table { + padding: 0 12px; + } + + .empty-state-centered { text-align: center; padding: 32px 16px; } + .empty-state-centered ha-button { margin-top: 16px; } + + .stat-value { + font-size: 24px; + font-weight: bold; + color: var(--primary-text-color); + } + + .stat-label { + font-size: 12px; + color: var(--secondary-text-color); + } + + .card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + } + + .card-header h1 { + margin: 0; + font-size: 20px; + font-weight: 500; + } + + .action-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; + } + + .action-buttons ha-button { + --ha-button-font-size: 13px; + } + + .history-timeline { padding: 0 16px 16px; } + + .history-entry { + display: flex; + gap: 12px; + padding: 8px 0; + border-bottom: 1px solid var(--divider-color); + /* Long histories: skip painting off-screen entries (flex, not subgrid, so + safe — subgrid task rows can't use this without breaking alignment). */ + content-visibility: auto; + contain-intrinsic-size: auto 48px; + } + .history-entry:last-child { border-bottom: none; } + + .history-icon { + flex-shrink: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: white; + } + + .history-icon.completed { background: var(--maint-ok-color); } + .history-icon.skipped { background: var(--secondary-text-color); } + .history-icon.reset { background: var(--info-color, #2196f3); } + .history-icon.triggered { background: var(--maint-triggered-color); } + + .history-content { flex: 1; min-width: 0; } + + /* v2.2.0 — row holds the type label + the small Edit button */ + .history-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + .history-edit-btn { + background: transparent; + color: var(--secondary-text-color); + border: none; + border-radius: 4px; + padding: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + transition: background 0.15s, color 0.15s; + } + .history-edit-btn:hover { + background: var(--secondary-background-color); + color: var(--primary-color); + } + .history-edit-btn ha-icon { --mdc-icon-size: 16px; } + + .history-date { + font-size: 12px; + color: var(--secondary-text-color); + } + + .history-details { + display: flex; + gap: 12px; + font-size: 13px; + color: var(--secondary-text-color); + margin-top: 4px; + } + + /* History filter chips */ + .history-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; + } + + .filter-chip { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: 16px; + font-size: 12px; + cursor: pointer; + background: var(--secondary-background-color, #f5f5f5); + color: var(--primary-text-color); + border: 1px solid var(--divider-color); + transition: all 0.2s; + user-select: none; + } + + .filter-chip:hover { background: var(--divider-color); } + + .filter-chip.active { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color); + } + + .filter-chip.clear { + font-style: italic; + opacity: 0.7; + } + + /* Cost/Duration history chart */ + .history-chart { + width: 100%; + height: 200px; + display: block; + } + + .chart-legend { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 4px; + font-size: 11px; + color: var(--secondary-text-color); + } + + .legend-item { + display: inline-flex; + align-items: center; + gap: 4px; + } + + .legend-swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 16px; + color: var(--secondary-text-color); + } + + .empty-state ha-svg-icon { + --mdc-icon-size: 48px; + margin-bottom: 16px; + } + + /* Sparkline chart */ + .sparkline-container { position: relative; margin: 8px 0; } + + .sparkline-svg { + width: 100%; + height: 140px; + display: block; + } + + /* Trigger info card */ + .trigger-card { + background: var(--card-background-color, #fff); + border-radius: 12px; + padding: 12px 16px; + margin: 8px 0; + border: 1px solid var(--divider-color); + } + + .trigger-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + + .trigger-entity-name { font-weight: 500; font-size: 14px; } + .trigger-entity-id { font-size: 11px; color: var(--secondary-text-color); font-family: monospace; } + + .entity-link { + cursor: pointer; + text-decoration: underline dotted; + text-underline-offset: 2px; + } + .entity-link:hover { + color: var(--primary-color); + text-decoration: underline solid; + } + + .trigger-value-row { + display: flex; + align-items: baseline; + gap: 6px; + margin: 4px 0; + } + + .trigger-current { font-size: 28px; font-weight: 700; color: var(--primary-text-color); } + .trigger-current.active { color: var(--maint-triggered-color); } + .trigger-unit { font-size: 14px; color: var(--secondary-text-color); } + + /* Counter progress ("8,507 / 15,000 km · 57 %" + bar) */ + .counter-progress { margin: 6px 0 4px; } + .counter-progress-nums { + display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; + } + .counter-progress-main { font-size: 26px; font-weight: 700; color: var(--primary-text-color); } + .counter-progress-target { font-size: 15px; font-weight: 500; color: var(--secondary-text-color); } + .counter-progress-pct { font-size: 15px; font-weight: 700; } + .counter-progress-pct.ok { color: var(--success-color, #4caf50); } + .counter-progress-pct.near { color: var(--warning-color, #ff9800); } + .counter-progress-pct.over { color: var(--error-color, #f44336); } + .counter-progress-bar { + height: 8px; border-radius: 4px; margin: 6px 0 4px; overflow: hidden; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.08)); + } + .counter-progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; } + .counter-progress-fill.ok { background: var(--success-color, #4caf50); } + .counter-progress-fill.near { background: var(--warning-color, #ff9800); } + .counter-progress-fill.over { background: var(--error-color, #f44336); } + .counter-progress-caption { font-size: 12px; color: var(--secondary-text-color); } + + /* Note under a chart that fell back to sparse maintenance-event values */ + .chart-note { + display: flex; align-items: center; gap: 6px; margin-top: 2px; + font-size: 12px; color: var(--secondary-text-color); + } + .chart-note ha-icon { --mdc-icon-size: 15px; flex: none; } + + .trigger-limits { + display: flex; + gap: 16px; + font-size: 13px; + color: var(--secondary-text-color); + margin: 6px 0; + flex-wrap: wrap; + } + + .trigger-limit-item { + display: flex; + align-items: center; + gap: 4px; + } + + .trigger-limit-item .dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + .trigger-limit-item .dot.warn { background: var(--error-color, #f44336); } + .trigger-limit-item .dot.range { background: var(--secondary-text-color); } + .trigger-limit-item .dot.ok { background: var(--maint-ok-color); } + + /* Row action buttons */ + .row-actions { + display: flex; + gap: 0; + flex-shrink: 0; + margin-left: auto; + } + + .row-actions mwc-icon-button { + --mdc-icon-button-size: 32px; + --mdc-icon-size: 18px; + } + + .row-actions .btn-complete { color: var(--maint-ok-color); } + .row-actions .btn-skip { color: var(--secondary-text-color); } + + /* Days bar for overview */ + .due-cell { + display: flex; + flex-direction: column; + align-items: flex-end; + min-width: 90px; + gap: 2px; + } + + .due-text { font-size: 13px; } + + .days-bar { + width: 100%; + height: 3px; + background: var(--divider-color); + border-radius: 2px; + overflow: hidden; + } + + .days-bar-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s; + } + + /* Trigger progress bar (overview rows) */ + .trigger-progress { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 90px; + } + + .trigger-progress-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .trigger-progress-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s; + } + + .trigger-progress-label { + font-size: 12px; + color: var(--secondary-text-color); + text-align: right; + } + + /* Days progress bar (detail view) */ + .days-progress { + margin: 8px 0 16px; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color); + } + + .days-progress-labels { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 6px; + } + + .days-progress-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .days-progress-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s; + } + + .days-progress-text { + font-size: 13px; + font-weight: 500; + text-align: center; + margin-top: 6px; + color: var(--primary-text-color); + } + + /* Mini-sparkline in overview rows */ + .mini-sparkline { + width: 60px; + height: 20px; + display: block; + margin-top: 2px; + opacity: 0.7; + } + + /* Overflow indicator for overdue progress bars */ + .days-bar-fill.overflow, + .days-progress-fill.overflow, + .trigger-progress-fill.overflow { + background-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 3px, + rgba(255,255,255,0.2) 3px, + rgba(255,255,255,0.2) 6px + ); + animation: overflow-pulse 2s ease-in-out infinite; + } + + @keyframes overflow-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } + } + + /* Budget bars */ + .budget-bars { + display: flex; + gap: 16px; + padding: 8px 16px; + flex-wrap: wrap; + } + + .budget-item { + flex: 1; + min-width: 200px; + } + + .budget-label { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 4px; + } + + .budget-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .budget-bar-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s; + } + + /* Groups section */ + .groups-section { + padding: 8px 16px 16px; + } + + .groups-section h3 { + font-size: 14px; + font-weight: 500; + color: var(--secondary-text-color); + margin: 0 0 8px; + } + + .groups-grid { + display: flex; + gap: 12px; + flex-wrap: wrap; + } + + .group-card { + background: var(--card-background-color, #fff); + border: 1px solid var(--divider-color); + border-radius: 12px; + padding: 12px 16px; + min-width: 180px; + flex: 1; + max-width: 300px; + cursor: default; + } + + .group-card-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; + } + + .group-card-name { + font-weight: 500; + font-size: 14px; + margin-bottom: 4px; + } + + .group-card-actions { + display: flex; + gap: 0; + } + .group-card-actions mwc-icon-button { + --mdc-icon-button-size: 28px; + --mdc-icon-size: 16px; + color: var(--secondary-text-color); + } + + .groups-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-bottom: 8px; + } + .groups-header h3 { margin: 0; } + + .seasonal-actions { + display: flex; + justify-content: flex-end; + padding: 4px 0; + } + + .group-card-desc { + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 8px; + } + + .group-card-tasks { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + + .group-task-chip { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + background: var(--secondary-background-color, #f5f5f5); + color: var(--primary-text-color); + } + + /* Adaptive scheduling suggestion badge */ + .suggestion-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 16px; + font-size: 12px; + font-weight: 500; + background: var(--info-color, #2196f3); + color: white; + margin-left: 8px; + } + + .suggestion-actions { + display: flex; + gap: 8px; + margin-top: 8px; + } + + .suggestion-actions ha-button { + --ha-button-font-size: 12px; + } + + .confidence-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + + .confidence-dot.low { background: var(--secondary-text-color); } + .confidence-dot.medium { background: var(--warning-color, #ff9800); } + .confidence-dot.high { background: var(--success-color, #4caf50); } + + /* Feedback toggle buttons in complete dialog */ + .feedback-section { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 0; + border-top: 1px solid var(--divider-color); + } + + .feedback-label { + font-weight: 500; + font-size: 13px; + color: var(--secondary-text-color); + } + + .feedback-buttons { + display: flex; + gap: 8px; + } + + .feedback-btn { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + cursor: pointer; + text-align: center; + transition: all 0.2s; + } + + .feedback-btn:hover { + background: var(--secondary-background-color, #f5f5f5); + } + + .feedback-btn.selected { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color); + } + + /* Seasonal chart */ + .seasonal-chart { + padding: 12px 16px; + margin: 8px 0; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color); + } + + .seasonal-chart-title { + font-size: 13px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; + } + + .seasonal-chart-title .source-tag { + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + font-weight: 400; + } + + .seasonal-chart svg { + width: 100%; + height: 100px; + display: block; + } + + .seasonal-labels { + display: flex; + justify-content: space-between; + padding: 0 2px; + margin-top: 4px; + } + + .seasonal-label { + font-size: 10px; + color: var(--secondary-text-color); + text-align: center; + flex: 1; + } + + .seasonal-label.active-month { + font-weight: 700; + color: var(--primary-color); + } + + .seasonal-factor-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 500; + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + margin-left: 6px; + } + + .seasonal-factor-tag.short { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #4caf50); + } + + .seasonal-factor-tag.long { + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + } + + /* --- Sensor Prediction Section (Phase 3) --- */ + + .prediction-section { + margin: 16px 0; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + + .prediction-urgency-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin-bottom: 12px; + border-radius: 8px; + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + font-size: 13px; + font-weight: 500; + } + .prediction-urgency-banner ha-svg-icon { + --mdc-icon-size: 18px; + flex-shrink: 0; + } + + .prediction-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary-text-color); + margin-bottom: 10px; + } + .prediction-title ha-svg-icon { + --mdc-icon-size: 16px; + color: var(--primary-color); + } + + .prediction-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; + } + + .prediction-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--secondary-text-color); + } + .prediction-item ha-svg-icon { + --mdc-icon-size: 14px; + color: var(--secondary-text-color); + flex-shrink: 0; + } + + .prediction-label { + font-weight: 500; + } + + .prediction-value { + font-weight: 600; + color: var(--primary-text-color); + } + .prediction-value.rising { color: var(--error-color, #f44336); } + .prediction-value.falling { color: var(--info-color, #2196f3); } + .prediction-value.stable { color: var(--success-color, #4caf50); } + .prediction-value.exceeded { color: var(--error-color, #f44336); font-weight: 700; } + .prediction-value.urgent { color: var(--warning-color, #ff9800); font-weight: 700; } + + .prediction-rate { + font-size: 11px; + opacity: 0.7; + font-family: monospace; + } + + .prediction-date { + font-size: 11px; + opacity: 0.7; + } + + .prediction-entity { + font-size: 10px; + opacity: 0.6; + font-family: monospace; + } + + /* --- Weibull Reliability Section (Phase 4) --- */ + + .weibull-section { + margin: 16px 0; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + + .weibull-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary-text-color); + margin-bottom: 10px; + } + .weibull-title ha-svg-icon { + --mdc-icon-size: 16px; + color: var(--primary-color); + } + + .weibull-chart svg { + width: 100%; + height: 160px; + display: block; + } + + .weibull-info-row { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 10px; + } + + .weibull-info-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--secondary-text-color); + } + + .weibull-info-value { + font-weight: 600; + color: var(--primary-text-color); + } + + /* Beta interpretation badge */ + .beta-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + white-space: nowrap; + } + .beta-badge ha-svg-icon { + --mdc-icon-size: 14px; + } + + .beta-badge.early_failures { + background: rgba(244, 67, 54, 0.15); + color: var(--error-color, #f44336); + } + .beta-badge.random_failures { + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + } + .beta-badge.wear_out { + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + } + .beta-badge.highly_predictable { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #4caf50); + } + + /* Confidence interval range bar */ + .confidence-range { + margin-top: 12px; + } + + .confidence-range-title { + font-size: 12px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 6px; + } + + .confidence-bar { + position: relative; + width: 100%; + height: 8px; + background: var(--divider-color, #e0e0e0); + border-radius: 4px; + overflow: visible; + } + + .confidence-fill { + position: absolute; + height: 100%; + border-radius: 4px; + background: var(--primary-color, #03a9f4); + opacity: 0.25; + } + + .confidence-marker { + position: absolute; + top: -4px; + width: 3px; + height: 16px; + border-radius: 1px; + transform: translateX(-50%); + } + .confidence-marker.recommended { + background: var(--success-color, #4caf50); + } + .confidence-marker.current { + background: var(--primary-color, #03a9f4); + } + + .confidence-labels { + display: flex; + justify-content: space-between; + margin-top: 4px; + } + + .confidence-text { + font-size: 10px; + color: var(--secondary-text-color); + } + .confidence-text.low { + text-align: left; + } + .confidence-text.high { + text-align: right; + } + + .task-disabled { opacity: 0.5; } + .badge-disabled { + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + background: var(--disabled-color, #9e9e9e); + color: white; + } + + /* ── Shared responsive styles (panel + card) ── */ + @media (max-width: 600px) { + .row-actions mwc-icon-button { + --mdc-icon-button-size: 44px; + --mdc-icon-size: 22px; + } + + .due-cell { min-width: 70px; } + + .trigger-card { padding: 10px 12px; } + .trigger-current { font-size: 22px; } + + .prediction-grid { flex-direction: column; gap: 8px; } + + .weibull-info-row { flex-direction: column; gap: 8px; } + + .budget-bars { flex-direction: column; } + .budget-item { min-width: 0; } + + .group-card { min-width: 0; max-width: 100%; } + + .filter-chip { padding: 6px 12px; font-size: 13px; } + + .history-details { flex-wrap: wrap; gap: 6px; } + + .sparkline-container { max-width: 100%; overflow: hidden; } + .sparkline-svg { height: 100px; } + + .stats-bar { gap: 8px; padding: 12px; } + .stat-item { min-width: 60px; } + .stat-value { font-size: 20px; } + } +`; diff --git a/custom_components/maintenance_supporter/frontend-src/test-entity-attributes.mjs b/custom_components/maintenance_supporter/frontend-src/test-entity-attributes.mjs new file mode 100644 index 0000000..4d3be33 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/test-entity-attributes.mjs @@ -0,0 +1,112 @@ +/** + * Quick Playwright test for entity attribute introspection. + * Run from frontend-src: node test-entity-attributes.mjs + */ +import { chromium } from "playwright"; + +const HA_URL = "http://homeassistant-dev:8123"; + +async function main() { + const browser = await chromium.connect("ws://localhost:3000"); + const context = await browser.newContext(); + const page = await context.newPage(); + + // Navigate to HA + await page.goto(HA_URL); + await page.waitForTimeout(2000); + + // Check if we need to login + const url = page.url(); + console.log("Current URL:", url); + + if (url.includes("/auth/authorize") || url.includes("onboarding")) { + // Try auto-login - look for login form + const loginField = await page.$('input[name="username"]'); + if (loginField) { + await loginField.fill("dev"); + const pwField = await page.$('input[name="password"]'); + if (pwField) { + await pwField.fill("dev"); + await page.click('mwc-button, button[type="submit"], ha-button'); + await page.waitForTimeout(3000); + } + } + } + + console.log("After login URL:", page.url()); + + // Test the WS endpoint via browser console + const result = await page.evaluate(async () => { + // Access hass object + const ha = document.querySelector("home-assistant"); + if (!ha || !ha.hass) { + return { error: "No hass object found" }; + } + + // Test entity/attributes endpoint + try { + const res = await ha.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/entity/attributes", + entity_id: "sensor.sun_solar_elevation", + }); + return { success: true, result: res }; + } catch (e) { + return { error: e.message || String(e) }; + } + }); + + console.log("\n=== Entity Attributes WS Test ==="); + console.log(JSON.stringify(result, null, 2)); + + // Test with a non-existent entity to verify graceful handling + const result2 = await page.evaluate(async () => { + const ha = document.querySelector("home-assistant"); + try { + const res = await ha.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/entity/attributes", + entity_id: "climate.nonexistent", + }); + return { success: true, result: res }; + } catch (e) { + return { error: e.message || String(e) }; + } + }); + + console.log("\n=== Non-existent Entity Test ==="); + console.log(JSON.stringify(result2, null, 2)); + + // Check binary_sensor entities + const binarySensors = await page.evaluate(async () => { + const ha = document.querySelector("home-assistant"); + if (!ha || !ha.hass) return { error: "No hass" }; + const states = ha.hass.states; + const ms = {}; + for (const [k, v] of Object.entries(states)) { + if (k.includes("maintenance")) { + ms[k] = { state: v.state, attributes: v.attributes }; + } + } + return ms; + }); + + console.log("\n=== Maintenance Entities ==="); + console.log(JSON.stringify(binarySensors, null, 2)); + + // Navigate to panel and test the UI + await page.goto(HA_URL + "/maintenance_supporter_panel"); + await page.waitForTimeout(3000); + + // Take screenshot + await page.screenshot({ path: "/tmp/panel-test.png", fullPage: true }); + console.log("\nScreenshot saved to /tmp/panel-test.png"); + + await context.close(); + await browser.close(); + + console.log("\n=== All tests completed ==="); +} + +main().catch((e) => { + console.error("Test failed:", e); + process.exit(1); +}); diff --git a/custom_components/maintenance_supporter/frontend-src/tsconfig.json b/custom_components/maintenance_supporter/frontend-src/tsconfig.json new file mode 100644 index 0000000..bd39eea --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "declaration": false, + "sourceMap": false, + "outDir": "../frontend", + "lib": ["ES2021", "DOM", "DOM.Iterable"] + }, + "include": ["*.ts", "components/*.ts", "renderers/*.ts"] +} diff --git a/custom_components/maintenance_supporter/frontend-src/types.ts b/custom_components/maintenance_supporter/frontend-src/types.ts new file mode 100644 index 0000000..c063784 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/types.ts @@ -0,0 +1,421 @@ +/** TypeScript interfaces for the Maintenance Supporter frontend. */ + +export interface MaintenanceObject { + id: string; + name: string; + area_id?: string | null; + manufacturer?: string | null; + model?: string | null; + serial_number?: string | null; + installation_date?: string | null; + /** (#67): warranty expiry date (ISO YYYY-MM-DD) for asset tracking */ + warranty_expiry?: string | null; + // 2.19: attach to an existing HA device / nest under another object + ha_device_id?: string | null; + parent_entry_id?: string | null; + /** v1.4.0 (#43): optional link to PDF manual / vendor page for the object */ + documentation_url?: string | null; + /** v1.4.10 (#46): free-form notes — part numbers, procedures, etc. */ + notes?: string | null; + /** v2.10.0 archive: archived (retire-but-retain) state; archived = archived_at != null. */ + archived?: boolean; + archived_at?: string | null; + /** v2.20 (N3) seasonal pause: paused = paused_at != null; paused_until is + * the optional auto-resume date. */ + paused?: boolean; + paused_at?: string | null; + paused_until?: string | null; + /** v2.20 (N1) replace-flow lineage (both directions). */ + predecessor_entry_id?: string | null; + replaced_by_entry_id?: string | null; + /** (roadmap P2) number of attached documents (files + web-links); drives the + * objects-table paperclip badge. Computed server-side, not persisted. */ + document_count?: number; +} + +export interface TriggerConfig { + entity_id?: string; + entity_ids?: string[]; + entity_logic?: "any" | "all"; + attribute?: string | null; + type?: string; // "threshold" | "counter" | "state_change" | "runtime" + trigger_above?: number | null; + trigger_below?: number | null; + trigger_for_minutes?: number; + trigger_target_value?: number; + trigger_delta_mode?: boolean; + trigger_baseline_value?: number | null; + trigger_from_state?: string | null; + trigger_to_state?: string | null; + trigger_target_changes?: number; + trigger_runtime_hours?: number; + compound_logic?: "AND" | "OR"; + conditions?: Array; + /** Record a completion when the trigger clears itself (#53). */ + auto_complete_on_recovery?: boolean; +} + +export interface TriggerEntityInfo { + entity_id: string; + friendly_name: string; + unit_of_measurement?: string | null; + min?: number | null; + max?: number | null; + step?: number | null; +} + +export interface HistoryEntry { + timestamp: string; + type: string; // "completed" | "skipped" | "reset" | "triggered" + notes?: string | null; + cost?: number | null; + duration?: number | null; + trigger_value?: number | null; + checklist_state?: Record | null; + feedback?: string | null; + completed_by?: string | null; + photo_doc_id?: string | null; + /** v2.20 (#83): recorded value for `reading`-type tasks. */ + reading_value?: number | null; +} + +export interface AdaptiveConfig { + enabled: boolean; + ewa_alpha?: number; + min_interval_days?: number; + max_interval_days?: number; + smoothed_interval?: number; + feedback_count?: number; + confidence?: string; + weibull_beta?: number | null; + weibull_eta?: number | null; + current_recommendation?: number | null; + recommendation_reason?: string | null; + last_analysis_date?: string | null; + seasonal_enabled?: boolean; + seasonal_overrides?: Record | null; + // Sensor prediction (Phase 3) + sensor_prediction_enabled?: boolean; + environmental_entity?: string | null; + environmental_attribute?: string | null; +} + +export interface IntervalAnalysis { + average_actual?: number | null; + ewa_prediction?: number | null; + weibull_beta?: number | null; + weibull_eta?: number | null; + weibull_r_squared?: number | null; + data_points?: number; + reason?: string | null; + seasonal_factor?: number | null; + seasonal_factors?: number[] | null; + seasonal_reason?: string | null; // "learned" | "manual" + confidence_interval_low?: number | null; + confidence_interval_high?: number | null; +} + +/** Nested recurrence object (schedule-model v2). The flat fields above remain + * for the interval/one_time kinds; the calendar kinds (weekdays / nth_weekday / + * day_of_month) can only be expressed here. weekday: 0=Mon … 6=Sun. */ +export interface TaskSchedule { + kind: string; // interval | weekdays | nth_weekday | day_of_month | one_time | manual + every?: number | null; + unit?: string; + anchor?: string; + due_date?: string | null; + weekdays?: number[]; + nth?: number; // 1..5, or -1 = last + weekday?: number; + day?: number; // 1..31, or -1 = last day of the month (#83) + months?: number[]; + /** (#83) day_of_month only: roll a weekend date back to Friday. */ + business?: boolean; + /** (#83) shift the computed occurrence by ±N days (clamped ±15). */ + offset?: number; +} + +export interface MaintenanceTask { + id: string; + name: string; + type: string; // "cleaning" | "inspection" | "replacement" | "calibration" | "service" | "reading" | "custom" + enabled: boolean; + schedule_type: string; // "time_based" | "sensor_based" | "one_time" | "manual" | calendar kind + interval_days?: number | null; + interval_unit?: string; // "days" | "weeks" | "months" | "years" + due_date?: string | null; // one-time task due date (ISO) + interval_anchor?: "completion" | "planned"; + schedule?: TaskSchedule; // nested recurrence (calendar kinds read this) + schedule_time?: string | null; // "HH:MM" or null/undefined = midnight + warning_days: number; + last_performed?: string | null; + notes?: string | null; + documentation_url?: string | null; + checklist?: string[]; + labels?: string[]; + assignee_pool?: string[]; + rotation_strategy?: string | null; + earliest_completion_days?: number | null; + // v1.3.0: completion-action + quick-complete (gated by completion_actions feature) + on_complete_action?: { + service: string; // "domain.service" + target?: { entity_id?: string | string[]; device_id?: string | string[]; area_id?: string | string[] }; + data?: Record; + } | null; + quick_complete_defaults?: { + notes?: string; + cost?: number; + duration?: number; + feedback?: "needed" | "not_needed"; + } | null; + trigger_config?: TriggerConfig | null; + trigger_entity_info?: TriggerEntityInfo | null; + trigger_entity_infos?: TriggerEntityInfo[] | null; + history: HistoryEntry[]; + // Computed + status: string; // "ok" | "due_soon" | "overdue" | "triggered" | "archived" + /** True for a one-time task that has been completed (done; never re-arms). */ + is_done?: boolean; + /** v2.10.0 archive: archived = archived_at != null; reason is manual|auto|object. */ + archived?: boolean; + archived_at?: string | null; + archived_reason?: string | null; + days_until_due?: number | null; + next_due?: string | null; + trigger_active: boolean; + trigger_current_value?: number | null; + trigger_current_delta?: number | null; + trigger_baseline_value?: number | null; + trigger_entity_state?: string; + times_performed: number; + total_cost: number; + average_duration?: number | null; + // Adaptive scheduling + adaptive_config?: AdaptiveConfig | null; + suggested_interval?: number | null; + interval_confidence?: string | null; + interval_analysis?: IntervalAnalysis | null; + // Seasonal scheduling (top-level convenience) + seasonal_factor?: number | null; + seasonal_factors?: number[] | null; + // Sensor-driven predictions (Phase 3) + degradation_rate?: number | null; + degradation_trend?: string | null; // "rising" | "falling" | "stable" | "insufficient_data" + degradation_r_squared?: number | null; + days_until_threshold?: number | null; + threshold_prediction_date?: string | null; + threshold_prediction_confidence?: string | null; // "low" | "medium" | "high" + environmental_factor?: number | null; + environmental_entity?: string | null; + environmental_correlation?: number | null; + sensor_prediction_urgency?: boolean; + // User assignment + responsible_user_id?: string | null; + custom_icon?: string | null; + nfc_tag_id?: string | null; + /** v2.20 (#83): display unit for `reading`-type tasks ("kWh", "m³", ...). */ + reading_unit?: string | null; + priority?: string | null; + entity_slug?: string | null; + // Auto-derived sensor + binary_sensor entity_ids (since 1.0.45) + sensor_entity_id?: string | null; + binary_sensor_entity_id?: string | null; +} + +export interface MaintenanceObjectResponse { + entry_id: string; + object: MaintenanceObject; + tasks: MaintenanceTask[]; +} + +export interface StatisticsResponse { + total_objects: number; + total_tasks: number; + overdue: number; + due_soon: number; + triggered: number; + total_cost: number; + budget?: { currency_symbol?: string } | null; +} + +export interface CardConfig { + type: string; + title?: string; + show_header?: boolean; + max_items?: number; + filter_status?: string[]; + filter_objects?: string[]; + // HA-native entity_ids: pattern (since 1.0.45). When set, only tasks whose + // sensor or binary_sensor entity_id matches one of these are shown. Combines + // additively with filter_status / filter_objects. + entity_ids?: string[]; + // Range filter on task.days_until_due (since 1.7.0). Inclusive on both + // ends. Used by the dashboard strategy's group_by=due_date buckets: + // Today: min=0, max=0 + // This Week: min=1, max=7 + // This Month: min=8, max=30 + // Later: min=31 + // Overdue: max=-1 + // Tasks with null/undefined days_until_due (e.g. sensor-triggered without + // a computed next_due) are excluded when either bound is set. + filter_due_min_days?: number; + filter_due_max_days?: number; + compact?: boolean; + show_actions?: boolean; +} + +export interface GroupTaskRef { + entry_id: string; + task_id: string; +} + +export interface MaintenanceGroup { + name: string; + description: string; + task_refs: GroupTaskRef[]; +} + +export interface BudgetStatus { + monthly_budget: number; + monthly_spent: number; + yearly_budget: number; + yearly_spent: number; + alert_threshold_pct: number; + currency_symbol: string; +} + +export interface AdvancedFeatures { + adaptive: boolean; + predictions: boolean; + seasonal: boolean; + environmental: boolean; + budget: boolean; + groups: boolean; + checklists: boolean; + schedule_time: boolean; + /** v1.3.0: gates per-task on_complete_action + quick_complete_defaults UI. */ + completion_actions: boolean; +} + +/** A single point in a recorder statistics time series. */ +export interface StatisticsPoint { + ts: number; // epoch ms + val: number; // mean (threshold) or state (counter) + min?: number; + max?: number; +} + +/** Cached statistics result for a single entity. */ +export interface EntityStatisticsCache { + entityId: string; + fetchedAt: number; + period: "hour" | "day"; + points: StatisticsPoint[]; +} + +/** Shape of a single row from HA recorder/statistics_during_period response */ +export interface HAStatisticsRow { + start: number; // epoch ms + end: number; + mean?: number | null; + min?: number | null; + max?: number | null; + state?: number | null; + sum?: number | null; +} + +// Flatten task with parent object info for table display +export interface TaskRow { + entry_id: string; + task_id: string; + object_name: string; + task_name: string; + type: string; + schedule_type: string; + status: string; + days_until_due: number | null; + next_due: string | null; + trigger_active: boolean; + trigger_current_value: number | null; + trigger_current_delta: number | null; + trigger_config: TriggerConfig | null; + trigger_entity_info: TriggerEntityInfo | null; + times_performed: number; + total_cost: number; + interval_days: number | null; + interval_unit?: string | null; + interval_anchor: "completion" | "planned" | null; + is_done: boolean; + archived: boolean; + history: HistoryEntry[]; + enabled: boolean; + nfc_tag_id: string | null; + priority: string; + labels: string[]; + area_id: string | null; + responsible_user_id: string | null; + group_names: string[]; +} + +// HomeAssistant type (minimal for our needs) +export interface HassEntity { + entity_id: string; + state: string; + attributes: Record; + last_changed: string; + last_updated: string; +} + +export interface HomeAssistant { + connection: { + // Result defaults to `any` (home-assistant-js-websocket ergonomics): + // call sites annotate `` where the shape matters. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendMessagePromise(msg: Record): Promise; + subscribeMessage( + callback: (msg: unknown) => void, + subscribeMsg: Record, + options?: Record + ): Promise<() => void>; + }; + callService( + domain: string, + service: string, + data?: Record + ): Promise; + states: Record; + areas?: Record; + /** + * HA service registry, mirroring the structure exposed to the frontend. + * Used by the task-dialog action section to drive ha-service-picker + * (autocomplete) + ha-form (schema-driven data fields). + */ + services?: Record; + fields?: Record; + }>; + }>>; + language: string; + locale?: { language: string; number_format?: string }; + localize(key: string, ...args: unknown[]): string; + user?: { id: string; name: string; is_admin: boolean; is_owner: boolean }; + /** Current access token — used for authenticated `fetch()` to our HTTP + * document views (a WebSocket frame can't carry a multipart file upload). */ + auth?: { data?: { access_token?: string } }; +} + +export interface HAUser { + id: string; + name: string; + // Only returned to admin callers of users/list; absent for non-admins. + is_admin?: boolean; + is_owner?: boolean; +} diff --git a/custom_components/maintenance_supporter/frontend-src/user-service.ts b/custom_components/maintenance_supporter/frontend-src/user-service.ts new file mode 100644 index 0000000..4f07f75 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/user-service.ts @@ -0,0 +1,125 @@ +/** + * User Service - Helper for user assignment operations + * Provides caching and convenience methods for working with HA users + */ + +import type { HomeAssistant, HAUser } from "./types"; + +export class UserService { + private hass: HomeAssistant; + private usersCache: HAUser[] | null = null; + private cacheTimestamp: number = 0; + private readonly CACHE_TTL_MS = 60000; // 1 minute cache + + constructor(hass: HomeAssistant) { + this.hass = hass; + } + + updateHass(hass: HomeAssistant): void { + this.hass = hass; + } + + /** + * Get list of all active HA users (with caching) + */ + async getUsers(forceRefresh = false): Promise { + const now = Date.now(); + + // Return cached data if valid + if ( + !forceRefresh && + this.usersCache && + now - this.cacheTimestamp < this.CACHE_TTL_MS + ) { + return this.usersCache; + } + + // Fetch from backend + try { + const response = (await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/users/list", + })) as { users: HAUser[] }; + + this.usersCache = response.users; + this.cacheTimestamp = now; + return this.usersCache; + } catch (error) { + console.error("Failed to fetch users:", error); + // Return cached data if available, even if expired + return this.usersCache || []; + } + } + + /** + * Assign a user to a task (or unassign if userId is null) + */ + async assignUser( + entryId: string, + taskId: string, + userId: string | null + ): Promise { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/assign_user", + entry_id: entryId, + task_id: taskId, + user_id: userId, + }); + } + + /** + * Get tasks assigned to a specific user + */ + async getTasksByUser(userId: string): Promise { + const response = (await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/tasks/by_user", + user_id: userId, + })) as { tasks: unknown[] }; + return response.tasks; + } + + /** + * Get user name by ID (from cache, returns null if not found) + */ + getUserName(userId: string | null): string | null { + if (!userId || !this.usersCache) { + return null; + } + + const user = this.usersCache.find((u) => u.id === userId); + return user?.name || null; + } + + /** + * Get full user object by ID (from cache) + */ + getUser(userId: string | null): HAUser | null { + if (!userId || !this.usersCache) { + return null; + } + + return this.usersCache.find((u) => u.id === userId) || null; + } + + /** + * Get current logged-in user ID + */ + getCurrentUserId(): string | null { + return this.hass.user?.id || null; + } + + /** + * Check if a user ID is the current user + */ + isCurrentUser(userId: string | null): boolean { + if (!userId) return false; + return userId === this.getCurrentUserId(); + } + + /** + * Clear the user cache (useful when users are added/removed) + */ + clearCache(): void { + this.usersCache = null; + this.cacheTimestamp = 0; + } +} diff --git a/custom_components/maintenance_supporter/frontend-src/verify-history-edit-e2e.mjs b/custom_components/maintenance_supporter/frontend-src/verify-history-edit-e2e.mjs new file mode 100644 index 0000000..312aa6d --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/verify-history-edit-e2e.mjs @@ -0,0 +1,275 @@ +/** v2.2.0 — intensive E2E for the history-edit feature. + * + * The user (Discussion #49 follow-up) explicitly asked: "das müsste intensiv + * getestet werden ob die Änderungen korrekt gespeichert werden". This script + * exercises the full roundtrip via the live HA WebSocket: + * + * 1. find an existing maintenance object + completed task with ≥1 history entry + * 2. record the original entry payload + * 3. send maintenance_supporter/task/history/update with patched fields + * 4. fetch the object again, verify the patch landed + * 5. test edge cases: clear notes (None), edit timestamp updates last_performed + * 6. read back AGAIN after a brief wait — confirms persistence isn't lost + * + * No browser UI needed — this is pure WS roundtrip, the most reliable layer. + */ +import { chromium } from "playwright"; + +const HA = "http://localhost:8125"; +const HA_TOKEN = process.env.HA_TOKEN; +if (!HA_TOKEN) { + console.error("Set HA_TOKEN env var."); + process.exit(1); +} + +async function getRefreshToken() { + async function post(path, body, ctype = "application/json") { + const data = typeof body === "string" ? body : JSON.stringify(body); + const r = await fetch(`${HA}${path}`, { + method: "POST", + headers: { "Content-Type": ctype }, + body: data, + }); + return await r.json(); + } + const flow = await post("/auth/login_flow", { + client_id: `${HA}/`, handler: ["homeassistant", null], redirect_uri: `${HA}/`, + }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { + client_id: `${HA}/`, username: "dev", password: "dev", + }); + const tokens = await post( + "/auth/token", + `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, + "application/x-www-form-urlencoded", + ); + return tokens.refresh_token; +} + +const browser = await chromium.launch({ headless: true }); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); +const page = await ctx.newPage(); + +const refreshToken = await getRefreshToken(); +await page.goto(HA); +await page.waitForTimeout(800); +await page.evaluate(({ ha, r }) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: ha, clientId: `${ha}/`, refresh_token: r, + access_token: "", token_type: "Bearer", expires_in: 1800, expires: 0, + })); +}, { ha: HA, r: refreshToken }); +await page.goto(HA); +await page.waitForTimeout(8000); + +// All assertions go through this — runs in browser context with hass.connection +const result = await page.evaluate(async () => { + const out = { steps: [] }; + const log = (msg, data) => out.steps.push({ msg, ...(data ? { data } : {}) }); + + const hass = document.querySelector("home-assistant")?.hass; + if (!hass) { + out.error = "no hass"; + return out; + } + const ws = (m) => hass.connection.sendMessagePromise(m); + + // 1. Find an object with a completed task that has ≥1 history entry + const { objects } = await ws({ type: "maintenance_supporter/objects" }); + let testEntry = null; + let testTask = null; + for (const obj of objects) { + for (const task of obj.tasks || []) { + if (Array.isArray(task.history) && task.history.length >= 1) { + testEntry = obj; + testTask = task; + break; + } + } + if (testEntry) break; + } + if (!testEntry) { + // None exists — create a fresh object + task + complete it once + log("no history found, creating one"); + const cr = await ws({ + type: "maintenance_supporter/object/create", + name: `histtest-${Math.random().toString(36).slice(2, 8)}`, + }); + const newEntryId = cr.entry_id; + await new Promise(r => setTimeout(r, 500)); + const tr = await ws({ + type: "maintenance_supporter/task/create", + entry_id: newEntryId, + name: "edit-test-task", + schedule_type: "time_based", + interval_days: 30, + }); + const newTaskId = tr.task_id; + await new Promise(r => setTimeout(r, 500)); + await ws({ + type: "maintenance_supporter/task/complete", + entry_id: newEntryId, + task_id: newTaskId, + notes: "initial completion", + cost: 25.0, + duration: 45, + }); + await new Promise(r => setTimeout(r, 800)); + const fresh = await ws({ + type: "maintenance_supporter/object", + entry_id: newEntryId, + }); + testEntry = { entry_id: newEntryId }; + testTask = fresh.tasks.find(t => t.id === newTaskId); + } + log("test target", { + entry_id: testEntry.entry_id, + task_id: testTask.id, + history_count: testTask.history.length, + }); + + // 2. Take the FIRST completed entry (most likely to be lifecycle-relevant) + const original = testTask.history.find(h => h.type === "completed") + || testTask.history[0]; + const originalTs = original.timestamp; + log("original entry", { ts: originalTs, notes: original.notes, cost: original.cost }); + + // 3. PATCH 1 — change notes only + const patchedNotes = `e2e-patch-${Date.now()}`; + const r1 = await ws({ + type: "maintenance_supporter/task/history/update", + entry_id: testEntry.entry_id, + task_id: testTask.id, + original_timestamp: originalTs, + notes: patchedNotes, + }); + log("patch1 result", r1); + out.patch1_ok = r1.success === true; + + // 4. Verify roundtrip + await new Promise(r => setTimeout(r, 800)); + const round1 = await ws({ + type: "maintenance_supporter/object", + entry_id: testEntry.entry_id, + }); + const taskAfter1 = round1.tasks.find(t => t.id === testTask.id); + const entryAfter1 = taskAfter1.history.find(h => h.timestamp === originalTs); + out.notes_persisted = entryAfter1?.notes === patchedNotes; + log("after patch1", { notes: entryAfter1?.notes }); + + // 5. PATCH 2 — change cost AND duration in one call + const r2 = await ws({ + type: "maintenance_supporter/task/history/update", + entry_id: testEntry.entry_id, + task_id: testTask.id, + original_timestamp: originalTs, + cost: 99.95, + duration: 123, + }); + out.patch2_ok = r2.success === true; + await new Promise(r => setTimeout(r, 800)); + const round2 = await ws({ + type: "maintenance_supporter/object", + entry_id: testEntry.entry_id, + }); + const taskAfter2 = round2.tasks.find(t => t.id === testTask.id); + const entryAfter2 = taskAfter2.history.find(h => h.timestamp === originalTs); + out.cost_persisted = entryAfter2?.cost === 99.95; + out.duration_persisted = entryAfter2?.duration === 123; + out.notes_still_there = entryAfter2?.notes === patchedNotes; // unchanged + out.type_unchanged = entryAfter2?.type === original.type; // never patchable + + // 6. PATCH 3 — clear notes via null + const r3 = await ws({ + type: "maintenance_supporter/task/history/update", + entry_id: testEntry.entry_id, + task_id: testTask.id, + original_timestamp: originalTs, + notes: null, + }); + out.patch3_ok = r3.success === true; + await new Promise(r => setTimeout(r, 800)); + const round3 = await ws({ + type: "maintenance_supporter/object", + entry_id: testEntry.entry_id, + }); + const taskAfter3 = round3.tasks.find(t => t.id === testTask.id); + const entryAfter3 = taskAfter3.history.find(h => h.timestamp === originalTs); + out.notes_cleared = entryAfter3?.notes == null; + + // 7. PATCH 4 — change timestamp; verify last_performed follows when this + // is the latest lifecycle entry + const lifecycleEntries = taskAfter3.history.filter( + h => ["completed", "reset", "skipped"].includes(h.type), + ); + const latestTs = lifecycleEntries.reduce( + (max, h) => h.timestamp > max ? h.timestamp : max, + "", + ); + const isLatest = entryAfter3.timestamp === latestTs; + out.is_latest_lifecycle = isLatest; + if (isLatest) { + // Bump timestamp by 1 hour + const dt = new Date(entryAfter3.timestamp); + dt.setHours(dt.getHours() + 1); + const newTs = dt.toISOString(); + const r4 = await ws({ + type: "maintenance_supporter/task/history/update", + entry_id: testEntry.entry_id, + task_id: testTask.id, + original_timestamp: entryAfter3.timestamp, + timestamp: newTs, + }); + out.patch4_ok = r4.success === true; + await new Promise(r => setTimeout(r, 800)); + const round4 = await ws({ + type: "maintenance_supporter/object", + entry_id: testEntry.entry_id, + }); + const taskAfter4 = round4.tasks.find(t => t.id === testTask.id); + const entryAfter4 = taskAfter4.history.find(h => h.timestamp === newTs); + out.timestamp_persisted = entryAfter4 != null; + out.last_performed_followed = + taskAfter4.last_performed === newTs.slice(0, 10); + } + + // 8. INVALID timestamp rejected + try { + await ws({ + type: "maintenance_supporter/task/history/update", + entry_id: testEntry.entry_id, + task_id: testTask.id, + original_timestamp: "nonexistent-2020-01-01", + notes: "oops", + }); + out.invalid_ts_rejected = false; + } catch (e) { + out.invalid_ts_rejected = true; + out.invalid_ts_error = String(e).substring(0, 200); + } + + return out; +}); + +console.log(JSON.stringify(result, null, 2)); + +const summary = [ + ["patch1 (notes) ", result.patch1_ok && result.notes_persisted], + ["patch2 (cost+duration) ", result.patch2_ok && result.cost_persisted && result.duration_persisted], + ["patch2 leaves notes intact ", result.notes_still_there], + ["patch2 leaves type intact ", result.type_unchanged], + ["patch3 (clear notes via null)", result.patch3_ok && result.notes_cleared], + ["patch4 (timestamp) ", result.is_latest_lifecycle ? (result.patch4_ok && result.timestamp_persisted) : true], + ["last_performed follows latest", result.is_latest_lifecycle ? result.last_performed_followed : true], + ["invalid original_ts rejected ", result.invalid_ts_rejected], +]; + +console.log("\n=== SUMMARY ==="); +let allOk = true; +for (const [label, ok] of summary) { + console.log(` ${ok ? "✓" : "✗"} ${label}`); + if (!ok) allOk = false; +} + +await browser.close(); +process.exit(allOk ? 0 : 1); diff --git a/custom_components/maintenance_supporter/frontend-src/verify-kpi-click.mjs b/custom_components/maintenance_supporter/frontend-src/verify-kpi-click.mjs new file mode 100644 index 0000000..7ee3482 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/verify-kpi-click.mjs @@ -0,0 +1,224 @@ +/** Verify v2.1.0 KPI click filtering (Discussion #49 — @byoung79). + * + * Loads the panel, clicks each clickable KPI, asserts the panel's + * filter dropdown reflects the new state and the .stat-item.active class + * lights up correctly. + */ +import { chromium } from "playwright"; + +const HA = "http://localhost:8125"; +const HA_TOKEN = process.env.HA_TOKEN; +if (!HA_TOKEN) { + console.error("Set HA_TOKEN env var."); + process.exit(1); +} + +async function getRefreshToken() { + async function post(path, body, ctype = "application/json") { + const data = typeof body === "string" ? body : JSON.stringify(body); + const r = await fetch(`${HA}${path}`, { + method: "POST", + headers: { "Content-Type": ctype }, + body: data, + }); + return await r.json(); + } + const flow = await post("/auth/login_flow", { + client_id: `${HA}/`, + handler: ["homeassistant", null], + redirect_uri: `${HA}/`, + }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { + client_id: `${HA}/`, + username: "dev", + password: "dev", + }); + const tokens = await post( + "/auth/token", + `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, + "application/x-www-form-urlencoded", + ); + return tokens.refresh_token; +} + +const browser = await chromium.launch({ headless: true }); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); +const page = await ctx.newPage(); +const errors = []; +page.on("pageerror", (e) => errors.push(`pageerror: ${e.message}`)); + +const refreshToken = await getRefreshToken(); +await page.goto(HA); +await page.waitForTimeout(1000); +await page.evaluate(({ ha, r }) => { + localStorage.setItem( + "hassTokens", + JSON.stringify({ + hassUrl: ha, + clientId: `${ha}/`, + refresh_token: r, + access_token: "", + token_type: "Bearer", + expires_in: 1800, + expires: 0, + }), + ); +}, { ha: HA, r: refreshToken }); +await page.goto(`${HA}/maintenance-supporter`); +await page.waitForTimeout(12000); + +if (errors.length) { + console.log("Pre-evaluate page errors:"); + errors.slice(0, 5).forEach((e) => console.log(" " + e.substring(0, 300))); +} + +// Helper that drills into the panel's shadow root and finds the stat items. +const PANEL_QUERY = ` + (function () { + const ha = document.querySelector("home-assistant"); + const sr = ha?.shadowRoot + ?.querySelector("home-assistant-main")?.shadowRoot + ?.querySelector("ha-drawer") + ?.querySelector("partial-panel-resolver") + ?.querySelector("ha-panel-custom") + ?.querySelector("maintenance-supporter-panel")?.shadowRoot; + return sr; + })() +`; + +const result = await page.evaluate(async (q) => { + const out = {}; + const sr = eval(q); + if (!sr) { + out.error = "panel shadowRoot not found"; + return out; + } + // Wrap each step so a single failure doesn't abort the whole probe. + const safe = async (label, fn) => { + try { await fn(); } + catch (e) { out[`${label}_error`] = String(e).substring(0, 150); } + }; + + const queryItems = () => [...sr.querySelectorAll(".stat-item.clickable")]; + const findKpi = (label) => + queryItems().find((i) => + i.querySelector(".stat-label")?.textContent?.trim().toLowerCase() === + label.toLowerCase(), + ); + const findTab = (label) => + [...sr.querySelectorAll(".tab")].find((t) => + t.textContent?.trim().toLowerCase().includes(label.toLowerCase()), + ); + const activeTabLabel = () => + sr.querySelector(".tab.active")?.textContent?.trim(); + + out.kpiCount = queryItems().length; + out.kpiLabels = queryItems().map((i) => + i.querySelector(".stat-label")?.textContent?.trim(), + ); + + // ── A. From Dashboard tab — overdue click ───────────────────────────── + out.startTab = activeTabLabel(); + await safe("A", async () => { + const overdueItem = findKpi("Overdue") || findKpi("Überfällig"); + overdueItem.click(); + await new Promise((r) => setTimeout(r, 600)); + const select = sr.querySelector(".filter-bar select"); + out.A_afterOverdueClick_dropdownValue = select?.value; + out.A_afterOverdueClick_overdueActive = findKpi("Overdue")?.classList.contains("active"); + out.A_tabAfter = activeTabLabel(); + }); + + // ── B. Click Tasks to clear filter ──────────────────────────────────── + await safe("B", async () => { + const tasksItem = findKpi("Tasks") || findKpi("Aufgaben"); + tasksItem.click(); + await new Promise((r) => setTimeout(r, 600)); + const select = sr.querySelector(".filter-bar select"); + out.B_afterTasksClick_dropdownValue = select?.value; + out.B_afterTasksClick_overdueActive = + findKpi("Overdue")?.classList.contains("active") ?? null; + }); + + // ── C. Switch to Calendar tab — are KPIs still visible? ─────────────── + await safe("C", async () => { + const calendarTab = findTab("Calendar") || findTab("Kalender"); + calendarTab.click(); + await new Promise((r) => setTimeout(r, 600)); + out.C_tabAfterCalendarClick = activeTabLabel(); + out.C_kpisVisibleOnCalendar = queryItems().length > 0; + }); + + // ── D. From Calendar tab, click Overdue KPI ─────────────────────────── + await safe("D", async () => { + const overdueOnCalendar = findKpi("Overdue") || findKpi("Überfällig"); + if (!overdueOnCalendar) { + out.D_skipped = "Overdue KPI not visible from Calendar tab"; + return; + } + overdueOnCalendar.click(); + await new Promise((r) => setTimeout(r, 800)); + out.D_tabAfterOverdueFromCalendar = activeTabLabel(); + const select = sr.querySelector(".filter-bar select"); + out.D_dropdownValue = select?.value; + }); + + // ── E. Switch to Settings tab — KPIs still visible? ─────────────────── + await safe("E", async () => { + const dashTab = findTab("Dashboard") || findTab("Übersicht") || findTab("Overview"); + dashTab?.click(); + await new Promise((r) => setTimeout(r, 300)); + const settingsTab = findTab("Settings") || findTab("Einstellungen"); + if (!settingsTab) return; + settingsTab.click(); + await new Promise((r) => setTimeout(r, 600)); + out.E_tabAfterSettingsClick = activeTabLabel(); + out.E_kpisVisibleOnSettings = queryItems().length > 0; + }); + + return out; +}, PANEL_QUERY); + +console.log(JSON.stringify(result, null, 2)); + +const okFiveKpis = result.kpiCount === 5; +const okOverdueClick = + result.A_afterOverdueClick_dropdownValue === "overdue" && + result.A_afterOverdueClick_overdueActive === true; +const okTasksClick = + result.B_afterTasksClick_dropdownValue === "" && + result.B_afterTasksClick_overdueActive === false; +const okKpisOnCalendar = result.C_kpisVisibleOnCalendar === true; +const okFilterFromCalendar = + result.D_tabAfterOverdueFromCalendar?.toLowerCase().includes("dashboard") === + true || + result.D_tabAfterOverdueFromCalendar?.toLowerCase().includes("übersicht") === + true; +const okKpisOnSettings = result.E_kpisVisibleOnSettings === true; + +console.log( + "\n=== SUMMARY ===\n" + + ` 5 clickable KPIs (Dashboard tab): ${okFiveKpis ? "✓" : "✗"} (${result.kpiCount})\n` + + ` Overdue click → dropdown=overdue: ${okOverdueClick ? "✓" : "✗"}\n` + + ` Tasks click clears + dehighlight: ${okTasksClick ? "✓" : "✗"}\n` + + ` KPIs visible on Calendar tab: ${okKpisOnCalendar ? "✓" : "✗"} (${result.C_kpisVisibleOnCalendar})\n` + + ` KPIs visible on Settings tab: ${okKpisOnSettings ? "✓" : "✗"} (${result.E_kpisVisibleOnSettings})\n` + + ` Filter-from-Calendar tab works: ${okFilterFromCalendar ? "✓" : "✗"} (tab-after=${result.D_tabAfterOverdueFromCalendar}, dropdown=${result.D_dropdownValue})`, +); + +if (errors.length) { + console.log("\n=== Browser errors ==="); + errors.slice(0, 5).forEach((e) => console.log(` ${e.substring(0, 200)}`)); +} + +await browser.close(); +process.exit( + okFiveKpis && + okOverdueClick && + okTasksClick && + okKpisOnCalendar && + okKpisOnSettings && + okFilterFromCalendar + ? 0 + : 1, +); diff --git a/custom_components/maintenance_supporter/frontend-src/verify-section-cards-write.mjs b/custom_components/maintenance_supporter/frontend-src/verify-section-cards-write.mjs new file mode 100644 index 0000000..4babe65 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/verify-section-cards-write.mjs @@ -0,0 +1,116 @@ +/** Smoke-test the WS write paths used by the new section cards. + * + * 1. Enable vacation via vacation/update → expect enabled=true + * 2. Set start/end + buffer → expect persisted + * 3. Disable vacation → expect enabled=false + * 4. Set monthly + yearly budget via global/update → expect persisted + * 5. Create + rename + delete a group → expect roundtrip + */ +import http from "http"; +import WebSocket from "ws"; + +const HA = "http://localhost:8125"; +const WSURL = "ws://localhost:8125/api/websocket"; + +function post(p, body, ct = "application/json") { + return new Promise((resolve, reject) => { + const data = typeof body === "string" ? body : JSON.stringify(body); + const opts = { hostname: "localhost", port: 8125, path: p, method: "POST", + headers: { "Content-Type": ct, "Content-Length": Buffer.byteLength(data) } }; + const req = http.request(opts, (r) => { let d = ""; r.on("data", (c) => (d += c)); r.on("end", () => resolve(JSON.parse(d))); }); + req.on("error", reject); req.write(data); req.end(); + }); +} + +async function getToken() { + const flow = await post("/auth/login_flow", { client_id: HA + "/", handler: ["homeassistant", null], redirect_uri: HA + "/" }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { client_id: HA + "/", username: "dev", password: "dev" }); + const tok = await post("/auth/token", `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, "application/x-www-form-urlencoded"); + return tok.access_token; +} + +async function main() { + const token = await getToken(); + const ws = new WebSocket(WSURL); + let id = 1; + const pending = new Map(); + + ws.on("message", (raw) => { + const m = JSON.parse(raw); + if (m.type === "auth_required") { + ws.send(JSON.stringify({ type: "auth", access_token: token })); + } else if (m.type === "auth_ok") { + pending.get("auth")?.resolve(); + } else if (m.type === "result") { + const p = pending.get(m.id); + if (p) { + if (m.success === false) p.reject(new Error(JSON.stringify(m.error))); + else p.resolve(m.result); + pending.delete(m.id); + } + } + }); + + await new Promise((resolve) => { ws.once("open", resolve); }); + await new Promise((resolve) => pending.set("auth", { resolve })); + + function call(payload) { + return new Promise((resolve, reject) => { + const i = id++; + pending.set(i, { resolve, reject }); + ws.send(JSON.stringify({ id: i, ...payload })); + }); + } + + let ok = 0, total = 0; + function check(label, cond, value) { + total++; + if (cond) ok++; + console.log(` ${cond ? "✓" : "✗"} ${label}${value !== undefined ? " — " + value : ""}`); + } + + console.log("\n=== Vacation enable + dates ==="); + await call({ type: "maintenance_supporter/vacation/update", enabled: true, start: "2026-06-01", end: "2026-06-15", buffer_days: 3 }); + let s = await call({ type: "maintenance_supporter/vacation/state" }); + check("enabled=true", s.enabled === true); + check("start=2026-06-01", s.start === "2026-06-01", s.start); + check("end=2026-06-15", s.end === "2026-06-15", s.end); + check("buffer_days=3", s.buffer_days === 3, s.buffer_days); + + console.log("\n=== Vacation disable ==="); + await call({ type: "maintenance_supporter/vacation/update", enabled: false }); + s = await call({ type: "maintenance_supporter/vacation/state" }); + check("enabled=false after disable", s.enabled === false); + check("dates preserved", s.start === "2026-06-01" && s.end === "2026-06-15"); + + console.log("\n=== Budget update via global/update ==="); + await call({ type: "maintenance_supporter/global/update", settings: { budget_monthly: 200, budget_yearly: 2000 } }); + let bs = await call({ type: "maintenance_supporter/budget_status" }); + check("monthly=200", bs.monthly_budget === 200, bs.monthly_budget); + check("yearly=2000", bs.yearly_budget === 2000, bs.yearly_budget); + + // Restore previous values + await call({ type: "maintenance_supporter/global/update", settings: { budget_monthly: 150, budget_yearly: 1500 } }); + + console.log("\n=== Groups: create + rename + delete ==="); + const created = await call({ type: "maintenance_supporter/group/create", name: "TmpTestGroup" }); + check("group created with id", typeof created.group_id === "string"); + const gid = created.group_id; + + let g = await call({ type: "maintenance_supporter/groups" }); + check("new group present", !!g.groups[gid] && g.groups[gid].name === "TmpTestGroup", g.groups[gid]?.name); + + await call({ type: "maintenance_supporter/group/update", group_id: gid, name: "RenamedGroup" }); + g = await call({ type: "maintenance_supporter/groups" }); + check("renamed", g.groups[gid]?.name === "RenamedGroup", g.groups[gid]?.name); + + await call({ type: "maintenance_supporter/group/delete", group_id: gid }); + g = await call({ type: "maintenance_supporter/groups" }); + check("deleted", !g.groups[gid]); + + console.log(`\n=== SUMMARY ===\n ${ok}/${total} checks passed`); + ws.close(); + process.exit(ok === total ? 0 : 1); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/custom_components/maintenance_supporter/frontend-src/verify-strategy-picker.mjs b/custom_components/maintenance_supporter/frontend-src/verify-strategy-picker.mjs new file mode 100644 index 0000000..b335b85 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/verify-strategy-picker.mjs @@ -0,0 +1,153 @@ +/** Diagnose: is the strategy registered in the Add-Dashboard picker context? + * + * Earlier verify-strategy.mjs checked window.customStrategies after navigating + * to /maintenance-supporter. This script navigates to /config/dashboards + * (Settings → Dashboards) instead — the actual context where the picker opens. + */ +import { chromium } from "playwright"; + +const HA = "http://localhost:8125"; +const HA_TOKEN = process.env.HA_TOKEN; +if (!HA_TOKEN) { + console.error("Set HA_TOKEN env var."); + process.exit(1); +} + +async function getRefreshToken() { + async function post(path, body, ctype = "application/json") { + const data = typeof body === "string" ? body : JSON.stringify(body); + const r = await fetch(`${HA}${path}`, { + method: "POST", + headers: { "Content-Type": ctype }, + body: data, + }); + return await r.json(); + } + const flow = await post("/auth/login_flow", { + client_id: `${HA}/`, handler: ["homeassistant", null], redirect_uri: `${HA}/`, + }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { + client_id: `${HA}/`, username: "dev", password: "dev", + }); + const tokens = await post( + "/auth/token", + `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, + "application/x-www-form-urlencoded", + ); + return tokens.refresh_token; +} + +const browser = await chromium.launch({ headless: true }); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); +const page = await ctx.newPage(); +const errors = []; +page.on("pageerror", (e) => errors.push(`pageerror: ${e.message}`)); +page.on("console", (m) => { + if (m.type() === "error") errors.push(`console: ${m.text().substring(0, 200)}`); +}); + +const refreshToken = await getRefreshToken(); +await page.goto(HA); +await page.waitForTimeout(800); +await page.evaluate(({ ha, r }) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: ha, clientId: `${ha}/`, refresh_token: r, + access_token: "", token_type: "Bearer", expires_in: 1800, expires: 0, + })); +}, { ha: HA, r: refreshToken }); + +// 1. After hitting root only — is window.customStrategies populated? +await page.goto(HA); +await page.waitForTimeout(8000); +const atRoot = await page.evaluate(() => ({ + customStrategies: window.customStrategies || null, + customCards: (window.customCards || []).filter(c => c.type?.includes?.("maintenance")), + scriptsLoaded: [...document.scripts].filter(s => s.src.includes("maintenance")).map(s => s.src), + extraModules: [...document.querySelectorAll("script[type=module]")].filter( + s => s.src.includes("maintenance") + ).map(s => s.src), +})); +console.log("=== After root navigation ==="); +console.log(JSON.stringify(atRoot, null, 2)); + +// 2. Now navigate to Settings → Dashboards (the picker's context) +await page.goto(`${HA}/config/dashboards`); +await page.waitForTimeout(6000); +const atDashboardsConfig = await page.evaluate(() => ({ + url: window.location.href, + customStrategies: window.customStrategies || null, + customCards: (window.customCards || []).filter(c => c.type?.includes?.("maintenance")), + scriptsLoaded: [...document.scripts].filter(s => s.src.includes("maintenance")).map(s => s.src), + // Check if our specific custom element is registered + strategyTagDefined: !!customElements.get("ll-strategy-dashboard-maintenance-supporter"), +})); +console.log("\n=== After /config/dashboards navigation ==="); +console.log(JSON.stringify(atDashboardsConfig, null, 2)); + +// 3. Open the Add-Dashboard picker dialog and look at what it renders +await page.goto(`${HA}/config/dashboards`); +await page.waitForTimeout(5000); + +// Open the Add-Dashboard dialog via HA's standard "show-dialog" event — +// way more robust than chasing the FAB through nested shadow DOMs. +const dialogResult = await page.evaluate(async () => { + const ha = document.querySelector("home-assistant"); + if (!ha) return { error: "no home-assistant element" }; + // HA opens dialogs via this DOM event. The dialog is dialog-new-dashboard. + ha.dispatchEvent( + new CustomEvent("show-dialog", { + detail: { + dialogTag: "dialog-new-dashboard", + dialogImport: () => + import("/frontend_latest/dialog-new-dashboard").catch(() => ({})), + dialogParams: {}, + }, + bubbles: true, + composed: true, + }), + ); + // The standard import path may differ — wait for the dialog to materialize + for (let i = 0; i < 30; i++) { + await new Promise((r) => setTimeout(r, 200)); + if (document.querySelector("dialog-new-dashboard")) break; + } + return { tried: true }; +}); +console.log("Dialog open attempt:", dialogResult); + +await page.waitForTimeout(2000); + +// Inspect the dialog's content +const dialogContent = await page.evaluate(() => { + const out = {}; + // The dialog opens at document level + const dialog = document.querySelector("dialog-new-dashboard"); + out.dialogPresent = !!dialog; + if (dialog) { + const sr = dialog.shadowRoot; + // Look for dashboard cards (each option in the picker) + const cards = sr?.querySelectorAll("dashboard-card") || []; + out.cardCount = cards.length; + out.cardTypes = [...cards].map((c) => { + const innerSr = c.shadowRoot; + return { + strategy: c.getAttribute("strategy") || c._strategy?.type, + name: innerSr?.querySelector("[name], h3, .name")?.textContent?.trim(), + }; + }); + // Also: did the component capture any custom strategies? + out._customStrategies = dialog._customStrategies || dialog.__customStrategies; + } + // Also peek at the global registry one more time + out.windowCustomStrategies = (window.customStrategies || []).map((s) => s.type); + return out; +}); +console.log("\n=== Dashboard picker dialog content ==="); +console.log(JSON.stringify(dialogContent, null, 2)); + +if (errors.length) { + console.log("\n=== Page errors ==="); + errors.slice(0, 8).forEach(e => console.log(" " + e.substring(0, 250))); +} + +await browser.close(); diff --git a/custom_components/maintenance_supporter/frontend-src/verify-strategy.mjs b/custom_components/maintenance_supporter/frontend-src/verify-strategy.mjs new file mode 100644 index 0000000..9d06d9c --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/verify-strategy.mjs @@ -0,0 +1,382 @@ +/** Verify the dashboard strategy registers in HA 2026.5+'s frontend. + * + * Headless Chromium logs into ha-maint, then evaluates window.customStrategies + * to confirm our entry is there. Plus tries the actual generate() call to make + * sure the WS round-trip works. + * + * Run after: npm run build && docker restart ha-maint + * node verify-strategy.mjs + */ +import { chromium } from "playwright"; + +const HA = "http://localhost:8125"; +const HA_TOKEN = process.env.HA_TOKEN; +if (!HA_TOKEN) { + console.error("Set HA_TOKEN env var (long-lived access token)."); + process.exit(1); +} + +// HA expects a *refresh* token in localStorage, not an LLAT. Quick refresh-token +// dance: hit /auth/login_flow with username/password. +async function getRefreshToken() { + async function post(path, body, ctype = "application/json") { + const data = typeof body === "string" ? body : JSON.stringify(body); + const r = await fetch(`${HA}${path}`, { + method: "POST", + headers: { "Content-Type": ctype }, + body: data, + }); + return await r.json(); + } + const flow = await post("/auth/login_flow", { + client_id: `${HA}/`, + handler: ["homeassistant", null], + redirect_uri: `${HA}/`, + }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { + client_id: `${HA}/`, + username: "dev", + password: "dev", + }); + const tokens = await post( + "/auth/token", + `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, + "application/x-www-form-urlencoded", + ); + return tokens.refresh_token; +} + +const browser = await chromium.launch({ headless: true }); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); +const page = await ctx.newPage(); +const consoleErrors = []; +page.on("pageerror", (err) => consoleErrors.push(`pageerror: ${err.message}`)); +page.on("console", (msg) => { + if (msg.type() === "error") consoleErrors.push(`console.error: ${msg.text()}`); +}); + +const refreshToken = await getRefreshToken(); +await page.goto(HA); +await page.waitForTimeout(1000); +await page.evaluate(({ ha, r }) => { + localStorage.setItem( + "hassTokens", + JSON.stringify({ + hassUrl: ha, + clientId: `${ha}/`, + refresh_token: r, + access_token: "", + token_type: "Bearer", + expires_in: 1800, + expires: 0, + }), + ); +}, { ha: HA, r: refreshToken }); +await page.goto(HA); +await page.waitForTimeout(8000); // let frontend extra modules load + +const result = await page.evaluate(async () => { + const out = {}; + + // 1. window.customStrategies populated? + out.customStrategies = (window.customStrategies || []).map((s) => ({ + type: s.type, + strategyType: s.strategyType, + name: s.name, + })); + + // 2. Custom element registered? + out.elementRegistered = !!customElements.get( + "ll-strategy-dashboard-maintenance-supporter", + ); + + // 3. Try the actual generate() call in ALL group_by modes + try { + const StrategyClass = customElements.get( + "ll-strategy-dashboard-maintenance-supporter", + ); + const ha = document.querySelector("home-assistant"); + if (StrategyClass?.generate && ha?.hass) { + for (const mode of ["area", "status", "floor", "due_date"]) { + const cfg = await StrategyClass.generate( + { type: "custom:maintenance-supporter", group_by: mode }, + ha.hass, + ); + out[`generated_${mode}`] = { + viewCount: cfg.views?.length ?? 0, + viewTitles: (cfg.views || []).map((v) => v.title), + }; + } + } + } catch (e) { + out.generateError = String(e); + } + + // 4. Editor element registered + setConfig works + dispatches event + try { + out.editorRegistered = !!customElements.get( + "hui-maintenance-supporter-strategy-editor", + ); + if (out.editorRegistered) { + const StrategyClass = customElements.get( + "ll-strategy-dashboard-maintenance-supporter", + ); + const editor = await StrategyClass.getConfigElement(); + document.body.appendChild(editor); + editor.setConfig({ + type: "custom:maintenance-supporter", + group_by: "status", + }); + const select = editor.querySelector("select"); + out.editorPreselected = select?.value; + let dispatchedConfig = null; + editor.addEventListener("config-changed", (e) => { + dispatchedConfig = e.detail.config; + }); + select.value = "due_date"; + select.dispatchEvent(new Event("change")); + out.editorDispatched = dispatchedConfig; + editor.remove(); + } + } catch (e) { + out.editorError = String(e); + } + + // 5. v1.8.0 — Header + Sidebar present on Overview view + try { + const StrategyClass = customElements.get( + "ll-strategy-dashboard-maintenance-supporter", + ); + const ha = document.querySelector("home-assistant"); + if (StrategyClass?.generate && ha?.hass) { + const cfg = await StrategyClass.generate( + { type: "custom:maintenance-supporter" }, + ha.hass, + ); + const overview = cfg.views?.find((v) => v.path === "overview"); + out.overviewHeader = overview?.header?.card?.type; + out.overviewSidebar = overview?.sidebar?.sections?.[0]?.cards?.length ?? 0; + out.overviewSidebarVisibility = + overview?.sidebar?.visibility?.[0]?.condition; + } + } catch (e) { + out.overviewError = String(e); + } + + // 6. v1.8.0 — Section strategy registered + generates + try { + out.sectionStrategyInRegistry = (window.customStrategies || []).some( + (s) => s.type === "maintenance-supporter-section" && s.strategyType === "section", + ); + out.sectionElementRegistered = !!customElements.get( + "ll-strategy-section-maintenance-supporter-section", + ); + const SectionClass = customElements.get( + "ll-strategy-section-maintenance-supporter-section", + ); + const ha = document.querySelector("home-assistant"); + if (SectionClass?.generate && ha?.hass) { + const sec = await SectionClass.generate( + { + type: "custom:maintenance-supporter-section", + filter_status: ["overdue", "triggered"], + title: "Hot list", + }, + ha.hass, + ); + out.sectionGenerated = { + type: sec.type, + cardCount: sec.cards?.length ?? 0, + firstCardType: sec.cards?.[0]?.type, + lastCardType: sec.cards?.[sec.cards.length - 1]?.type, + }; + } + } catch (e) { + out.sectionError = String(e); + } + + // 7. v1.9.0 — fire-dom-event handler opens dialog IN PLACE (object dialog) + // rather than navigating. Detect by checking if the dialog element was + // appended to body and is in "open" state. + try { + // Use HA's REAL fire-dom-event shape — the action config is the event + // detail and our payload sits under `.ll_custom`. Dispatching the payload + // at the top level (as this verifier used to) tested a shape that never + // occurs in production and masked issue #69. + document.dispatchEvent( + new CustomEvent("ll-custom", { + detail: { + action: "fire-dom-event", + ll_custom: { type: "maintenance-supporter:add-object" }, + }, + bubbles: true, + composed: true, + }), + ); + // Give the synchronous mount a tick to settle + await new Promise((r) => setTimeout(r, 100)); + const dlg = document.body.querySelector("maintenance-object-dialog"); + out.objectDialogMounted = !!dlg; + out.objectDialogOpen = !!dlg && dlg.shadowRoot?.querySelector("ha-dialog, dialog, .dialog") != null; + // Close it via internal state if possible (don't leave a dialog open) + if (dlg) { + dlg._open = false; + dlg.requestUpdate?.(); + } + } catch (e) { + out.fireDomError = String(e); + } + + // 8. v1.9.0 — group_by=calendar generates 4 windowed calendar views + try { + const StrategyClass = customElements.get( + "ll-strategy-dashboard-maintenance-supporter", + ); + const ha = document.querySelector("home-assistant"); + if (StrategyClass?.generate && ha?.hass) { + const cfg = await StrategyClass.generate( + { type: "custom:maintenance-supporter", group_by: "calendar" }, + ha.hass, + ); + out.generated_calendar = { + viewCount: cfg.views?.length ?? 0, + viewTitles: (cfg.views || []).map((v) => v.title), + firstCalendarCard: cfg.views?.[1]?.cards?.[0]?.type, + }; + } + } catch (e) { + out.generateCalendarError = String(e); + } + + // 9. v1.9.0 — Calendar card element registered + customCards entry present + try { + out.calendarCardRegistered = !!customElements.get( + "maintenance-supporter-calendar-card", + ); + out.calendarCardInRegistry = (window.customCards || []).some( + (c) => c.type === "maintenance-supporter-calendar-card", + ); + } catch (e) { + out.calendarError = String(e); + } + + // 10. v1.9.1 — Calendar card editor: registers, setConfig works, dispatches + // config-changed when user picks a different window_days option. + try { + out.calendarEditorRegistered = !!customElements.get( + "maintenance-supporter-calendar-card-editor", + ); + if (out.calendarEditorRegistered) { + const CardClass = customElements.get( + "maintenance-supporter-calendar-card", + ); + const editor = CardClass.getConfigElement(); + document.body.appendChild(editor); + editor.setConfig({ + type: "custom:maintenance-supporter-calendar-card", + window_days: 14, + }); + // Wait for first render + await new Promise((r) => setTimeout(r, 100)); + const sel = editor.shadowRoot?.querySelector("#window"); + out.calendarEditorPreselected = sel?.value; + let dispatchedConfig = null; + editor.addEventListener("config-changed", (e) => { + dispatchedConfig = e.detail.config; + }); + sel.value = "365"; + sel.dispatchEvent(new Event("change")); + out.calendarEditorDispatched = dispatchedConfig; + editor.remove(); + } + } catch (e) { + out.calendarEditorError = String(e); + } + + // 4. HA version + const ha = document.querySelector("home-assistant"); + out.haVersion = ha?.hass?.config?.version; + + return out; +}); + +console.log(JSON.stringify(result, null, 2)); + +const ours = result.customStrategies.find( + (s) => s.type === "maintenance-supporter", +); +const okRegister = !!ours; +const okElement = result.elementRegistered; +const okArea = (result.generated_area?.viewCount ?? 0) > 0; +const okStatus = (result.generated_status?.viewCount ?? 0) > 0; +const okFloor = (result.generated_floor?.viewCount ?? 0) > 0; +const okDueDate = (result.generated_due_date?.viewCount ?? 0) > 0; +const okEditor = + result.editorRegistered === true && + result.editorPreselected === "status" && + result.editorDispatched?.group_by === "due_date"; +const okHeader = result.overviewHeader === "markdown"; +const okSidebar = + (result.overviewSidebar ?? 0) > 0 && + result.overviewSidebarVisibility === "view_columns"; +const okSection = + result.sectionStrategyInRegistry === true && + result.sectionElementRegistered === true && + (result.sectionGenerated?.cardCount ?? 0) >= 2 && + result.sectionGenerated?.firstCardType === "heading" && + result.sectionGenerated?.lastCardType === "custom:maintenance-supporter-card"; +const okFireDom = result.objectDialogMounted === true; +const okCalendarMode = + (result.generated_calendar?.viewCount ?? 0) === 5 && // Overview + 4 windows + result.generated_calendar?.firstCalendarCard === "custom:maintenance-supporter-calendar-card"; +const okCalendarCard = + result.calendarCardRegistered === true && + result.calendarCardInRegistry === true; +const okCalendarEditor = + result.calendarEditorRegistered === true && + result.calendarEditorPreselected === "14" && + result.calendarEditorDispatched?.window_days === 365; + +console.log( + "\n=== SUMMARY ===\n" + + ` HA version: ${result.haVersion}\n` + + ` customStrategies has our entry: ${okRegister ? "✓" : "✗"}\n` + + ` custom element registered: ${okElement ? "✓" : "✗"}\n` + + ` generate(group_by=area): ${okArea ? "✓" : "✗"} (${result.generated_area?.viewCount} views)\n` + + ` generate(group_by=status): ${okStatus ? "✓" : "✗"} (${result.generated_status?.viewCount} views)\n` + + ` generate(group_by=floor): ${okFloor ? "✓" : "✗"} (${result.generated_floor?.viewCount} views)\n` + + ` generate(group_by=due_date): ${okDueDate ? "✓" : "✗"} (${result.generated_due_date?.viewCount} views)\n` + + ` editor registered + works: ${okEditor ? "✓" : "✗"}\n` + + ` Overview has KPI header: ${okHeader ? "✓" : "✗"} (${result.overviewHeader})\n` + + ` Overview has sidebar (large): ${okSidebar ? "✓" : "✗"} (${result.overviewSidebar} cards, visibility=${result.overviewSidebarVisibility})\n` + + ` Section strategy registered+works: ${okSection ? "✓" : "✗"} (${result.sectionGenerated?.cardCount} cards)\n` + + ` fire-dom-event opens in-place: ${okFireDom ? "✓" : "✗"} (object dialog mounted=${result.objectDialogMounted})\n` + + ` generate(group_by=calendar): ${okCalendarMode ? "✓" : "✗"} (${result.generated_calendar?.viewCount} views, first-card=${result.generated_calendar?.firstCalendarCard})\n` + + ` Calendar card registered+listed: ${okCalendarCard ? "✓" : "✗"} (element=${result.calendarCardRegistered}, customCards=${result.calendarCardInRegistry})\n` + + ` Calendar editor + works: ${okCalendarEditor ? "✓" : "✗"} (preselect=${result.calendarEditorPreselected}, dispatched=${result.calendarEditorDispatched?.window_days})`, +); + +if (consoleErrors.length) { + console.log("\n=== BROWSER CONSOLE ERRORS ==="); + consoleErrors.slice(0, 10).forEach((e) => console.log(` ${e.substring(0, 300)}`)); +} + +await browser.close(); +process.exit( + okRegister && + okElement && + okArea && + okStatus && + okFloor && + okDueDate && + okEditor && + okHeader && + okSidebar && + okSection && + okFireDom && + okCalendarMode && + okCalendarCard && + okCalendarEditor + ? 0 + : 1, +); diff --git a/custom_components/maintenance_supporter/frontend-src/web-test-runner.config.mjs b/custom_components/maintenance_supporter/frontend-src/web-test-runner.config.mjs new file mode 100644 index 0000000..3f349bb --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/web-test-runner.config.mjs @@ -0,0 +1,41 @@ +/** + * Lit-component test runner for the panel UI. + * + * Runs in real Chromium (headless) but mounts components in isolation + * with mocked hass — no HA shell, no shadow-DOM-deep-piercing nightmares. + * Each test is ~10-50ms, the whole suite runs in seconds. + * + * npm test — one-shot + * npm run test:watch — watch mode + */ + +import { esbuildPlugin } from "@web/dev-server-esbuild"; +import { playwrightLauncher } from "@web/test-runner-playwright"; +import { defaultReporter, summaryReporter } from "@web/test-runner"; + +export default { + files: "__tests__/**/*.test.ts", + nodeResolve: true, + plugins: [ + esbuildPlugin({ + ts: true, + json: true, + target: "es2020", + tsconfig: "./tsconfig.json", + }), + ], + browsers: [ + playwrightLauncher({ + product: "chromium", + // Channel-less; uses bundled chromium from playwright cache. + }), + ], + testFramework: { + config: { ui: "bdd", timeout: "5000" }, + }, + coverage: false, + reporters: [ + summaryReporter(), + defaultReporter({ reportTestResults: true, reportTestProgress: true }), + ], +}; diff --git a/custom_components/maintenance_supporter/frontend-src/ws-errors.ts b/custom_components/maintenance_supporter/frontend-src/ws-errors.ts new file mode 100644 index 0000000..faefd44 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/ws-errors.ts @@ -0,0 +1,160 @@ +/** + * Translate raw voluptuous / Home-Assistant WS error messages into a + * localized, human-readable sentence. + * + * The HA WS layer wraps voluptuous errors in + * { code: "invalid_format", message: "" } + * The voluptuous text is consistently shaped — we parse the common + * patterns and emit a localized template string. Falls back to the raw + * message when nothing matches, so the user never sees a silent failure. + */ +import { t } from "./styles"; + +/** + * Map of internal field keys (as they appear in voluptuous error paths) + * to existing i18n label keys. When a field has no entry here, we surface + * the raw key as-is so the user can still tell which input was rejected. + */ +const FIELD_LABEL_KEYS: Record = { + name: "name", + task_type: "maintenance_type", + schedule_type: "schedule_type", + interval_days: "interval_days", + interval_anchor: "interval_anchor", + warning_days: "warning_days", + last_performed: "last_performed_optional", + notes: "notes_optional", + documentation_url: "documentation_url_optional", + custom_icon: "custom_icon_optional", + nfc_tag_id: "nfc_tag_id_optional", + responsible_user_id: "responsible_user", + entity_slug: "entity_slug", + entity_id: "entity_id", + area_id: "area_id_optional", + manufacturer: "manufacturer_optional", + model: "model_optional", + serial_number: "serial_number_optional", + installation_date: "installation_date_optional", + warranty_expiry: "warranty_expiry_optional", + checklist: "checklist_steps_optional", + reason: "reason", + feedback: "feedback", + cost: "cost", + duration: "duration", + description: "description_optional", + group_name: "name", + group_description: "description_optional", + environmental_entity: "environmental_entity_optional", + environmental_attribute: "environmental_attribute_optional", + trigger_above: "trigger_above", + trigger_below: "trigger_below", + trigger_for_minutes: "trigger_for_minutes", +}; + +function _label(field: string, lang: string): string { + const key = FIELD_LABEL_KEYS[field]; + if (!key) return field; + // t() returns the key itself when missing — we'd rather show the raw + // field name in that case than the i18n key with underscores. + const translated = t(key, lang); + return translated && translated !== key ? translated : field; +} + +interface ParsedError { + field?: string; + rule: + | "too_long" + | "too_short" + | "value_too_high" + | "value_too_low" + | "required" + | "wrong_type" + | "invalid_choice" + | "invalid_value" + | "unknown"; + param?: string; +} + +function _parse(message: string): ParsedError { + // Examples voluptuous produces: + // "length of value must be at most 64 for dictionary value @ data['entry_id']" + // "length of value must be at least 1 for dictionary value @ data['name']" + // "value must be at most 365 for dictionary value @ data['warning_days']" + // "value must be at least 0 for dictionary value @ data['warning_days']" + // "required key not provided @ data['name']" + // "expected str for dictionary value @ data['notes']" + // "expected int for dictionary value @ data['interval_days']" + // "value must be one of ['completion', 'planned'] for dictionary value @ data['interval_anchor']" + // "not a valid value for dictionary value @ data['date']" + const fieldMatch = message.match(/data\['([^']+)'\]/); + const field = fieldMatch?.[1]; + + let m: RegExpMatchArray | null; + if ((m = message.match(/length of value must be at most (\d+)/))) { + return { field, rule: "too_long", param: m[1] }; + } + if ((m = message.match(/length of value must be at least (\d+)/))) { + return { field, rule: "too_short", param: m[1] }; + } + if ((m = message.match(/value must be at most (\S+)/))) { + return { field, rule: "value_too_high", param: m[1] }; + } + if ((m = message.match(/value must be at least (\S+)/))) { + return { field, rule: "value_too_low", param: m[1] }; + } + if (/required key not provided/.test(message)) { + return { field, rule: "required" }; + } + if ((m = message.match(/expected (\w+)/))) { + return { field, rule: "wrong_type", param: m[1] }; + } + if (/value must be one of/.test(message)) { + return { field, rule: "invalid_choice" }; + } + if (/not a valid value/.test(message)) { + return { field, rule: "invalid_value" }; + } + return { field, rule: "unknown" }; +} + +/** + * Convert a thrown WS-promise rejection into a localized one-line string. + * Pass the lang code (e.g. `this._lang`) and optionally a fallback for + * non-WS errors (defaults to the localized generic "action failed"). + */ +export function describeWsError(e: unknown, lang: string, fallback?: string): string { + fallback = fallback ?? t("action_error", lang); + if (typeof e === "string") return e; + if (typeof e !== "object" || e === null) return fallback; + + const err = e as { message?: string; error?: { message?: string }; code?: string }; + const raw = err.message || err.error?.message || ""; + if (!raw) return fallback; + + const parsed = _parse(raw); + const field = parsed.field ? _label(parsed.field, lang) : ""; + + const tpl = (key: string) => t(key, lang).replace("{field}", field).replace("{n}", parsed.param ?? ""); + + switch (parsed.rule) { + case "too_long": + return tpl("err_too_long"); + case "too_short": + return tpl("err_too_short"); + case "value_too_high": + return tpl("err_value_too_high"); + case "value_too_low": + return tpl("err_value_too_low"); + case "required": + return tpl("err_required"); + case "wrong_type": + return tpl("err_wrong_type").replace("{type}", parsed.param ?? ""); + case "invalid_choice": + return tpl("err_invalid_choice"); + case "invalid_value": + return tpl("err_invalid_value"); + default: + // Unknown shape — show the raw message so debugging is possible + return raw || fallback; + } +} diff --git a/custom_components/maintenance_supporter/frontend/__init__.py b/custom_components/maintenance_supporter/frontend/__init__.py new file mode 100644 index 0000000..ff378d1 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/__init__.py @@ -0,0 +1,105 @@ +"""Frontend module registration for the Maintenance Supporter integration.""" + +from __future__ import annotations + +import logging +from pathlib import Path + +from homeassistant.components.http import StaticPathConfig +from homeassistant.core import HomeAssistant + +from ..const import ( + CALENDAR_CARD_URL, + CARD_URL, + DOMAIN, + LOCALES_URL, + STRATEGY_CHUNKS_URL, + STRATEGY_SHIM_URL, + STRATEGY_URL, + VENDOR_URL, +) + +_LOGGER = logging.getLogger(__name__) + +DATA_EXTRA_MODULE_URL = "frontend_extra_module_url" + +# Survives the hass.data[DOMAIN] pop on last-entry unload (see docstring). +_CARD_REGISTERED_KEY = f"{DOMAIN}_card_registered" + + +async def async_register_card(hass: HomeAssistant) -> None: + """Register the Lovelace card + dashboard strategy JS modules. + + Card: works on every supported HA version. + Strategy: HA 2026.5+ picks it up via ``window.customStrategies``; on older + versions the registration is a silent no-op. We ship both unconditionally + so users on any version get the card and 2026.5+ users additionally get + the auto-generated dashboard in the "Add Dashboard" picker. + + The already-registered guard lives OUTSIDE hass.data[DOMAIN]: that dict is + popped when the last entry unloads, but the static paths stay mounted on + the HTTP app — re-registering them after an uninstall → reinstall in the + same HA run would raise on the duplicate routes (journey O1). + """ + if hass.data.get(_CARD_REGISTERED_KEY): + return + + frontend_dir = Path(__file__).parent + # v2.3.4 (issue #52): the strategy is a multi-file split bundle living + # in frontend/strategy/. Mount the directory under STRATEGY_DIR_URL so + # the entry's relative ./chunks/* dynamic imports resolve without per- + # chunk URL registration. The entry file URL stays STRATEGY_URL so the + # frontend_extra_module_url registration that HA serves to the browser + # doesn't need to know about the chunk layout. + static_paths = [ + StaticPathConfig(CARD_URL, str(frontend_dir / "maintenance-card.js"), False), + # The shim (tiny, zero-import) is the frontend extra-module-url; it + # lazy-imports the full strategy bundle below on first use. + StaticPathConfig( + STRATEGY_SHIM_URL, + str(frontend_dir / "maintenance-strategy-shim.js"), + False, + ), + StaticPathConfig( + STRATEGY_URL, + str(frontend_dir / "strategy" / "maintenance-dashboard-strategy.js"), + False, + ), + StaticPathConfig( + STRATEGY_CHUNKS_URL, + str(frontend_dir / "strategy" / "chunks"), + False, + ), + StaticPathConfig( + CALENDAR_CARD_URL, + str(frontend_dir / "maintenance-calendar-card.js"), + False, + ), + # Runtime-loaded UI translations: a directory of .json fetched by + # the panel/card on demand (only EN is bundled into the JS). cache=False + # so a translation edit shows up on reload without a version bump. + StaticPathConfig(LOCALES_URL, str(frontend_dir / "locales"), False), + # v2.21: pdf.js for the work sheet's inline manual excerpt (cache=True — + # versioned by release like the JS bundles). + StaticPathConfig(VENDOR_URL, str(frontend_dir / "vendor"), True), + ] + await hass.http.async_register_static_paths(static_paths) + + # Add to extra module URLs so HA auto-loads them in the frontend. + # IMPORTANT: register the SHIM, not the heavy strategy bundle — the shim + # defines the dashboard-strategy element synchronously so it wins HA's 5 s + # whenDefined race even under heavy HACS plugin load (the bundle is loaded + # lazily by the shim). The heavy bundle's static path stays mounted above + # so the shim's import() can fetch it on demand. + extra = hass.data.setdefault(DATA_EXTRA_MODULE_URL, set()) + extra.add(CARD_URL) + extra.add(STRATEGY_SHIM_URL) + extra.add(CALENDAR_CARD_URL) + + hass.data[_CARD_REGISTERED_KEY] = True + _LOGGER.debug( + "Maintenance Supporter frontend resources registered: card=%s, strategy=%s, calendar=%s", + CARD_URL, + STRATEGY_URL, + CALENDAR_CARD_URL, + ) diff --git a/custom_components/maintenance_supporter/frontend/__pycache__/__init__.cpython-314.pyc b/custom_components/maintenance_supporter/frontend/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6a1b5a7d5f2a5923f9d4610a8e635501e373536 GIT binary patch literal 3742 zcma(UOKcm*b(UQImXt(&Y)P?PjilN!ZHr1A*$C9i2qQ5fD@tNlwA!F@S8MKyT!-2v zc4zHSXaKq`Vgx9VqUpg0A5y>v_m*RTpO&Q_Kb7xnw%0tIz;n6uY8gQ!Qy0F=e6|I$kwQOv);rlQl{t>qQkW+SRII64k^MS8^$n3C@Yi zx^580XwAc#& ?-}oX$7U^or&{wq*;My|AnwqjKI7-$@BJ6ZdrE=-{>69odZ#r9EDsIUa5 zEQoBUf3<15q`nOnORfhb+pIWosVJL@+H@=80fvBcdl-N^T7rx0{ z7M<$oIhy6T;16v6aBi1T=IGToPihjHpKUBuI|qT1jtj6@?!>+ANetI`KKOk z*-K9}-b9Vl3k;*`U-IV@E_`rqWuT`CoP0dxey;sr%KgZ-D8=aU_OxyoEeOjOJ>Bx7 z9@P4Zv6i;Wt;}tm*1WBQ;L8ydLb*p*{Ti2Ov;PK85Lt16r=J#uoN-63$WRgJQ;9EO zMYhVf3<|UQp_Xd+7mLlZVkHG(3brQjUBi6K!cbe(JE~d3-W#UiObP?Wg0hno7HI(j z0TUE^<)tgvl2%` z`A~7wEG%P7tH8U8bz{`1Ch@OSY?ZYtzGaXywsm-g6^#}`xl*YKmVs>xG^Da+z2<3) zODbW)nHyxNq@KbqwXC#cP+EcdPPU0LT2ghD+Nm6Hou)Hc!zo3<)1E_zneD0xPApL7 zimA#oUG&yS71@h7yjeP{S<VJhEVqZ5+FHwWmu)IoJ23ZH$nhp{w75vS+e*sc&7D3=Dh9;?`5o9W%)#y`c0FT zJSQwktB~xHXL_==A1UszRc^4Y0?@=A?4xn)C ze(hfE{yX>H**ull4qf~rbn(k@`#-yTABI1;zTJJ{N%w_%?pYi~`XBZ^dga64ZCsn( zdU3WflKY3+2p66OVPhwDb|=>H=h(*JL*?e?+V2kTq$#Kz@? zt@uJCmOnx=*dlqH*@}-hVpoq){I?^RW@jE1A7B2sf9vAwjq_8V-DreonPLxLc(nL< z;N$RC;?3?_1Y^<$VOMRSvXJO?rcLc%4AGOO)jTooGM8 z(##YJzyMB)2zC?51q2X^)`&X#%J2deXy8N7vaK7gV zrae0Co<`BGZQ;xp!kGt|hH$PP`nvCx4RmTZh|UZ>=-U{2d87Br2I_epq+L8a^*