From 0f7585754defb6e58fae7fbffc20717ded79c09f Mon Sep 17 00:00:00 2001 From: Franco Pellicciotti Date: Wed, 8 Jul 2026 12:00:20 -0400 Subject: [PATCH] Updated Scheduler and Maintainance Apps --- .ha_run.lock | 2 +- automations.yaml | 53 +- .../maintenance_supporter/__init__.py | 2 +- .../__pycache__/__init__.cpython-314.pyc | Bin 73153 -> 73202 bytes .../__pycache__/binary_sensor.cpython-314.pyc | Bin 9157 -> 9157 bytes .../__pycache__/button.cpython-314.pyc | Bin 7122 -> 7122 bytes .../__pycache__/calendar.cpython-314.pyc | Bin 23835 -> 23835 bytes .../__pycache__/condition.cpython-314.pyc | Bin 2192 -> 2192 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 60016 -> 60016 bytes .../config_flow_helpers.cpython-314.pyc | Bin 12244 -> 16585 bytes ...config_flow_options_global.cpython-314.pyc | Bin 41821 -> 41821 bytes .../config_flow_trigger.cpython-314.pyc | Bin 48044 -> 48044 bytes .../__pycache__/const.cpython-314.pyc | Bin 16689 -> 16830 bytes .../__pycache__/coordinator.cpython-314.pyc | Bin 60917 -> 62257 bytes .../__pycache__/diagnostics.cpython-314.pyc | Bin 10306 -> 10306 bytes .../__pycache__/logbook.cpython-314.pyc | Bin 12851 -> 12851 bytes .../__pycache__/panel.cpython-314.pyc | Bin 5123 -> 5123 bytes .../__pycache__/repairs.cpython-314.pyc | Bin 34819 -> 34819 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 32074 -> 32074 bytes .../__pycache__/storage.cpython-314.pyc | Bin 20932 -> 20946 bytes .../__pycache__/templates.cpython-314.pyc | Bin 9371 -> 9371 bytes .../templates_i18n.cpython-314.pyc | Bin 45580 -> 45580 bytes .../__pycache__/todo.cpython-314.pyc | Bin 9384 -> 9384 bytes .../__pycache__/trigger.cpython-314.pyc | Bin 2954 -> 2954 bytes .../__pycache__/views.cpython-314.pyc | Bin 16783 -> 16783 bytes .../config_flow_helpers.py | 75 +- .../config_flow_options_task_crud.py | 22 + .../maintenance_supporter/const.py | 4 + .../maintenance_supporter/coordinator.py | 19 + .../__pycache__/__init__.cpython-314.pyc | Bin 365 -> 365 bytes .../__pycache__/entity_base.cpython-314.pyc | Bin 5235 -> 5235 bytes .../summary_coordinator.cpython-314.pyc | Bin 7694 -> 7694 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 8303 -> 8303 bytes .../__pycache__/base_trigger.cpython-314.pyc | Bin 14559 -> 14559 bytes .../__pycache__/compound.cpython-314.pyc | Bin 16033 -> 16033 bytes .../__pycache__/counter.cpython-314.pyc | Bin 8148 -> 8148 bytes .../__pycache__/runtime.cpython-314.pyc | Bin 16850 -> 16850 bytes .../__pycache__/state_change.cpython-314.pyc | Bin 9768 -> 9768 bytes .../__pycache__/threshold.cpython-314.pyc | Bin 9628 -> 9628 bytes .../maintenance_supporter/export.py | 15 + .../__tests__/task-detail-renderer.test.ts | 24 +- .../__tests__/task-detail-view.test.ts | 184 ++++ .../task-dialog-recurrence-extras.test.ts | 106 ++ .../components/task-detail-view.ts | 37 + .../frontend-src/components/task-dialog.ts | 117 ++- .../frontend-src/locales/cs.json | 16 +- .../frontend-src/locales/da.json | 16 +- .../frontend-src/locales/de.json | 16 +- .../frontend-src/locales/en.json | 16 +- .../frontend-src/locales/es.json | 16 +- .../frontend-src/locales/fi.json | 16 +- .../frontend-src/locales/fr.json | 16 +- .../frontend-src/locales/hi.json | 16 +- .../frontend-src/locales/it.json | 16 +- .../frontend-src/locales/ja.json | 16 +- .../frontend-src/locales/nb.json | 16 +- .../frontend-src/locales/nl.json | 16 +- .../frontend-src/locales/pl.json | 16 +- .../frontend-src/locales/pt.json | 16 +- .../frontend-src/locales/ru.json | 16 +- .../frontend-src/locales/sv.json | 16 +- .../frontend-src/locales/uk.json | 16 +- .../frontend-src/locales/zh.json | 16 +- .../frontend-src/maintenance-card.ts | 27 +- .../maintenance-dashboard-strategy.ts | 32 +- .../frontend-src/maintenance-panel.ts | 44 +- .../frontend-src/panel-styles.ts | 24 + .../frontend-src/renderers/task-detail.ts | 5 + .../frontend-src/styles.ts | 5 +- .../frontend-src/types.ts | 6 + .../__pycache__/__init__.cpython-314.pyc | Bin 3742 -> 3742 bytes .../frontend/locales/cs.json | 16 +- .../frontend/locales/da.json | 16 +- .../frontend/locales/de.json | 16 +- .../frontend/locales/en.json | 16 +- .../frontend/locales/es.json | 16 +- .../frontend/locales/fi.json | 16 +- .../frontend/locales/fr.json | 16 +- .../frontend/locales/hi.json | 16 +- .../frontend/locales/it.json | 16 +- .../frontend/locales/ja.json | 16 +- .../frontend/locales/nb.json | 16 +- .../frontend/locales/nl.json | 16 +- .../frontend/locales/pl.json | 16 +- .../frontend/locales/pt.json | 16 +- .../frontend/locales/ru.json | 16 +- .../frontend/locales/sv.json | 16 +- .../frontend/locales/uk.json | 16 +- .../frontend/locales/zh.json | 16 +- .../frontend/maintenance-calendar-card.js | 7 +- .../frontend/maintenance-card.js | 566 ++++++----- .../frontend/maintenance-panel.js | 960 ++++++++++-------- ...UYP.js => budget-section-card-PVK5ECID.js} | 2 +- .../{chunk-2LLDFX26.js => chunk-GKOWV2CF.js} | 13 +- .../{chunk-6EEEWEML.js => chunk-SHU63LHK.js} | 2 +- ...t-RTTXPFO3.js => dialog-mount-3CFCNZ5R.js} | 254 +++-- ...QBF.js => groups-section-card-VAKKX3RY.js} | 2 +- ...Y.js => vacation-section-card-2434QSQ7.js} | 2 +- .../maintenance-dashboard-strategy.js | 4 +- .../__pycache__/__init__.cpython-314.pyc | Bin 285 -> 285 bytes .../action_listener.cpython-314.pyc | Bin 6226 -> 6226 bytes .../__pycache__/aggregate.cpython-314.pyc | Bin 5113 -> 5113 bytes .../helpers/__pycache__/dates.cpython-314.pyc | Bin 9812 -> 9812 bytes .../__pycache__/documents.cpython-314.pyc | Bin 30574 -> 30574 bytes .../entity_analyzer.cpython-314.pyc | Bin 9598 -> 9598 bytes .../__pycache__/entity_rename.cpython-314.pyc | Bin 8934 -> 0 bytes .../global_options.cpython-314.pyc | Bin 3716 -> 3716 bytes .../helpers/__pycache__/i18n.cpython-314.pyc | Bin 1337 -> 1337 bytes .../notification_manager.cpython-314.pyc | Bin 62232 -> 62232 bytes .../notify_targets.cpython-314.pyc | Bin 2701 -> 2701 bytes .../helpers/__pycache__/pause.cpython-314.pyc | Bin 4217 -> 4217 bytes .../__pycache__/permissions.cpython-314.pyc | Bin 5960 -> 5960 bytes .../__pycache__/qr_generator.cpython-314.pyc | Bin 8279 -> 8279 bytes .../__pycache__/qrcodegen.cpython-314.pyc | Bin 50344 -> 50344 bytes .../__pycache__/retention.cpython-314.pyc | Bin 10544 -> 10544 bytes .../__pycache__/sanitize.cpython-314.pyc | Bin 13146 -> 13146 bytes .../__pycache__/schedule.cpython-314.pyc | Bin 21261 -> 27060 bytes .../settings_registry.cpython-314.pyc | Bin 7903 -> 7903 bytes .../__pycache__/status.cpython-314.pyc | Bin 2481 -> 2481 bytes .../__pycache__/task_fields.cpython-314.pyc | Bin 2021 -> 2021 bytes .../threshold_calculator.cpython-314.pyc | Bin 6238 -> 6238 bytes .../__pycache__/vacation.cpython-314.pyc | Bin 16497 -> 16497 bytes .../__pycache__/workday.cpython-314.pyc | Bin 8654 -> 9006 bytes .../maintenance_supporter/helpers/schedule.py | 128 ++- .../maintenance_supporter/helpers/workday.py | 10 +- .../maintenance_supporter/manifest.json | 2 +- .../__pycache__/__init__.cpython-314.pyc | Bin 529 -> 529 bytes .../maintenance_object.cpython-314.pyc | Bin 5037 -> 5037 bytes .../maintenance_task.cpython-314.pyc | Bin 26475 -> 27020 bytes .../maintenance_type.cpython-314.pyc | Bin 3085 -> 3085 bytes .../models/maintenance_task.py | 22 +- .../maintenance_supporter/storage.py | 2 +- .../maintenance_supporter/strings.json | 10 +- .../translations/cs.json | 10 +- .../translations/da.json | 10 +- .../translations/de.json | 10 +- .../translations/en.json | 10 +- .../translations/es.json | 10 +- .../translations/fi.json | 10 +- .../translations/fr.json | 10 +- .../translations/hi.json | 10 +- .../translations/it.json | 10 +- .../translations/ja.json | 10 +- .../translations/nb.json | 10 +- .../translations/nl.json | 10 +- .../translations/pl.json | 10 +- .../translations/pt.json | 10 +- .../translations/ru.json | 10 +- .../translations/sv.json | 10 +- .../translations/uk.json | 10 +- .../translations/zh-Hans.json | 10 +- .../websocket/__init__.py | 5 + .../__pycache__/__init__.cpython-314.pyc | Bin 23599 -> 23939 bytes .../__pycache__/analysis.cpython-314.pyc | Bin 12878 -> 12878 bytes .../__pycache__/dashboard.cpython-314.pyc | Bin 25933 -> 26401 bytes .../__pycache__/documents.cpython-314.pyc | Bin 13158 -> 13158 bytes .../__pycache__/groups.cpython-314.pyc | Bin 8318 -> 8318 bytes .../websocket/__pycache__/io.cpython-314.pyc | Bin 29225 -> 29379 bytes .../__pycache__/objects.cpython-314.pyc | Bin 45695 -> 45695 bytes .../__pycache__/tags.cpython-314.pyc | Bin 2930 -> 2930 bytes .../__pycache__/tasks.cpython-314.pyc | Bin 1723 -> 1755 bytes .../__pycache__/tasks_actions.cpython-314.pyc | Bin 14844 -> 16986 bytes .../__pycache__/tasks_crud.cpython-314.pyc | Bin 34236 -> 34236 bytes .../__pycache__/tasks_history.cpython-314.pyc | Bin 6477 -> 6477 bytes .../tasks_lifecycle.cpython-314.pyc | Bin 9040 -> 9040 bytes .../__pycache__/tasks_persist.cpython-314.pyc | Bin 8441 -> 8441 bytes .../tasks_validation.cpython-314.pyc | Bin 10170 -> 10170 bytes .../__pycache__/users.cpython-314.pyc | Bin 7375 -> 7375 bytes .../__pycache__/vacation.cpython-314.pyc | Bin 11145 -> 11145 bytes .../websocket/dashboard.py | 17 + .../maintenance_supporter/websocket/io.py | 12 + .../maintenance_supporter/websocket/tasks.py | 2 + .../websocket/tasks_actions.py | 37 + custom_components/taskmate/__init__.py | 70 +- .../__pycache__/__init__.cpython-314.pyc | Bin 91346 -> 93659 bytes .../__pycache__/binary_sensor.cpython-314.pyc | Bin 5642 -> 5642 bytes .../__pycache__/button.cpython-314.pyc | Bin 14932 -> 14932 bytes .../__pycache__/calendar.cpython-314.pyc | Bin 13685 -> 13685 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 2852 -> 2852 bytes .../__pycache__/const.cpython-314.pyc | Bin 23665 -> 23665 bytes .../coord_assignments.cpython-314.pyc | Bin 31708 -> 31708 bytes .../__pycache__/coord_avatars.cpython-314.pyc | Bin 8254 -> 8254 bytes .../__pycache__/coord_badges.cpython-314.pyc | Bin 17532 -> 17532 bytes .../coord_calendar.cpython-314.pyc | Bin 23069 -> 23069 bytes .../coord_challenges.cpython-314.pyc | Bin 12678 -> 12678 bytes .../__pycache__/coord_chores.cpython-314.pyc | Bin 81413 -> 81413 bytes .../coord_mandatory.cpython-314.pyc | Bin 25601 -> 25601 bytes .../coord_notifications.cpython-314.pyc | Bin 38321 -> 38321 bytes .../__pycache__/coord_points.cpython-314.pyc | Bin 58994 -> 58994 bytes .../__pycache__/coord_quests.cpython-314.pyc | Bin 11086 -> 11086 bytes .../__pycache__/coord_rewards.cpython-314.pyc | Bin 34521 -> 34521 bytes .../coord_templates.cpython-314.pyc | Bin 9310 -> 9310 bytes .../__pycache__/coord_timed.cpython-314.pyc | Bin 13506 -> 13506 bytes .../__pycache__/coordinator.cpython-314.pyc | Bin 55327 -> 55327 bytes .../__pycache__/frontend.cpython-314.pyc | Bin 9642 -> 9642 bytes .../__pycache__/http_calendar.cpython-314.pyc | Bin 5114 -> 5114 bytes .../__pycache__/http_photos.cpython-314.pyc | Bin 9264 -> 9264 bytes .../taskmate/__pycache__/ics.cpython-314.pyc | Bin 8068 -> 8068 bytes .../__pycache__/intents.cpython-314.pyc | Bin 7216 -> 7216 bytes .../__pycache__/models.cpython-314.pyc | Bin 62228 -> 62228 bytes .../__pycache__/number.cpython-314.pyc | Bin 5571 -> 5571 bytes .../__pycache__/panel.cpython-314.pyc | Bin 3883 -> 3883 bytes .../__pycache__/photos.cpython-314.pyc | Bin 10788 -> 10788 bytes .../__pycache__/select.cpython-314.pyc | Bin 5013 -> 5013 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 75211 -> 75347 bytes .../__pycache__/storage.cpython-314.pyc | Bin 111575 -> 113108 bytes .../__pycache__/templates.cpython-314.pyc | Bin 5810 -> 5810 bytes .../taskmate/__pycache__/todo.cpython-314.pyc | Bin 6024 -> 6024 bytes .../__pycache__/websocket.cpython-314.pyc | Bin 128519 -> 128791 bytes custom_components/taskmate/manifest.json | 2 +- custom_components/taskmate/sensor.py | 2 + custom_components/taskmate/storage.py | 15 + custom_components/taskmate/websocket.py | 8 +- .../taskmate/www/locales/de.json | 3 + .../taskmate/www/locales/en-GB.json | 3 + .../taskmate/www/locales/en.json | 3 + .../taskmate/www/locales/fr.json | 3 + .../taskmate/www/locales/nb.json | 3 + .../taskmate/www/locales/nn.json | 3 + .../taskmate/www/locales/pt-BR.json | 3 + .../taskmate/www/locales/pt.json | 3 + .../taskmate/www/taskmate-attr-resolver.js | 29 + .../taskmate/www/taskmate-child-card.js | 8 +- .../taskmate/www/taskmate-overview-card.js | 18 +- .../taskmate/www/taskmate-panel.js | 29 +- .../www/taskmate-parent-dashboard-card.js | 12 +- www/blink_Front.jpg | Bin 203810 -> 213715 bytes 227 files changed, 2877 insertions(+), 958 deletions(-) create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-view.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-recurrence-extras.test.ts create mode 100644 custom_components/maintenance_supporter/frontend-src/components/task-detail-view.ts rename custom_components/maintenance_supporter/frontend/strategy/chunks/{budget-section-card-V4BTQUYP.js => budget-section-card-PVK5ECID.js} (98%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{chunk-2LLDFX26.js => chunk-GKOWV2CF.js} (92%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{chunk-6EEEWEML.js => chunk-SHU63LHK.js} (97%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{dialog-mount-RTTXPFO3.js => dialog-mount-3CFCNZ5R.js} (78%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{groups-section-card-U36TKQBF.js => groups-section-card-VAKKX3RY.js} (97%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{vacation-section-card-YFJ6JUPY.js => vacation-section-card-2434QSQ7.js} (97%) delete mode 100644 custom_components/maintenance_supporter/helpers/__pycache__/entity_rename.cpython-314.pyc diff --git a/.ha_run.lock b/.ha_run.lock index ce2b266..e467328 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 71, "version": 1, "ha_version": "2026.7.1", "start_ts": 1783446559.6159952} \ No newline at end of file +{"pid": 71, "version": 1, "ha_version": "2026.7.1", "start_ts": 1783526323.4156625} \ No newline at end of file diff --git a/automations.yaml b/automations.yaml index 58f9590..fc284c9 100644 --- a/automations.yaml +++ b/automations.yaml @@ -704,11 +704,11 @@ the cycle to finish: it requires a minimum of 4 minutes to pass AND the power to drop back to the TV baseline.' triggers: - - trigger: numeric_state + - trigger: state entity_id: sensor.coffee_maker_power - above: 1000 - id: coffee_started conditions: + - condition: template + value_template: '{{ states(''sensor.coffee_maker_power'') | float(0) > 1000 }}' - condition: state entity_id: input_boolean.coffee_maker_running state: 'off' @@ -907,12 +907,12 @@ target: entity_id: camera.backyard_shed data: - filename: /media/blink.jpg + filename: /media/blink_shed.jpg - action: camera.snapshot target: entity_id: camera.backyard_shed data: - filename: /config/www/blink.jpg + filename: /config/www/blink_shed.jpg - delay: 00:00:02 - action: ai_task.generate_data continue_on_error: true @@ -922,7 +922,7 @@ 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_id: media-source://media_source/local/blink_shed.jpg media_content_type: image/jpeg response_variable: ai_profile - action: notify.notify @@ -939,7 +939,7 @@ else ''Motion detected, but the AI Task integration failed to return a response.'' }} - ![Snapshot](/local/blink.jpg)' + ![Snapshot](/local/blink_shed.jpg)' data: notification_id: backyard_shed_motion mode: queued @@ -1125,10 +1125,45 @@ event_type: state_changed event_data: entity_id: event.front_door_front_door_motion + enabled: false + - trigger: state + entity_id: event.living_room_living_room_camera_motion + enabled: false + - trigger: state + entity_id: event.front_door_front_door_motion + enabled: false + - trigger: event.received + target: + entity_id: event.entryway_entryway_camera_motion + options: + event_type: + - camera_motion + - camera_person + - camera_sound + enabled: false + - trigger: event.received + target: + device_id: 290f56e5a655e3af82cbf3cf9fd22406 + options: + event_type: + - camera_motion + - camera_person + - camera_sound + - ring + - trigger: event.received + target: + device_id: 679d52e0d16cf48648d11dd024b961f8 + options: + event_type: + - camera_motion + - camera_sound + - camera_person + enabled: false conditions: - condition: template value_template: '{{ trigger.event.data.new_state.attributes.event_type in [''motion'', ''person'', ''animal'', ''chime''] }}' + enabled: false actions: - action: blink.trigger_camera target: @@ -1160,13 +1195,13 @@ - action: notify.notify continue_on_error: true data: - title: Motion Detected - Backyard Shed + title: Motion Detected - Front Door 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 + title: Motion Detected - Front Door 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.'' }} diff --git a/custom_components/maintenance_supporter/__init__.py b/custom_components/maintenance_supporter/__init__.py index f7d8244..e9eb79b 100644 --- a/custom_components/maintenance_supporter/__init__.py +++ b/custom_components/maintenance_supporter/__init__.py @@ -374,7 +374,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool: # custom working weekdays instead of the plain Mon-Fri rule (#83 follow-up). from .helpers.workday import async_setup_business_days - async_setup_business_days(hass) + await async_setup_business_days(hass) # Create the notification manager (shared across all entries) hass.data[DOMAIN][NOTIFICATION_MANAGER_KEY] = NotificationManager(hass) diff --git a/custom_components/maintenance_supporter/__pycache__/__init__.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/__init__.cpython-314.pyc index 0c299aca7777fb5de6b1c7c5c235f8d21f9ebe21..12fbc7560c72e17d32a53d1358de3775456ea2ba 100644 GIT binary patch delta 168 zcmX@Oo8{AP7G7;WUM>b8Xe;*1+O?5a#ECItvXWEgX@&cF3T$V}Lr#Xb85SQ`I+Om;X#EH>qvXWEg=Gjgc+1VGcGq^JPL~O3~s*Y!z zG5PCC6?PWc87Ws-lsBubN?|s06kuRg{1n8&z^d}ISPf`OtD{MPJmW!2#sEo{gI1ng hK+b6v#sC?X)2yzdK+Xjj#sK-r2kiqltF1ee4*=HBCQASS diff --git a/custom_components/maintenance_supporter/__pycache__/binary_sensor.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/binary_sensor.cpython-314.pyc index 7c7867292eb53bb79905c7d72800375ed90e6cee..37ea107e3d0ea37dc24b6ebf757cb83b0a062017 100644 GIT binary patch delta 20 acmX@=e$<^?n~#@^0SMZP{Wfy%QU(Ax#syRW delta 20 acmX@=e$<^?n~#@^0SLrpd^d9MQU(Ast^|z$ diff --git a/custom_components/maintenance_supporter/__pycache__/button.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/button.cpython-314.pyc index 8b1f4772387bcd6d1391dbb7fcba6d749843a8f2..070caf77c39a7169f866ef7dbd8e89e8af11b749 100644 GIT binary patch delta 20 acmca)e#x9$n~#@^0SMZP{WfwRmj(bhh6Pgq delta 20 acmca)e#x9$n~#@^0SLrpd^d6*mj(bcZUl?~ diff --git a/custom_components/maintenance_supporter/__pycache__/calendar.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/calendar.cpython-314.pyc index ebf1aab7e968103140dd51738084692f4aa063fa..572f7459f72726b4bb7293cfea422fc4db3bb60f 100644 GIT binary patch delta 22 ccmbQei*fcYMs96BUM>b8Xe;*H$Sn{H07b$DcmMzZ delta 22 ccmbQei*fcYMs96BUM>b85SQ`Y$Sn{H06?k)u>b%7 diff --git a/custom_components/maintenance_supporter/__pycache__/condition.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/condition.cpython-314.pyc index e0ab72ebc60448da1bc832e9104bcfb96d2fbdda..c2ef1143eae85f3203ed2feba50f40c5b4c5aa02 100644 GIT binary patch delta 20 acmbOrI6;tGn~#@^0SMZP{Wfy9asU7*%LFa} delta 20 acmbOrI6;tGn~#@^0SLrpd^d8pasU7$vjb-U diff --git a/custom_components/maintenance_supporter/__pycache__/config_flow.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/config_flow.cpython-314.pyc index ab808627f6dc636a35bbf1b106c32c727b619b8d..0d42c8067761b9e4a2ccd5ee6371bf22f40b9928 100644 GIT binary patch delta 22 ccmexxh55r3W^QdhUM>b8Xe;*H$esEM09e)sJOBUy delta 22 ccmexxh55r3W^QdhUM>b85SQ`Y$esEM08_pObpQYW diff --git a/custom_components/maintenance_supporter/__pycache__/config_flow_helpers.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/config_flow_helpers.cpython-314.pyc index f2d692a1cb606b5e72b6beb679c3171771da01f3..2762eae0f0aaa86e40cf06b03a86a1a844211876 100644 GIT binary patch delta 5664 zcma)AYj9h~b>4gN1_2NtK@cE8@`4mWf)FX{P1}0W5-m`CNCGLCu^>Vaa7n@f0cdd{ znr<&BucGL=oxopwar8+J*}s8TVqi8;v|w1RA7a$B$pO)`2xOpe}>U#i%4_ztJ& z;^IU!my~=w-3I#93!tb{(v-sORCF_iVvISB>(7O$CC`3gI}V+sy=$}0F> zn0F%70r)st505MkRWo&CF}8%k1$oix=NcE^v;GOc6U`|4Y-~!5=aM1~q7pOpc~<1_ zR`2IT`Tgn^u2DAFule^v6(6st5-EuuMp;GApcO2qrvb7m0Ix7d-SXet`=GA6<_V~K zsOGF=4BkLpzN=f|;#r>Qf!go4?)eg!~L!?0uvmeA#_7Qj0pKw@2{xaDQLTTA0m zd_n#j$NzxWe_eZIAL}!<6wO}z68!~rS+CE@olfYo#na9S{yEkx)*wH=XRYc)iM5Kg z51Ur#!zf5T1GU0K(PW}DD^gaUO`6SKDRhWztFPWUfvQ}Vj>EIq`^6SnW6`9TibrWA zmZMaJA-f1VbMl$`hgxhFb;*P72G8dWK5F1Rx-MUHH$Y?Fa3AEgg()xps?pu?1S&iU zps16HtVAm?{}e(m0-B|b2yS_2lUs8M#OrdfDF7Y&YSXc!tYfuOlun9Lp=;S#;e1ro z!|9axg>!pBZWcPgu_0lM8;C)@VOWzmE2#khre+ppw5q6csf1MM9dEIBs-u}qay9}p zS){PiBd9@VKzTvl@a$%z-;-DGz;L6X%vo!(8zbcv#Fij>{GuhEh)HLAQ?uIzfOB8y z`}d$y7FX>Y`Gj~l?L&km-=PhB-aEvyv{63obZP%WjaqWF-LBt3GF*r|#mPI3)hcce zC!exAd%B2~oZ)kP7s>I@z$(b_VO)ePh4@kcALa*OQW;1i$*3}X$;i?2`;0oQDc4em zadoh?2Nx17>+;Mg3s~O!EtJ<)>V1tmytipnD zlaO|t>vLx#~YnH+|sw{=HCHq0m{-S?Fu7dvQvVKV?qu=6#E?m_A zEk1NHA9UrGZuJ5eBuiWRR#*DM<6hL1^-DS#L&3kAs*c&Kw5J-GESjB}9vxxp>JqPN?F z#{DpVF_kMBMa6{Fd7Dw-5#5~+B4M(_(030T2mCq&GIm=z%KZ5J|BoJb^-V%#`E)6=XTM)J0- zkjjfd$#LQQ`O;!Of4)mNnM%$I(_%E0NKH=UlI&wt#AOc!!bSK}R`|EM8%7AMz@J%R zGAfBeLK4y`p=2f^&1OU)8jGcA94aMeyA*T5*T{4lb_rQU4X$OCye5{;r6ffIkVw*Y zXet!|MrjLRq7r5yg$YS~wS|y|1fUob*+eQUMN=^m_zWUWf$$E(J9->Jhk&b$;x3Wm zDyExpT0^6$Ns+=x6D4mhY<(hwy-)WIjuZm-0n~pQ;V8l}ggyYpbPQTLA|}O{l%~E4 zI*V`m@eMbvSc>f{Zj2N?HZVe?GxV#d_PYpIP}jhk9JwgYDuz63H_G$`yvrhcc#(|@ zsldrqMa#?}_V9F+UIj%KJ>TXqE^g>aMg6)_STYJXFU%di-_~(^=CzqO@~dr!*W3D* z+WPM80$8m%zTDQgV7n2!>G|dah;#YjrJCdSE32;0T%DP}{8D}{@Stty-S)SSt+(~A zwDo?w=N_FqcHd;Z9=IBqe|FW>FxU6l+#@|PIp)P%BkL8Ol?u;-_{OEzpMRtx2Ge!Z zHPc#+@6FwJbBj;@RDbB-&DQJTYvHxJu2u7n4IR-~9$83!YvtSGm#t_j+uwdSwWja; z;S1}>$5)PzKQP(XO}-VA@4>{yrDSStBE9O&EKj7Dq8DyC*Im0-T)W<`e(Ur-+uxn} z+q$0|T>6Unxt|W4U3Y~+XW5yNADZj(0nWH#A%?0OJ?qsSE7cukn{Pz#j^5*bqF)ch zmIJZ%z~pjZa?LdLk&3q)H?+kX?JL#otJNJFwI-8xgP1hhPe1e!M}YhELoab%;@ANH znZ4hn{wK?Uew!r*qet+M3FoOwzT31Tk0G$Ir3|7}-V#s=vtP7TSp}u62LM^s$+bL0 zSg>s1%#__4$a+0G#&w6B&RN``c<>+{+KZAp$ucgKH`NW>g_n>Em(ec{h2sJIDdqk)f&(Ic3JpMV<@t zC5~<)!1U_R>4eAGR`WTUk29h+TwQD}32DL@uvkLw$rnYO7%c5pmE0-2egs_)W5i=A zq$xG4mXVLUCc#OxHlUox1{g>=Q83G9)ezoejw^_L^0 zExQ9JlFztP-lK%nK{aZJ)MZ_88mt@%YOownwAk;3&lgZWXw>C3TgL#kHfd5IVcdJN z6vkD-(r#W$;xH}Uq=__;!6y^sDOCeWa6Vn`TcFUNdLH8Umt{{O&o~pPHwpBL=T`yt z%4s2LY%f!y?=$=lG2K>%6U$>0 z>tjh?EH$zZ$*`EK15KAvV_sn}b z^Zjid^Gz#y&oA`W>!z!w`N%?iP4E9jlV>h)-{iPHe06xOe#fe*8@bDo-)l6|yN|4PKmB9hTKC8XuQAvjSgUXJ-E5TySFN6} z_uj9x&iB52`Tn-no3+bU&u>1_5@Y29chiFA)e~RW&(|*NUGM2C@7K4?J6841l53%x z!O@@Tn?L=~Pil_vzj@>)w)$KBOO@V7gfrOQF$L3nXp>)RZ8g$ALPIiu{Lifo72n3(A0yn7f8FZjz9Z|sZn&yx^|os6K=B8Q zC%tF6HV9rtwO3L44uDdT%}q|ielw9yWee`P;LF)(Y^&mI&Sel{o6P=3)?zRaDF-5>3a@uE)>;XY0AgH$an5k z)Pqr2VPPosnH1HDC|r0wBVJHcLs6=zdNTk+a2=*Pk%Jc}a!Dx5O)9DpF{7wX#w0~G zoW886j)^hf4*9jtgWN&+z0TO7!WI_ElkH;_^OTs(z=pdk4%^TyyJ=8rOJ#5>NJz7h zC>#c6Ulb`d$(OnuRt%?wJ0S?ngj(fqbv0{kC|1e;(B;a%4kFwX_71|uVQ(-p&_8@E z66zg1(>Fr*qt+1wU{AtsgxJjqy@c5rgx^EBih#?X;`meSF*fgtw;qsH^arNXc-MA_ zz7JX4QDpxVU_;fxaojs(@O{$!OJaGKIDSc-?~?8BlKt7%Q6!xxvV#l##C;tDzj*~cS+?J$iofML$QJczcf}z{=M-CL+ToPtw4^$VzJj0n1$PyD3ca*f@XlhO z5TwCEh=vMb8ZJa=M9AI6Xdy;pg*c6;EM@D96YS_vV5^+mBjFb;7Wz~GSCX`h-dX_ z9u@p)RqnSa$<4#Z*qhRgn8d>eUnm{c7G{pkX~ojQta@T*UOQeYF0{_cuSxb8oNl!B zhxMn99!2K%b9$|5aC=RE*{Cfj?05G-K#_Py#aPjsHLA!Y1^T3qjdXt91bV zJhz$D*no}2t}~8a)ahcMcb$^lEapi_oow8*=IB9jfZg?s$D=@b_p)9y>J^=6=JJ}c zu9E^tM_Ug(U(0Sk>I#Ei_N_0S3*kD55C%{r5`mYA+077iMlcQmWtR5mqTGp;_O`DYHk;pKw_-bIwcdxt)zrrP)?2jm{x#n z`Vxt%of=z&@yG%c;NrO@zJ21Fvt5*a|6Mc9Fm zL)Zzh@Z;a`C=SvE3+^x*gc=ob65>W3{06~31{IMnJQGm?xUHgJFx#H+$fg4zyOM@S zq>Lw+J)E?cL0V=*;XmQfH!~A#DdNnFwArX`zVbg(2KFNH7ul@{WUX~CvLd%56EO|q zz6fjxnFvgTh|#sv_!2P1MLh80_6?nw1`(MNee`c)9_56)8!IcOK{X5z3ZkK*WRjEc zZkHqVFh<{_9$Rh_Vn7iqpmdRa5&y-3QIZh*B$4cUTsxo!cGgw)Ad!UFg2^e_u{{E( zQiZl8w%N9xtW6^)efqYRfD#Z zEeeQD%59}a-FRZd-)7g-!*I4w)2F1JOJG-mF~wFT9RQG?8glf?ZOwX>wlkE4CScQ< zsC@%AZnJV`tbL~Y<9q!!)vh|!4%PXL4XzRw@v~odPmau?8!EF!0Tp~kOtwdg$o;P%BJ(l0fOH!5t;zpsn_+|g-wHO zGqY-v^XyVSAw}7p{Gk1HP`catGQTL5MOlR73S z7ol!D(g5Wr#&plZwk9SZsE;Q;kW+lyx!t5XHMnQH!!&V+k_Nl{+{qO#jc_}K#;Yiu zFLUbwnCu-%TkA96N5^OV<>6Q{FksZSy(=dmN;S!j|?=NqKf_|FWNZ z){Gi_)7%ALH%#JW$?4>f3ypmbcOG4&ogi-CKX@$>57VbJTVAA?T<(Hqo^L?Lpi#VdlClqW_ec&gj{gAiI>}@J diff --git a/custom_components/maintenance_supporter/__pycache__/config_flow_options_global.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/config_flow_options_global.cpython-314.pyc index c9e917ca16ddc1b7ec1cd5a894f684a97b44ad9a..21858e449e8e1f41942f0a5539e0d30c4fb25cdf 100644 GIT binary patch delta 22 ccmcb6jOp$%CT?v$UM>b8Xe;*H$Q`s808b85SQ`Y$Q`s808Re|*8l(j diff --git a/custom_components/maintenance_supporter/__pycache__/config_flow_trigger.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/config_flow_trigger.cpython-314.pyc index 2cbd7f54fcbcb9a3d1b9d09ebbea67ed1e295077..40c6bce6bf8816d8154bdd410cba62c9f49fc384 100644 GIT binary patch delta 22 ccmZ4UooUT?CT?v$UM>b8Xe;*H$h~kk08?uR1poj5 delta 22 ccmZ4UooUT?CT?v$UM>b85SQ`Y$h~kk08Uc|J^%m! diff --git a/custom_components/maintenance_supporter/__pycache__/const.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/const.cpython-314.pyc index ed6ffdf1ec6dce2f2c60ecbab40c6e2f196150d5..2d54776a3daa6ac92c44aa93f5e8bd5850fec616 100644 GIT binary patch delta 1910 zcmZ{j3s96*6vw~2z)q~Lh`}rixB`~QWT0fIV@;x>P`K+Ha4Kf@+c1_~!*842<4l-I zX=yWMDz}H0rCGk8u)gpKK0zIaQWQ7eii#jU$+Cx?d(mkp*PYql_rL#h?zwl*ceY|G z+cAw9pXp*WDD;``wT`cwV>Gk!isUYA_aGf7LqB{U z-ohy`4$HuWAE*iVA&keV@HS3^68s1z;K!ihba3MgxPzalW)2}*8GMS1;WJzUGjXYE z!DXrem&50{LM_0R&>2_3EL;t+YhezqgL13@j+O8Q`ru1k4`1O1_!>9DT-=nV zz&!Y7EQ4=hKJh#Fp11%O5`Tb2#2?`&;$m1rTq^ytWw4wUD_|vY6|5$%fwjbSP(iE& zA920p2FZ=EX)MFdu$j77CA&8dt6&Rmfvvbz?Sk7>1-HXC+yUEhClp{c?7$k>iMya0 zcS8;CfnB&4cH=(SgZu5_sht(ro36uJdpN0mr0S&FPwIeFwWJP8RhO>gt)a1HP>)C8 zXFLkOU_-Q$sK61E0!Quo{vl!t6a|k-w}ISa0q$|>9w)akz-_djNL1{NS#0$Bj@^Nj z{>DIubQyhRVDtY1wCo$wpLMu+N*;HDj`If`cUro?l6x}1J!3yZ7pQ);9-fo=rzrpQ z!+bg)&rACZ*=HZxIhrq`FAzl2lEkE=zTZ)D^gl%`}2n)mm(U zE7%Incug(BHdC1{1aO6 zFEtGRhF1JX)#H88^;A;YeUjVx6927XjOUzcw5g|JX zL>Z!`~N4OHwkc9M^e9oT0%h5S#rHKzkz6P$9RHO1fwUjJ}x-=OuIM16si zJU%X&HOb4`q9~87$nKr!-4LPicPXJ5CqV z@i5B(y_kB>UOv>4_LyHX-IC1e`5H@dk6;Jt=>OpnpmKLC89E)~eR>oyBcIWuD0sp0 zgruOIi4c_pjC@E!k}iVr`Q2mqvrtH9tGgAN5}#%Q`&IG0Zl`n*WGPUQVuW_p@Pow!~NW+-%*`HKOHmYQhahcJOV}qRh*Jb-<=R77~ uZAP(6vuyeflZ!8!tmVX)wD7)kZ6H3Jw!#Z;o%&tCT8|WIY4RxGt1$R(Av_{ zW-3FkT3Tjl;sfQiB7(Xq%G0rohtf(ah`fEE89j99+-o&E)3P)Bx!>RSoO|y5o!_yp zWtD4~=X5VmH-&zF!6_>q+vpj<)>Rt3*uvp72Hw#MjL`Zdt)sL~rS)bSi*d$q@G*LW z@6)sop7SgJbVLD+zY;Bzd4Vl0P^SOJ@`5;#`DX0*T-JRGCIR`_BmgD+tl zaXWlP{2F!;zk!{^Z{a)QF4#@{Uiy2Wg!Eq6M>Ow;1GJRFLE<4OBbGx2u~M>1(gKH< zGOUJb>evz4v8{LnYOn^5;!)KLYgGm7pcaoo9Ug}aJORhh3divzoWN6H#nW&S>){lh zfzx=_=<1go=hUBpao9mQ<5e4Wyc-Y9!SHP1p*}cn(_lsB@s%R|!+#f}a9E z8MU*V#FtkTY?p2ux$Smthjcs0?X+_{jTgfdV`m~;T=ry4duOiJix&)eS zvhYPJe95lxW$AX2`?Hi%C6ugH8GN=?#(1-mrYy)aD4#Eu_f_@xUvv7o-N7Y!oN&j2)d>h>H z4$bM9It}kq?4CLYe^q_(zB+-wksgNu{2hD;@ekDn|AZm@OV#27&_pPacP&=!E;q}1 z?_n2@YNjYfx^#RK3k!I(&Yy4b>u|GD3rqslyukliX5~NVUgZ=1zO0q|1{C@>h;XAo zlR&dT3*Q-#s=pwlyTIcDo&s(EqLsS`#xTr5M#!GeK zY@9B;O!JWOczv9^$$$8$ukFo)FswsGK3>In9K)dj@mmbs<@k z6r33v?yw6UG#n-YS6&bruJK^}N1BlG&_w3V?}tt{2ApBfs}(JKCsO-$CO``omY^J zrhf6ZLjr>$GIc?6!jjDNc}a=$UrA2PT=M$j#B})`WwT8WxGpZpU3N&mn~!B)T#n|c rnRJH&`YUtf$l9hnpPQdmxH9`iQ=mpsM4;f3<^1dvCri`|o1Fg!kw6iS diff --git a/custom_components/maintenance_supporter/__pycache__/coordinator.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/coordinator.cpython-314.pyc index 03e0229e746fc59e5ad1335b08a27915a68ba1a3..a797fbb66d0f42364a904a860da508a6290fec64 100644 GIT binary patch delta 2024 zcmZuydr(wW7{A}WyUV(`yZGRttZ~(4WksOykp_ZbW|<*#Ma*IbX!oWR?9#m#G;LBF z$3SP2>0^3~AZg`mYEn0i$!S!|Bn$%=T|y>hM0|}UrZLqY^_{zx)_7*_?|kp?JLi1o z+&kxl{R4t6*=DtHG&ZVvFWIi|x1BY@0lZ`!fNcEU6hCi{t+`G?}<$T{l+Bky40D4eM}9#H6RD6F)=OCXnZ&(v&oiW|C}{Zo^Y_ zQF@H{4BckDlu7h#ot_|NX%h>%4A7htId^WLE@BqWi8f9ezS|oA zUR9UX*&P*K=j(_{#FlM0pgORxsUDym=WS0P*+vDsjY5N_Ht8=3;Gdtxk3W*AmpV$=YG%M7kC(4${P{_~3H{~3mD zaKq>8pbw1)&$|Doe_$XMGKhvl)$lE@IkcQ=xOk}85Yf)$TLHzk6o?P3JnS?A=Nre&FFc22RX_pYit-33M!d?Qv1JnCY(!64<`p z#+=r%5X7R^0`l44DgfU4a?!9JW0g-UD_0EIxg#~Lj9Now-Lj4w;v$Vc7}Rm!sGIuS zTo9g*dc$(;?2hN_0goI`!n)&DY}mV$FS20vNe3?eAYS0}?YRE4c&ow3x<(&`9A2=G z#x+|lxbmZsd=Zb^zZxe9OP!by6ODboXq;R$dhIkSgN+1E;wN$u+`^3MLK(OXt3S^7 zt6C);r}$Eps)^bPS@cvXitN)wzb8z2WKr{rB_;Z@N=n3}N?A#{r?hzG7iGnrDlV2a zRV)=LR*Jg9uaU>UUB9->E0d?TqBNYsC$G{(kH4a_T-M6`KG9Pymnv$8MNePqm3@@6 zOcg1kzsj>h_NFS=Y1{KwI~U!nsq${&URGVA`WStbo6v1lD%c@CLF-k1X4Le#jGWIP zmBCsD8iS)uJ;>lDV{;j-Blyn+8cr_*HCa(>T+FCa^;Msz_@CUxTB-VS&t%Nm96R9w z8vXF=DV%=i%yj1-UfXMbC}dAMZBN=A^JzlRKC8yi?HJ#*=>0`OM@o&M%RHvrHZo*O z@3f`wdo*av37K;`%sIVg`xa}hwIe38Sw6As*s_lNf>8eQ&iv(}{PNEH^0VfOL4g~V z3%{!)Dcc*l=DB8QvA|rDp(Qet=qEh1M2>y9p(_Xf`pyphXm87+mm{w&R zWT1>drF{Gp*cR%A zqk&!B_3UzHp4&`Cww}ud5zW1uNm1YX8o>F$h@Tn(#^7h?Czui`Xiu^p;g|EH;5-T! zE=KlKDM|{fb{V^&Fc1*?Tml@&`Tb5v!Sep)Bt}cMfcV6u#%)mLdyai0v&DYzwte1rbH3pb=&~$qm>O6*(MpFH!AeahwFS2kr_1)9RueV;@cww-^PKa% z=RN1#_w5P@f87<579=GmaO@M)*X^)8>Pot!hHr5~9e@%Xj5Ab}N*0lm=84?S**n1{ zN>;|QiMjMriIl^b?PV-gDw7s4p8JxgNqLMdB!;<~?6MMRsWd+yq-D(eVN}Djj7DiW z<12`}OwvcpE{fV_so0gdfhz)6Ru*R~AJO4dpH8`~F{2?_ojt`dI9D5?rPml;+||s9 zkrNsG3s90<1wg++hmMo;L4~a+uYnprZ>zzpZ8B&uuiYH))Uc+lRx569uYv2>)!u;B z9U*Wi_EXIO12}woaf%x_&Zd^DnNmJXFo1?L4dB4$GylQ?rRnTZ0eoog%!gsD>fHW; zm+51|VeId$XObo7_JZhP?tc=TF4?rtw0_qYb#fEw2Jxr!o573e7fRq8Wzz*+f`x>! zpr5RV1QP@Sf@t`-~M6g|Ee(yn@sGEA?^uxVavHkOT# zmQizSOL8qM$@!E?gIpsQNx`Zc8x{R+J;0YrfmbVl8$a|?&&VQw`YHih zl$9g9c~Q%9IjJvp2Xp13SBD~`rJ;XRgcf{0ItrH*-@RsAL+n9RbkfS7RbkIk2et!C!6>g6<(a|fI?gwl8AJKwt@x!3mvo5qmYLP?i0|p zkbwrHAxI*KPC5D~%|xj#IhUY-U>SO!dUa43LVn~#@^0SMZP{Wfx&YXAT^mjvPf delta 20 acmX>Ua43LVn~#@^0SLrpd^d8NYXATUK>z>% diff --git a/custom_components/maintenance_supporter/__pycache__/sensor.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/sensor.cpython-314.pyc index 14e1c7deedcf1c76995bc56b85e3a53262fd831f..1e33d919dd49192bdf21bfc83635149674d65317 100644 GIT binary patch delta 22 ccmX^0i}BPiMs96BUM>b8Xe;*H$ZcN>09S_wr2qf` delta 22 ccmX^0i}BPiMs96BUM>b85SQ`Y$ZcN>08(!S-T(jq diff --git a/custom_components/maintenance_supporter/__pycache__/storage.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/storage.cpython-314.pyc index 5a49be7f5faa34afb9e52bc3a482621a58aba164..7a91072f0f4772a2860ce6f734050b53c6e9e713 100644 GIT binary patch delta 79 zcmX@InDNqLMqX_`UM>b8Xe;*1QrpOT#9x)QC=tlNC6JR?ToPZ9lbDy6ni8K_U#*> delta 65 zcmcb#nDNMBMqX_`UM>b85SQ`I65Ggo#9y4HC=tlNC6JR?ToPZ9lbDy6ni8KPyqlke+1hA delta 20 acmbR3Iop$4n~#@^0SLrpd^d7WPyqlfX9N@g diff --git a/custom_components/maintenance_supporter/__pycache__/templates_i18n.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/templates_i18n.cpython-314.pyc index 84564b1e0b04d650d1d23d88dd23d200ae67ffd2..30c5a42e3ca8797788c11b37be564121948d747b 100644 GIT binary patch delta 22 ccmeBq!qoGGiCdeGmx}=i+KT-)ax-oM07@MN_5c6? delta 22 ccmeBq!qoGGiCdeGmx}=i#ASRpax-oM07V4_F8}}l diff --git a/custom_components/maintenance_supporter/__pycache__/todo.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/todo.cpython-314.pyc index 89097e12d703d6218b1c669520efb705ab81d3b0..b37ec284937a562f6d9590d6bfa827b41db7864b 100644 GIT binary patch delta 20 acmZ4Cxx$lMn~#@^0SMZP{WfyXRRI7rhXn!v delta 20 acmZ4Cxx$lMn~#@^0SLrpd^d8>RRI7mZv;C4 diff --git a/custom_components/maintenance_supporter/__pycache__/trigger.cpython-314.pyc b/custom_components/maintenance_supporter/__pycache__/trigger.cpython-314.pyc index 11c7505905cd45428426e3ab65c7921b17416ce7..a0b3ec6ca86c3b94d14801001196c7990c21b9f7 100644 GIT binary patch delta 20 ZcmeAY?-J+M=HumJ0D`t+zm44W+yE%H1S
z diff --git a/custom_components/maintenance_supporter/config_flow_helpers.py b/custom_components/maintenance_supporter/config_flow_helpers.py index 24415c4..ebd68dc 100644 --- a/custom_components/maintenance_supporter/config_flow_helpers.py +++ b/custom_components/maintenance_supporter/config_flow_helpers.py @@ -10,7 +10,12 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import selector -from .const import CONF_TASK_INTERVAL_UNIT +from .const import ( + CONF_TASK_ENDS_COUNT, + CONF_TASK_ENDS_UNTIL, + CONF_TASK_INTERVAL_UNIT, + CONF_TASK_SEASON_MONTHS, +) from .helpers.dates import INTERVAL_UNITS from .helpers.entity_analyzer import EntityAnalyzer from .helpers.schedule import ( @@ -162,6 +167,74 @@ def apply_interval_unit(target: dict[str, Any], user_input: dict[str, Any]) -> N target[CONF_TASK_INTERVAL_UNIT] = unit +# Hardcoded English month labels for the seasonal-window multi-select — same +# "keep the config-flow i18n surface small" choice as the weekday labels above. +_MONTH_LABELS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") + + +def season_ends_schema(current_schedule: dict[str, Any] | None) -> dict[Any, Any]: + """Schema fragment for the seasonal window + finite-series end, prefilled + from the task's nested ``schedule``. Only meaningful for recurring kinds — + callers gate it on schedule_type accordingly.""" + cur = current_schedule or {} + season_default = [str(m) for m in (cur.get("season_months") or [])] + ends_raw = cur.get("ends") + ends: dict[str, Any] = ends_raw if isinstance(ends_raw, dict) else {} + count = ends.get("count") + until = ends.get("until") + + count_key = ( + vol.Optional(CONF_TASK_ENDS_COUNT, default=int(count)) + if isinstance(count, int) and count >= 1 + else vol.Optional(CONF_TASK_ENDS_COUNT) + ) + until_key = ( + vol.Optional(CONF_TASK_ENDS_UNTIL, default=str(until)) + if isinstance(until, str) and until + else vol.Optional(CONF_TASK_ENDS_UNTIL) + ) + return { + vol.Optional(CONF_TASK_SEASON_MONTHS, default=season_default): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[selector.SelectOptionDict(value=str(i + 1), label=_MONTH_LABELS[i]) for i in range(12)], + multiple=True, + mode=selector.SelectSelectorMode.LIST, + ) + ), + count_key: selector.NumberSelector( + selector.NumberSelectorConfig(min=1, max=9999, step=1, mode=selector.NumberSelectorMode.BOX) + ), + until_key: selector.DateSelector(), + } + + +def apply_season_ends(schedule: dict[str, Any], user_input: dict[str, Any]) -> None: + """Inject the seasonal window + finite-series end from a flow step into a + (recurring) ``schedule`` dict in place — count wins over until; empties clear.""" + season = sorted({int(m) for m in (user_input.get(CONF_TASK_SEASON_MONTHS) or []) if str(m).isdigit() and 1 <= int(m) <= 12}) + if season: + schedule["season_months"] = season + else: + schedule.pop("season_months", None) + + new_ends: dict[str, Any] = {} + count = user_input.get(CONF_TASK_ENDS_COUNT) + until = user_input.get(CONF_TASK_ENDS_UNTIL) + if isinstance(count, (int, float, str)) and str(count).strip() not in ("", "0"): + try: + n = int(float(count)) # NumberSelector may return a float + if n >= 1: + new_ends["count"] = n + except (ValueError, TypeError): + pass + elif until: + new_ends["until"] = str(until) + if new_ends: + schedule["ends"] = new_ends + else: + schedule.pop("ends", None) + + async def async_get_threshold_suggestions( hass: HomeAssistant, trigger_entity_id: str | None, diff --git a/custom_components/maintenance_supporter/config_flow_options_task_crud.py b/custom_components/maintenance_supporter/config_flow_options_task_crud.py index 2cb0147..93d126c 100644 --- a/custom_components/maintenance_supporter/config_flow_options_task_crud.py +++ b/custom_components/maintenance_supporter/config_flow_options_task_crud.py @@ -10,10 +10,12 @@ from homeassistant.helpers import selector from .config_flow_helpers import ( CALENDAR_KIND_VALUES, + apply_season_ends, calendar_current, calendar_schema, interval_unit_selector, schedule_from_calendar_input, + season_ends_schema, ) from .const import ( CONF_ADVANCED_SCHEDULE_TIME, @@ -169,6 +171,19 @@ class TaskCrudMixin: for key in ("interval_days", "interval_unit", "interval_anchor", "due_date"): updated_task.pop(key, None) updated_task["schedule"] = schedule + # Seasonal window + finite-series end (recurring kinds). Calendar + # kinds carry them on their nested schedule; interval tasks get a + # minimal authoritative nested schedule only when extras are set + # (the backend carries them onto the flat-derived interval). + if edit_kind in CALENDAR_KIND_VALUES and isinstance(updated_task.get("schedule"), dict): + apply_season_ends(updated_task["schedule"], user_input) + elif edit_kind == ScheduleType.TIME_BASED: + extras: dict[str, Any] = {} + apply_season_ends(extras, user_input) + if extras: + updated_task["schedule"] = {"kind": "interval", **extras} + else: + updated_task.pop("schedule", None) # schedule_time only present when global advanced flag is on; clear by submitting "". if CONF_TASK_SCHEDULE_TIME in user_input: sched = (user_input.get(CONF_TASK_SCHEDULE_TIME) or "").strip() @@ -373,6 +388,13 @@ class TaskCrudMixin: if sched["schedule_type"] in CALENDAR_KIND_VALUES else dict[Any, Any]() ), + # Seasonal window + finite-series end — recurring kinds only. + **( + season_ends_schema(task.get("schedule")) + if sched["schedule_type"] == ScheduleType.TIME_BASED + or sched["schedule_type"] in CALENDAR_KIND_VALUES + else dict[Any, Any]() + ), vol.Optional( CONF_TASK_WARNING_DAYS, default=task.get("warning_days", get_default_warning_days(self.hass)), diff --git a/custom_components/maintenance_supporter/const.py b/custom_components/maintenance_supporter/const.py index 440b739..1248437 100644 --- a/custom_components/maintenance_supporter/const.py +++ b/custom_components/maintenance_supporter/const.py @@ -270,6 +270,10 @@ CONF_TASK_LAST_PERFORMED = "last_performed" CONF_TASK_SCHEDULE_TYPE = "schedule_type" CONF_TASK_INTERVAL_UNIT = "interval_unit" CONF_TASK_DUE_DATE = "due_date" +# Recurrence extras (config/options flow): seasonal window + finite-series end. +CONF_TASK_SEASON_MONTHS = "season_months" +CONF_TASK_ENDS_COUNT = "ends_count" +CONF_TASK_ENDS_UNTIL = "ends_until" CONF_TASK_NOTES = "notes" CONF_TASK_DOCUMENTATION_URL = "documentation_url" CONF_TASK_ICON = "custom_icon" diff --git a/custom_components/maintenance_supporter/coordinator.py b/custom_components/maintenance_supporter/coordinator.py index 30c74c0..b88403d 100644 --- a/custom_components/maintenance_supporter/coordinator.py +++ b/custom_components/maintenance_supporter/coordinator.py @@ -870,6 +870,12 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): state["last_planned_due"] = lpd elif "last_planned_due" in state: del state["last_planned_due"] + # Per-occurrence postpone (set by async_postpone_task, cleared on complete). + do = td.get("due_override") + if do is not None: + state["due_override"] = do + elif "due_override" in state: + del state["due_override"] self._store.set_history(task_id, td.get("history", [])) if task.adaptive_config: self._store.set_adaptive_config(task_id, task.adaptive_config) @@ -1120,6 +1126,19 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): ), ) + async def async_postpone_task(self, task_id: str, until: date) -> None: + """Postpone just the current occurrence to ``until`` (per-occurrence + defer). Sets a one-shot ``due_override`` that the next completion clears; + the cadence is untouched.""" + merged = self._get_merged_tasks_data() + if task_id not in merged: + _LOGGER.error("Task %s not found in entry %s", task_id, self.entry.title) + return + task = MaintenanceTask.from_dict(merged[task_id]) + task.due_override = until.isoformat() + await self._persist_and_signal_task_change(task_id, task) + _LOGGER.debug("Occurrence postponed to %s: %s on %s", until, task.name, self.maintenance_object.name) + async def skip_maintenance( self, task_id: str, diff --git a/custom_components/maintenance_supporter/entity/__pycache__/__init__.cpython-314.pyc b/custom_components/maintenance_supporter/entity/__pycache__/__init__.cpython-314.pyc index d1c5a697d8acc8f2e4ae907c8d29c8fe7ee3f502..591f9bc4b42d424bdb81c067bbae3a6858db134b 100644 GIT binary patch delta 20 acmaFM^p=TRn~#@^0SMZP{WfwZF#-TIFa)3g delta 20 acmaFM^p=TRn~#@^0SLrpd^d6@F#-TD7z5b= diff --git a/custom_components/maintenance_supporter/entity/__pycache__/entity_base.cpython-314.pyc b/custom_components/maintenance_supporter/entity/__pycache__/entity_base.cpython-314.pyc index b0fff4ade8b4e75b0c252182be6f5d32752779b8..7136304b7f767e7d6e0f3ab6b98da8eaaad8b450 100644 GIT binary patch delta 20 acmeyY@mYggn~#@^0SMZP{WfxEhyVaOiv9gV1=HumJ0D`t+zm43?asVxz1QY-O delta 20 ZcmeCP>9gV1=HumJ00MCt-;LbNasViW15E$` diff --git a/custom_components/maintenance_supporter/entity/triggers/__pycache__/__init__.cpython-314.pyc b/custom_components/maintenance_supporter/entity/triggers/__pycache__/__init__.cpython-314.pyc index 58a2f6b4132d6441c6e38c499d6f538f216c23cc..50f61c771200ee5f235ec4c933d8503802d765c4 100644 GIT binary patch delta 20 acmaFw@ZN!2n~#@^0SMZP{Wfx^C;$LGR|P=; delta 20 acmaFw@ZN!2n~#@^0SLrpd^d8ZC;$LBKLmOJ diff --git a/custom_components/maintenance_supporter/entity/triggers/__pycache__/base_trigger.cpython-314.pyc b/custom_components/maintenance_supporter/entity/triggers/__pycache__/base_trigger.cpython-314.pyc index ed3377590d07cafa522fdc97c4e5d9971293e7be..3e1b864cfa952a5aa6013cad5d5e5c00ca300384 100644 GIT binary patch delta 20 acmca#c)yTan~#@^0SMZP{Wfx6u>b%<(*_Fw delta 20 acmca#c)yTan~#@^0SLrpd^d7mu>b%)y9Go5 diff --git a/custom_components/maintenance_supporter/entity/triggers/__pycache__/compound.cpython-314.pyc b/custom_components/maintenance_supporter/entity/triggers/__pycache__/compound.cpython-314.pyc index 5e80003e6699a0b5bd183d5a02ac33665c3333b9..2f47bafe06fd9a63ceb5f388bdbbc5d3d32fa7a0 100644 GIT binary patch delta 20 acmZ2jyRep9n~#@^0SMZP{Wfw>vjYG+*#&O^ delta 20 acmZ2jyRep9n~#@^0SLrpd^d7WvjYG%!33xP diff --git a/custom_components/maintenance_supporter/entity/triggers/__pycache__/counter.cpython-314.pyc b/custom_components/maintenance_supporter/entity/triggers/__pycache__/counter.cpython-314.pyc index e2ebcf7951ee328f9b7f6dd62683499f22b4764e..469ef8351137b13d62aefe4f55d851861993ddd8 100644 GIT binary patch delta 20 acmca&f5o0#n~#@^0SMZP{Wfx+lm`Gg`UPzO delta 20 acmca&f5o0#n~#@^0SLrpd^d8Rlm`Gb;smAu diff --git a/custom_components/maintenance_supporter/entity/triggers/__pycache__/runtime.cpython-314.pyc b/custom_components/maintenance_supporter/entity/triggers/__pycache__/runtime.cpython-314.pyc index 4a4fae9c6572b74272f154029904c017b6637eb4..06126fb6a2a378e51b658993d59980e8406b3885 100644 GIT binary patch delta 22 ccmccA%y_Apkz1ROmx}=i+KT-)avygD07rrb6951J delta 22 ccmccA%y_Apkz1ROmx}=i#ASRpavygD077a7OaK4? diff --git a/custom_components/maintenance_supporter/entity/triggers/__pycache__/state_change.cpython-314.pyc b/custom_components/maintenance_supporter/entity/triggers/__pycache__/state_change.cpython-314.pyc index 881f1a3ea963859ca3218dd4c74676e72c8e03f2..ae10ec3c84bc8956ddfa38fefc2e5eb5a13d8586 100644 GIT binary patch delta 20 acmZ4Cv%-g4n~#@^0SMZP{WfyTssR8q^#qCl delta 20 acmZ4Cv%-g4n~#@^0SLrpd^d8-ssR8l-2=k_ diff --git a/custom_components/maintenance_supporter/entity/triggers/__pycache__/threshold.cpython-314.pyc b/custom_components/maintenance_supporter/entity/triggers/__pycache__/threshold.cpython-314.pyc index c90aff0a07a91bb3775d461cdbddf3f47c44e1c3..4bf19de9de7cde07e78f31ad69638212de22718f 100644 GIT binary patch delta 20 acmbQ^J;$3{n~#@^0SMZP{Wfw>R0RMs@C4%k delta 20 acmbQ^J;$3{n~#@^0SLrpd^d7WR0RMn*aRE^ diff --git a/custom_components/maintenance_supporter/export.py b/custom_components/maintenance_supporter/export.py index 1f2fdeb..2c9e25b 100644 --- a/custom_components/maintenance_supporter/export.py +++ b/custom_components/maintenance_supporter/export.py @@ -90,6 +90,9 @@ def _build_export_object( # nth_weekday / day_of_month) that the flat fields above can't. "schedule": Schedule.parse(tdata).to_dict(), "last_planned_due": tdata.get("last_planned_due"), + # A pending per-occurrence postpone is user intent — round-trip it + # like last_planned_due so a backup/restore keeps the deferral. + "due_override": tdata.get("due_override"), "warning_days": tdata.get("warning_days", DEFAULT_WARNING_DAYS), "last_performed": tdata.get("last_performed"), "notes": tdata.get("notes"), @@ -100,6 +103,18 @@ def _build_export_object( "entity_slug": tdata.get("entity_slug"), "adaptive_config": tdata.get("adaptive_config"), "checklist": tdata.get("checklist") or [], + "schedule_time": tdata.get("schedule_time"), + # v2.17+ / #83 task fields — persisted and user-facing, so a JSON + # backup must restore them (same field-completeness contract as #67 + # for documentation_url/notes; import mirrors these keys). + "priority": tdata.get("priority", "normal"), + "labels": tdata.get("labels") or [], + "earliest_completion_days": tdata.get("earliest_completion_days"), + "on_complete_action": tdata.get("on_complete_action"), + "quick_complete_defaults": tdata.get("quick_complete_defaults"), + "assignee_pool": tdata.get("assignee_pool") or [], + "rotation_strategy": tdata.get("rotation_strategy"), + "reading_unit": tdata.get("reading_unit"), "status": ct.get("_status", "ok"), "days_until_due": ct.get("_days_until_due"), "next_due": ct.get("_next_due"), diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-renderer.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-renderer.test.ts index 2a36ec2..2bf951c 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-renderer.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-renderer.test.ts @@ -105,6 +105,7 @@ function ctx(overrides: Partial = {}): TaskDetailContext { openQr: () => undefined, duplicateTask: () => undefined, promptReset: () => undefined, + promptPostpone: () => undefined, snoozeTask: () => undefined, printWorksheet: () => undefined, deleteTask: () => undefined, @@ -162,21 +163,32 @@ describe("task-detail renderer", () => { expect(labels.some((l) => /archive/i.test(l))).to.be.false; }); - it("open more-menu lists edit/duplicate/reset/snooze/worksheet/delete and fires callbacks", () => { + it("open more-menu lists edit/duplicate/reset/postpone/snooze/worksheet/delete and fires callbacks", () => { const calls: string[] = []; const host = mount(task(), ctx({ moreMenuOpen: true, closeMoreMenu: () => calls.push("close"), deleteTask: () => calls.push("delete"), + promptPostpone: () => calls.push("postpone"), snoozeTask: () => calls.push("snooze"), printWorksheet: () => calls.push("worksheet"), })); const items = [...host.querySelectorAll(".popup-menu-item")]; - expect(items.length).to.equal(6); - (items[3] as HTMLElement).click(); // snooze - (items[4] as HTMLElement).click(); // work sheet (v2.21) - (items[5] as HTMLElement).click(); // delete (danger) - expect(calls).to.deep.equal(["close", "snooze", "close", "worksheet", "close", "delete"]); + expect(items.length).to.equal(7); + (items[3] as HTMLElement).click(); // postpone + (items[4] as HTMLElement).click(); // snooze + (items[5] as HTMLElement).click(); // work sheet (v2.21) + (items[6] as HTMLElement).click(); // delete (danger) + expect(calls).to.deep.equal(["close", "postpone", "close", "snooze", "close", "worksheet", "close", "delete"]); + }); + + it("shows a postponed badge when the task has a due_override, and none otherwise", () => { + const plain = mount(task(), ctx()); + expect(plain.querySelector(".postponed-badge")).to.be.null; + const postponed = mount(task({ due_override: "2026-06-20" }), ctx()); + const badge = postponed.querySelector(".postponed-badge"); + expect(badge, "postponed badge").to.exist; + expect(badge!.textContent).to.match(/2026|20/); }); it("tab bar switches via setActiveTab; history tab renders the timeline", () => { diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-view.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-view.test.ts new file mode 100644 index 0000000..500e078 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/task-detail-view.test.ts @@ -0,0 +1,184 @@ +/** Tests for — the task-detail sub-view as a + * web component (incremental step over the renderers/task-detail + * extraction). Pins the contract the component adds on top of the renderer: + * - registers and renders into LIGHT DOM (no own shadow root) so the + * panel's shadow-scoped styles keep matching + * - header / breadcrumb / actions come through the component boundary + * - callbacks in the passed TaskDetailContext still fire (Complete, tab + * switch) — props in, panel callbacks out + * - property changes re-render (new task object → new name in the DOM) + * - renders nothing until both `task` and `ctx` are set + */ + +import { expect, fixture } from "@open-wc/testing"; +import { html } from "lit"; +import "../components/task-detail-view.js"; +import type { MaintenanceTaskDetailView } from "../components/task-detail-view.js"; +import type { TaskDetailContext } from "../renderers/task-detail.js"; +import type { MaintenanceTask } from "../types"; +import { createMockHass } from "./_test-utils.js"; + +function task(overrides: Record = {}): MaintenanceTask { + return { + id: "t1", + name: "Filter Wechsel", + type: "cleaning", + enabled: true, + status: "due_soon", + schedule_type: "time_based", + interval_days: 30, + warning_days: 7, + days_until_due: 3, + next_due: "2026-07-10", + last_performed: "2026-06-10", + times_performed: 2, + total_cost: 50, + average_duration: 20, + history: [], + checklist: [], + is_done: false, + archived: false, + trigger_active: false, + ...overrides, + } as unknown as MaintenanceTask; +} + +function ctx(overrides: Partial = {}): TaskDetailContext { + const { hass } = createMockHass({ + handler: () => ({ documents: [] }), + }); + return { + lang: "en", + hass: hass as TaskDetailContext["hass"], + entryId: "entry1", + taskId: "t1", + objectName: "Pool Pump", + objectDocUrl: null, + isOperator: false, + actionLoading: false, + moreMenuOpen: false, + activeTab: "overview", + features: { + adaptive: false, predictions: false, seasonal: false, + environmental: false, budget: false, groups: false, + checklists: false, schedule_time: false, completion_actions: false, + }, + currencySymbol: "€", + collapsedSections: new Set(), + costDurationToggle: "both", + suggestionDismissed: false, + sparkline: { + lang: "en", + detailStatsData: new Map(), + hasStatsService: false, + isCounterEntity: () => false, + rangeDays: 30, + setRangeDays: () => undefined, + hideOutliers: false, + setHideOutliers: () => undefined, + }, + history: { + lang: "en", + hass: hass as TaskDetailContext["hass"], + filter: null, + search: "", + currencySymbol: "€", + setFilter: () => undefined, + setSearch: () => undefined, + openEdit: () => undefined, + }, + getUserName: () => null, + setActiveTab: () => undefined, + toggleSection: () => undefined, + setCostDurationToggle: () => undefined, + showTaskView: () => undefined, + showObject: () => undefined, + toggleMoreMenu: () => undefined, + closeMoreMenu: () => undefined, + openEdit: () => undefined, + openComplete: () => undefined, + promptSkip: () => undefined, + toggleArchive: () => undefined, + openQr: () => undefined, + duplicateTask: () => undefined, + promptReset: () => undefined, + promptPostpone: () => undefined, + snoozeTask: () => undefined, + printWorksheet: () => undefined, + deleteTask: () => undefined, + applySuggestion: () => undefined, + reanalyze: () => undefined, + dismissSuggestion: () => undefined, + openSeasonalOverrides: () => undefined, + ...overrides, + }; +} + +async function mount(t: MaintenanceTask, c: TaskDetailContext): Promise { + const el = await fixture(html` + + `); + await el.updateComplete; + return el; +} + +describe("maintenance-task-detail-view", () => { + it("is registered as a custom element", () => { + expect(customElements.get("maintenance-task-detail-view")).to.exist; + }); + + it("renders into light DOM so the panel's shadow-scoped styles keep applying", async () => { + const el = await mount(task(), ctx()); + expect(el.shadowRoot, "no own shadow root").to.equal(null); + // The detail markup is queryable directly on the element (= light DOM). + expect(el.querySelector(".detail-section"), "detail section in light DOM").to.exist; + }); + + it("renders header, breadcrumb and status through the component boundary", async () => { + const el = await mount(task(), ctx()); + expect(el.querySelector(".task-name-breadcrumb")?.textContent).to.contain("Filter Wechsel"); + expect(el.querySelector(".object-name-breadcrumb")?.textContent).to.contain("Pool Pump"); + expect(el.querySelector(".status-chip"), "status chip").to.exist; + expect(el.querySelector(".kpi-bar"), "KPI bar").to.exist; + }); + + it("routes the Complete action to the panel callback with the task", async () => { + let completed: MaintenanceTask | null = null; + const el = await mount(task(), ctx({ openComplete: (tk) => { completed = tk; } })); + const btn = [...el.querySelectorAll(".task-header-actions ha-button")] + .find((b) => b.textContent?.match(/complete/i)) as HTMLElement; + expect(btn, "complete button").to.exist; + btn.click(); + expect(completed, "openComplete received the task").to.not.equal(null); + expect((completed as unknown as MaintenanceTask).id).to.equal("t1"); + }); + + it("switches tabs via the panel-owned setActiveTab callback", async () => { + let tab = ""; + const el = await mount(task(), ctx({ setActiveTab: (v) => { tab = v; } })); + const tabs = [...el.querySelectorAll(".tab-bar .tab")] as HTMLElement[]; + expect(tabs.length).to.equal(2); + tabs[1].click(); + expect(tab).to.equal("history"); + }); + + it("re-renders when the task property changes", async () => { + const el = await mount(task(), ctx()); + el.task = task({ name: "Pumpe entkalken" }); + await el.updateComplete; + expect(el.querySelector(".task-name-breadcrumb")?.textContent).to.contain("Pumpe entkalken"); + }); + + it("renders nothing until both task and ctx are set", async () => { + const el = await fixture(html` + + `); + await el.updateComplete; + expect(el.querySelector(".detail-section")).to.equal(null); + }); + + it("hides the more-menu for operators (read-only surface preserved)", async () => { + const el = await mount(task(), ctx({ isOperator: true })); + expect(el.querySelector(".more-menu-wrapper")).to.equal(null); + }); +}); diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-recurrence-extras.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-recurrence-extras.test.ts new file mode 100644 index 0000000..7291c4e --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/task-dialog-recurrence-extras.test.ts @@ -0,0 +1,106 @@ +/** + * Lit tests for the seasonal-window + finite-series recurrence extras in + * . Pins hydration from the nested schedule and the + * outgoing `schedule` payload (backend: test_schedule.py / test_ws_io.py). + */ + +import { expect, fixture, html } from "@open-wc/testing"; +import "../components/task-dialog.js"; +import type { MaintenanceTaskDialog } from "../components/task-dialog"; +import { type SentMessage, createMockHass } from "./_test-utils.js"; + +async function mountDialog(): Promise<{ el: MaintenanceTaskDialog; sent: SentMessage[] }> { + const { hass, sent } = createMockHass({ + handlers: { + "maintenance_supporter/task/create": () => ({ task_id: "new1" }), + "maintenance_supporter/task/update": () => ({}), + }, + }); + const el = await fixture(html` + + `); + await el.updateComplete; + return { el, sent }; +} + +describe("task-dialog recurrence extras (season / finite series)", () => { + it("hydrates season_months and a count end from the nested schedule", async () => { + const { el } = await mountDialog(); + await el.openEdit("e", { + id: "t1", name: "Mow", type: "custom", schedule_type: "time_based", + interval_days: 14, interval_unit: "days", warning_days: 7, enabled: true, + schedule: { kind: "interval", every: 14, unit: "days", season_months: [4, 5, 6], ends: { count: 6 } }, + } as any); + await el.updateComplete; + expect((el as any)._seasonMonths).to.deep.equal([4, 5, 6]); + expect((el as any)._endsMode).to.equal("count"); + expect((el as any)._endsCount).to.equal("6"); + }); + + it("hydrates an until end", async () => { + const { el } = await mountDialog(); + await el.openEdit("e", { + id: "t1", name: "Cure", type: "custom", schedule_type: "time_based", + interval_days: 30, warning_days: 7, enabled: true, + schedule: { kind: "interval", every: 30, unit: "days", ends: { until: "2027-01-01" } }, + } as any); + await el.updateComplete; + expect((el as any)._endsMode).to.equal("until"); + expect((el as any)._endsUntil).to.equal("2027-01-01"); + }); + + it("create sends season_months + ends on an interval task's nested schedule", async () => { + const { el, sent } = await mountDialog(); + await el.openCreate("e"); + (el as any)._name = "Mow"; + (el as any)._scheduleType = "time_based"; + (el as any)._intervalDays = "14"; + (el as any)._intervalUnit = "days"; + (el as any)._seasonMonths = [7, 4, 5]; + (el as any)._endsMode = "count"; + (el as any)._endsCount = "6"; + await (el as any)._save(); + const msg = sent.find((m) => m.type === "maintenance_supporter/task/create") as any; + expect(msg.schedule).to.deep.equal({ kind: "interval", season_months: [4, 5, 7], ends: { count: 6 } }); + expect(msg.interval_days).to.equal(14); // flat fields still carry the cadence + }); + + it("attaches the extras to a calendar-kind schedule too", async () => { + const { el, sent } = await mountDialog(); + await el.openCreate("e"); + (el as any)._name = "Gutter"; + (el as any)._scheduleType = "day_of_month"; + (el as any)._domDay = "15"; + (el as any)._seasonMonths = [10, 11]; + await (el as any)._save(); + const msg = sent.find((m) => m.type === "maintenance_supporter/task/create") as any; + expect(msg.schedule).to.deep.equal({ kind: "day_of_month", day: 15, season_months: [10, 11] }); + }); + + it("clearing season/ends on edit sends an authoritative schedule without them", async () => { + const { el, sent } = await mountDialog(); + await el.openEdit("e", { + id: "t1", name: "Mow", type: "custom", schedule_type: "time_based", + interval_days: 14, warning_days: 7, enabled: true, + schedule: { kind: "interval", every: 14, unit: "days", season_months: [4, 5], ends: { count: 3 } }, + } as any); + (el as any)._seasonMonths = []; + (el as any)._endsMode = "never"; + await (el as any)._save(); + const msg = sent.find((m) => m.type === "maintenance_supporter/task/update") as any; + expect(msg.schedule).to.deep.equal({ kind: "interval" }); // no season_months, no ends + }); + + it("shows the season chips + ends selector for recurring, not for one_time", async () => { + const { el } = await mountDialog(); + await el.openCreate("e"); + (el as any)._scheduleType = "time_based"; + await el.updateComplete; + expect(el.shadowRoot!.querySelectorAll(".season-chip").length).to.equal(12); + expect(el.shadowRoot!.querySelector('select option[value="count"]')).to.exist; + + (el as any)._scheduleType = "one_time"; + await el.updateComplete; + expect(el.shadowRoot!.querySelectorAll(".season-chip").length).to.equal(0); + }); +}); diff --git a/custom_components/maintenance_supporter/frontend-src/components/task-detail-view.ts b/custom_components/maintenance_supporter/frontend-src/components/task-detail-view.ts new file mode 100644 index 0000000..0022c91 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/components/task-detail-view.ts @@ -0,0 +1,37 @@ +/** — the task-detail sub-view as a web + * component. The incremental step over the renderers/task-detail + * extraction: the panel now hands the component a task + the + * TaskDetailContext instead of calling the render function inline, giving + * the sub-view a real element boundary (own update lifecycle, testable in + * isolation, future home for detail-only state). + * + * Deliberately renders into LIGHT DOM (createRenderRoot returns `this`): + * the element lives inside the panel's shadow root, so every selector in + * panel-styles.ts (.task-header, .kpi-bar, .popup-menu, …) keeps matching + * and every dialog-opening callback still runs against the panel's shadow + * root. Moving the ~1000 lines of detail CSS and the dialog ownership in + * here would be the big-bang rewrite this component exists to avoid. + */ + +import { LitElement, html, nothing } from "lit"; +import { property } from "lit/decorators.js"; +import { renderTaskDetail, type TaskDetailContext } from "../renderers/task-detail"; +import type { MaintenanceTask } from "../types"; + +export class MaintenanceTaskDetailView extends LitElement { + @property({ attribute: false }) public task?: MaintenanceTask; + @property({ attribute: false }) public ctx?: TaskDetailContext; + + protected createRenderRoot(): HTMLElement { + return this; + } + + protected render(): unknown { + if (!this.task || !this.ctx) return nothing; + return html`${renderTaskDetail(this.task, this.ctx)}`; + } +} + +if (!customElements.get("maintenance-task-detail-view")) { + customElements.define("maintenance-task-detail-view", MaintenanceTaskDetailView); +} diff --git a/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts b/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts index 5ec6d72..8b26c38 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts @@ -88,6 +88,12 @@ function weekdayNames(lang?: string): string[] { return Array.from({ length: 7 }, (_, i) => weekdayName(i, lang, "short")); } +/** Short localized month names, indexed 0=Jan … 11=Dec. */ +function monthNames(lang?: string): string[] { + const fmt = new Intl.DateTimeFormat(lang || "en", { month: "short" }); + return Array.from({ length: 12 }, (_, i) => fmt.format(new Date(2021, i, 1))); +} + export class MaintenanceTaskDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean, attribute: "checklists-enabled" }) public checklistsEnabled = false; @@ -122,6 +128,12 @@ export class MaintenanceTaskDialog extends LitElement { @state() private _domLastDay = false; @state() private _domBusiness = false; @state() private _calOffset = "0"; + // Recurrence extras (apply to interval + calendar kinds): a seasonal active + // window and a finite-series end condition. + @state() private _seasonMonths: number[] = []; + @state() private _endsMode: "never" | "count" | "until" = "never"; + @state() private _endsCount = ""; + @state() private _endsUntil = ""; @state() private _notes = ""; @state() private _documentationUrl = ""; @state() private _customIcon = ""; @@ -245,6 +257,16 @@ export class MaintenanceTaskDialog extends LitElement { 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"; + // Recurrence extras — read from the nested schedule wherever it lives. + this._seasonMonths = Array.isArray(sched?.season_months) ? [...sched!.season_months] : []; + const ends = sched?.ends; + if (ends && typeof ends.count === "number") { + this._endsMode = "count"; this._endsCount = String(ends.count); this._endsUntil = ""; + } else if (ends && typeof ends.until === "string") { + this._endsMode = "until"; this._endsUntil = ends.until; this._endsCount = ""; + } else { + this._endsMode = "never"; this._endsCount = ""; this._endsUntil = ""; + } this._warningDays = task.warning_days.toString(); this._earliestCompletionDays = task.earliest_completion_days != null ? String(task.earliest_completion_days) : ""; @@ -345,6 +367,10 @@ export class MaintenanceTaskDialog extends LitElement { this._domLastDay = false; this._domBusiness = false; this._calOffset = "0"; + this._seasonMonths = []; + this._endsMode = "never"; + this._endsCount = ""; + this._endsUntil = ""; this._notes = ""; this._documentationUrl = ""; this._customIcon = ""; @@ -700,7 +726,7 @@ export class MaintenanceTaskDialog extends LitElement { } 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.schedule = { ...this._buildSchedule(), ...this._recurrenceExtras() }; data.interval_days = null; if (this._taskId) data.due_date = null; } else { @@ -710,6 +736,14 @@ export class MaintenanceTaskDialog extends LitElement { data.interval_days = parseInt(this._intervalDays, 10); data.interval_unit = this._intervalUnit; data.interval_anchor = this._intervalAnchor; + // Season / finite-series ride a minimal nested schedule alongside the + // flat interval fields; the backend carries them onto the interval it + // derives from the flat fields (they can't be expressed flatly). + // Always send an authoritative nested schedule for interval tasks so + // an edit that clears season/ends actually clears them. + if (this._scheduleType === "time_based") { + data.schedule = { kind: "interval", ...this._recurrenceExtras() }; + } } else if (this._taskId) { data.interval_days = null; data.interval_anchor = "completion"; @@ -1133,6 +1167,72 @@ export class MaintenanceTaskDialog extends LitElement { return withOffset(schedule); } + /** The season/finite-series extras to attach to a recurring schedule. Empty + * values are omitted, so a clean schedule stays clean (and, since the sent + * schedule is authoritative, an omitted extra clears a previously-set one). */ + private _recurrenceExtras(): { season_months?: number[]; ends?: { count?: number; until?: string } } { + const extras: { season_months?: number[]; ends?: { count?: number; until?: string } } = {}; + if (this._seasonMonths.length) extras.season_months = [...this._seasonMonths].sort((a, b) => a - b); + if (this._endsMode === "count") { + const n = parseInt(this._endsCount, 10); + if (n >= 1) extras.ends = { count: n }; + } else if (this._endsMode === "until" && this._endsUntil) { + extras.ends = { until: this._endsUntil }; + } + return extras; + } + + private _toggleSeasonMonth(m: number): void { + this._seasonMonths = this._seasonMonths.includes(m) + ? this._seasonMonths.filter((x) => x !== m) + : [...this._seasonMonths, m]; + } + + /** Seasonal window + finite-series end — shown for recurring (interval + + * calendar) kinds, where both apply. */ + private _renderRecurrenceExtras() { + const L = this._lang; + const recurring = this._scheduleType === "time_based" || CALENDAR_KINDS.includes(this._scheduleType); + if (!recurring) return nothing; + const months = monthNames(L); + return html` + +
${t("season_window_hint", L)}
+
+ ${months.map((name, i) => html` + `)} +
+ + +
+ +
+ ${this._endsMode === "count" ? html` + (this._endsCount = (e.target as HTMLInputElement).value)} + >` : nothing} + ${this._endsMode === "until" ? html` + (this._endsUntil = (e.target as HTMLInputElement).value)} + >` : nothing} + `; + } + /** Per-kind field groups for the calendar recurrence kinds. */ private _renderCalendarFields() { const L = this._lang; @@ -1415,6 +1515,7 @@ export class MaintenanceTaskDialog extends LitElement { > ` : nothing} + ${this._renderRecurrenceExtras()} ${tasks.length === 0 - ? html`
-
${t("card_no_tasks_title", L)}
- ${t("card_no_tasks_cta", L)} -
` + ? this._objects.some((o) => o.tasks.length > 0) + ? html`
+ +
✓ ${t("card_all_caught_up", L)}
+
` + : html`
+
${t("card_no_tasks_title", L)}
+ ${t("card_no_tasks_cta", L)} +
` : html`
${tasks.map( @@ -262,7 +268,16 @@ export class MaintenanceSupporterCard extends LitElement { title="${t("open_task", L) || "Open task"}">
-
${task.name}
+
+ ${task.name} + ${(task as any).due_override + ? html`` + : nothing} +
${!compact ? html`
${object_name} · ${t(task.type, L)}
` : nothing}
@@ -366,6 +381,7 @@ export class MaintenanceSupporterCard extends LitElement { font-size: 13px; } .empty-link:hover { text-decoration: underline; } + .all-caught-up { color: var(--success-color, #4caf50); font-weight: 500; } .task-list { padding: 0 16px 16px; } .task-item { @@ -387,6 +403,7 @@ export class MaintenanceSupporterCard extends LitElement { .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; } + .postponed-icon { --mdc-icon-size: 14px; color: var(--secondary-text-color); vertical-align: text-bottom; margin-inline-start: 4px; } .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; } diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-dashboard-strategy.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-dashboard-strategy.ts index 44273fb..75ccb2d 100644 --- a/custom_components/maintenance_supporter/frontend-src/maintenance-dashboard-strategy.ts +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-dashboard-strategy.ts @@ -200,15 +200,22 @@ const SMALL_SCREEN_CONDITION: ViewColumnsCondition = { // 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 { +// (#86) Entity ids come registry-resolved from the statistics WS: the +// documented sensor.maintenance_supporter_ ids are NOT guaranteed — +// a user rename, a _2 collision suffix, or a pre-pinning install that +// registered localized ids all made the hardcoded templates read "unknown". +type SummaryEntityIds = Partial>; + +function kpiMarkdownCard(ids: SummaryEntityIds = {}): CardConfig { + const eid = (key: keyof SummaryEntityIds) => ids[key] || `sensor.maintenance_supporter_${key}`; 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", + `🔴 **{{ states('${eid("overdue")}') }}** overdue`, + `⚡ **{{ states('${eid("triggered")}') }}** triggered`, + `🟡 **{{ states('${eid("due_soon")}') }}** due soon`, + `🟢 **{{ states('${eid("ok")}') }}** ok`, ].join(" · "), }; } @@ -263,8 +270,8 @@ function emptyStateView(): ViewConfig { }; } -function overviewView(): ViewConfig { - const kpiCard = kpiMarkdownCard(); +function overviewView(summaryIds: SummaryEntityIds = {}): ViewConfig { + const kpiCard = kpiMarkdownCard(summaryIds); return { title: "Overview", icon: "mdi:wrench-clock", @@ -614,10 +621,19 @@ export class MaintenanceDashboardStrategy extends HTMLElement { calendar: () => viewsByCalendar(objects), }; + // Registry-resolved summary-sensor ids (#86); tolerate older backends. + let summaryIds: SummaryEntityIds = {}; + try { + const stats = await hass.connection.sendMessagePromise<{ summary_entity_ids?: SummaryEntityIds }>({ + type: "maintenance_supporter/statistics", + }); + summaryIds = stats.summary_entity_ids || {}; + } catch { /* fall back to the documented default ids */ } + return { title: "Maintenance", views: [ - overviewView(), + overviewView(summaryIds), ...(viewBuilders[groupBy] ?? viewBuilders.area)(), ], }; diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts index 688d340..e37e505 100644 --- a/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts @@ -57,7 +57,11 @@ 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 { renderUserBadge, type TaskDetailContext } from "./renderers/task-detail"; +// The task-detail sub-view as a web component (light-DOM, panel styles apply). +// Side-effect import: type-only imports get tree-shaken and the element +// would never register. +import "./components/task-detail-view"; import { computeWindow, VIRTUAL_MIN_ROWS } from "./helpers/virtual-window"; type View = "overview" | "object" | "task" | "all_objects"; @@ -1392,6 +1396,38 @@ export class MaintenanceSupporterPanel extends LitElement { this._resetTask(entryId, taskId, result.value || undefined); } + private async _postponeTask(entryId: string, taskId: string, until: string): Promise { + this._actionLoading = true; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/postpone", + entry_id: entryId, + task_id: taskId, + until, + }); + this._showToast(t("postponed", this._lang)); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + private async _promptPostponeTask(entryId: string, taskId: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + if (!dlg) return; + const result = await dlg.prompt({ + title: t("postpone", this._lang), + message: t("postpone_date_prompt", this._lang), + confirmText: t("postpone", this._lang), + inputLabel: t("postpone_date_label", this._lang), + inputType: "date", + }); + if (!result.confirmed || !result.value) return; + this._postponeTask(entryId, taskId, result.value); + } + private async _snoozeTask(entryId: string, taskId: string): Promise { this._actionLoading = true; try { @@ -2855,6 +2891,7 @@ export class MaintenanceSupporterPanel extends LitElement { openQr: (taskName) => this._openQrForTask(entryId, taskId, obj?.object.name || "", taskName), duplicateTask: () => this._duplicateTask(entryId, taskId), promptReset: () => this._promptResetTask(entryId, taskId), + promptPostpone: () => this._promptPostponeTask(entryId, taskId), snoozeTask: () => this._snoozeTask(entryId, taskId), printWorksheet: () => this._printTaskWorksheet(entryId, taskId), deleteTask: () => this._deleteTask(entryId, taskId), @@ -2869,7 +2906,10 @@ export class MaintenanceSupporterPanel extends LitElement { 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()); + return html``; } /** v2.2.0: open the in-place history-edit dialog for the given entry. diff --git a/custom_components/maintenance_supporter/frontend-src/panel-styles.ts b/custom_components/maintenance_supporter/frontend-src/panel-styles.ts index 8f9eadc..b9db51c 100644 --- a/custom_components/maintenance_supporter/frontend-src/panel-styles.ts +++ b/custom_components/maintenance_supporter/frontend-src/panel-styles.ts @@ -304,6 +304,10 @@ export const panelStyles = css` --mdc-icon-size: 26px; } + /* Custom elements default to display:inline; the task-detail component + renders light-DOM and must behave like the block it wraps. */ + maintenance-task-detail-view { display: block; } + .detail-section { padding: 16px 0; } .detail-header { @@ -589,9 +593,29 @@ export const panelStyles = css` border-radius: 12px; padding: 2px; } + /* Inside .cell-badges the parent's gap does the spacing — the badges' own + margin-left (meant for inline use, e.g. the detail header) would double + it and push the priority chevron out of line with the other badges. */ + .cell-badges .nfc-badge, + .cell-badges .priority-badge { + margin-left: 0; + } .priority-badge ha-icon { --mdc-icon-size: 16px; } + .postponed-badge { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 2px 8px; + margin-left: 8px; + background: var(--secondary-background-color, #e8e8e8); + color: var(--secondary-text-color); + border-radius: 10px; + font-size: 11px; + font-weight: 500; + } + .postponed-badge ha-icon { --mdc-icon-size: 13px; } .priority-high { color: var(--error-color, #db4437); } diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/task-detail.ts b/custom_components/maintenance_supporter/frontend-src/renderers/task-detail.ts index cf42114..8bd9375 100644 --- a/custom_components/maintenance_supporter/frontend-src/renderers/task-detail.ts +++ b/custom_components/maintenance_supporter/frontend-src/renderers/task-detail.ts @@ -61,6 +61,7 @@ export interface TaskDetailContext { openQr: (taskName: string) => void; duplicateTask: () => void; promptReset: () => void; + promptPostpone: () => void; snoozeTask: () => void; /** v2.21: open the printable one-pager for this task. */ printWorksheet: () => void; @@ -104,6 +105,9 @@ function renderTaskHeader(task: MaintenanceTask, ctx: TaskDetailContext) { · ctx.showObject()}>${ctx.objectName} ${statusText} + ${task.due_override ? html` + ${formatDate(task.due_override, L)} + ` : nothing} ${renderUserBadge(task, ctx.getUserName)} ${task.nfc_tag_id ? html` NFC` @@ -131,6 +135,7 @@ function renderTaskHeader(task: MaintenanceTask, ctx: TaskDetailContext) { + diff --git a/custom_components/maintenance_supporter/frontend-src/styles.ts b/custom_components/maintenance_supporter/frontend-src/styles.ts index be83280..57c396a 100644 --- a/custom_components/maintenance_supporter/frontend-src/styles.ts +++ b/custom_components/maintenance_supporter/frontend-src/styles.ts @@ -740,11 +740,14 @@ export const sharedStyles = css` transition: width 0.3s; } - /* Trigger progress bar (overview rows) */ + /* Trigger progress bar (overview rows). width:100% — the due-cell doesn't + stretch its children (align-items: flex-end), so without it the bar + shrinks to its label and reads shorter than the days-bar in other rows. */ .trigger-progress { display: flex; flex-direction: column; gap: 2px; + width: 100%; min-width: 90px; } diff --git a/custom_components/maintenance_supporter/frontend-src/types.ts b/custom_components/maintenance_supporter/frontend-src/types.ts index c063784..17053aa 100644 --- a/custom_components/maintenance_supporter/frontend-src/types.ts +++ b/custom_components/maintenance_supporter/frontend-src/types.ts @@ -133,6 +133,10 @@ export interface TaskSchedule { business?: boolean; /** (#83) shift the computed occurrence by ±N days (clamped ±15). */ offset?: number; + /** Seasonal active window — months (1..12) the task may be due in. */ + season_months?: number[]; + /** Finite-series end condition: after N completions and/or past a date. */ + ends?: { count?: number; until?: string }; } export interface MaintenanceTask { @@ -182,6 +186,8 @@ export interface MaintenanceTask { archived_reason?: string | null; days_until_due?: number | null; next_due?: string | null; + /** Per-occurrence postpone: the ISO date the current cycle was deferred to. */ + due_override?: string | null; trigger_active: boolean; trigger_current_value?: number | null; trigger_current_delta?: number | null; diff --git a/custom_components/maintenance_supporter/frontend/__pycache__/__init__.cpython-314.pyc b/custom_components/maintenance_supporter/frontend/__pycache__/__init__.cpython-314.pyc index e6a1b5a7d5f2a5923f9d4610a8e635501e373536..860dd6ecd15cf303f836f82c2a042d7b3bd340bf 100644 GIT binary patch delta 20 acmbOyJ5QEdn~#@^0SMZP{Wfw><^upMu>^Ji delta 20 acmbOyJ5QEdn~#@^0SLrpd^d7W<^upHnFFr? diff --git a/custom_components/maintenance_supporter/frontend/locales/cs.json b/custom_components/maintenance_supporter/frontend/locales/cs.json index 5225db4..6d9639d 100644 --- a/custom_components/maintenance_supporter/frontend/locales/cs.json +++ b/custom_components/maintenance_supporter/frontend/locales/cs.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Výňatek z manuálu", "worksheet_pages": "strany", "worksheet_printed": "Vytištěno", - "worksheet_never": "Nikdy" + "worksheet_never": "Nikdy", + "card_all_caught_up": "Vše hotovo — nic nevyžaduje pozornost", + "postpone": "Odložit", + "postpone_date_prompt": "Na jaké datum odložit tento termín?", + "postpone_date_label": "Nové datum", + "postponed": "Odloženo", + "postponed_to": "Odloženo na", + "season_window_label": "Sezónní okno (měsíce)", + "season_window_hint": "Termín jen ve vybraných měsících; data mimo sezónu se posunou na další aktivní měsíc. Žádné = celý rok.", + "series_end_label": "Končí", + "series_end_never": "Nikdy (opakuje se stále)", + "series_end_after_count": "Po počtu opakování", + "series_end_until": "K datu", + "series_end_count_label": "Počet opakování", + "series_end_until_label": "Datum konce" } diff --git a/custom_components/maintenance_supporter/frontend/locales/da.json b/custom_components/maintenance_supporter/frontend/locales/da.json index 648bfc0..0842899 100644 --- a/custom_components/maintenance_supporter/frontend/locales/da.json +++ b/custom_components/maintenance_supporter/frontend/locales/da.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Uddrag af manualen", "worksheet_pages": "sider", "worksheet_printed": "Udskrevet", - "worksheet_never": "Aldrig" + "worksheet_never": "Aldrig", + "card_all_caught_up": "Alt er klaret — intet kræver opmærksomhed", + "postpone": "Udskyd", + "postpone_date_prompt": "Udskyd denne forekomst til hvilken dato?", + "postpone_date_label": "Ny forfaldsdato", + "postponed": "Udskudt", + "postponed_to": "Udskudt til", + "season_window_label": "Sæsonvindue (måneder)", + "season_window_hint": "Kun forfalden i de valgte måneder; datoer uden for sæsonen rykkes til næste aktive måned. Ingen = hele året.", + "series_end_label": "Slutter", + "series_end_never": "Aldrig (gentages uendeligt)", + "series_end_after_count": "Efter et antal gange", + "series_end_until": "På en dato", + "series_end_count_label": "Antal gange", + "series_end_until_label": "Slutdato" } diff --git a/custom_components/maintenance_supporter/frontend/locales/de.json b/custom_components/maintenance_supporter/frontend/locales/de.json index 72ec8e6..2e44b2e 100644 --- a/custom_components/maintenance_supporter/frontend/locales/de.json +++ b/custom_components/maintenance_supporter/frontend/locales/de.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Handbuch-Auszug", "worksheet_pages": "Seiten", "worksheet_printed": "Gedruckt", - "worksheet_never": "Nie" + "worksheet_never": "Nie", + "card_all_caught_up": "Alles erledigt — nichts braucht Aufmerksamkeit", + "postpone": "Verschieben", + "postpone_date_prompt": "Diesen Termin auf welches Datum verschieben?", + "postpone_date_label": "Neues Fälligkeitsdatum", + "postponed": "Verschoben", + "postponed_to": "Verschoben auf", + "season_window_label": "Saisonfenster (Monate)", + "season_window_hint": "Nur in den gewählten Monaten fällig; Termine außerhalb rollen in den nächsten aktiven Monat. Keine = ganzjährig.", + "series_end_label": "Endet", + "series_end_never": "Nie (wiederholt unbegrenzt)", + "series_end_after_count": "Nach Anzahl", + "series_end_until": "An einem Datum", + "series_end_count_label": "Anzahl", + "series_end_until_label": "Enddatum" } diff --git a/custom_components/maintenance_supporter/frontend/locales/en.json b/custom_components/maintenance_supporter/frontend/locales/en.json index 2b373cb..17ba449 100644 --- a/custom_components/maintenance_supporter/frontend/locales/en.json +++ b/custom_components/maintenance_supporter/frontend/locales/en.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Manual excerpt", "worksheet_pages": "pages", "worksheet_printed": "Printed", - "worksheet_never": "Never" + "worksheet_never": "Never", + "card_all_caught_up": "All caught up — nothing needs attention", + "postpone": "Postpone", + "postpone_date_prompt": "Postpone this occurrence to which date?", + "postpone_date_label": "New due date", + "postponed": "Postponed", + "postponed_to": "Postponed to", + "season_window_label": "Seasonal window (months)", + "season_window_hint": "Only due in the selected months; off-season dates roll to the next active month. None = all year.", + "series_end_label": "Ends", + "series_end_never": "Never (repeats indefinitely)", + "series_end_after_count": "After a number of times", + "series_end_until": "On a date", + "series_end_count_label": "Number of times", + "series_end_until_label": "End date" } diff --git a/custom_components/maintenance_supporter/frontend/locales/es.json b/custom_components/maintenance_supporter/frontend/locales/es.json index bd44e4b..b3e325c 100644 --- a/custom_components/maintenance_supporter/frontend/locales/es.json +++ b/custom_components/maintenance_supporter/frontend/locales/es.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Extracto del manual", "worksheet_pages": "páginas", "worksheet_printed": "Impreso", - "worksheet_never": "Nunca" + "worksheet_never": "Nunca", + "card_all_caught_up": "Todo al día — nada requiere atención", + "postpone": "Posponer", + "postpone_date_prompt": "¿A qué fecha posponer esta vez?", + "postpone_date_label": "Nueva fecha de vencimiento", + "postponed": "Pospuesto", + "postponed_to": "Pospuesto hasta", + "season_window_label": "Ventana estacional (meses)", + "season_window_hint": "Solo vence en los meses seleccionados; las fechas fuera de temporada pasan al siguiente mes activo. Ninguno = todo el año.", + "series_end_label": "Termina", + "series_end_never": "Nunca (se repite indefinidamente)", + "series_end_after_count": "Tras un número de veces", + "series_end_until": "En una fecha", + "series_end_count_label": "Número de veces", + "series_end_until_label": "Fecha de fin" } diff --git a/custom_components/maintenance_supporter/frontend/locales/fi.json b/custom_components/maintenance_supporter/frontend/locales/fi.json index e641728..7156029 100644 --- a/custom_components/maintenance_supporter/frontend/locales/fi.json +++ b/custom_components/maintenance_supporter/frontend/locales/fi.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Ote käyttöohjeesta", "worksheet_pages": "sivut", "worksheet_printed": "Tulostettu", - "worksheet_never": "Ei koskaan" + "worksheet_never": "Ei koskaan", + "card_all_caught_up": "Kaikki kunnossa — mikään ei vaadi huomiota", + "postpone": "Lykkää", + "postpone_date_prompt": "Mihin päivään tämä kerta lykätään?", + "postpone_date_label": "Uusi eräpäivä", + "postponed": "Lykätty", + "postponed_to": "Lykätty päivään", + "season_window_label": "Kausi-ikkuna (kuukaudet)", + "season_window_hint": "Erääntyy vain valittuina kuukausina; kauden ulkopuoliset päivät siirtyvät seuraavaan aktiiviseen kuukauteen. Ei mitään = koko vuosi.", + "series_end_label": "Päättyy", + "series_end_never": "Ei koskaan (toistuu loputtomasti)", + "series_end_after_count": "Tietyn kertamäärän jälkeen", + "series_end_until": "Päivämääränä", + "series_end_count_label": "Kertojen määrä", + "series_end_until_label": "Päättymispäivä" } diff --git a/custom_components/maintenance_supporter/frontend/locales/fr.json b/custom_components/maintenance_supporter/frontend/locales/fr.json index 46cfd17..a173d95 100644 --- a/custom_components/maintenance_supporter/frontend/locales/fr.json +++ b/custom_components/maintenance_supporter/frontend/locales/fr.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Extrait du manuel", "worksheet_pages": "pages", "worksheet_printed": "Imprimé", - "worksheet_never": "Jamais" + "worksheet_never": "Jamais", + "card_all_caught_up": "Tout est à jour — rien ne requiert d'attention", + "postpone": "Reporter", + "postpone_date_prompt": "Reporter cette échéance à quelle date ?", + "postpone_date_label": "Nouvelle date d’échéance", + "postponed": "Reporté", + "postponed_to": "Reporté au", + "season_window_label": "Fenêtre saisonnière (mois)", + "season_window_hint": "Dû uniquement les mois sélectionnés ; les dates hors saison passent au mois actif suivant. Aucun = toute l’année.", + "series_end_label": "Se termine", + "series_end_never": "Jamais (répète indéfiniment)", + "series_end_after_count": "Après un nombre de fois", + "series_end_until": "À une date", + "series_end_count_label": "Nombre de fois", + "series_end_until_label": "Date de fin" } diff --git a/custom_components/maintenance_supporter/frontend/locales/hi.json b/custom_components/maintenance_supporter/frontend/locales/hi.json index 0b0ca9d..9d06b5e 100644 --- a/custom_components/maintenance_supporter/frontend/locales/hi.json +++ b/custom_components/maintenance_supporter/frontend/locales/hi.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "मैनुअल अंश", "worksheet_pages": "पृष्ठ", "worksheet_printed": "मुद्रित", - "worksheet_never": "कभी नहीं" + "worksheet_never": "कभी नहीं", + "card_all_caught_up": "सब पूरा — किसी चीज़ पर ध्यान देने की ज़रूरत नहीं", + "postpone": "टालें", + "postpone_date_prompt": "इस बार को किस तारीख तक टालें?", + "postpone_date_label": "नई नियत तिथि", + "postponed": "टाल दिया गया", + "postponed_to": "तक टाला गया", + "season_window_label": "मौसमी अवधि (महीने)", + "season_window_hint": "केवल चुने गए महीनों में देय; ऑफ-सीज़न तिथियाँ अगले सक्रिय महीने में चली जाती हैं। कोई नहीं = पूरे वर्ष।", + "series_end_label": "समाप्त होता है", + "series_end_never": "कभी नहीं (अनिश्चित रूप से दोहराता है)", + "series_end_after_count": "कुछ बार के बाद", + "series_end_until": "एक तिथि पर", + "series_end_count_label": "कितनी बार", + "series_end_until_label": "समाप्ति तिथि" } diff --git a/custom_components/maintenance_supporter/frontend/locales/it.json b/custom_components/maintenance_supporter/frontend/locales/it.json index 8397385..0527856 100644 --- a/custom_components/maintenance_supporter/frontend/locales/it.json +++ b/custom_components/maintenance_supporter/frontend/locales/it.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Estratto del manuale", "worksheet_pages": "pagine", "worksheet_printed": "Stampato", - "worksheet_never": "Mai" + "worksheet_never": "Mai", + "card_all_caught_up": "Tutto in ordine — niente richiede attenzione", + "postpone": "Posticipa", + "postpone_date_prompt": "A quale data posticipare questa scadenza?", + "postpone_date_label": "Nuova data di scadenza", + "postponed": "Posticipato", + "postponed_to": "Posticipato al", + "season_window_label": "Finestra stagionale (mesi)", + "season_window_hint": "Scade solo nei mesi selezionati; le date fuori stagione passano al mese attivo successivo. Nessuno = tutto l’anno.", + "series_end_label": "Termina", + "series_end_never": "Mai (ripete all’infinito)", + "series_end_after_count": "Dopo un numero di volte", + "series_end_until": "In una data", + "series_end_count_label": "Numero di volte", + "series_end_until_label": "Data di fine" } diff --git a/custom_components/maintenance_supporter/frontend/locales/ja.json b/custom_components/maintenance_supporter/frontend/locales/ja.json index 6c87564..fbef739 100644 --- a/custom_components/maintenance_supporter/frontend/locales/ja.json +++ b/custom_components/maintenance_supporter/frontend/locales/ja.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "マニュアル抜粋", "worksheet_pages": "ページ", "worksheet_printed": "印刷日", - "worksheet_never": "未実施" + "worksheet_never": "未実施", + "card_all_caught_up": "すべて完了 — 対応が必要なものはありません", + "postpone": "延期", + "postpone_date_prompt": "この回をどの日付に延期しますか?", + "postpone_date_label": "新しい期日", + "postponed": "延期済み", + "postponed_to": "延期先", + "season_window_label": "季節ウィンドウ(月)", + "season_window_hint": "選択した月のみ期限;シーズン外の日付は次の有効な月へ繰り越し。なし=通年。", + "series_end_label": "終了", + "series_end_never": "なし(無期限に繰り返す)", + "series_end_after_count": "回数の後", + "series_end_until": "日付で", + "series_end_count_label": "回数", + "series_end_until_label": "終了日" } diff --git a/custom_components/maintenance_supporter/frontend/locales/nb.json b/custom_components/maintenance_supporter/frontend/locales/nb.json index dacefde..7ce5e1f 100644 --- a/custom_components/maintenance_supporter/frontend/locales/nb.json +++ b/custom_components/maintenance_supporter/frontend/locales/nb.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Utdrag fra manualen", "worksheet_pages": "sider", "worksheet_printed": "Skrevet ut", - "worksheet_never": "Aldri" + "worksheet_never": "Aldri", + "card_all_caught_up": "Alt er i orden — ingenting trenger oppmerksomhet", + "postpone": "Utsett", + "postpone_date_prompt": "Utsett denne forekomsten til hvilken dato?", + "postpone_date_label": "Ny forfallsdato", + "postponed": "Utsatt", + "postponed_to": "Utsatt til", + "season_window_label": "Sesongvindu (måneder)", + "season_window_hint": "Forfaller kun i valgte måneder; datoer utenfor sesongen flyttes til neste aktive måned. Ingen = hele året.", + "series_end_label": "Slutter", + "series_end_never": "Aldri (gjentas uendelig)", + "series_end_after_count": "Etter et antall ganger", + "series_end_until": "På en dato", + "series_end_count_label": "Antall ganger", + "series_end_until_label": "Sluttdato" } diff --git a/custom_components/maintenance_supporter/frontend/locales/nl.json b/custom_components/maintenance_supporter/frontend/locales/nl.json index ed43fbd..db30c9e 100644 --- a/custom_components/maintenance_supporter/frontend/locales/nl.json +++ b/custom_components/maintenance_supporter/frontend/locales/nl.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Handleidingfragment", "worksheet_pages": "pagina's", "worksheet_printed": "Afgedrukt", - "worksheet_never": "Nooit" + "worksheet_never": "Nooit", + "card_all_caught_up": "Alles bijgewerkt — niets vraagt aandacht", + "postpone": "Uitstellen", + "postpone_date_prompt": "Deze keer uitstellen tot welke datum?", + "postpone_date_label": "Nieuwe vervaldatum", + "postponed": "Uitgesteld", + "postponed_to": "Uitgesteld tot", + "season_window_label": "Seizoensvenster (maanden)", + "season_window_hint": "Alleen verschuldigd in de gekozen maanden; data buiten het seizoen schuiven naar de volgende actieve maand. Geen = hele jaar.", + "series_end_label": "Eindigt", + "series_end_never": "Nooit (herhaalt onbeperkt)", + "series_end_after_count": "Na een aantal keer", + "series_end_until": "Op een datum", + "series_end_count_label": "Aantal keer", + "series_end_until_label": "Einddatum" } diff --git a/custom_components/maintenance_supporter/frontend/locales/pl.json b/custom_components/maintenance_supporter/frontend/locales/pl.json index e95f4e5..08b21ac 100644 --- a/custom_components/maintenance_supporter/frontend/locales/pl.json +++ b/custom_components/maintenance_supporter/frontend/locales/pl.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Fragment instrukcji", "worksheet_pages": "strony", "worksheet_printed": "Wydrukowano", - "worksheet_never": "Nigdy" + "worksheet_never": "Nigdy", + "card_all_caught_up": "Wszystko zrobione — nic nie wymaga uwagi", + "postpone": "Odłóż", + "postpone_date_prompt": "Przełożyć ten termin na jaką datę?", + "postpone_date_label": "Nowa data", + "postponed": "Odłożono", + "postponed_to": "Odłożono do", + "season_window_label": "Okno sezonowe (miesiące)", + "season_window_hint": "Termin tylko w wybranych miesiącach; daty poza sezonem przechodzą na następny aktywny miesiąc. Brak = cały rok.", + "series_end_label": "Kończy się", + "series_end_never": "Nigdy (powtarza bez końca)", + "series_end_after_count": "Po liczbie razy", + "series_end_until": "W dniu", + "series_end_count_label": "Liczba razy", + "series_end_until_label": "Data końcowa" } diff --git a/custom_components/maintenance_supporter/frontend/locales/pt.json b/custom_components/maintenance_supporter/frontend/locales/pt.json index 233fdcc..2576e5f 100644 --- a/custom_components/maintenance_supporter/frontend/locales/pt.json +++ b/custom_components/maintenance_supporter/frontend/locales/pt.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Excerto do manual", "worksheet_pages": "páginas", "worksheet_printed": "Impresso", - "worksheet_never": "Nunca" + "worksheet_never": "Nunca", + "card_all_caught_up": "Tudo em dia — nada requer atenção", + "postpone": "Adiar", + "postpone_date_prompt": "Adiar esta ocorrência para qual data?", + "postpone_date_label": "Nova data de vencimento", + "postponed": "Adiado", + "postponed_to": "Adiado para", + "season_window_label": "Janela sazonal (meses)", + "season_window_hint": "Só vence nos meses selecionados; datas fora de época passam para o próximo mês ativo. Nenhum = todo o ano.", + "series_end_label": "Termina", + "series_end_never": "Nunca (repete indefinidamente)", + "series_end_after_count": "Após um número de vezes", + "series_end_until": "Numa data", + "series_end_count_label": "Número de vezes", + "series_end_until_label": "Data final" } diff --git a/custom_components/maintenance_supporter/frontend/locales/ru.json b/custom_components/maintenance_supporter/frontend/locales/ru.json index cabacb7..f59ac0e 100644 --- a/custom_components/maintenance_supporter/frontend/locales/ru.json +++ b/custom_components/maintenance_supporter/frontend/locales/ru.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Выдержка из руководства", "worksheet_pages": "страницы", "worksheet_printed": "Напечатано", - "worksheet_never": "Никогда" + "worksheet_never": "Никогда", + "card_all_caught_up": "Всё выполнено — ничего не требует внимания", + "postpone": "Отложить", + "postpone_date_prompt": "На какую дату отложить этот раз?", + "postpone_date_label": "Новый срок", + "postponed": "Отложено", + "postponed_to": "Отложено до", + "season_window_label": "Сезонное окно (месяцы)", + "season_window_hint": "Срок только в выбранные месяцы; даты вне сезона переносятся на следующий активный месяц. Нет = весь год.", + "series_end_label": "Заканчивается", + "series_end_never": "Никогда (повторяется бесконечно)", + "series_end_after_count": "После числа раз", + "series_end_until": "В дату", + "series_end_count_label": "Число раз", + "series_end_until_label": "Дата окончания" } diff --git a/custom_components/maintenance_supporter/frontend/locales/sv.json b/custom_components/maintenance_supporter/frontend/locales/sv.json index 6e88345..74b1aad 100644 --- a/custom_components/maintenance_supporter/frontend/locales/sv.json +++ b/custom_components/maintenance_supporter/frontend/locales/sv.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Utdrag ur manualen", "worksheet_pages": "sidor", "worksheet_printed": "Utskriven", - "worksheet_never": "Aldrig" + "worksheet_never": "Aldrig", + "card_all_caught_up": "Allt klart — inget behöver åtgärdas", + "postpone": "Skjut upp", + "postpone_date_prompt": "Skjut upp den här gången till vilket datum?", + "postpone_date_label": "Nytt förfallodatum", + "postponed": "Uppskjuten", + "postponed_to": "Uppskjuten till", + "season_window_label": "Säsongsfönster (månader)", + "season_window_hint": "Förfaller endast valda månader; datum utanför säsongen flyttas till nästa aktiva månad. Inga = hela året.", + "series_end_label": "Slutar", + "series_end_never": "Aldrig (upprepas i oändlighet)", + "series_end_after_count": "Efter ett antal gånger", + "series_end_until": "På ett datum", + "series_end_count_label": "Antal gånger", + "series_end_until_label": "Slutdatum" } diff --git a/custom_components/maintenance_supporter/frontend/locales/uk.json b/custom_components/maintenance_supporter/frontend/locales/uk.json index ff18efc..aebf171 100644 --- a/custom_components/maintenance_supporter/frontend/locales/uk.json +++ b/custom_components/maintenance_supporter/frontend/locales/uk.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "Витяг з посібника", "worksheet_pages": "сторінки", "worksheet_printed": "Надруковано", - "worksheet_never": "Ніколи" + "worksheet_never": "Ніколи", + "card_all_caught_up": "Усе виконано — ніщо не потребує уваги", + "postpone": "Відкласти", + "postpone_date_prompt": "На яку дату відкласти цей раз?", + "postpone_date_label": "Новий термін", + "postponed": "Відкладено", + "postponed_to": "Відкладено до", + "season_window_label": "Сезонне вікно (місяці)", + "season_window_hint": "Термін лише у вибрані місяці; дати поза сезоном переносяться на наступний активний місяць. Немає = весь рік.", + "series_end_label": "Завершується", + "series_end_never": "Ніколи (повторюється безкінечно)", + "series_end_after_count": "Після кількості разів", + "series_end_until": "У дату", + "series_end_count_label": "Кількість разів", + "series_end_until_label": "Дата завершення" } diff --git a/custom_components/maintenance_supporter/frontend/locales/zh.json b/custom_components/maintenance_supporter/frontend/locales/zh.json index c1cee8a..35288f8 100644 --- a/custom_components/maintenance_supporter/frontend/locales/zh.json +++ b/custom_components/maintenance_supporter/frontend/locales/zh.json @@ -692,5 +692,19 @@ "worksheet_manual_excerpt": "手册摘录", "worksheet_pages": "页", "worksheet_printed": "打印于", - "worksheet_never": "从未" + "worksheet_never": "从未", + "card_all_caught_up": "全部完成——没有需要处理的事项", + "postpone": "推迟", + "postpone_date_prompt": "将此次推迟到哪一天?", + "postpone_date_label": "新的到期日", + "postponed": "已推迟", + "postponed_to": "推迟至", + "season_window_label": "季节窗口(月份)", + "season_window_hint": "仅在所选月份到期;非当季日期顺延至下一个活动月份。无 = 全年。", + "series_end_label": "结束", + "series_end_never": "从不(无限重复)", + "series_end_after_count": "达到次数后", + "series_end_until": "在某日期", + "series_end_count_label": "次数", + "series_end_until_label": "结束日期" } diff --git a/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js b/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js index af2d2bb..07d335d 100644 --- a/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js +++ b/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js @@ -195,7 +195,7 @@ var Xe=Object.defineProperty;var Ze=Object.getOwnPropertyDescriptor;var v=(n,e,t } `;var Fe={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\u2026",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\u201331)",recurrence_last_day:"Last day of the month",recurrence_business_day:"Business days only (roll back from weekend)",recurrence_offset:"Offset (days, \xB1)",recurrence_offset_help:"Shift the date by \xB1N 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 Replace seal -Test 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\u2026",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 \u2014 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\u20135.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\xB2",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\u2026",qr_error:"Failed to generate QR code.",qr_error_no_url:"No HA URL configured. Please set an external or internal URL in Settings \u2192 System \u2192 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 \u201CAdaptive Intervals\u201D or \u201CSeasonal Patterns\u201D 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 \u2014 all conditions must trigger",compound_logic_or:"OR \u2014 any condition triggers",compound_help:"Combine several sensor conditions into one trigger.",compound_no_conditions:"No conditions yet \u2014 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\u2026",card_load_error:"Could not load objects \u2014 check the WebSocket connection.",card_no_tasks_title:"No maintenance tasks yet",card_no_tasks_cta:"\u2192 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:"\u2014 No tag \u2014",dashboard:"Dashboard",tab_today:"Today",palette_placeholder:"Search objects and tasks\u2026",palette_no_results:"No matches",palette_hint:"\u2191\u2193 to navigate \xB7 Enter to open \xB7 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\u2026",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 \u2014 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\u2026",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\u2026",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\u2026",vacation_open_panel:"Open in panel",enable:"Enable",saved:"Saved",budget_monthly_set:"Set monthly",budget_yearly_set:"Set yearly",budget_advanced:"Currency, alerts\u2026",budget_open_panel:"Open in panel",groups_empty:"No groups yet.",group_new_placeholder:"Add group\u2026",group_delete_confirm:'Delete group "{name}"?',groups_manage_tasks:"Manage task assignments\u2026",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 \u2014 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 \u2192 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 \u2014 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 \u2014 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:"\u2713 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 \u2014 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\u2026",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\u2026",doc_add_link:"Add link",doc_link_url:"URL (https://\u2026)",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\u2026",remove:"Remove",doc_deduped:"Already stored elsewhere \u2014 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\u2026",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 \u2014 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 \u2014 e.g. salt refilled, filter replaced.",doc_search:"Search documents\u2026",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 \u2014 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 \u2014 schedules restarted",object_paused_badge:"Paused",paused_until_label:"until",replace_object:"Replace\u2026",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 \u2014 successor created",reading_unit_label:"Reading unit (e.g. kWh, m\xB3)",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 \u2014 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"};var Be="\u20AC";var ye="en",We=(()=>{let n=window;return n.__msLocales||(n.__msLocales={store:{},inflight:{}}),n.__msLocales})(),T=We.store;T.en||(T.en=Fe);var wt=new Set(["de","nl","fr","it","es","pt","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi"]),kt="/maintenance_supporter_locales",Q=We.inflight;function be(n){return(n||ye).substring(0,2).toLowerCase()}function f(n,e){let t=be(e);return T[t]?.[n]??T.en[n]??n}function Ge(n){let e=be(n);return e===ye||e in T}function Ve(n){let e=be(n);return e===ye||e in T||!wt.has(e)?Promise.resolve():(e in Q||(Q[e]=fetch(`${kt}/${e}.json`).then(t=>t.ok?t.json():null).then(t=>{t?T[e]=t:delete Q[e]}).catch(()=>{delete Q[e]})),Q[e])}var Ye=S` +Test 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\u2026",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 \u2014 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\u20135.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\xB2",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\u2026",qr_error:"Failed to generate QR code.",qr_error_no_url:"No HA URL configured. Please set an external or internal URL in Settings \u2192 System \u2192 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 \u201CAdaptive Intervals\u201D or \u201CSeasonal Patterns\u201D 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 \u2014 all conditions must trigger",compound_logic_or:"OR \u2014 any condition triggers",compound_help:"Combine several sensor conditions into one trigger.",compound_no_conditions:"No conditions yet \u2014 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\u2026",card_load_error:"Could not load objects \u2014 check the WebSocket connection.",card_no_tasks_title:"No maintenance tasks yet",card_no_tasks_cta:"\u2192 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:"\u2014 No tag \u2014",dashboard:"Dashboard",tab_today:"Today",palette_placeholder:"Search objects and tasks\u2026",palette_no_results:"No matches",palette_hint:"\u2191\u2193 to navigate \xB7 Enter to open \xB7 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\u2026",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 \u2014 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\u2026",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\u2026",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\u2026",vacation_open_panel:"Open in panel",enable:"Enable",saved:"Saved",budget_monthly_set:"Set monthly",budget_yearly_set:"Set yearly",budget_advanced:"Currency, alerts\u2026",budget_open_panel:"Open in panel",groups_empty:"No groups yet.",group_new_placeholder:"Add group\u2026",group_delete_confirm:'Delete group "{name}"?',groups_manage_tasks:"Manage task assignments\u2026",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 \u2014 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 \u2192 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 \u2014 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 \u2014 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:"\u2713 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 \u2014 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\u2026",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\u2026",doc_add_link:"Add link",doc_link_url:"URL (https://\u2026)",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\u2026",remove:"Remove",doc_deduped:"Already stored elsewhere \u2014 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\u2026",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 \u2014 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 \u2014 e.g. salt refilled, filter replaced.",doc_search:"Search documents\u2026",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 \u2014 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 \u2014 schedules restarted",object_paused_badge:"Paused",paused_until_label:"until",replace_object:"Replace\u2026",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 \u2014 successor created",reading_unit_label:"Reading unit (e.g. kWh, m\xB3)",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 \u2014 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",card_all_caught_up:"All caught up \u2014 nothing needs attention",postpone:"Postpone",postpone_date_prompt:"Postpone this occurrence to which date?",postpone_date_label:"New due date",postponed:"Postponed",postponed_to:"Postponed to",season_window_label:"Seasonal window (months)",season_window_hint:"Only due in the selected months; off-season dates roll to the next active month. None = all year.",series_end_label:"Ends",series_end_never:"Never (repeats indefinitely)",series_end_after_count:"After a number of times",series_end_until:"On a date",series_end_count_label:"Number of times",series_end_until_label:"End date"};var Be="\u20AC";var ye="en",We=(()=>{let n=window;return n.__msLocales||(n.__msLocales={store:{},inflight:{}}),n.__msLocales})(),T=We.store;T.en||(T.en=Fe);var wt=new Set(["de","nl","fr","it","es","pt","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi"]),kt="/maintenance_supporter_locales",Q=We.inflight;function be(n){return(n||ye).substring(0,2).toLowerCase()}function f(n,e){let t=be(e);return T[t]?.[n]??T.en[n]??n}function Ge(n){let e=be(n);return e===ye||e in T}function Ve(n){let e=be(n);return e===ye||e in T||!wt.has(e)?Promise.resolve():(e in Q||(Q[e]=fetch(`${kt}/${e}.json`).then(t=>t.ok?t.json():null).then(t=>{t?T[e]=t:delete Q[e]}).catch(()=>{delete Q[e]})),Q[e])}var Ye=S` :host { --maint-ok-color: var(--success-color, #4caf50); --maint-due-soon-color: var(--warning-color, #ff9800); @@ -670,11 +670,14 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:" transition: width 0.3s; } - /* Trigger progress bar (overview rows) */ + /* Trigger progress bar (overview rows). width:100% — the due-cell doesn't + stretch its children (align-items: flex-end), so without it the bar + shrinks to its label and reads shorter than the days-bar in other rows. */ .trigger-progress { display: flex; flex-direction: column; gap: 2px; + width: 100%; min-width: 90px; } diff --git a/custom_components/maintenance_supporter/frontend/maintenance-card.js b/custom_components/maintenance_supporter/frontend/maintenance-card.js index 9ba6723..5b68e2d 100644 --- a/custom_components/maintenance_supporter/frontend/maintenance-card.js +++ b/custom_components/maintenance_supporter/frontend/maintenance-card.js @@ -1,8 +1,8 @@ -var st=Object.defineProperty;var _i=Object.getOwnPropertyDescriptor;var y=(a,s,e)=>()=>{if(e)throw e[0];try{return a&&(s=a(a=0)),s}catch(t){throw e=[t],t}};var ui=(a,s)=>{for(var e in s)st(a,e,{get:s[e],enumerable:!0})};var l=(a,s,e,t)=>{for(var i=t>1?void 0:t?_i(s,e):s,o=a.length-1,d;o>=0;o--)(d=a[o])&&(i=(t?d(s,e,i):d(i))||i);return t&&i&&st(s,e,i),i};var ye,xe,Pe,rt,ne,at,E,ot,ze,Ne=y(()=>{ye=globalThis,xe=ye.ShadowRoot&&(ye.ShadyCSS===void 0||ye.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Pe=Symbol(),rt=new WeakMap,ne=class{constructor(s,e,t){if(this._$cssResult$=!0,t!==Pe)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=s,this.t=e}get styleSheet(){let s=this.o,e=this.t;if(xe&&s===void 0){let t=e!==void 0&&e.length===1;t&&(s=rt.get(e)),s===void 0&&((this.o=s=new CSSStyleSheet).replaceSync(this.cssText),t&&rt.set(e,s))}return s}toString(){return this.cssText}},at=a=>new ne(typeof a=="string"?a:a+"",void 0,Pe),E=(a,...s)=>{let e=a.length===1?a[0]:s.reduce((t,i,o)=>t+(d=>{if(d._$cssResult$===!0)return d.cssText;if(typeof d=="number")return d;throw Error("Value passed to 'css' function must be a 'css' function result: "+d+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+a[o+1],a[0]);return new ne(e,a,Pe)},ot=(a,s)=>{if(xe)a.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of s){let t=document.createElement("style"),i=ye.litNonce;i!==void 0&&t.setAttribute("nonce",i),t.textContent=e.cssText,a.appendChild(t)}},ze=xe?a=>a:a=>a instanceof CSSStyleSheet?(s=>{let e="";for(let t of s.cssRules)e+=t.cssText;return at(e)})(a):a});var gi,mi,fi,vi,bi,yi,$e,nt,xi,$i,le,ce,we,lt,q,de=y(()=>{Ne();Ne();({is:gi,defineProperty:mi,getOwnPropertyDescriptor:fi,getOwnPropertyNames:vi,getOwnPropertySymbols:bi,getPrototypeOf:yi}=Object),$e=globalThis,nt=$e.trustedTypes,xi=nt?nt.emptyScript:"",$i=$e.reactiveElementPolyfillSupport,le=(a,s)=>a,ce={toAttribute(a,s){switch(s){case Boolean:a=a?xi:null;break;case Object:case Array:a=a==null?a:JSON.stringify(a)}return a},fromAttribute(a,s){let e=a;switch(s){case Boolean:e=a!==null;break;case Number:e=a===null?null:Number(a);break;case Object:case Array:try{e=JSON.parse(a)}catch{e=null}}return e}},we=(a,s)=>!gi(a,s),lt={attribute:!0,type:String,converter:ce,reflect:!1,useDefault:!1,hasChanged:we};Symbol.metadata??=Symbol("metadata"),$e.litPropertyMetadata??=new WeakMap;q=class extends HTMLElement{static addInitializer(s){this._$Ei(),(this.l??=[]).push(s)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(s,e=lt){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(s)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(s,e),!e.noAccessor){let t=Symbol(),i=this.getPropertyDescriptor(s,t,e);i!==void 0&&mi(this.prototype,s,i)}}static getPropertyDescriptor(s,e,t){let{get:i,set:o}=fi(this.prototype,s)??{get(){return this[e]},set(d){this[e]=d}};return{get:i,set(d){let p=i?.call(this);o?.call(this,d),this.requestUpdate(s,p,t)},configurable:!0,enumerable:!0}}static getPropertyOptions(s){return this.elementProperties.get(s)??lt}static _$Ei(){if(this.hasOwnProperty(le("elementProperties")))return;let s=yi(this);s.finalize(),s.l!==void 0&&(this.l=[...s.l]),this.elementProperties=new Map(s.elementProperties)}static finalize(){if(this.hasOwnProperty(le("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(le("properties"))){let e=this.properties,t=[...vi(e),...bi(e)];for(let i of t)this.createProperty(i,e[i])}let s=this[Symbol.metadata];if(s!==null){let e=litPropertyMetadata.get(s);if(e!==void 0)for(let[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(let[e,t]of this.elementProperties){let i=this._$Eu(e,t);i!==void 0&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(s){let e=[];if(Array.isArray(s)){let t=new Set(s.flat(1/0).reverse());for(let i of t)e.unshift(ze(i))}else s!==void 0&&e.push(ze(s));return e}static _$Eu(s,e){let t=e.attribute;return t===!1?void 0:typeof t=="string"?t:typeof s=="string"?s.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(s=>this.enableUpdating=s),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(s=>s(this))}addController(s){(this._$EO??=new Set).add(s),this.renderRoot!==void 0&&this.isConnected&&s.hostConnected?.()}removeController(s){this._$EO?.delete(s)}_$E_(){let s=new Map,e=this.constructor.elementProperties;for(let t of e.keys())this.hasOwnProperty(t)&&(s.set(t,this[t]),delete this[t]);s.size>0&&(this._$Ep=s)}createRenderRoot(){let s=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return ot(s,this.constructor.elementStyles),s}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(s=>s.hostConnected?.())}enableUpdating(s){}disconnectedCallback(){this._$EO?.forEach(s=>s.hostDisconnected?.())}attributeChangedCallback(s,e,t){this._$AK(s,t)}_$ET(s,e){let t=this.constructor.elementProperties.get(s),i=this.constructor._$Eu(s,t);if(i!==void 0&&t.reflect===!0){let o=(t.converter?.toAttribute!==void 0?t.converter:ce).toAttribute(e,t.type);this._$Em=s,o==null?this.removeAttribute(i):this.setAttribute(i,o),this._$Em=null}}_$AK(s,e){let t=this.constructor,i=t._$Eh.get(s);if(i!==void 0&&this._$Em!==i){let o=t.getPropertyOptions(i),d=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:ce;this._$Em=i;let p=d.fromAttribute(e,o.type);this[i]=p??this._$Ej?.get(i)??p,this._$Em=null}}requestUpdate(s,e,t,i=!1,o){if(s!==void 0){let d=this.constructor;if(i===!1&&(o=this[s]),t??=d.getPropertyOptions(s),!((t.hasChanged??we)(o,e)||t.useDefault&&t.reflect&&o===this._$Ej?.get(s)&&!this.hasAttribute(d._$Eu(s,t))))return;this.C(s,e,t)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(s,e,{useDefault:t,reflect:i,wrapped:o},d){t&&!(this._$Ej??=new Map).has(s)&&(this._$Ej.set(s,d??e??this[s]),o!==!0||d!==void 0)||(this._$AL.has(s)||(this.hasUpdated||t||(e=void 0),this._$AL.set(s,e)),i===!0&&this._$Em!==s&&(this._$Eq??=new Set).add(s))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}let s=this.scheduleUpdate();return s!=null&&await s,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[i,o]of this._$Ep)this[i]=o;this._$Ep=void 0}let t=this.constructor.elementProperties;if(t.size>0)for(let[i,o]of t){let{wrapped:d}=o,p=this[i];d!==!0||this._$AL.has(i)||p===void 0||this.C(i,void 0,o,p)}}let s=!1,e=this._$AL;try{s=this.shouldUpdate(e),s?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(t){throw s=!1,this._$EM(),t}s&&this._$AE(e)}willUpdate(s){}_$AE(s){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(s)),this.updated(s)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(s){return!0}update(s){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(s){}firstUpdated(s){}};q.elementStyles=[],q.shadowRootOptions={mode:"open"},q[le("elementProperties")]=new Map,q[le("finalized")]=new Map,$i?.({ReactiveElement:q}),($e.reactiveElementVersions??=[]).push("2.1.2")});function bt(a,s){if(!Ve(a)||!a.hasOwnProperty("raw"))throw Error("invalid template strings array");return dt!==void 0?dt.createHTML(s):s}function te(a,s,e=a,t){if(s===J)return s;let i=t!==void 0?e._$Co?.[t]:e._$Cl,o=_e(s)?void 0:s._$litDirective$;return i?.constructor!==o&&(i?._$AO?.(!1),o===void 0?i=void 0:(i=new o(a),i._$AT(a,e,t)),t!==void 0?(e._$Co??=[])[t]=i:e._$Cl=i),i!==void 0&&(s=te(a,i._$AS(a,s.values),i,t)),s}var We,ct,ke,dt,mt,B,ft,wi,Y,he,_e,Ve,ki,Me,pe,pt,ht,G,_t,ut,vt,Be,n,se,us,J,_,gt,K,Ei,ue,qe,ge,ie,De,Oe,Ue,Fe,Ai,yt,Ee=y(()=>{We=globalThis,ct=a=>a,ke=We.trustedTypes,dt=ke?ke.createPolicy("lit-html",{createHTML:a=>a}):void 0,mt="$lit$",B=`lit$${Math.random().toFixed(9).slice(2)}$`,ft="?"+B,wi=`<${ft}>`,Y=document,he=()=>Y.createComment(""),_e=a=>a===null||typeof a!="object"&&typeof a!="function",Ve=Array.isArray,ki=a=>Ve(a)||typeof a?.[Symbol.iterator]=="function",Me=`[ +var st=Object.defineProperty;var ui=Object.getOwnPropertyDescriptor;var y=(a,s,e)=>()=>{if(e)throw e[0];try{return a&&(s=a(a=0)),s}catch(t){throw e=[t],t}};var gi=(a,s)=>{for(var e in s)st(a,e,{get:s[e],enumerable:!0})};var l=(a,s,e,t)=>{for(var i=t>1?void 0:t?ui(s,e):s,n=a.length-1,c;n>=0;n--)(c=a[n])&&(i=(t?c(s,e,i):c(i))||i);return t&&i&&st(s,e,i),i};var ye,xe,Pe,rt,oe,at,E,nt,ze,Ne=y(()=>{ye=globalThis,xe=ye.ShadowRoot&&(ye.ShadyCSS===void 0||ye.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Pe=Symbol(),rt=new WeakMap,oe=class{constructor(s,e,t){if(this._$cssResult$=!0,t!==Pe)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=s,this.t=e}get styleSheet(){let s=this.o,e=this.t;if(xe&&s===void 0){let t=e!==void 0&&e.length===1;t&&(s=rt.get(e)),s===void 0&&((this.o=s=new CSSStyleSheet).replaceSync(this.cssText),t&&rt.set(e,s))}return s}toString(){return this.cssText}},at=a=>new oe(typeof a=="string"?a:a+"",void 0,Pe),E=(a,...s)=>{let e=a.length===1?a[0]:s.reduce((t,i,n)=>t+(c=>{if(c._$cssResult$===!0)return c.cssText;if(typeof c=="number")return c;throw Error("Value passed to 'css' function must be a 'css' function result: "+c+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+a[n+1],a[0]);return new oe(e,a,Pe)},nt=(a,s)=>{if(xe)a.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of s){let t=document.createElement("style"),i=ye.litNonce;i!==void 0&&t.setAttribute("nonce",i),t.textContent=e.cssText,a.appendChild(t)}},ze=xe?a=>a:a=>a instanceof CSSStyleSheet?(s=>{let e="";for(let t of s.cssRules)e+=t.cssText;return at(e)})(a):a});var mi,fi,vi,bi,yi,xi,$e,ot,$i,wi,le,ce,we,lt,q,de=y(()=>{Ne();Ne();({is:mi,defineProperty:fi,getOwnPropertyDescriptor:vi,getOwnPropertyNames:bi,getOwnPropertySymbols:yi,getPrototypeOf:xi}=Object),$e=globalThis,ot=$e.trustedTypes,$i=ot?ot.emptyScript:"",wi=$e.reactiveElementPolyfillSupport,le=(a,s)=>a,ce={toAttribute(a,s){switch(s){case Boolean:a=a?$i:null;break;case Object:case Array:a=a==null?a:JSON.stringify(a)}return a},fromAttribute(a,s){let e=a;switch(s){case Boolean:e=a!==null;break;case Number:e=a===null?null:Number(a);break;case Object:case Array:try{e=JSON.parse(a)}catch{e=null}}return e}},we=(a,s)=>!mi(a,s),lt={attribute:!0,type:String,converter:ce,reflect:!1,useDefault:!1,hasChanged:we};Symbol.metadata??=Symbol("metadata"),$e.litPropertyMetadata??=new WeakMap;q=class extends HTMLElement{static addInitializer(s){this._$Ei(),(this.l??=[]).push(s)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(s,e=lt){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(s)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(s,e),!e.noAccessor){let t=Symbol(),i=this.getPropertyDescriptor(s,t,e);i!==void 0&&fi(this.prototype,s,i)}}static getPropertyDescriptor(s,e,t){let{get:i,set:n}=vi(this.prototype,s)??{get(){return this[e]},set(c){this[e]=c}};return{get:i,set(c){let d=i?.call(this);n?.call(this,c),this.requestUpdate(s,d,t)},configurable:!0,enumerable:!0}}static getPropertyOptions(s){return this.elementProperties.get(s)??lt}static _$Ei(){if(this.hasOwnProperty(le("elementProperties")))return;let s=xi(this);s.finalize(),s.l!==void 0&&(this.l=[...s.l]),this.elementProperties=new Map(s.elementProperties)}static finalize(){if(this.hasOwnProperty(le("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(le("properties"))){let e=this.properties,t=[...bi(e),...yi(e)];for(let i of t)this.createProperty(i,e[i])}let s=this[Symbol.metadata];if(s!==null){let e=litPropertyMetadata.get(s);if(e!==void 0)for(let[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(let[e,t]of this.elementProperties){let i=this._$Eu(e,t);i!==void 0&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(s){let e=[];if(Array.isArray(s)){let t=new Set(s.flat(1/0).reverse());for(let i of t)e.unshift(ze(i))}else s!==void 0&&e.push(ze(s));return e}static _$Eu(s,e){let t=e.attribute;return t===!1?void 0:typeof t=="string"?t:typeof s=="string"?s.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(s=>this.enableUpdating=s),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(s=>s(this))}addController(s){(this._$EO??=new Set).add(s),this.renderRoot!==void 0&&this.isConnected&&s.hostConnected?.()}removeController(s){this._$EO?.delete(s)}_$E_(){let s=new Map,e=this.constructor.elementProperties;for(let t of e.keys())this.hasOwnProperty(t)&&(s.set(t,this[t]),delete this[t]);s.size>0&&(this._$Ep=s)}createRenderRoot(){let s=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return nt(s,this.constructor.elementStyles),s}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(s=>s.hostConnected?.())}enableUpdating(s){}disconnectedCallback(){this._$EO?.forEach(s=>s.hostDisconnected?.())}attributeChangedCallback(s,e,t){this._$AK(s,t)}_$ET(s,e){let t=this.constructor.elementProperties.get(s),i=this.constructor._$Eu(s,t);if(i!==void 0&&t.reflect===!0){let n=(t.converter?.toAttribute!==void 0?t.converter:ce).toAttribute(e,t.type);this._$Em=s,n==null?this.removeAttribute(i):this.setAttribute(i,n),this._$Em=null}}_$AK(s,e){let t=this.constructor,i=t._$Eh.get(s);if(i!==void 0&&this._$Em!==i){let n=t.getPropertyOptions(i),c=typeof n.converter=="function"?{fromAttribute:n.converter}:n.converter?.fromAttribute!==void 0?n.converter:ce;this._$Em=i;let d=c.fromAttribute(e,n.type);this[i]=d??this._$Ej?.get(i)??d,this._$Em=null}}requestUpdate(s,e,t,i=!1,n){if(s!==void 0){let c=this.constructor;if(i===!1&&(n=this[s]),t??=c.getPropertyOptions(s),!((t.hasChanged??we)(n,e)||t.useDefault&&t.reflect&&n===this._$Ej?.get(s)&&!this.hasAttribute(c._$Eu(s,t))))return;this.C(s,e,t)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(s,e,{useDefault:t,reflect:i,wrapped:n},c){t&&!(this._$Ej??=new Map).has(s)&&(this._$Ej.set(s,c??e??this[s]),n!==!0||c!==void 0)||(this._$AL.has(s)||(this.hasUpdated||t||(e=void 0),this._$AL.set(s,e)),i===!0&&this._$Em!==s&&(this._$Eq??=new Set).add(s))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}let s=this.scheduleUpdate();return s!=null&&await s,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[i,n]of this._$Ep)this[i]=n;this._$Ep=void 0}let t=this.constructor.elementProperties;if(t.size>0)for(let[i,n]of t){let{wrapped:c}=n,d=this[i];c!==!0||this._$AL.has(i)||d===void 0||this.C(i,void 0,n,d)}}let s=!1,e=this._$AL;try{s=this.shouldUpdate(e),s?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(t){throw s=!1,this._$EM(),t}s&&this._$AE(e)}willUpdate(s){}_$AE(s){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(s)),this.updated(s)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(s){return!0}update(s){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(s){}firstUpdated(s){}};q.elementStyles=[],q.shadowRootOptions={mode:"open"},q[le("elementProperties")]=new Map,q[le("finalized")]=new Map,wi?.({ReactiveElement:q}),($e.reactiveElementVersions??=[]).push("2.1.2")});function bt(a,s){if(!Ve(a)||!a.hasOwnProperty("raw"))throw Error("invalid template strings array");return dt!==void 0?dt.createHTML(s):s}function te(a,s,e=a,t){if(s===J)return s;let i=t!==void 0?e._$Co?.[t]:e._$Cl,n=_e(s)?void 0:s._$litDirective$;return i?.constructor!==n&&(i?._$AO?.(!1),n===void 0?i=void 0:(i=new n(a),i._$AT(a,e,t)),t!==void 0?(e._$Co??=[])[t]=i:e._$Cl=i),i!==void 0&&(s=te(a,i._$AS(a,s.values),i,t)),s}var We,ct,ke,dt,mt,B,ft,ki,Y,he,_e,Ve,Ei,Me,pe,pt,ht,G,_t,ut,vt,Be,o,se,gs,J,_,gt,K,Ai,ue,qe,ge,ie,Ue,Oe,De,Fe,Si,yt,Ee=y(()=>{We=globalThis,ct=a=>a,ke=We.trustedTypes,dt=ke?ke.createPolicy("lit-html",{createHTML:a=>a}):void 0,mt="$lit$",B=`lit$${Math.random().toFixed(9).slice(2)}$`,ft="?"+B,ki=`<${ft}>`,Y=document,he=()=>Y.createComment(""),_e=a=>a===null||typeof a!="object"&&typeof a!="function",Ve=Array.isArray,Ei=a=>Ve(a)||typeof a?.[Symbol.iterator]=="function",Me=`[ \f\r]`,pe=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,pt=/-->/g,ht=/>/g,G=RegExp(`>|${Me}(?:([^\\s"'>=/]+)(${Me}*=${Me}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`,"g"),_t=/'/g,ut=/"/g,vt=/^(?:script|style|textarea|title)$/i,Be=a=>(s,...e)=>({_$litType$:a,strings:s,values:e}),n=Be(1),se=Be(2),us=Be(3),J=Symbol.for("lit-noChange"),_=Symbol.for("lit-nothing"),gt=new WeakMap,K=Y.createTreeWalker(Y,129);Ei=(a,s)=>{let e=a.length-1,t=[],i,o=s===2?"":s===3?"":"",d=pe;for(let p=0;p"?(d=i??pe,m=-1):b[1]===void 0?m=-2:(m=d.lastIndex-b[2].length,g=b[1],d=b[3]===void 0?G:b[3]==='"'?ut:_t):d===ut||d===_t?d=G:d===pt||d===ht?d=pe:(d=G,i=void 0);let f=d===G&&a[p+1].startsWith("/>")?" ":"";o+=d===pe?c+wi:m>=0?(t.push(g),c.slice(0,m)+mt+c.slice(m)+B+f):c+B+(m===-2?p:f)}return[bt(a,o+(a[e]||"")+(s===2?"":s===3?"":"")),t]},ue=class a{constructor({strings:s,_$litType$:e},t){let i;this.parts=[];let o=0,d=0,p=s.length-1,c=this.parts,[g,b]=Ei(s,e);if(this.el=a.createElement(g,t),K.currentNode=this.el.content,e===2||e===3){let m=this.el.content.firstChild;m.replaceWith(...m.childNodes)}for(;(i=K.nextNode())!==null&&c.length0){i.textContent=ke?ke.emptyScript:"";for(let f=0;f2||t[0]!==""||t[1]!==""?(this._$AH=Array(t.length-1).fill(new String),this.strings=t):this._$AH=_}_$AI(s,e=this,t,i){let o=this.strings,d=!1;if(o===void 0)s=te(this,s,e,0),d=!_e(s)||s!==this._$AH&&s!==J,d&&(this._$AH=s);else{let p=s,c,g;for(s=o[0],c=0;c{let t=e?.renderBefore??s,i=t._$litPart$;if(i===void 0){let o=e?.renderBefore??null;t._$litPart$=i=new ge(s.insertBefore(he(),o),o,void 0,e??{})}return i._$AI(a),i}});var Ge,$,Si,xt=y(()=>{de();de();Ee();Ee();Ge=globalThis,$=class extends q{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let s=super.createRenderRoot();return this.renderOptions.renderBefore??=s.firstChild,s}update(s){let e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(s),this._$Do=yt(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return J}};$._$litElement$=!0,$.finalized=!0,Ge.litElementHydrateSupport?.({LitElement:$});Si=Ge.litElementPolyfillSupport;Si?.({LitElement:$});(Ge.litElementVersions??=[]).push("4.2.2")});var $t=y(()=>{});var T=y(()=>{de();Ee();xt();$t()});var wt=y(()=>{});function v(a){return(s,e)=>typeof e=="object"?Ci(a,s,e):((t,i,o)=>{let d=i.hasOwnProperty(o);return i.constructor.createProperty(o,t),d?Object.getOwnPropertyDescriptor(i,o):void 0})(a,s,e)}var Ti,Ci,Ke=y(()=>{de();Ti={attribute:!0,type:String,converter:ce,reflect:!1,hasChanged:we},Ci=(a=Ti,s,e)=>{let{kind:t,metadata:i}=e,o=globalThis.litPropertyMetadata.get(i);if(o===void 0&&globalThis.litPropertyMetadata.set(i,o=new Map),t==="setter"&&((a=Object.create(a)).wrapped=!0),o.set(e.name,a),t==="accessor"){let{name:d}=e;return{set(p){let c=s.get.call(this);s.set.call(this,p),this.requestUpdate(d,c,a,!0,p)},init(p){return p!==void 0&&this.C(d,void 0,a,p),p}}}if(t==="setter"){let{name:d}=e;return function(p){let c=this[d];s.call(this,p),this.requestUpdate(d,c,a,!0,p)}}throw Error("Unsupported decorator location: "+t)}});function h(a){return v({...a,state:!0,attribute:!1})}var kt=y(()=>{Ke();});var Et=y(()=>{});var re=y(()=>{});var At=y(()=>{re();});var St=y(()=>{re();});var Tt=y(()=>{re();});var Ct=y(()=>{re();});var It=y(()=>{re();});var j=y(()=>{wt();Ke();kt();Et();At();St();Tt();Ct();It()});var Ht,Lt=y(()=>{Ht={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\u2026",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\u201331)",recurrence_last_day:"Last day of the month",recurrence_business_day:"Business days only (roll back from weekend)",recurrence_offset:"Offset (days, \xB1)",recurrence_offset_help:"Shift the date by \xB1N 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 +\f\r"'\`<>=]|("|')|))|$)`,"g"),_t=/'/g,ut=/"/g,vt=/^(?:script|style|textarea|title)$/i,Be=a=>(s,...e)=>({_$litType$:a,strings:s,values:e}),o=Be(1),se=Be(2),gs=Be(3),J=Symbol.for("lit-noChange"),_=Symbol.for("lit-nothing"),gt=new WeakMap,K=Y.createTreeWalker(Y,129);Ai=(a,s)=>{let e=a.length-1,t=[],i,n=s===2?"":s===3?"":"",c=pe;for(let d=0;d"?(c=i??pe,m=-1):b[1]===void 0?m=-2:(m=c.lastIndex-b[2].length,u=b[1],c=b[3]===void 0?G:b[3]==='"'?ut:_t):c===ut||c===_t?c=G:c===pt||c===ht?c=pe:(c=G,i=void 0);let f=c===G&&a[d+1].startsWith("/>")?" ":"";n+=c===pe?h+ki:m>=0?(t.push(u),h.slice(0,m)+mt+h.slice(m)+B+f):h+B+(m===-2?d:f)}return[bt(a,n+(a[e]||"")+(s===2?"":s===3?"":"")),t]},ue=class a{constructor({strings:s,_$litType$:e},t){let i;this.parts=[];let n=0,c=0,d=s.length-1,h=this.parts,[u,b]=Ai(s,e);if(this.el=a.createElement(u,t),K.currentNode=this.el.content,e===2||e===3){let m=this.el.content.firstChild;m.replaceWith(...m.childNodes)}for(;(i=K.nextNode())!==null&&h.length0){i.textContent=ke?ke.emptyScript:"";for(let f=0;f2||t[0]!==""||t[1]!==""?(this._$AH=Array(t.length-1).fill(new String),this.strings=t):this._$AH=_}_$AI(s,e=this,t,i){let n=this.strings,c=!1;if(n===void 0)s=te(this,s,e,0),c=!_e(s)||s!==this._$AH&&s!==J,c&&(this._$AH=s);else{let d=s,h,u;for(s=n[0],h=0;h{let t=e?.renderBefore??s,i=t._$litPart$;if(i===void 0){let n=e?.renderBefore??null;t._$litPart$=i=new ge(s.insertBefore(he(),n),n,void 0,e??{})}return i._$AI(a),i}});var Ge,$,Ti,xt=y(()=>{de();de();Ee();Ee();Ge=globalThis,$=class extends q{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let s=super.createRenderRoot();return this.renderOptions.renderBefore??=s.firstChild,s}update(s){let e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(s),this._$Do=yt(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return J}};$._$litElement$=!0,$.finalized=!0,Ge.litElementHydrateSupport?.({LitElement:$});Ti=Ge.litElementPolyfillSupport;Ti?.({LitElement:$});(Ge.litElementVersions??=[]).push("4.2.2")});var $t=y(()=>{});var T=y(()=>{de();Ee();xt();$t()});var wt=y(()=>{});function v(a){return(s,e)=>typeof e=="object"?Ii(a,s,e):((t,i,n)=>{let c=i.hasOwnProperty(n);return i.constructor.createProperty(n,t),c?Object.getOwnPropertyDescriptor(i,n):void 0})(a,s,e)}var Ci,Ii,Ke=y(()=>{de();Ci={attribute:!0,type:String,converter:ce,reflect:!1,hasChanged:we},Ii=(a=Ci,s,e)=>{let{kind:t,metadata:i}=e,n=globalThis.litPropertyMetadata.get(i);if(n===void 0&&globalThis.litPropertyMetadata.set(i,n=new Map),t==="setter"&&((a=Object.create(a)).wrapped=!0),n.set(e.name,a),t==="accessor"){let{name:c}=e;return{set(d){let h=s.get.call(this);s.set.call(this,d),this.requestUpdate(c,h,a,!0,d)},init(d){return d!==void 0&&this.C(c,void 0,a,d),d}}}if(t==="setter"){let{name:c}=e;return function(d){let h=this[c];s.call(this,d),this.requestUpdate(c,h,a,!0,d)}}throw Error("Unsupported decorator location: "+t)}});function p(a){return v({...a,state:!0,attribute:!1})}var kt=y(()=>{Ke();});var Et=y(()=>{});var re=y(()=>{});var At=y(()=>{re();});var St=y(()=>{re();});var Tt=y(()=>{re();});var Ct=y(()=>{re();});var It=y(()=>{re();});var j=y(()=>{wt();Ke();kt();Et();At();St();Tt();Ct();It()});var Ht,Lt=y(()=>{Ht={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\u2026",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\u201331)",recurrence_last_day:"Last day of the month",recurrence_business_day:"Business days only (roll back from weekend)",recurrence_offset:"Offset (days, \xB1)",recurrence_offset_help:"Shift the date by \xB1N 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 Replace seal -Test 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\u2026",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 \u2014 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\u20135.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\xB2",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\u2026",qr_error:"Failed to generate QR code.",qr_error_no_url:"No HA URL configured. Please set an external or internal URL in Settings \u2192 System \u2192 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 \u201CAdaptive Intervals\u201D or \u201CSeasonal Patterns\u201D 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 \u2014 all conditions must trigger",compound_logic_or:"OR \u2014 any condition triggers",compound_help:"Combine several sensor conditions into one trigger.",compound_no_conditions:"No conditions yet \u2014 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\u2026",card_load_error:"Could not load objects \u2014 check the WebSocket connection.",card_no_tasks_title:"No maintenance tasks yet",card_no_tasks_cta:"\u2192 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:"\u2014 No tag \u2014",dashboard:"Dashboard",tab_today:"Today",palette_placeholder:"Search objects and tasks\u2026",palette_no_results:"No matches",palette_hint:"\u2191\u2193 to navigate \xB7 Enter to open \xB7 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\u2026",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 \u2014 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\u2026",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\u2026",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\u2026",vacation_open_panel:"Open in panel",enable:"Enable",saved:"Saved",budget_monthly_set:"Set monthly",budget_yearly_set:"Set yearly",budget_advanced:"Currency, alerts\u2026",budget_open_panel:"Open in panel",groups_empty:"No groups yet.",group_new_placeholder:"Add group\u2026",group_delete_confirm:'Delete group "{name}"?',groups_manage_tasks:"Manage task assignments\u2026",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 \u2014 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 \u2192 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 \u2014 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 \u2014 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:"\u2713 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 \u2014 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\u2026",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\u2026",doc_add_link:"Add link",doc_link_url:"URL (https://\u2026)",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\u2026",remove:"Remove",doc_deduped:"Already stored elsewhere \u2014 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\u2026",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 \u2014 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 \u2014 e.g. salt refilled, filter replaced.",doc_search:"Search documents\u2026",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 \u2014 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 \u2014 schedules restarted",object_paused_badge:"Paused",paused_until_label:"until",replace_object:"Replace\u2026",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 \u2014 successor created",reading_unit_label:"Reading unit (e.g. kWh, m\xB3)",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 \u2014 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"}});function Je(a){return(a||Ye).substring(0,2).toLowerCase()}function r(a,s){let e=Je(s);return Z[e]?.[a]??Z.en[a]??a}function Te(a){let s=Je(a);return s===Ye||s in Z}function Ce(a){let s=Je(a);return s===Ye||s in Z||!Li.has(s)?Promise.resolve():(s in me||(me[s]=fetch(`${Hi}/${s}.json`).then(e=>e.ok?e.json():null).then(e=>{e?Z[s]=e:delete me[s]}).catch(()=>{delete me[s]})),me[s])}function Pt(a){let s=(a||"en").substring(0,2).toLowerCase();return{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"}[s]??"en-US"}function Q(a,s){if(!a)return"\u2014";try{let e=a.includes("T")?a:a+"T00:00:00";return new Date(e).toLocaleDateString(Pt(s),{day:"2-digit",month:"2-digit",year:"numeric"})}catch{return a}}function zt(a,s){if(!a)return"\u2014";try{let e=Pt(s),t=new Date(a);return t.toLocaleDateString(e,{day:"2-digit",month:"2-digit",year:"numeric"})+" "+t.toLocaleTimeString(e,{hour:"2-digit",minute:"2-digit"})}catch{return a}}function Rt(a,s,e){return a==null?"\u2014":`${a} ${r("unit_"+(s||"days"),e)}`}function Se(a,s,e="long"){let t=(s||"en").substring(0,2);return new Date(Date.UTC(2024,0,1+a)).toLocaleDateString(t,{weekday:e,timeZone:"UTC"})}function Nt(a,s){let e=a.schedule,t=e?.offset?` ${e.offset>0?"+":"\u2212"}${Math.abs(e.offset)}d`:"";switch(e?.kind){case"weekdays":return((e.weekdays||[]).map(i=>Se(i,s,"short")).join(" & ")||"\u2014")+t;case"nth_weekday":return e.weekday==null||e.nth==null?"\u2014":`${e.nth===-1?r("ord_last",s):r("ord_"+e.nth,s)} ${Se(e.weekday,s,"long")}${t}`;case"day_of_month":return e.day==null?"\u2014":(e.day===-1?r(e.business?"last_business_day_month":"last_day_month",s):`${r("day_word",s)} ${e.day}`)+t;case"one_time":return a.due_date?Q(a.due_date,s):r("one_time",s);case"manual":return r("manual",s);case"interval":return Rt(e.every,e.unit,s)}return a.schedule_type==="one_time"?a.due_date?Q(a.due_date,s):r("one_time",s):a.schedule_type==="manual"?r("manual",s):a.schedule_type==="sensor_based"?r("sensor_based",s):a.interval_days!=null?Rt(a.interval_days,a.interval_unit,s):"\u2014"}function Mt(a,s){a.currentTarget.dispatchEvent(new CustomEvent("hass-more-info",{detail:{entityId:s},bubbles:!0,composed:!0}))}var ae,Ye,jt,Z,Li,Hi,me,Ie,C=y(()=>{"use strict";T();Lt();ae={ok:"var(--success-color, #4caf50)",due_soon:"var(--warning-color, #ff9800)",overdue:"var(--error-color, #f44336)",triggered:"var(--deep-orange-color, #ff5722)",archived:"var(--disabled-color, #9e9e9e)",paused:"var(--info-color, #2196f3)"},Ye="en",jt=(()=>{let a=window;return a.__msLocales||(a.__msLocales={store:{},inflight:{}}),a.__msLocales})(),Z=jt.store;Z.en||(Z.en=Ht);Li=new Set(["de","nl","fr","it","es","pt","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi"]),Hi="/maintenance_supporter_locales",me=jt.inflight;Ie=E` +Test 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\u2026",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 \u2014 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\u20135.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\xB2",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\u2026",qr_error:"Failed to generate QR code.",qr_error_no_url:"No HA URL configured. Please set an external or internal URL in Settings \u2192 System \u2192 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 \u201CAdaptive Intervals\u201D or \u201CSeasonal Patterns\u201D 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 \u2014 all conditions must trigger",compound_logic_or:"OR \u2014 any condition triggers",compound_help:"Combine several sensor conditions into one trigger.",compound_no_conditions:"No conditions yet \u2014 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\u2026",card_load_error:"Could not load objects \u2014 check the WebSocket connection.",card_no_tasks_title:"No maintenance tasks yet",card_no_tasks_cta:"\u2192 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:"\u2014 No tag \u2014",dashboard:"Dashboard",tab_today:"Today",palette_placeholder:"Search objects and tasks\u2026",palette_no_results:"No matches",palette_hint:"\u2191\u2193 to navigate \xB7 Enter to open \xB7 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\u2026",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 \u2014 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\u2026",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\u2026",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\u2026",vacation_open_panel:"Open in panel",enable:"Enable",saved:"Saved",budget_monthly_set:"Set monthly",budget_yearly_set:"Set yearly",budget_advanced:"Currency, alerts\u2026",budget_open_panel:"Open in panel",groups_empty:"No groups yet.",group_new_placeholder:"Add group\u2026",group_delete_confirm:'Delete group "{name}"?',groups_manage_tasks:"Manage task assignments\u2026",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 \u2014 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 \u2192 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 \u2014 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 \u2014 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:"\u2713 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 \u2014 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\u2026",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\u2026",doc_add_link:"Add link",doc_link_url:"URL (https://\u2026)",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\u2026",remove:"Remove",doc_deduped:"Already stored elsewhere \u2014 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\u2026",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 \u2014 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 \u2014 e.g. salt refilled, filter replaced.",doc_search:"Search documents\u2026",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 \u2014 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 \u2014 schedules restarted",object_paused_badge:"Paused",paused_until_label:"until",replace_object:"Replace\u2026",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 \u2014 successor created",reading_unit_label:"Reading unit (e.g. kWh, m\xB3)",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 \u2014 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",card_all_caught_up:"All caught up \u2014 nothing needs attention",postpone:"Postpone",postpone_date_prompt:"Postpone this occurrence to which date?",postpone_date_label:"New due date",postponed:"Postponed",postponed_to:"Postponed to",season_window_label:"Seasonal window (months)",season_window_hint:"Only due in the selected months; off-season dates roll to the next active month. None = all year.",series_end_label:"Ends",series_end_never:"Never (repeats indefinitely)",series_end_after_count:"After a number of times",series_end_until:"On a date",series_end_count_label:"Number of times",series_end_until_label:"End date"}});function Je(a){return(a||Ye).substring(0,2).toLowerCase()}function r(a,s){let e=Je(s);return Z[e]?.[a]??Z.en[a]??a}function Te(a){let s=Je(a);return s===Ye||s in Z}function Ce(a){let s=Je(a);return s===Ye||s in Z||!Hi.has(s)?Promise.resolve():(s in me||(me[s]=fetch(`${Ri}/${s}.json`).then(e=>e.ok?e.json():null).then(e=>{e?Z[s]=e:delete me[s]}).catch(()=>{delete me[s]})),me[s])}function Pt(a){let s=(a||"en").substring(0,2).toLowerCase();return{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"}[s]??"en-US"}function Q(a,s){if(!a)return"\u2014";try{let e=a.includes("T")?a:a+"T00:00:00";return new Date(e).toLocaleDateString(Pt(s),{day:"2-digit",month:"2-digit",year:"numeric"})}catch{return a}}function zt(a,s){if(!a)return"\u2014";try{let e=Pt(s),t=new Date(a);return t.toLocaleDateString(e,{day:"2-digit",month:"2-digit",year:"numeric"})+" "+t.toLocaleTimeString(e,{hour:"2-digit",minute:"2-digit"})}catch{return a}}function Rt(a,s,e){return a==null?"\u2014":`${a} ${r("unit_"+(s||"days"),e)}`}function Se(a,s,e="long"){let t=(s||"en").substring(0,2);return new Date(Date.UTC(2024,0,1+a)).toLocaleDateString(t,{weekday:e,timeZone:"UTC"})}function Nt(a,s){let e=a.schedule,t=e?.offset?` ${e.offset>0?"+":"\u2212"}${Math.abs(e.offset)}d`:"";switch(e?.kind){case"weekdays":return((e.weekdays||[]).map(i=>Se(i,s,"short")).join(" & ")||"\u2014")+t;case"nth_weekday":return e.weekday==null||e.nth==null?"\u2014":`${e.nth===-1?r("ord_last",s):r("ord_"+e.nth,s)} ${Se(e.weekday,s,"long")}${t}`;case"day_of_month":return e.day==null?"\u2014":(e.day===-1?r(e.business?"last_business_day_month":"last_day_month",s):`${r("day_word",s)} ${e.day}`)+t;case"one_time":return a.due_date?Q(a.due_date,s):r("one_time",s);case"manual":return r("manual",s);case"interval":return Rt(e.every,e.unit,s)}return a.schedule_type==="one_time"?a.due_date?Q(a.due_date,s):r("one_time",s):a.schedule_type==="manual"?r("manual",s):a.schedule_type==="sensor_based"?r("sensor_based",s):a.interval_days!=null?Rt(a.interval_days,a.interval_unit,s):"\u2014"}function Mt(a,s){a.currentTarget.dispatchEvent(new CustomEvent("hass-more-info",{detail:{entityId:s},bubbles:!0,composed:!0}))}var ae,Ye,jt,Z,Hi,Ri,me,Ie,C=y(()=>{"use strict";T();Lt();ae={ok:"var(--success-color, #4caf50)",due_soon:"var(--warning-color, #ff9800)",overdue:"var(--error-color, #f44336)",triggered:"var(--deep-orange-color, #ff5722)",archived:"var(--disabled-color, #9e9e9e)",paused:"var(--info-color, #2196f3)"},Ye="en",jt=(()=>{let a=window;return a.__msLocales||(a.__msLocales={store:{},inflight:{}}),a.__msLocales})(),Z=jt.store;Z.en||(Z.en=Ht);Hi=new Set(["de","nl","fr","it","es","pt","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi"]),Ri="/maintenance_supporter_locales",me=jt.inflight;Ie=E` :host { --maint-ok-color: var(--success-color, #4caf50); --maint-due-soon-color: var(--warning-color, #ff9800); @@ -477,11 +477,14 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:" transition: width 0.3s; } - /* Trigger progress bar (overview rows) */ + /* Trigger progress bar (overview rows). width:100% — the due-cell doesn't + stretch its children (align-items: flex-end), so without it the bar + shrinks to its label and reads shorter than the days-bar in other rows. */ .trigger-progress { display: flex; flex-direction: column; gap: 2px; + width: 100%; min-width: 90px; } @@ -1127,15 +1130,15 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:" .stat-item { min-width: 60px; } .stat-value { font-size: 20px; } } -`});function Pi(a,s){let e=ji[a];if(!e)return a;let t=r(e,s);return t&&t!==e?t:a}function zi(a){let e=a.match(/data\['([^']+)'\]/)?.[1],t;return(t=a.match(/length of value must be at most (\d+)/))?{field:e,rule:"too_long",param:t[1]}:(t=a.match(/length of value must be at least (\d+)/))?{field:e,rule:"too_short",param:t[1]}:(t=a.match(/value must be at most (\S+)/))?{field:e,rule:"value_too_high",param:t[1]}:(t=a.match(/value must be at least (\S+)/))?{field:e,rule:"value_too_low",param:t[1]}:/required key not provided/.test(a)?{field:e,rule:"required"}:(t=a.match(/expected (\w+)/))?{field:e,rule:"wrong_type",param:t[1]}:/value must be one of/.test(a)?{field:e,rule:"invalid_choice"}:/not a valid value/.test(a)?{field:e,rule:"invalid_value"}:{field:e,rule:"unknown"}}function I(a,s,e){if(e=e??r("action_error",s),typeof a=="string")return a;if(typeof a!="object"||a===null)return e;let t=a,i=t.message||t.error?.message||"";if(!i)return e;let o=zi(i),d=o.field?Pi(o.field,s):"",p=c=>r(c,s).replace("{field}",d).replace("{n}",o.param??"");switch(o.rule){case"too_long":return p("err_too_long");case"too_short":return p("err_too_short");case"value_too_high":return p("err_value_too_high");case"value_too_low":return p("err_value_too_low");case"required":return p("err_required");case"wrong_type":return p("err_wrong_type").replace("{type}",o.param??"");case"invalid_choice":return p("err_invalid_choice");case"invalid_value":return p("err_invalid_value");default:return i||e}}var ji,X=y(()=>{"use strict";C();ji={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"}});var w,Ze=y(()=>{"use strict";T();j();C();X();w=class extends ${constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this._open=!1;this._notes="";this._cost="";this._duration="";this._loading=!1;this._error="";this._checklistState={};this._feedback="needed";this._photoDocId="";this._photoPreview="";this._photoUploading=!1;this._readingValue=""}open(){this._open||(this._open=!0,this._notes="",this._cost="",this._duration="",this._error="",this._checklistState={},this._feedback="needed",this._photoDocId="",this._photoPreview="",this._photoUploading=!1,this._readingValue="")}_toggleCheck(e){let t=String(e);this._checklistState={...this._checklistState,[t]:!this._checklistState[t]}}_setFeedback(e){this._feedback=e}async _onPhotoInput(e){let t=e.target,i=t.files?.[0];if(t.value="",!!i){this._photoUploading=!0,this._error="";try{let o=new FormData;o.append("entry_id",this.entryId),o.append("tags","photo"),o.append("file",i,i.name);let d=await fetch("/api/maintenance_supporter/document/upload",{method:"POST",headers:{Authorization:`Bearer ${this.hass.auth?.data?.access_token??""}`},body:o});if(!d.ok){this._error=d.status===413?r("doc_too_large",this.lang):r("doc_upload_failed",this.lang);return}let p=await d.json();p.id&&(this._photoDocId=p.id,this._photoPreview=URL.createObjectURL(i))}catch{this._error=r("doc_upload_failed",this.lang)}finally{this._photoUploading=!1}}}_removePhoto(){this._photoPreview&&URL.revokeObjectURL(this._photoPreview),this._photoDocId="",this._photoPreview=""}async _complete(){this._loading=!0,this._error="";try{let e={type:"maintenance_supporter/task/complete",entry_id:this.entryId,task_id:this.taskId};if(this._notes&&(e.notes=this._notes),this._cost){let t=parseFloat(this._cost);!isNaN(t)&&t>=0&&(e.cost=t)}if(this._duration){let t=parseInt(this._duration,10);!isNaN(t)&&t>=0&&(e.duration=t)}if(this.checklist.length>0&&(e.checklist_state=this._checklistState),this.adaptiveEnabled&&(e.feedback=this._feedback),this._photoDocId&&(e.photo_doc_id=this._photoDocId),this._readingValue!==""){let t=parseFloat(this._readingValue);isNaN(t)||(e.reading_value=t)}await this.hass.connection.sendMessagePromise(e),this._open=!1,this.dispatchEvent(new CustomEvent("task-completed"))}catch(e){this._error=I(e,this.lang,r("save_error",this.lang))}finally{this._loading=!1}}_close(){this._open=!1}render(){if(!this._open)return n``;let e=this.lang||this.hass?.language||"en";return n` +`});function zi(a,s){let e=Pi[a];if(!e)return a;let t=r(e,s);return t&&t!==e?t:a}function Ni(a){let e=a.match(/data\['([^']+)'\]/)?.[1],t;return(t=a.match(/length of value must be at most (\d+)/))?{field:e,rule:"too_long",param:t[1]}:(t=a.match(/length of value must be at least (\d+)/))?{field:e,rule:"too_short",param:t[1]}:(t=a.match(/value must be at most (\S+)/))?{field:e,rule:"value_too_high",param:t[1]}:(t=a.match(/value must be at least (\S+)/))?{field:e,rule:"value_too_low",param:t[1]}:/required key not provided/.test(a)?{field:e,rule:"required"}:(t=a.match(/expected (\w+)/))?{field:e,rule:"wrong_type",param:t[1]}:/value must be one of/.test(a)?{field:e,rule:"invalid_choice"}:/not a valid value/.test(a)?{field:e,rule:"invalid_value"}:{field:e,rule:"unknown"}}function I(a,s,e){if(e=e??r("action_error",s),typeof a=="string")return a;if(typeof a!="object"||a===null)return e;let t=a,i=t.message||t.error?.message||"";if(!i)return e;let n=Ni(i),c=n.field?zi(n.field,s):"",d=h=>r(h,s).replace("{field}",c).replace("{n}",n.param??"");switch(n.rule){case"too_long":return d("err_too_long");case"too_short":return d("err_too_short");case"value_too_high":return d("err_value_too_high");case"value_too_low":return d("err_value_too_low");case"required":return d("err_required");case"wrong_type":return d("err_wrong_type").replace("{type}",n.param??"");case"invalid_choice":return d("err_invalid_choice");case"invalid_value":return d("err_invalid_value");default:return i||e}}var Pi,X=y(()=>{"use strict";C();Pi={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"}});var w,Ze=y(()=>{"use strict";T();j();C();X();w=class extends ${constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this._open=!1;this._notes="";this._cost="";this._duration="";this._loading=!1;this._error="";this._checklistState={};this._feedback="needed";this._photoDocId="";this._photoPreview="";this._photoUploading=!1;this._readingValue=""}open(){this._open||(this._open=!0,this._notes="",this._cost="",this._duration="",this._error="",this._checklistState={},this._feedback="needed",this._photoDocId="",this._photoPreview="",this._photoUploading=!1,this._readingValue="")}_toggleCheck(e){let t=String(e);this._checklistState={...this._checklistState,[t]:!this._checklistState[t]}}_setFeedback(e){this._feedback=e}async _onPhotoInput(e){let t=e.target,i=t.files?.[0];if(t.value="",!!i){this._photoUploading=!0,this._error="";try{let n=new FormData;n.append("entry_id",this.entryId),n.append("tags","photo"),n.append("file",i,i.name);let c=await fetch("/api/maintenance_supporter/document/upload",{method:"POST",headers:{Authorization:`Bearer ${this.hass.auth?.data?.access_token??""}`},body:n});if(!c.ok){this._error=c.status===413?r("doc_too_large",this.lang):r("doc_upload_failed",this.lang);return}let d=await c.json();d.id&&(this._photoDocId=d.id,this._photoPreview=URL.createObjectURL(i))}catch{this._error=r("doc_upload_failed",this.lang)}finally{this._photoUploading=!1}}}_removePhoto(){this._photoPreview&&URL.revokeObjectURL(this._photoPreview),this._photoDocId="",this._photoPreview=""}async _complete(){this._loading=!0,this._error="";try{let e={type:"maintenance_supporter/task/complete",entry_id:this.entryId,task_id:this.taskId};if(this._notes&&(e.notes=this._notes),this._cost){let t=parseFloat(this._cost);!isNaN(t)&&t>=0&&(e.cost=t)}if(this._duration){let t=parseInt(this._duration,10);!isNaN(t)&&t>=0&&(e.duration=t)}if(this.checklist.length>0&&(e.checklist_state=this._checklistState),this.adaptiveEnabled&&(e.feedback=this._feedback),this._photoDocId&&(e.photo_doc_id=this._photoDocId),this._readingValue!==""){let t=parseFloat(this._readingValue);isNaN(t)||(e.reading_value=t)}await this.hass.connection.sendMessagePromise(e),this._open=!1,this.dispatchEvent(new CustomEvent("task-completed"))}catch(e){this._error=I(e,this.lang,r("save_error",this.lang))}finally{this._loading=!1}}_close(){this._open=!1}render(){if(!this._open)return o``;let e=this.lang||this.hass?.language||"en";return o`
${r("complete_title",e)}${this.taskName}
- ${this._error?n`
${this._error}
`:_} - ${this.checklist.length>0?n` + ${this._error?o`
${this._error}
`:_} + ${this.checklist.length>0?o`
- ${this.checklist.map((t,i)=>n` + ${this.checklist.map((t,i)=>o`
`:_} - ${this.taskType==="reading"?n` + ${this.taskType==="reading"?o`
- ${this.adaptiveEnabled?n` + ${this.adaptiveEnabled?o`
@@ -1956,10 +1994,10 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:" .value=${this._scheduleType} @change=${i=>this._scheduleType=i.target.value} > - ${qi.map(i=>n``)} + ${Ui.map(i=>o``)}
- ${this._scheduleType==="time_based"?n` + ${this._scheduleType==="time_based"?o` ${r("anchor_planned",e)}
- ${this.scheduleTimeEnabled?n` + ${this.scheduleTimeEnabled?o` this._dueDate=i.target.value} > `:_} + ${this._renderRecurrenceExtras()} this._earliestCompletionDays=i.target.value} > - ${this.checklistsEnabled?n` + ${this.checklistsEnabled?o`

${r("checklist_steps_optional",e)}

@@ -2374,16 +2427,16 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:" ${r("cost",e)||"Cost"} {let o=i.target.value;this._set("cost",o?Number(o):null)}} /> + @input=${i=>{let n=i.target.value;this._set("cost",n?Number(n):null)}} />
- ${this._error?n`
${this._error}
`:_} + ${this._error?o`
${this._error}
`:_}