From 7afe7add1de359695efe3f571b9cf25b11995e5d Mon Sep 17 00:00:00 2001 From: Home Assistant Version Control Date: Thu, 6 Aug 2026 13:56:25 +0000 Subject: [PATCH] 329 files --- .ha_run.lock | 2 +- .storage/lovelace_resources | 4 +- custom_components/ha_mcp_tools/const.py | 2 +- .../ha_mcp_tools/embedded_server.py | 377 ++- custom_components/ha_mcp_tools/manifest.json | 2 +- .../maintenance_supporter/__init__.py | 34 +- .../maintenance_supporter/coordinator.py | 5 + .../entity/entity_base.py | 17 +- .../frontend-src/__tests__/_panel-utils.ts | 4 +- .../frontend-src/__tests__/_test-utils.ts | 25 +- .../__tests__/parts-section.test.ts | 25 + .../__tests__/task-detail-renderer.test.ts | 54 + .../__tests__/task-detail-view.test.ts | 3 + .../components/battery-fleet-section.ts | 344 ++- .../components/complete-dialog.ts | 12 +- .../frontend-src/components/parts-section.ts | 26 + .../frontend-src/locales/cs.json | 9 + .../frontend-src/locales/da.json | 9 + .../frontend-src/locales/de.json | 9 + .../frontend-src/locales/en.json | 9 + .../frontend-src/locales/es.json | 9 + .../frontend-src/locales/fi.json | 9 + .../frontend-src/locales/fr.json | 9 + .../frontend-src/locales/hi.json | 9 + .../frontend-src/locales/hu.json | 9 + .../frontend-src/locales/it.json | 9 + .../frontend-src/locales/ja.json | 9 + .../frontend-src/locales/ko.json | 9 + .../frontend-src/locales/nb.json | 9 + .../frontend-src/locales/nl.json | 9 + .../frontend-src/locales/pl.json | 9 + .../frontend-src/locales/pt-br.json | 9 + .../frontend-src/locales/pt.json | 9 + .../frontend-src/locales/ru.json | 9 + .../frontend-src/locales/sv.json | 9 + .../frontend-src/locales/tr.json | 9 + .../frontend-src/locales/uk.json | 9 + .../frontend-src/locales/zh.json | 9 + .../frontend-src/maintenance-card.ts | 11 +- .../frontend-src/maintenance-panel.ts | 142 +- .../frontend-src/maintenance-strategy-shim.ts | 13 +- .../frontend-src/package-lock.json | 713 +---- .../frontend-src/package.json | 4 +- .../frontend-src/panel-styles.ts | 20 +- .../frontend-src/renderers/task-detail.ts | 47 +- .../frontend-src/types.ts | 18 + .../frontend/__init__.py | 21 +- .../frontend/locales/cs.json | 9 + .../frontend/locales/da.json | 9 + .../frontend/locales/de.json | 9 + .../frontend/locales/en.json | 9 + .../frontend/locales/es.json | 9 + .../frontend/locales/fi.json | 9 + .../frontend/locales/fr.json | 9 + .../frontend/locales/hi.json | 9 + .../frontend/locales/hu.json | 9 + .../frontend/locales/it.json | 9 + .../frontend/locales/ja.json | 9 + .../frontend/locales/ko.json | 9 + .../frontend/locales/nb.json | 9 + .../frontend/locales/nl.json | 9 + .../frontend/locales/pl.json | 9 + .../frontend/locales/pt-br.json | 9 + .../frontend/locales/pt.json | 9 + .../frontend/locales/ru.json | 9 + .../frontend/locales/sv.json | 9 + .../frontend/locales/tr.json | 9 + .../frontend/locales/uk.json | 9 + .../frontend/locales/zh.json | 9 + .../frontend/maintenance-calendar-card.js | 4 +- .../frontend/maintenance-card.js | 144 +- .../frontend/maintenance-panel.js | 667 +++-- .../frontend/maintenance-strategy-shim.js | 4 +- .../chunks/budget-section-card-OBF3DYBH.js | 125 + .../strategy/chunks/chunk-7IBGRLM5.js | 60 + .../strategy/chunks/chunk-C6VY6OOC.js | 1223 +++++++++ .../strategy/chunks/chunk-D4IFN5R3.js | 2 + .../strategy/chunks/dialog-mount-G4OSJIHX.js | 2435 +++++++++++++++++ .../chunks/groups-section-card-5Q5NH4JJ.js | 137 + .../chunks/vacation-section-card-BRJM72JC.js | 122 + .../maintenance-dashboard-strategy.js | 6 +- .../helpers/battery_fleet.py | 505 +++- .../helpers/battery_fleet_setup.py | 34 +- .../helpers/sensor_predictor.py | 46 +- .../helpers/signatures/kitchen.py | 18 +- .../maintenance_supporter/manifest.json | 2 +- .../maintenance_supporter/repairs.py | 136 + .../maintenance_supporter/storage.py | 19 + .../maintenance_supporter/strings.json | 64 +- .../translations/cs.json | 64 +- .../translations/da.json | 64 +- .../translations/de.json | 64 +- .../translations/en.json | 64 +- .../translations/es.json | 64 +- .../translations/fi.json | 64 +- .../translations/fr.json | 64 +- .../translations/hi.json | 64 +- .../translations/hu.json | 64 +- .../translations/it.json | 64 +- .../translations/ja.json | 64 +- .../translations/ko.json | 64 +- .../translations/nb.json | 64 +- .../translations/nl.json | 64 +- .../translations/pl.json | 64 +- .../translations/pt-BR.json | 64 +- .../translations/pt.json | 64 +- .../translations/ru.json | 64 +- .../translations/sv.json | 64 +- .../translations/tr.json | 64 +- .../translations/uk.json | 64 +- .../translations/zh-Hans.json | 64 +- .../websocket/__init__.py | 61 +- .../websocket/battery_fleet.py | 53 +- .../websocket/dashboard.py | 121 +- .../websocket/objects.py | 19 +- .../maintenance_supporter/websocket/tasks.py | 4 + .../websocket/tasks_actions.py | 43 + .../websocket/tasks_lifecycle.py | 37 +- custom_components/powercalc/__init__.py | 2 - .../__pycache__/__init__.cpython-314.pyc | Bin 34509 -> 0 bytes .../__pycache__/common.cpython-314.pyc | Bin 12532 -> 0 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 45209 -> 0 bytes .../__pycache__/const.cpython-314.pyc | Bin 15110 -> 0 bytes .../device_binding.cpython-314.pyc | Bin 7515 -> 0 bytes .../__pycache__/diagnostics.cpython-314.pyc | Bin 3663 -> 0 bytes .../__pycache__/discovery.cpython-314.pyc | Bin 38001 -> 0 bytes .../__pycache__/errors.cpython-314.pyc | Bin 4032 -> 0 bytes .../__pycache__/helpers.cpython-314.pyc | Bin 17946 -> 0 bytes .../__pycache__/migrate.cpython-314.pyc | Bin 12964 -> 0 bytes .../__pycache__/repairs.cpython-314.pyc | Bin 8927 -> 0 bytes .../__pycache__/select.cpython-314.pyc | Bin 4947 -> 0 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 36799 -> 0 bytes .../__pycache__/unit.cpython-314.pyc | Bin 4874 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 156 -> 0 bytes .../__pycache__/analytics.cpython-314.pyc | Bin 17889 -> 0 bytes .../powercalc/analytics/analytics.py | 2 - custom_components/powercalc/common.py | 3 +- custom_components/powercalc/config_flow.py | 40 +- .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 0 bytes .../config_entry_conversion.cpython-314.pyc | Bin 5253 -> 0 bytes .../discovery_info.cpython-314.pyc | Bin 1534 -> 0 bytes .../__pycache__/global_config.cpython-314.pyc | Bin 5385 -> 0 bytes .../__pycache__/normalization.cpython-314.pyc | Bin 2450 -> 0 bytes .../__pycache__/sensor_config.cpython-314.pyc | Bin 11200 -> 0 bytes .../configuration/config_entry_conversion.py | 2 - .../powercalc/configuration/discovery_info.py | 2 - .../powercalc/configuration/normalization.py | 2 - .../powercalc/configuration/sensor_config.py | 6 +- custom_components/powercalc/const.py | 7 +- custom_components/powercalc/device_binding.py | 30 +- custom_components/powercalc/discovery.py | 54 +- custom_components/powercalc/errors.py | 2 - .../__pycache__/__init__.cpython-314.pyc | Bin 153 -> 0 bytes .../__pycache__/outlier.cpython-314.pyc | Bin 3439 -> 0 bytes custom_components/powercalc/filter/outlier.py | 2 - .../__pycache__/__init__.cpython-314.pyc | Bin 158 -> 0 bytes .../__pycache__/common.cpython-314.pyc | Bin 11413 -> 0 bytes .../dynamic_field_builder.cpython-314.pyc | Bin 2356 -> 0 bytes .../profile_preview.cpython-314.pyc | Bin 10722 -> 0 bytes .../__pycache__/schema.cpython-314.pyc | Bin 7596 -> 0 bytes .../__pycache__/strategy_form.cpython-314.pyc | Bin 5597 -> 0 bytes .../powercalc/flow_helper/common.py | 1 + .../__pycache__/__init__.cpython-314.pyc | Bin 164 -> 0 bytes .../flows/__pycache__/cost.cpython-314.pyc | Bin 6841 -> 0 bytes .../__pycache__/daily_energy.cpython-314.pyc | Bin 8677 -> 0 bytes .../global_configuration.cpython-314.pyc | Bin 25243 -> 0 bytes .../flows/__pycache__/group.cpython-314.pyc | Bin 30227 -> 0 bytes .../flows/__pycache__/library.cpython-314.pyc | Bin 38772 -> 0 bytes .../__pycache__/real_power.cpython-314.pyc | Bin 4362 -> 0 bytes .../__pycache__/virtual_power.cpython-314.pyc | Bin 32852 -> 0 bytes .../powercalc/flow_helper/flows/cost.py | 54 +- .../flow_helper/flows/daily_energy.py | 2 - .../flow_helper/flows/global_configuration.py | 6 +- .../powercalc/flow_helper/flows/group.py | 2 - .../powercalc/flow_helper/flows/library.py | 71 +- .../powercalc/flow_helper/flows/real_power.py | 2 - .../flow_helper/flows/virtual_power.py | 9 +- .../powercalc/flow_helper/profile_preview.py | 2 - .../powercalc/flow_helper/schema.py | 89 +- .../powercalc/flow_helper/strategy_form.py | 2 - .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 0 bytes .../__pycache__/filter.cpython-314.pyc | Bin 31071 -> 0 bytes .../__pycache__/include.cpython-314.pyc | Bin 9804 -> 0 bytes .../powercalc/group_include/filter.py | 2 - custom_components/powercalc/manifest.json | 2 +- .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 0 bytes .../__pycache__/error.cpython-314.pyc | Bin 994 -> 0 bytes .../__pycache__/factory.cpython-314.pyc | Bin 3289 -> 0 bytes .../__pycache__/library.cpython-314.pyc | Bin 20347 -> 0 bytes .../__pycache__/power_profile.cpython-314.pyc | Bin 31141 -> 0 bytes .../sub_profile_selector.cpython-314.pyc | Bin 14391 -> 0 bytes .../powercalc/power_profile/factory.py | 2 - .../powercalc/power_profile/library.py | 2 - .../__pycache__/__init__.cpython-314.pyc | Bin 167 -> 0 bytes .../__pycache__/composite.cpython-314.pyc | Bin 6384 -> 0 bytes .../loader/__pycache__/local.cpython-314.pyc | Bin 14531 -> 0 bytes .../__pycache__/protocol.cpython-314.pyc | Bin 3942 -> 0 bytes .../loader/__pycache__/remote.cpython-314.pyc | Bin 37170 -> 0 bytes .../powercalc/power_profile/power_profile.py | 3 +- .../power_profile/sub_profile_selector.py | 2 - custom_components/powercalc/repairs.py | 2 - custom_components/powercalc/select.py | 2 - custom_components/powercalc/sensor.py | 12 +- .../__pycache__/__init__.cpython-314.pyc | Bin 154 -> 0 bytes .../__pycache__/abstract.cpython-314.pyc | Bin 7801 -> 0 bytes .../sensors/__pycache__/cost.cpython-314.pyc | Bin 23044 -> 0 bytes .../__pycache__/daily_energy.cpython-314.pyc | Bin 15928 -> 0 bytes .../__pycache__/energy.cpython-314.pyc | Bin 22324 -> 0 bytes .../energy_related.cpython-314.pyc | Bin 2949 -> 0 bytes .../sensors/__pycache__/power.cpython-314.pyc | Bin 51625 -> 0 bytes .../__pycache__/utility_meter.cpython-314.pyc | Bin 11671 -> 0 bytes .../powercalc/sensors/abstract.py | 42 +- custom_components/powercalc/sensors/cost.py | 10 +- .../powercalc/sensors/daily_energy.py | 2 - custom_components/powercalc/sensors/energy.py | 120 +- .../powercalc/sensors/energy_related.py | 2 - .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 0 bytes .../config_entry_utils.cpython-314.pyc | Bin 9373 -> 0 bytes .../group/__pycache__/custom.cpython-314.pyc | Bin 55582 -> 0 bytes .../group/__pycache__/factory.cpython-314.pyc | Bin 3856 -> 0 bytes .../group/__pycache__/standby.cpython-314.pyc | Bin 5197 -> 0 bytes .../__pycache__/subtract.cpython-314.pyc | Bin 5877 -> 0 bytes .../tracked_untracked.cpython-314.pyc | Bin 14662 -> 0 bytes .../powercalc/sensors/group/custom.py | 5 +- .../powercalc/sensors/group/standby.py | 2 - .../powercalc/sensors/group/subtract.py | 2 - .../sensors/group/tracked_untracked.py | 2 - custom_components/powercalc/sensors/power.py | 128 +- .../powercalc/sensors/utility_meter.py | 2 - .../__pycache__/__init__.cpython-314.pyc | Bin 154 -> 0 bytes .../gui_configuration.cpython-314.pyc | Bin 3004 -> 0 bytes .../powercalc/service/gui_configuration.py | 3 + custom_components/powercalc/services.yaml | 1 + .../__pycache__/__init__.cpython-314.pyc | Bin 155 -> 0 bytes .../__pycache__/composite.cpython-314.pyc | Bin 18737 -> 0 bytes .../__pycache__/factory.cpython-314.pyc | Bin 17722 -> 0 bytes .../__pycache__/fixed.cpython-314.pyc | Bin 5590 -> 0 bytes .../__pycache__/linear.cpython-314.pyc | Bin 17102 -> 0 bytes .../strategy/__pycache__/lut.cpython-314.pyc | Bin 26154 -> 0 bytes .../__pycache__/multi_switch.cpython-314.pyc | Bin 5054 -> 0 bytes .../__pycache__/playbook.cpython-314.pyc | Bin 18986 -> 0 bytes .../__pycache__/selector.cpython-314.pyc | Bin 1908 -> 0 bytes .../strategy_interface.cpython-314.pyc | Bin 3770 -> 0 bytes .../strategy/__pycache__/wled.cpython-314.pyc | Bin 6842 -> 0 bytes .../powercalc/strategy/composite.py | 2 - .../powercalc/strategy/factory.py | 2 - custom_components/powercalc/strategy/fixed.py | 2 - .../powercalc/strategy/linear.py | 2 - custom_components/powercalc/strategy/lut.py | 2 - .../powercalc/strategy/multi_switch.py | 2 - .../powercalc/strategy/playbook.py | 2 - .../powercalc/strategy/strategy_interface.py | 2 - custom_components/powercalc/strategy/wled.py | 2 - .../powercalc/translations/cs.json | 14 + .../powercalc/translations/cz.json | 1138 -------- .../powercalc/translations/da.json | 1284 ++++----- .../powercalc/translations/de.json | 14 + .../powercalc/translations/en.json | 14 + .../powercalc/translations/es.json | 14 + .../powercalc/translations/fi.json | 1298 ++++----- .../powercalc/translations/fr.json | 14 + .../powercalc/translations/hu.json | 1298 ++++----- .../powercalc/translations/it.json | 14 + .../powercalc/translations/nb.json | 1292 ++++----- .../powercalc/translations/nl.json | 14 + .../powercalc/translations/pl.json | 14 + .../powercalc/translations/pt-BR.json | 14 + .../powercalc/translations/pt.json | 14 + .../powercalc/translations/ro.json | 14 + .../powercalc/translations/ru.json | 14 + .../powercalc/translations/sk.json | 14 + .../powercalc/translations/sv.json | 14 + .../powercalc/translations/zh-Hans.json | 1300 ++++----- custom_components/powercalc/unit.py | 2 - .../__pycache__/__init__.cpython-314.pyc | Bin 97850 -> 97850 bytes .../__pycache__/binary_sensor.cpython-314.pyc | Bin 5642 -> 0 bytes .../__pycache__/button.cpython-314.pyc | Bin 14966 -> 0 bytes .../__pycache__/calendar.cpython-314.pyc | Bin 13685 -> 0 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 2852 -> 2852 bytes .../__pycache__/const.cpython-314.pyc | Bin 24181 -> 24316 bytes .../coord_assignments.cpython-314.pyc | Bin 35771 -> 35771 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 86720 -> 86720 bytes .../coord_mandatory.cpython-314.pyc | Bin 25601 -> 25601 bytes .../coord_notifications.cpython-314.pyc | Bin 44820 -> 46382 bytes .../__pycache__/coord_points.cpython-314.pyc | Bin 59211 -> 59211 bytes .../__pycache__/coord_quests.cpython-314.pyc | Bin 11086 -> 11086 bytes .../__pycache__/coord_rewards.cpython-314.pyc | Bin 34640 -> 34640 bytes .../coord_templates.cpython-314.pyc | Bin 16866 -> 16866 bytes .../__pycache__/coord_timed.cpython-314.pyc | Bin 13506 -> 13506 bytes .../__pycache__/coordinator.cpython-314.pyc | Bin 56508 -> 56508 bytes .../__pycache__/frontend.cpython-314.pyc | Bin 9669 -> 9669 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 69120 -> 69120 bytes .../__pycache__/number.cpython-314.pyc | Bin 5571 -> 0 bytes .../__pycache__/panel.cpython-314.pyc | Bin 3883 -> 3883 bytes .../__pycache__/photos.cpython-314.pyc | Bin 10791 -> 10791 bytes .../__pycache__/select.cpython-314.pyc | Bin 5025 -> 0 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 76843 -> 0 bytes .../__pycache__/storage.cpython-314.pyc | Bin 117405 -> 118637 bytes .../__pycache__/templates.cpython-314.pyc | Bin 5906 -> 5906 bytes .../taskmate/__pycache__/todo.cpython-314.pyc | Bin 6024 -> 0 bytes .../__pycache__/websocket.cpython-314.pyc | Bin 146464 -> 146683 bytes custom_components/taskmate/const.py | 4 + .../taskmate/coord_notifications.py | 32 +- custom_components/taskmate/manifest.json | 2 +- custom_components/taskmate/storage.py | 18 +- custom_components/taskmate/websocket.py | 12 +- .../taskmate/www/locales/de.json | 2 +- .../taskmate/www/locales/en-GB.json | 2 +- .../taskmate/www/locales/en.json | 2 +- .../taskmate/www/locales/fr.json | 2 +- .../taskmate/www/locales/nb.json | 2 +- .../taskmate/www/locales/nn.json | 2 +- .../taskmate/www/locales/pt-BR.json | 2 +- .../taskmate/www/locales/pt.json | 2 +- .../taskmate/www/taskmate-panel.js | 2 +- .../multiple-entity-row.js | 2 +- .../multiple-entity-row.js.gz | Bin 19872 -> 22095 bytes .../simple-thermostat.debug.js | 297 +- .../simple-thermostat.debug.js.gz | Bin 57613 -> 58839 bytes .../simple-thermostat/simple-thermostat.js | 221 +- .../simple-thermostat/simple-thermostat.js.gz | Bin 45966 -> 46957 bytes zigbee2mqtt/state.json | 44 +- 330 files changed, 13098 insertions(+), 5942 deletions(-) create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/budget-section-card-OBF3DYBH.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-7IBGRLM5.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-C6VY6OOC.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-D4IFN5R3.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/dialog-mount-G4OSJIHX.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/groups-section-card-5Q5NH4JJ.js create mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/vacation-section-card-BRJM72JC.js delete mode 100644 custom_components/powercalc/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/common.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/config_flow.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/const.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/device_binding.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/diagnostics.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/discovery.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/errors.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/helpers.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/migrate.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/repairs.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/select.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/sensor.cpython-314.pyc delete mode 100644 custom_components/powercalc/__pycache__/unit.cpython-314.pyc delete mode 100644 custom_components/powercalc/analytics/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/analytics/__pycache__/analytics.cpython-314.pyc delete mode 100644 custom_components/powercalc/configuration/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/configuration/__pycache__/config_entry_conversion.cpython-314.pyc delete mode 100644 custom_components/powercalc/configuration/__pycache__/discovery_info.cpython-314.pyc delete mode 100644 custom_components/powercalc/configuration/__pycache__/global_config.cpython-314.pyc delete mode 100644 custom_components/powercalc/configuration/__pycache__/normalization.cpython-314.pyc delete mode 100644 custom_components/powercalc/configuration/__pycache__/sensor_config.cpython-314.pyc delete mode 100644 custom_components/powercalc/filter/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/filter/__pycache__/outlier.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/__pycache__/common.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/__pycache__/dynamic_field_builder.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/__pycache__/profile_preview.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/__pycache__/schema.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/__pycache__/strategy_form.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/flows/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/flows/__pycache__/cost.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/flows/__pycache__/daily_energy.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/flows/__pycache__/global_configuration.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/flows/__pycache__/group.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/flows/__pycache__/library.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/flows/__pycache__/real_power.cpython-314.pyc delete mode 100644 custom_components/powercalc/flow_helper/flows/__pycache__/virtual_power.cpython-314.pyc delete mode 100644 custom_components/powercalc/group_include/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/group_include/__pycache__/filter.cpython-314.pyc delete mode 100644 custom_components/powercalc/group_include/__pycache__/include.cpython-314.pyc delete mode 100644 custom_components/powercalc/power_profile/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/power_profile/__pycache__/error.cpython-314.pyc delete mode 100644 custom_components/powercalc/power_profile/__pycache__/factory.cpython-314.pyc delete mode 100644 custom_components/powercalc/power_profile/__pycache__/library.cpython-314.pyc delete mode 100644 custom_components/powercalc/power_profile/__pycache__/power_profile.cpython-314.pyc delete mode 100644 custom_components/powercalc/power_profile/__pycache__/sub_profile_selector.cpython-314.pyc delete mode 100644 custom_components/powercalc/power_profile/loader/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/power_profile/loader/__pycache__/composite.cpython-314.pyc delete mode 100644 custom_components/powercalc/power_profile/loader/__pycache__/local.cpython-314.pyc delete mode 100644 custom_components/powercalc/power_profile/loader/__pycache__/protocol.cpython-314.pyc delete mode 100644 custom_components/powercalc/power_profile/loader/__pycache__/remote.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/__pycache__/abstract.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/__pycache__/cost.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/__pycache__/daily_energy.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/__pycache__/energy.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/__pycache__/energy_related.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/__pycache__/power.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/__pycache__/utility_meter.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/group/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/group/__pycache__/config_entry_utils.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/group/__pycache__/custom.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/group/__pycache__/factory.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/group/__pycache__/standby.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/group/__pycache__/subtract.cpython-314.pyc delete mode 100644 custom_components/powercalc/sensors/group/__pycache__/tracked_untracked.cpython-314.pyc delete mode 100644 custom_components/powercalc/service/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/service/__pycache__/gui_configuration.cpython-314.pyc delete mode 100644 custom_components/powercalc/strategy/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/powercalc/strategy/__pycache__/composite.cpython-314.pyc delete mode 100644 custom_components/powercalc/strategy/__pycache__/factory.cpython-314.pyc delete mode 100644 custom_components/powercalc/strategy/__pycache__/fixed.cpython-314.pyc delete mode 100644 custom_components/powercalc/strategy/__pycache__/linear.cpython-314.pyc delete mode 100644 custom_components/powercalc/strategy/__pycache__/lut.cpython-314.pyc delete mode 100644 custom_components/powercalc/strategy/__pycache__/multi_switch.cpython-314.pyc delete mode 100644 custom_components/powercalc/strategy/__pycache__/playbook.cpython-314.pyc delete mode 100644 custom_components/powercalc/strategy/__pycache__/selector.cpython-314.pyc delete mode 100644 custom_components/powercalc/strategy/__pycache__/strategy_interface.cpython-314.pyc delete mode 100644 custom_components/powercalc/strategy/__pycache__/wled.cpython-314.pyc delete mode 100644 custom_components/powercalc/translations/cz.json delete mode 100644 custom_components/taskmate/__pycache__/binary_sensor.cpython-314.pyc delete mode 100644 custom_components/taskmate/__pycache__/button.cpython-314.pyc delete mode 100644 custom_components/taskmate/__pycache__/calendar.cpython-314.pyc delete mode 100644 custom_components/taskmate/__pycache__/number.cpython-314.pyc delete mode 100644 custom_components/taskmate/__pycache__/select.cpython-314.pyc delete mode 100644 custom_components/taskmate/__pycache__/sensor.cpython-314.pyc delete mode 100644 custom_components/taskmate/__pycache__/todo.cpython-314.pyc diff --git a/.ha_run.lock b/.ha_run.lock index d07c09b..23e6385 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 71, "version": 1, "ha_version": "2026.7.4", "start_ts": 1785684875.7532482} \ No newline at end of file +{"pid": 70, "version": 1, "ha_version": "2026.7.4", "start_ts": 1786024552.5388079} \ No newline at end of file diff --git a/.storage/lovelace_resources b/.storage/lovelace_resources index 824c6e1..89b200c 100644 --- a/.storage/lovelace_resources +++ b/.storage/lovelace_resources @@ -196,7 +196,7 @@ }, { "id": "e92ef0caff41454e9f49ea966ed99e41", - "url": "/hacsfiles/lovelace-multiple-entity-row/multiple-entity-row.js?hacstag=178921037471", + "url": "/hacsfiles/lovelace-multiple-entity-row/multiple-entity-row.js?hacstag=178921037490", "type": "module" }, { @@ -261,7 +261,7 @@ }, { "id": "6c3f7d87683d4473b5dc8fe8177535f6", - "url": "/hacsfiles/simple-thermostat/simple-thermostat.js?hacstag=1230152807410", + "url": "/hacsfiles/simple-thermostat/simple-thermostat.js?hacstag=1230152807420", "type": "module" }, { diff --git a/custom_components/ha_mcp_tools/const.py b/custom_components/ha_mcp_tools/const.py index a648573..48e2862 100644 --- a/custom_components/ha_mcp_tools/const.py +++ b/custom_components/ha_mcp_tools/const.py @@ -25,7 +25,7 @@ DOMAIN = "ha_mcp_tools" # in CI. The # capability negotiation — not this version — gates each WS command (see # ``websocket_api.CAPABILITIES``). -COMPONENT_VERSION = "1.3.1" +COMPONENT_VERSION = "1.3.2" # Config-entry discriminator (``entry.data[CONF_ENTRY_TYPE]``). A missing value # means "tools" so the pre-existing services entry keeps working across the diff --git a/custom_components/ha_mcp_tools/embedded_server.py b/custom_components/ha_mcp_tools/embedded_server.py index 18db056..0019d3a 100644 --- a/custom_components/ha_mcp_tools/embedded_server.py +++ b/custom_components/ha_mcp_tools/embedded_server.py @@ -32,6 +32,7 @@ import importlib.metadata import importlib.util import logging import os +import site import subprocess import sys import threading @@ -39,6 +40,7 @@ from contextlib import suppress from datetime import timedelta from functools import partial from typing import TYPE_CHECKING, Any, Literal +from urllib.parse import urlparse from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.models import TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN @@ -49,7 +51,7 @@ from homeassistant.requirements import ( async_process_requirements, pip_kwargs, ) -from homeassistant.util.package import install_package +from homeassistant.util.package import is_virtual_env from packaging.requirements import InvalidRequirement, Requirement from packaging.utils import canonicalize_name from packaging.version import InvalidVersion, Version @@ -133,6 +135,11 @@ _PIP_INSTALL_TIMEOUT_SECONDS = 300 # Uninstall just removes files/metadata, so it is quick; cap it so a wedged # subprocess can never tie up an executor thread indefinitely. _PIP_UNINSTALL_TIMEOUT_SECONDS = 120 +# Upper bound for ONE uv install attempt. Generous (a cold ARM wheel build +# is slow but finite) and bounded, so a wedged uv cannot hold the +# process-wide tracked-install slot — and with it the next bring-up — +# forever. The extra-index fallback can spend this twice. +_UV_INSTALL_TIMEOUT_SECONDS = 1800 # How long a bring-up waits for an install job orphaned by a CANCELLED # previous bring-up before giving up: asyncio cancellation detaches the @@ -651,23 +658,28 @@ class EmbeddedServerManager: With auto-update on (the default) both channels install their distribution UNPINNED, so every entry reload / HA restart must pick up the newest build. Such a spec ALWAYS takes the force-install path - (``upgrade=True``, bypassing the requirements manager's is-installed - shortcut) — that is what makes the channel auto-update. This runs in a + (``--upgrade-package ``, bypassing the requirements manager's + is-installed shortcut) — that is what makes the channel auto-update, + and scoping the upgrade to our own distribution is what keeps it + from replacing packages Home Assistant ships (#2135/#2146). This runs in a background task, so it never blocks HA startup, and uv no-ops quickly when the newest build is already installed. - Fast path: reserved for a STABLE spec — an explicit pip-spec override (a - version pin or tarball URL) or a channel with auto-update turned OFF + Fast path: reserved for a stable INDEX spec — an explicit pip-spec + override that is a version pin, or a channel with auto-update turned OFF (which pins to the installed version, see :meth:`_resolve_pip_spec`). When that spec matches the one last installed and the package imports, delegate the "already satisfied?" decision to Home Assistant's requirements manager; a pinned spec does not move, so there is nothing to - upgrade to. A CHANGED spec (a new override, a cleared override, a + upgrade to. A URL override (a tarball or ``file://`` wheel) is + deliberately EXCLUDED: HA's is-installed check cannot verify a URL + requirement, so delegating one always reaches its bare ``--upgrade`` + install — see the comment on ``spec_is_stable`` below. A CHANGED spec (a new override, a cleared override, a toggled auto-update, a channel switch) falls through to the force-install path below — and additionally uninstalls the replaced distribution first (:meth:`_async_remove_replaced_source`), because - ``upgrade=True`` alone decides by version and a changed SOURCE can keep - the version string (issue #1914). + the upgrade flag alone decides by version and a changed SOURCE can + keep the version string (issue #1914). On a channel switch the other channel's distribution is uninstalled first (:meth:`_async_remove_conflicting_dist`): ``ha-mcp`` and ``ha-mcp-dev`` @@ -737,7 +749,20 @@ class EmbeddedServerManager: # A "stable" spec (an explicit override, or a channel pinned because # auto-update is off) is eligible for the fast path; an unpinned # auto-updating channel never is. - spec_is_stable = bool(self._pip_spec_override) or not self._auto_update + # A URL spec is never eligible, however stable it looks. The fast + # path delegates to HA's requirements manager, and + # homeassistant.util.package.is_installed() returns False for ANY + # requirement carrying a URL ("we cannot verify versions, so let the + # package manager handle it"), so async_process_requirements always + # reaches install_package(), whose upgrade default appends a bare + # --upgrade. That re-resolves the whole graph and replaces packages + # HA only floors — the #2135/#2146 tear, on every restart. Routing + # URL specs to the force path costs a scoped --reinstall-package of + # OUR distribution only, which is the install HA would have done + # anyway, minus the stomp. + spec_is_stable = ( + bool(self._pip_spec_override) or not self._auto_update + ) and not _spec_is_url_requirement(self._pip_spec) fast_path_ok = ( spec_is_stable and stored_spec == self._pip_spec @@ -773,9 +798,10 @@ class EmbeddedServerManager: raise EmbeddedServerError( f"The installer left installed ha-mcp {version}, but this " f"in-process component requires {MIN_EMBEDDED_SERVER_VERSION} " - "or newer. Review resolver details logged under " - "homeassistant.util.package, correct the package conflict, and " - "reload this integration.", + "or newer. Review the installer output logged under " + "custom_components.ha_mcp_tools.embedded_server (or, for an " + "index spec taking the fast path, homeassistant.util.package), " + "correct the package conflict, and reload this integration.", kind="package", ) _LOGGER.info("HA-MCP in-process server package ready (version %s)", version) @@ -865,7 +891,8 @@ class EmbeddedServerManager: Returns None for an override that names an unknown distribution or does not parse as a requirement at all (a direct URL): the installer - re-fetches and rebuilds URL requirements under ``upgrade=True`` + reinstalls a named URL requirement outright + (``--reinstall-package``, see :func:`_force_install_package`) regardless of the installed version, so a URL install is already real and nothing needs removing. """ @@ -885,7 +912,7 @@ class EmbeddedServerManager: ) -> None: """Uninstall the replaced distribution when the requested source changed. - The forced install that follows relies on ``upgrade=True``, and the + The forced install that follows relies on its upgrade flag, and the installer decides "already satisfied" by VERSION alone — but a source change can keep the version string. A PR branch's committed ``project.version`` equals the release it branched from (only release @@ -903,7 +930,7 @@ class EmbeddedServerManager: Skipped when nothing is installed, when the last-installed spec is unknown (nothing to compare: first install, or entry data predating the spec tracking), when the spec is unchanged (the routine - reload/restart path, where ``upgrade=True`` alone is correct and an + reload/restart path, where the upgrade flag alone is correct and an uninstall would churn — and briefly break — a healthy install on every restart), when the new spec is a direct URL (always installs for real), when the named distribution is not installed (e.g. a @@ -928,6 +955,19 @@ class EmbeddedServerManager: return if stored_spec == self._pip_spec: return + if _spec_is_url_requirement(self._pip_spec): + # A URL spec is reinstalled outright (--reinstall-package, see + # _scoped_install_flags), so the install cannot be skipped as + # "already satisfied" and there is nothing for this uninstall to + # unblock. Removing first would only delete the working build + # BEFORE the new URL is fetched, so a failed fetch (bad path, + # network, moved tarball) leaves no server installed at all — + # and it reopens the uninstall-then-extract window on our own + # package. _replaced_dist_name() already declines for a BARE + # url; a NAMED one ("ha-mcp @ file:///…", the shape the config + # flow and the e2e lane use) parses fine and would fall through + # to the removal below without this. + return replaced_dist = self._replaced_dist_name() if replaced_dist is None: return @@ -936,20 +976,27 @@ class EmbeddedServerManager: # code on disk came from the index too, so "already satisfied by # version" is the truth, not the #1914 lie. return - pinned = _exact_pinned_version(self._pip_spec) - if pinned is not None: - try: - version_moves = Version(pinned) != Version(installed_version) - except InvalidVersion: - version_moves = False # unprovable — keep the uninstall - if version_moves: - # The new pin cannot be satisfied by the installed version, so - # the forced install is guaranteed to be real without any - # uninstall — and keeping the working build in place preserves - # it as the fallback if that install fails (e.g. offline). - return if not await self._hass.async_add_executor_job(_dist_installed, replaced_dist): return + # Compare the pin against the version of the distribution actually + # being replaced, not the caller's ``installed_version``: that one is + # read from whichever dist provides ``ha_mcp`` and is read BEFORE + # _async_remove_conflicting_dist() runs, so on a cross-channel switch + # it can describe the other channel's dist — or one already + # uninstalled. Comparing against it could report "the pin moved" for a + # target that is in fact already at the pinned version, skip this + # uninstall, and let the install no-op as satisfied (#1914). + replaced_version = await self._hass.async_add_executor_job( + _installed_dist_version, replaced_dist + ) + if replaced_version is not None and _pin_moves_off_installed( + self._pip_spec, replaced_version + ): + # The new pin cannot be satisfied by the installed version, so the + # forced install is guaranteed to be real without any uninstall — + # and keeping the working build in place preserves it as the + # fallback if that install fails (e.g. offline). + return _LOGGER.info( "The requested server source changed (%r -> %r); removing the " "installed %r first so the reinstall cannot be skipped as " @@ -992,23 +1039,36 @@ class EmbeddedServerManager: Mirrors how ``homeassistant.requirements`` builds its pip invocation (HA's own constraints file + ``config/deps`` target where applicable) so - the resolver honors Home Assistant's constraints, then installs with - ``upgrade=True`` and a generous per-download timeout. + the resolver honors Home Assistant's constraints, with one deliberate + difference from ``install_package(upgrade=True)``: that maps to uv's + EAGER ``--upgrade``, which re-resolves the whole dependency graph to + the newest allowed versions and replaces packages Home Assistant + already ships even when the installed version satisfies our spec — + exactly how the image's websockets kept getting force-replaced + (#2135/#2146). ``--upgrade-package`` scopes the upgrade to ha-mcp's + own distribution: the server still auto-updates, every other + installed package is kept whenever it satisfies the resolution. """ kwargs = pip_kwargs(self._hass.config.config_dir) - kwargs["timeout"] = max( - int(kwargs.get("timeout") or 0), _PIP_INSTALL_TIMEOUT_SECONDS - ) + timeout = max(int(kwargs.get("timeout") or 0), _PIP_INSTALL_TIMEOUT_SECONDS) installed = await self._async_run_tracked_install_job( - partial(install_package, self._pip_spec, upgrade=True, **kwargs) + partial( + _force_install_package, + self._pip_spec, + channel_dist=dist_for_channel(self._channel), + constraints=kwargs.get("constraints"), + target=kwargs.get("target"), + timeout=timeout, + ) ) if not installed: raise EmbeddedServerError( f"Could not install the server ({self._pip_spec!r}). The " f"in-process server requires ha-mcp " f"{MIN_EMBEDDED_SERVER_VERSION} or newer and Home Assistant " - f"{MIN_EMBEDDED_HOME_ASSISTANT_VERSION} or newer. Resolver " - "details are logged under homeassistant.util.package.", + f"{MIN_EMBEDDED_HOME_ASSISTANT_VERSION} or newer. The " + "installer's output is logged under " + "custom_components.ha_mcp_tools.embedded_server.", kind="package", ) @@ -1380,7 +1440,16 @@ class EmbeddedServerManager: port=self._port, timeout_graceful_shutdown=2, lifespan="on", - ws="websockets-sansio", + # HTTP-ONLY listener, so no WebSocket protocol is loaded. uvicorn + # resolves its ``ws`` class EAGERLY in Config.load(), and + # "websockets-sansio" imports the SHARED websockets package — + # the unowned, tearable copy ha-mcp vendors its own copy to stay + # clear of (#2135/#2146). With that setting a torn shared install + # crashed this server at listener startup no matter what the + # client imports. "none" resolves to None and imports nothing; + # the MCP app serves Streamable HTTP and registers no WebSocket + # route. Pinned by tests/src/unit/test_vendored_websockets.py. + ws="none", # Leave Home Assistant's logging untouched — do not let uvicorn # reconfigure the root logger from this thread. log_config=None, @@ -1919,6 +1988,242 @@ def _uninstall_distribution(dist_name: str, *, target: str | None = None) -> boo return True +def _force_install_package( + spec: str, + *, + channel_dist: str | None, + constraints: str | None, + target: str | None, + timeout: int | None, +) -> bool: + """Install ``spec``, touching ONLY our own distribution (blocking). + + Mirrors ``homeassistant.util.package.install_package``'s uv invocation + (index strategy, constraints, target, the uv --user workaround, and the + HTTP_TIMEOUT env) but never its eager ``--upgrade``, which re-resolves + EVERY dependency to the newest allowed version and replaces packages the + Home Assistant image already ships (#2135/#2146). The replacement flag + is chosen per spec shape by :func:`_scoped_install_flags`; + ``channel_dist`` is the distribution the active channel installs, used + to scope a bare URL that names none of its own. + """ + env = os.environ.copy() + if timeout: + env["HTTP_TIMEOUT"] = str(timeout) + args = _uv_install_args( + spec, + channel_dist=channel_dist, + constraints=constraints, + target=target, + env=env, + ) + _LOGGER.info("Installing the in-process server package: %s", spec) + stderr = _run_uv_install(args, env) + if stderr is None: + return True + + # install_package's extra-index fallback, mirrored: uv treats a failing + # extra index as FATAL where pip merely skips it, so a wheels-index + # outage would otherwise fail a bring-up that PyPI could satisfy on its + # own. When the error names an extra-index host, retry with that host + # dropped. Matched on host because wheel files may live outside the + # index path. The warning names the failing HOSTS rather than the + # configured URLs (which can carry credentials); uv's own stderr is + # included as-is, exactly as install_package logs it. + extra_urls = env.get("UV_EXTRA_INDEX_URL", "").split() + failing = { + url: host for url in extra_urls if (host := _url_host(url)) and host in stderr + } + if failing: + _LOGGER.warning( + "Could not install %r using extra index host %s: %s; retrying without it", + spec, + ", ".join(failing.values()), + stderr, + ) + retry_env = env.copy() + if remaining := [url for url in extra_urls if url not in failing]: + retry_env["UV_EXTRA_INDEX_URL"] = " ".join(remaining) + else: + del retry_env["UV_EXTRA_INDEX_URL"] + stderr = _run_uv_install(args, retry_env) + if stderr is None: + return True + + _LOGGER.error("Could not install %r: %s", spec, stderr) + return False + + +def _scoped_install_flags(spec: str, channel_dist: str | None) -> list[str]: + """Return the uv flag that scopes this install to OUR distribution. + + Never a bare ``--upgrade``: that re-resolves the whole graph and + replaces packages Home Assistant ships (#2135/#2146). Which scoped flag + is right depends on the SPEC SHAPE, not on which distribution it names: + + * A URL requirement must be REINSTALLED. Measured on uv 0.11.33 (the + version CI pins), re-running an unchanged ``name @ file://…`` spec + reports "Checked 1 package" under both no flag and + ``--upgrade-package`` — it installs nothing — while + ``--reinstall-package`` replaces it. Auditing-and-skipping would keep + the OLD code running while the bring-up logs success (the #1914 + shape), and it is exactly the "a URL install is always real" + guarantee that ``_replaced_dist_name`` and + ``_async_remove_replaced_source`` skip their uninstall on. + * An index requirement only needs an UPGRADE, scoped to the + distribution the spec itself names. Force-reinstalling one instead + would reopen the non-atomic uninstall-then-extract window this PR + exists to close, on every bring-up, for a spec that never needed it. + + A bare URL names no distribution of its own, and the channel's dist is + the wrong guess: a repository tarball installs as ``ha-mcp`` whatever + channel is selected (see :meth:`_replaced_dist_name`), so on the dev + channel scoping to ``ha-mcp-dev`` would name a package the URL does not + provide — uv would report success while leaving the real ``ha-mcp`` + un-refreshed, and a mutable URL (a branch tarball, a rebuilt artifact) + keeps its version string, so nothing else would catch it. Both known + dists are therefore named: reinstalling one that is not installed is a + harmless no-op for uv (verified: exit 0, package still installed from + the URL). + """ + try: + requirement = Requirement(spec) + except InvalidRequirement: + candidates = [channel_dist] if channel_dist else [] + candidates += [DIST_NAME_STABLE, DIST_NAME_DEV] + flags: list[str] = [] + for dist in dict.fromkeys(candidates): # ordered, deduplicated + flags += ["--reinstall-package", dist] + return flags + if requirement.url is not None: + return ["--reinstall-package", requirement.name] + return ["--upgrade-package", requirement.name] + + +def _url_host(url: str) -> str | None: + """Host of ``url``, or None when it cannot be parsed. + + ``urlparse().hostname`` RAISES on a malformed URL (``ValueError: + Invalid IPv6 URL`` for an unclosed bracket), and the one caller runs on + the INSTALL-FAILURE path — where an operator's typo'd + ``UV_EXTRA_INDEX_URL`` entry would replace uv's real stderr with a + traceback from the error handler. + """ + try: + return urlparse(url).hostname + except ValueError: + return None + + +def _pin_moves_off_installed(spec: str, installed_version: str) -> bool: + """True when an exact-pin ``spec`` CANNOT be satisfied by what's installed. + + Asks the pin's own specifier rather than comparing parsed versions: PEP + 440 ``==1.0`` matches an installed ``1.0+local``, while + ``Version("1.0") != Version("1.0+local")`` is True. A version comparison + would therefore call that pin "moved", skip the caller's uninstall, and + let the installer no-op it as already satisfied — the #1914 shape. + + False when the spec is not an exact pin, and False whenever the answer + is unprovable (unparseable requirement or version): "unknown" must not + be mistaken for "guaranteed to move", since the caller skips its + uninstall on a True. + """ + if _exact_pinned_version(spec) is None: + return False + try: + requirement = Requirement(spec) + # Validate the installed version explicitly: SpecifierSet.contains() + # answers False for an unparseable version rather than raising, and + # False here would invert to "moved" — the unsafe direction. + Version(installed_version) + except (InvalidRequirement, InvalidVersion): + return False + if requirement.marker is not None and not requirement.marker.evaluate(): + # The requirement does not apply to this interpreter, so the + # installer will skip it entirely — the pin cannot make the install + # real, whatever version it names. + return False + return not requirement.specifier.contains(installed_version, prereleases=True) + + +def _spec_is_url_requirement(spec: str) -> bool: + """True when ``spec`` installs from a URL rather than an index. + + Same shape test :func:`_scoped_install_flags` routes on, and for the + same reason — an unparseable spec is treated as URL-ish so it takes the + conservative path. + """ + try: + return Requirement(spec).url is not None + except InvalidRequirement: + return True + + +def _uv_install_args( + spec: str, + *, + channel_dist: str | None, + constraints: str | None, + target: str | None, + env: dict[str, str], +) -> list[str]: + """Build the ``uv pip install`` argv (mirrors install_package's shape).""" + args = [ + sys.executable, + "-m", + "uv", + "pip", + "install", + "--quiet", + spec, + # Mirrors install_package: custom components may need a different + # version of a package than the one HA built wheels for. + "--index-strategy", + "unsafe-first-match", + ] + args += _scoped_install_flags(spec, channel_dist) + if constraints is not None: + args += ["--constraint", constraints] + if target: + args += ["--target", os.path.abspath(target)] + elif ( + not is_virtual_env() + # install_package's _UV_ENV_PYTHON_VARS, mirrored: an explicit uv + # python selection means uv already installs to the right place. + and not any(var in env for var in ("UV_SYSTEM_PYTHON", "UV_PYTHON")) + and (user_site := site.getusersitepackages()) + ): + # uv has no --user (astral-sh/uv#2077); install_package's workaround. + args += ["--python", sys.executable, "--target", os.path.abspath(user_site)] + return args + + +def _run_uv_install(args: list[str], env: dict[str, str]) -> str | None: + """Run one uv install attempt; return None on success, else its stderr. + + Bounded by ``_UV_INSTALL_TIMEOUT_SECONDS``: this runs inside the + process-wide tracked-install slot, and the extra-index fallback can run + it twice, so a wedged uv would otherwise pin an executor thread (and + block the next bring-up) with no upper bound. The budget is deliberately + generous — a cold ARM wheel build is slow but finite. + """ + try: + result = subprocess.run( + args, + capture_output=True, + text=True, + check=False, + env=env, + timeout=_UV_INSTALL_TIMEOUT_SECONDS, + ) + except (OSError, subprocess.SubprocessError) as err: + return f"{type(err).__name__}: {err}" + if result.returncode != 0: + return (result.stderr or "").strip() or f"exit code {result.returncode}" + return None + + def _exact_pinned_version(spec: str) -> str | None: """Return the version of an exact ``==``/``===`` single-clause pin, or None. diff --git a/custom_components/ha_mcp_tools/manifest.json b/custom_components/ha_mcp_tools/manifest.json index 9ebba5b..db8fac0 100644 --- a/custom_components/ha_mcp_tools/manifest.json +++ b/custom_components/ha_mcp_tools/manifest.json @@ -22,5 +22,5 @@ "requirements": [ "ruamel.yaml>=0.18.0" ], - "version": "1.3.1" + "version": "1.3.2" } diff --git a/custom_components/maintenance_supporter/__init__.py b/custom_components/maintenance_supporter/__init__.py index 818f1d9..9e2352e 100644 --- a/custom_components/maintenance_supporter/__init__.py +++ b/custom_components/maintenance_supporter/__init__.py @@ -1114,9 +1114,15 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # here means the split never has anything to split. if minor < 5: if is_object_entry and (device_id := (data.get(CONF_OBJECT) or {}).get("ha_device_id")): - from .helpers.device_link import shed_owned_devices + from .helpers.device_link import is_self_link, shed_owned_devices - shed_owned_devices(hass, own_entry_id=entry.entry_id, source_device_id=device_id) + # A SELF-link (the stored id names our own device — the old picker + # offered the object's doppelgänger) must not be shed: the "source" + # is the very device the entities live on, and shedding it deletes + # and restores it in one boot (prod 2026-08-01, three Roborocks). + # Setup raises the device_link_self notice for these instead. + if not is_self_link(hass, device_id, own_entry_id=entry.entry_id): + shed_owned_devices(hass, own_entry_id=entry.entry_id, source_device_id=device_id) minor = 5 hass.config_entries.async_update_entry(entry, data=data, minor_version=minor) @@ -1277,16 +1283,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf # moment the link resolves again. from homeassistant.helpers import issue_registry as ir + from .helpers.device_link import is_self_link + issue_id = f"device_link_lost_{entry.entry_id}" if resolved is None: + # Two distinct stories share one issue id (so resolve/delete + # stays single-path): the linked device is GONE, or the link + # points at the object's own doppelgänger device — the old + # picker offered it under the appliance's exact name. The + # advice differs, so the translation key does too. Both are + # fixable: the flow relinks to the device the user picks. + self_link = is_self_link(hass, stored_device_id, own_entry_id=entry.entry_id) ir.async_create_issue( hass, DOMAIN, issue_id, - is_fixable=False, + is_fixable=True, severity=ir.IssueSeverity.WARNING, - translation_key="device_link_lost", + translation_key="device_link_self" if self_link else "device_link_lost", translation_placeholders={"object": obj_data.get("name") or entry.title}, + data={"entry_id": entry.entry_id}, ) else: ir.async_delete_issue(hass, DOMAIN, issue_id) @@ -1347,6 +1363,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + if not is_global: + # Parent nesting is written as an explicit registry `via_device_id` + # AFTER the platforms created the devices — DeviceInfo's `via_device` + # identifier tuple is deprecated (removal HA 2027.8) and its + # cross-entry identifier lookup is what 2026.8's scoping ends. Runs + # both directions, so a child that boots before its parent still nests. + from .helpers.device_link import sync_via_device_links + + sync_via_device_links(hass, entry) + if linked_device_live: # The entities of a linked object live on the appliance'''s device, so a # device of our own with nothing on it is a leftover — of the object'''s diff --git a/custom_components/maintenance_supporter/coordinator.py b/custom_components/maintenance_supporter/coordinator.py index 4d743b0..3702a37 100644 --- a/custom_components/maintenance_supporter/coordinator.py +++ b/custom_components/maintenance_supporter/coordinator.py @@ -1050,6 +1050,9 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): used_parts=enriched_used, auto=auto, ) + # #73: a completed cycle retires its in-cycle checklist ticks — the + # snapshot that matters is in the history entry above. + self._store.clear_checklist_progress(task_id) # Link the completion photo to this task so it also surfaces under the # object's documents and is deref'd correctly on cleanup. Best-effort: @@ -1248,6 +1251,8 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): # (not a deliberate skip). An explicit as_missed=True always wins. missed = as_missed or task.status == MaintenanceStatus.OVERDUE task.skip(reason=reason, as_missed=missed) + # #73: skipping restarts the cycle — the ticks belong to the old one. + self._store.clear_checklist_progress(task_id) await self._persist_and_signal_task_change(task_id, task) diff --git a/custom_components/maintenance_supporter/entity/entity_base.py b/custom_components/maintenance_supporter/entity/entity_base.py index 388a479..a832b82 100644 --- a/custom_components/maintenance_supporter/entity/entity_base.py +++ b/custom_components/maintenance_supporter/entity/entity_base.py @@ -8,7 +8,7 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from ..const import CONF_OBJECT, DOMAIN, GLOBAL_UNIQUE_ID +from ..const import CONF_OBJECT, DOMAIN from ..coordinator import MaintenanceCoordinator from ..helpers import device_link @@ -71,12 +71,14 @@ class MaintenanceEntity(CoordinatorEntity[MaintenanceCoordinator]): * linked to an existing device → ``None``. The entity is already attached via ``device_entry`` (see ``__init__``); describing a device here would create one of our own instead. - * ``parent_entry_id`` set → our own device, nested under the parent - object's device via ``via_device``. - * neither → our own stand-alone device (the historical shape). + * otherwise → our own device. Parent nesting (``parent_entry_id``) is + NOT declared here: DeviceInfo's ``via_device`` identifier tuple is + deprecated (removal HA 2027.8) and resolves identifiers across + config entries — exactly what 2026.8's registry scoping ends. + Setup writes ``via_device_id`` into the registry after the platforms + load (``device_link.sync_via_device_links``). """ obj = self._object_data - hass = self.coordinator.hass if self._linked_device is not None: return None @@ -102,11 +104,6 @@ class MaintenanceEntity(CoordinatorEntity[MaintenanceCoordinator]): if obj.get("area_id"): device_info["suggested_area"] = obj["area_id"] - if parent_entry_id := obj.get("parent_entry_id"): - parent = hass.config_entries.async_get_entry(parent_entry_id) - if parent is not None and parent.domain == DOMAIN and parent.unique_id and parent.unique_id != GLOBAL_UNIQUE_ID: - device_info["via_device"] = (DOMAIN, parent.unique_id) - return device_info @property diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/_panel-utils.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/_panel-utils.ts index ffbf6d0..4044b63 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/_panel-utils.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/_panel-utils.ts @@ -66,7 +66,7 @@ export async function mountPanel( objects: unknown[], extraHandlers: Record = {}, ) { - const { hass, sent } = createMockHass({ + const { hass, sent, subscriptions } = createMockHass({ handlers: { "maintenance_supporter/objects": () => ({ objects }), "maintenance_supporter/statistics": () => ({ @@ -97,7 +97,7 @@ export async function mountPanel( await el.updateComplete; await new Promise((r) => setTimeout(r, 10)); await el.updateComplete; - return { el, sent }; + return { el, sent, subscriptions }; } export function sr(el: HTMLElement): ShadowRoot { diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/_test-utils.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/_test-utils.ts index 91c4657..7a49489 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/_test-utils.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/_test-utils.ts @@ -97,7 +97,10 @@ export type WsHandler = (msg: SentMessage) => Promise | unknown; export interface CreateMockHassResult { hass: { language: string; - connection: { sendMessagePromise: (msg: SentMessage) => Promise }; + connection: { + sendMessagePromise: (msg: SentMessage) => Promise; + subscribeMessage: (cb: (event: unknown) => void, msg: SentMessage) => Promise<() => void>; + }; callService: ( domain: string, service: string, data?: Record, target?: Record, @@ -107,6 +110,8 @@ export interface CreateMockHassResult { }; sent: SentMessage[]; serviceCalls: ServiceCall[]; + /** Captured subscribeMessage registrations — push events via `.push(ev)`. */ + subscriptions: Array<{ msg: SentMessage; push: (event: unknown) => void }>; } export interface CreateMockHassOptions { @@ -157,15 +162,31 @@ export function createMockHass(opts: CreateMockHassOptions = {}): CreateMockHass serviceCalls.push({ domain, service, data, target }); }; + // Captured subscriptions: tests push events into components via + // `subscriptions.find(...).push(event)`. + const subscriptions: Array<{ msg: SentMessage; push: (event: unknown) => void }> = []; + const subscribeMessage = async ( + cb: (event: unknown) => void, + msg: SentMessage, + ): Promise<() => void> => { + const entry = { msg, push: (event: unknown) => cb(event) }; + subscriptions.push(entry); + return () => { + const i = subscriptions.indexOf(entry); + if (i >= 0) subscriptions.splice(i, 1); + }; + }; + return { hass: { language: opts.language ?? "en", - connection: { sendMessagePromise }, + connection: { sendMessagePromise, subscribeMessage }, callService, services: opts.services, states: opts.states, }, sent, serviceCalls, + subscriptions, }; } diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/parts-section.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/parts-section.test.ts index 3d8751d..2f77c89 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/parts-section.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/parts-section.test.ts @@ -38,6 +38,31 @@ async function mount(canWrite: boolean): Promise { } describe("parts-section", () => { + it("shows the inventory value only when a part has price AND tracked stock (#104)", async () => { + // PARTS as-is: p1 has stock but no cost, p2 no stock → chip hidden. + const bare = await mount(false); + expect(bare.shadowRoot!.querySelector(".inventory-value")).to.equal(null); + + const priced: MaintenancePart[] = [ + { id: "p1", name: "Filter", cost: 12.5, stock: 3, is_low: false }, + { id: "p2", name: "Brush", cost: 4, stock: null, is_low: false }, // untracked → no contribution + { id: "p3", name: "Seal", cost: null, stock: 5, is_low: false }, // unpriced → no contribution + ]; + const el = await fixture(html` + ({}) } } as never} + .entryId=${"e1"} + .parts=${priced} + .currencySymbol=${"€"} + > + `); + await el.updateComplete; + const chip = el.shadowRoot!.querySelector(".inventory-value")!; + expect(chip, "value chip rendered").to.exist; + expect(chip.textContent).to.include("37.50"); + expect(chip.textContent).to.include("€"); + }); + it("renders a row per part with stock badge, identifiers and location", async () => { const el = await mount(false); const rows = el.shadowRoot!.querySelectorAll(".part-row"); 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 76137a9..6bc320a 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 @@ -57,6 +57,9 @@ function ctx(overrides: Partial = {}): TaskDetailContext { taskId: "t1", objectName: "Pool Pump", objectDocUrl: null, + objectManualDocs: [], + openManualDoc: () => {}, + setChecklistItem: () => {}, isOperator: false, actionLoading: false, moreMenuOpen: false, @@ -212,6 +215,57 @@ describe("task-detail renderer", () => { expect(host2.querySelector(".kpi-bar")).to.be.null; }); + it("object-manual row falls back to an attached manual when the URL is empty", () => { + let opened: unknown = null; + const host = mount(task(), ctx({ + objectDocUrl: null, + objectManualDocs: [{ id: "d1", title: "Pump Handbook", kind: "file" }], + openManualDoc: (d) => { opened = d; }, + })); + const link = [...host.querySelectorAll(".task-meta-link a")] + .find((a) => a.textContent?.includes("Pool Pump")) as HTMLElement | undefined; + expect(link, "fallback manual link rendered").to.exist; + expect(link!.getAttribute("title")).to.equal("Pump Handbook"); + link!.click(); + expect((opened as { id?: string })?.id).to.equal("d1"); + }); + + it("the URL field still wins over attached manuals", () => { + const host = mount(task(), ctx({ + objectDocUrl: "https://vendor.example/manual", + objectManualDocs: [{ id: "d1", title: "Pump Handbook", kind: "file" }], + })); + const link = [...host.querySelectorAll(".task-meta-link a")] + .find((a) => a.textContent?.includes("Pool Pump")) as HTMLElement | undefined; + expect(link, "manual row rendered").to.exist; + expect(link!.getAttribute("href")).to.equal("https://vendor.example/manual"); + }); + + it("checklist ticks render from progress and fire setChecklistItem (#73)", () => { + const calls: Array<[string, boolean]> = []; + const host = mount( + task({ checklist: ["Drain", "Clean", "Refill"], checklist_progress: { Clean: true } }), + ctx({ + features: { + adaptive: false, predictions: false, seasonal: false, + environmental: false, budget: false, groups: false, + checklists: true, schedule_time: false, completion_actions: false, + }, + setChecklistItem: (item, done) => calls.push([item, done]), + }), + ); + const header = host.querySelector(".checklist-preview-header")!; + expect(header.textContent).to.include("1/3"); + const boxes = [...host.querySelectorAll(".checklist-preview-list input")]; + expect(boxes.length).to.equal(3); + expect(boxes[1].checked).to.be.true; + expect(boxes[0].checked).to.be.false; + expect(host.querySelectorAll(".checklist-preview-list li.checked").length).to.equal(1); + + boxes[0].click(); + expect(calls).to.deep.equal([["Drain", true]]); + }); + it("KPI bar shows warning days and currency symbol", () => { const host = mount(task(), ctx({ currencySymbol: "$" })); const kpi = host.querySelector(".kpi-bar")!; 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 index 12c8c7a..178b6e3 100644 --- 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 @@ -54,6 +54,9 @@ function ctx(overrides: Partial = {}): TaskDetailContext { taskId: "t1", objectName: "Pool Pump", objectDocUrl: null, + objectManualDocs: [], + openManualDoc: () => {}, + setChecklistItem: () => {}, isOperator: false, actionLoading: false, moreMenuOpen: false, diff --git a/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts b/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts index 7bffea4..88162cc 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts @@ -18,10 +18,35 @@ interface BatteryRow { level: number | null; days_until: number | null; available?: boolean; + /** Where the ~date comes from: "trend" (discharge regression) or "typical" + * (type-lifetime table). */ + predicted_source?: "trend" | "typical"; + prediction_confidence?: "medium" | "high" | null; + /** Charged, never bought — the row never feeds the shopping groupings and + * a ~date only appears when the discharge trend earned one. */ + rechargeable?: boolean; + /** This battery's own low threshold (Battery Notes' configured value or + * the fleet floor, whichever is higher) — the level bar colors against + * it, not against a fixed 20 %. */ + low_threshold?: number; } interface RosterRow extends BatteryRow { status: "low" | "soon" | "ok"; } +/** 30 d downsampled level history per battery, for the roster sparklines. + * threshold = the same low threshold the trend forecast regresses toward, + * so the dotted projection ends exactly where the ~date comes from. + * jump = an upward step that looks like a swap nobody recorded in Battery + * Notes (the forecast still anchors on the dead battery's date) — with the + * device to call `battery_notes.set_battery_replaced` on. */ +type HistorySeries = Record< + string, + { + points: [number, number][]; + threshold: number; + jump?: { at: number; from: number; to: number; device_id: string }; + } +>; interface Overview { available: boolean; configured: boolean; @@ -45,6 +70,13 @@ export class MaintenanceBatteryFleetSection extends LitElement { @state() private _loading = false; @state() private _marking = false; @state() private _error = ""; + @state() private _history: HistorySeries | null = null; + // Urgency by default (issue #123: "soon sat in the middle of the list"); + // the choice is remembered per browser. + @state() private _rosterSort: "name" | "urgency" = MaintenanceBatteryFleetSection._storedSort(); + @state() private _typeFilter: string | null = null; + @state() private _recorded: string[] = []; + private _historyRequested = false; private _localeReady = false; private get _lang(): string { @@ -139,20 +171,165 @@ export class MaintenanceBatteryFleetSection extends LitElement { } } + /** Lazy: the recorder-backed history is fetched once, when the roster is + * first expanded — most panel visits never open it. */ + private _loadHistory = async (e: Event): Promise => { + if (!(e.target as HTMLDetailsElement).open || this._historyRequested) return; + this._historyRequested = true; + try { + const res = await this.hass.connection.sendMessagePromise<{ series: HistorySeries }>({ + type: "maintenance_supporter/battery_fleet/overview_history", + }); + this._history = res.series; + } catch { + this._history = null; // sparklines are an enhancement — rows render without them + } + }; + + /** Inline-SVG sparkline: 30 d level line, a faint threshold line, and — + * where the ~date comes from the discharge trend — a dotted projection + * from the last reading down to the threshold, so the date is visible + * instead of merely stated. */ + private _sparkline(b: RosterRow) { + const h = this._history?.[b.entity_id]; + if (!h || h.points.length < 2) return nothing; + const W = 110, H = 24, P = 2; + const t0 = h.points[0][0]; + const tLast = h.points[h.points.length - 1][0]; + const nowSec = Date.now() / 1000; + const projEnd = + b.status !== "low" && b.predicted_source === "trend" && b.days_until != null + ? nowSec + b.days_until * 86400 + : null; + const tMax = Math.max(tLast, projEnd ?? tLast); + const x = (t: number) => (tMax === t0 ? P : P + ((t - t0) / (tMax - t0)) * (W - 2 * P)); + const y = (v: number) => P + (1 - Math.min(100, Math.max(0, v)) / 100) * (H - 2 * P); + const line = h.points.map(([t, v]) => `${x(t).toFixed(1)},${y(v).toFixed(1)}`).join(" "); + const vLast = h.points[h.points.length - 1][1]; + const yTh = y(h.threshold).toFixed(1); + return html` + ${t("battery_fleet_sparkline_hint", this._lang)} + + + ${projEnd !== null + ? html`` + : nothing} + `; + } + + private static readonly _SORT_KEY = "ms_bf_roster_sort"; + + private static _storedSort(): "name" | "urgency" { + try { + const v = localStorage.getItem(MaintenanceBatteryFleetSection._SORT_KEY); + return v === "name" ? "name" : "urgency"; + } catch { + return "urgency"; + } + } + + private _setSort(mode: "name" | "urgency"): void { + this._rosterSort = mode; + try { + localStorage.setItem(MaintenanceBatteryFleetSection._SORT_KEY, mode); + } catch { + // storage unavailable — the toggle still works for this visit + } + } + + /** Urgency (the default, issue #123): low rows first — emptiest first — + * then the soonest forecast, dateless rows last. Name mode keeps the + * alphabetical lookup list. */ + private _sortedRoster(rows: RosterRow[]): RosterRow[] { + const filtered = this._typeFilter === null ? rows : rows.filter((r) => r.battery_type === this._typeFilter); + if (this._rosterSort === "name") return filtered; + // Low rows rank far below everything and among themselves by LEVEL + // ascending (a 6 % battery before an 18 % one); the rest by days-until. + const rank = (r: RosterRow) => (r.status === "low" ? -1000 + (r.level ?? 101) / 101 : (r.days_until ?? Infinity)); + return [...filtered].sort( + (a, b) => rank(a) - rank(b) || a.device_name.localeCompare(b.device_name), + ); + } + /** The forecast as a date a person can plan with, not a day count. * `days_until` comes from last-replaced + typical lifetime, so it is an * estimate — the tilde in the template says so. Negative values (past the * typical lifetime but not reported low yet) render as past dates, which * is honest: the battery is living on borrowed time. */ private _predictedDate(daysUntil: number): string { - const when = new Date(Date.now() + daysUntil * 864e5); - return new Intl.DateTimeFormat(this._lang, { day: "numeric", month: "numeric", year: "numeric" }).format(when); + return this._fmtDate(Date.now() + daysUntil * 864e5); } - private _shoppingLine(needs: Record): string { - return Object.entries(needs) - .map(([type, qty]) => `${qty}× ${type}`) - .join(" · "); + private _fmtDate(epochMs: number): string { + return new Intl.DateTimeFormat(this._lang, { day: "numeric", month: "numeric", year: "numeric" }).format(new Date(epochMs)); + } + + /** The grouped shopping quantities as CLICKABLE chips: a type filters the + * roster to the devices that need it — "which devices need those 4× AAA?" + * without scanning. Clicking the active chip clears the filter. */ + private _shoppingLine(needs: Record) { + return Object.entries(needs).map( + ([type, qty]) => html``, + ); + } + + private _toggleTypeFilter(type: string): void { + this._typeFilter = this._typeFilter === type ? null : type; + if (this._typeFilter !== null) { + const details = this.shadowRoot?.querySelector("details.bf-roster"); + if (details && !details.open) details.open = true; // fires toggle → history loads + } + } + + /** One-click fix for a detected-but-unrecorded swap: record the DETECTED + * jump time in Battery Notes, so the forecast re-anchors on the real + * replacement instead of the dead battery's date. */ + private async _recordJump(entityId: string, jump: { at: number; device_id: string }): Promise { + if (this._marking) return; + this._marking = true; + this._error = ""; + try { + await this.hass.callService("battery_notes", "set_battery_replaced", { + device_id: jump.device_id, + datetime_replaced: new Date(jump.at * 1000).toISOString(), + }); + this._recorded = [...this._recorded, entityId]; + await this._load(); + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._marking = false; + } + } + + /** Purely visual level bar next to the number — scannable at a glance. + * Colored against the battery's OWN low threshold: red at/below it, + * amber inside a 20-point approach band, green above. */ + private _levelBar(b: BatteryRow) { + const level = b.level; + if (level == null) return nothing; + const t = b.low_threshold ?? 20; + const cls = level <= t ? "bad" : level <= t + 20 ? "warn" : "good"; + return html``; } render() { @@ -199,10 +376,16 @@ export class MaintenanceBatteryFleetSection extends LitElement { ? html`${t("battery_fleet_offline", L)}` : nothing} ${b.quantity}× ${b.battery_type} + ${b.rechargeable + ? html`` + : nothing} + ${this._levelBar(b)} ${b.level != null ? html`${b.level}%` : nothing} + +
- ${ov.all.map( + ${this._sortedRoster(ov.all).map( (b) => html`
${b.device_name} ${t("battery_fleet_status_" + b.status, L)} ${b.quantity}× ${b.battery_type} + ${b.rechargeable + ? html`` + : nothing} + ${this._sparkline(b)} + ${this._levelBar(b)} ${b.level != null ? html`${b.level}%` : nothing} + ${(() => { + const jump = this._history?.[b.entity_id]?.jump; + if (!jump || this._recorded.includes(b.entity_id)) return nothing; + return html``; + })()} ${b.days_until != null ? html`~${this._predictedDate(b.days_until)}` : nothing} @@ -385,6 +605,102 @@ export class MaintenanceBatteryFleetSection extends LitElement { color: var(--secondary-text-color); font-size: 13px; } + .bf-recharge { + color: var(--secondary-text-color); + display: inline-flex; + cursor: help; + } + .bf-recharge ha-icon { + --mdc-icon-size: 16px; + } + .bf-spark { + width: 110px; + height: 24px; + flex: 0 0 auto; + cursor: help; + } + /* On phones the row cannot fit name + chips + curve + bar + date: the + * decorations yield (the percentage still carries the number). */ + @media (max-width: 640px) { + .bf-spark, + .bf-bar { + display: none; + } + } + .bf-spark-line { + fill: none; + stroke: var(--primary-color); + stroke-width: 1.5; + stroke-linejoin: round; + } + .bf-spark-proj { + stroke: var(--primary-color); + stroke-width: 1.2; + stroke-dasharray: 2 3; + opacity: 0.7; + } + .bf-spark-th { + stroke: var(--error-color, #f44336); + stroke-width: 1; + opacity: 0.35; + } + .bf-type-chip { + background: none; + border: 1px solid var(--divider-color); + border-radius: 10px; + padding: 1px 8px; + margin: 0 4px 2px 0; + font-size: 13px; + color: inherit; + cursor: pointer; + } + .bf-type-chip-active { + border-color: var(--primary-color); + color: var(--primary-color); + } + .bf-bar { + width: 30px; + height: 6px; + border-radius: 3px; + background: var(--divider-color); + overflow: hidden; + flex: 0 0 auto; + } + .bf-bar-fill { + display: block; + height: 100%; + border-radius: 3px; + } + .bf-bar-good { + background: var(--success-color, #4caf50); + } + .bf-bar-warn { + background: var(--warning-color, #ff9800); + } + .bf-bar-bad { + background: var(--error-color, #f44336); + } + .bf-jump ha-icon { + color: var(--warning-color, #ff9800); + } + .bf-roster-tools { + display: flex; + gap: 6px; + margin: 8px 0 2px; + } + .bf-sort { + background: none; + border: 1px solid var(--divider-color); + border-radius: 12px; + padding: 2px 10px; + font-size: 12px; + color: var(--secondary-text-color); + cursor: pointer; + } + .bf-sort-active { + border-color: var(--primary-color); + color: var(--primary-color); + } .bf-level { font-size: 12px; color: var(--error-color, #f44336); @@ -448,6 +764,12 @@ export class MaintenanceBatteryFleetSection extends LitElement { color: var(--secondary-text-color); white-space: nowrap; } + /* Trend-based dates (discharge regression) get a dotted underline — the + tooltip carries source + confidence. */ + .bf-predicted.bf-trend { + text-decoration: underline dotted; + text-underline-offset: 2px; + } .bf-total { font-size: 12px; color: var(--secondary-text-color); diff --git a/custom_components/maintenance_supporter/frontend-src/components/complete-dialog.ts b/custom_components/maintenance_supporter/frontend-src/components/complete-dialog.ts index 2896af2..09118b5 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/complete-dialog.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/complete-dialog.ts @@ -52,6 +52,10 @@ export class MaintenanceCompleteDialog extends LitElement { * objects can carry the same part id, so part_id alone would merge pools. */ @state() private _usedParts: Record = {}; + /** #73: in-cycle ticks (keyed by item TEXT) recorded on the task detail — + * prefill the dialog so nobody re-ticks what is already done. */ + @property({ attribute: false }) public checklistPrefill: Record = {}; + public open(): void { if (this._open) return; this._open = true; @@ -59,7 +63,13 @@ export class MaintenanceCompleteDialog extends LitElement { this._cost = ""; this._duration = ""; this._error = ""; - this._checklistState = {}; + // The dialog's own state is INDEX-keyed (historical shape, flows into the + // history entry as-is) — map the text-keyed in-cycle ticks onto indices. + this._checklistState = Object.fromEntries( + this.checklist + .map((item, i) => [String(i), !!this.checklistPrefill[item]] as const) + .filter(([, done]) => done), + ); this._feedback = "needed"; this._photoDocId = ""; this._photoPreview = ""; diff --git a/custom_components/maintenance_supporter/frontend-src/components/parts-section.ts b/custom_components/maintenance_supporter/frontend-src/components/parts-section.ts index 05a4e7c..9102324 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/parts-section.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/parts-section.ts @@ -59,6 +59,7 @@ export class MaintenancePartsSection extends LitElement { @property({ attribute: false }) public entryId!: string; @property({ attribute: false }) public parts: MaintenancePart[] = []; @property({ type: Boolean }) public canWrite = false; + @property({ attribute: false }) public currencySymbol = "€"; @state() private _editing: PartForm | null = null; @state() private _busy = false; @@ -335,6 +336,19 @@ export class MaintenancePartsSection extends LitElement { `; } + /** Inventory value = Σ unit cost × tracked stock (#104). Parts without a + * price or without tracked stock contribute nothing; null = no part has + * both, so the chip stays hidden rather than showing a misleading 0. */ + private _inventoryValue(): number | null { + let sum = 0, any = false; + for (const p of this.parts) { + const cost = typeof p.cost === "number" ? p.cost : null; + const stock = typeof p.stock === "number" ? p.stock : null; + if (cost !== null && stock !== null) { sum += cost * stock; any = true; } + } + return any ? sum : null; + } + protected render() { const L = this._lang; if (!this.parts.length && !this.canWrite) return nothing; @@ -343,6 +357,11 @@ export class MaintenancePartsSection extends LitElement {

${t("parts_section", L)} (${this.parts.length}) + ${this._inventoryValue() !== null + ? html`${t("parts_inventory_value", L)}: + ${this._inventoryValue()!.toFixed(2)} ${this.currencySymbol}` + : nothing}

${this.canWrite && !this._editing ? html` this._openAdd()}> @@ -361,6 +380,13 @@ export class MaintenancePartsSection extends LitElement { display: block; margin: 12px 0; } + .inventory-value { + margin-left: 8px; + font-size: 0.75em; + font-weight: 400; + color: var(--secondary-text-color); + white-space: nowrap; + } .section-head { display: flex; align-items: center; diff --git a/custom_components/maintenance_supporter/frontend-src/locales/cs.json b/custom_components/maintenance_supporter/frontend-src/locales/cs.json index 5d3e9a4..9d54d86 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/cs.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/cs.json @@ -716,6 +716,7 @@ "series_end_count_label": "Počet opakování", "series_end_until_label": "Datum konce", "parts_section": "Díly a spotřební materiál", + "parts_inventory_value": "Hodnota zásob", "part_add": "Přidat díl", "part_name": "Název", "part_vendor": "Výrobce", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Brzy", "battery_fleet_status_ok": "V pořádku", "battery_fleet_predicted_on": "Očekáváno kolem {date}", + "battery_fleet_predicted_trend": "Předpověď z trendu vybíjení této baterie: přibližně {date} ({confidence})", + "battery_fleet_rechargeable": "Akumulátor: nabíjí se místo výměny — nikdy na nákupním seznamu", + "battery_fleet_sort_name": "Řadit podle názvu", + "battery_fleet_sort_urgency": "Řadit podle naléhavosti", + "battery_fleet_mark_recharged": "Označit jako nabitou", + "battery_fleet_sparkline_hint": "Stav baterie za posledních 30 dní — tečkovaně: projekce k prahu vybití", + "battery_fleet_filter_type": "Zobrazit pouze tento typ baterie", + "battery_fleet_record_replacement": "Stav poskočil kolem {date} — zaznamenat tuto výměnu do Battery Notes", "battery_fleet_total": "Sledováno baterií: {n}", "battery_fleet_setup_button": "Flotila baterií", "battery_fleet_setup_done": "Flotila baterií nastavena — jeden úkol sleduje všechny.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/da.json b/custom_components/maintenance_supporter/frontend-src/locales/da.json index 631681f..ce3c16f 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/da.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/da.json @@ -716,6 +716,7 @@ "series_end_count_label": "Antal gange", "series_end_until_label": "Slutdato", "parts_section": "Dele & forbrugsvarer", + "parts_inventory_value": "Lagerværdi", "part_add": "Tilføj del", "part_name": "Navn", "part_vendor": "Producent", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Snart", "battery_fleet_status_ok": "I orden", "battery_fleet_predicted_on": "Forventes omkring {date}", + "battery_fleet_predicted_trend": "Forudsagt ud fra batteriets afladningstendens: omkring {date} ({confidence})", + "battery_fleet_rechargeable": "Genopladeligt: oplad i stedet for at udskifte — aldrig på indkøbslisten", + "battery_fleet_sort_name": "Sortér efter navn", + "battery_fleet_sort_urgency": "Sortér efter hastende grad", + "battery_fleet_mark_recharged": "Markér som genopladet", + "battery_fleet_sparkline_hint": "Batteriniveau de seneste 30 dage — stiplet: fremskrivning ned til lav-tærsklen", + "battery_fleet_filter_type": "Vis kun denne batteritype", + "battery_fleet_record_replacement": "Niveauet sprang omkring {date} — registrér denne udskiftning i Battery Notes", "battery_fleet_total": "{n} batterier overvåges", "battery_fleet_setup_button": "Batteriflåde", "battery_fleet_setup_done": "Batteriflåde opsat — én opgave følger alle batterier.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/de.json b/custom_components/maintenance_supporter/frontend-src/locales/de.json index fcf3ebb..70991d2 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/de.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/de.json @@ -716,6 +716,7 @@ "series_end_count_label": "Anzahl", "series_end_until_label": "Enddatum", "parts_section": "Teile & Verbrauchsmaterial", + "parts_inventory_value": "Lagerwert", "part_add": "Teil hinzufügen", "part_name": "Name", "part_vendor": "Hersteller", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Bald", "battery_fleet_status_ok": "In Ordnung", "battery_fleet_predicted_on": "Voraussichtlich um {date}", + "battery_fleet_predicted_trend": "Aus dem Entladetrend dieser Batterie vorhergesagt: etwa {date} ({confidence})", + "battery_fleet_rechargeable": "Wiederaufladbar: laden statt ersetzen — nie auf der Einkaufsliste", + "battery_fleet_sort_name": "Nach Name sortieren", + "battery_fleet_sort_urgency": "Nach Dringlichkeit sortieren", + "battery_fleet_mark_recharged": "Als aufgeladen markieren", + "battery_fleet_sparkline_hint": "Batteriestand der letzten 30 Tage — gepunktet: Prognose bis zur Low-Schwelle", + "battery_fleet_filter_type": "Nur diesen Batterietyp anzeigen", + "battery_fleet_record_replacement": "Der Batteriestand ist um den {date} gesprungen — diesen Wechsel in Battery Notes nachtragen", "battery_fleet_total": "{n} Batterien überwacht", "battery_fleet_setup_button": "Batterie-Flotte", "battery_fleet_setup_done": "Batterie-Flotte eingerichtet — ein Task überwacht alle Batterien.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/en.json b/custom_components/maintenance_supporter/frontend-src/locales/en.json index 556a52b..ca508fc 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/en.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/en.json @@ -722,6 +722,7 @@ "series_end_count_label": "Number of times", "series_end_until_label": "End date", "parts_section": "Parts & consumables", + "parts_inventory_value": "Inventory value", "part_add": "Add part", "part_name": "Name", "part_vendor": "Manufacturer", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Soon", "battery_fleet_status_ok": "Healthy", "battery_fleet_predicted_on": "Expected around {date}", + "battery_fleet_predicted_trend": "Predicted from this battery's discharge trend: around {date} ({confidence})", + "battery_fleet_rechargeable": "Rechargeable: charge instead of replacing — never on the shopping list", + "battery_fleet_sort_name": "Sort by name", + "battery_fleet_sort_urgency": "Sort by urgency", + "battery_fleet_mark_recharged": "Mark as recharged", + "battery_fleet_sparkline_hint": "Battery level over the last 30 days — dotted: projected until the low threshold", + "battery_fleet_filter_type": "Show only this battery type", + "battery_fleet_record_replacement": "The level jumped around {date} — record this replacement in Battery Notes", "battery_fleet_total": "{n} batteries tracked", "battery_fleet_setup_button": "Battery fleet", "battery_fleet_setup_done": "Battery fleet set up — one task tracks all your batteries.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/es.json b/custom_components/maintenance_supporter/frontend-src/locales/es.json index 49be220..5743df8 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/es.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/es.json @@ -716,6 +716,7 @@ "series_end_count_label": "Número de veces", "series_end_until_label": "Fecha de fin", "parts_section": "Piezas y consumibles", + "parts_inventory_value": "Valor del inventario", "part_add": "Añadir pieza", "part_name": "Nombre", "part_vendor": "Fabricante", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Pronto", "battery_fleet_status_ok": "Correcta", "battery_fleet_predicted_on": "Previsto hacia {date}", + "battery_fleet_predicted_trend": "Predicción según la tendencia de descarga de esta pila: hacia {date} ({confidence})", + "battery_fleet_rechargeable": "Recargable: se recarga en lugar de sustituirse — nunca en la lista de compra", + "battery_fleet_sort_name": "Ordenar por nombre", + "battery_fleet_sort_urgency": "Ordenar por urgencia", + "battery_fleet_mark_recharged": "Marcar como recargada", + "battery_fleet_sparkline_hint": "Nivel de batería de los últimos 30 días — punteado: proyección hasta el umbral bajo", + "battery_fleet_filter_type": "Mostrar solo este tipo de pila", + "battery_fleet_record_replacement": "El nivel dio un salto hacia el {date} — registrar esta sustitución en Battery Notes", "battery_fleet_total": "{n} baterías monitorizadas", "battery_fleet_setup_button": "Flota de baterías", "battery_fleet_setup_done": "Flota de baterías configurada — una tarea controla todas.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/fi.json b/custom_components/maintenance_supporter/frontend-src/locales/fi.json index 91051ee..6d01eb0 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/fi.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/fi.json @@ -716,6 +716,7 @@ "series_end_count_label": "Kertojen määrä", "series_end_until_label": "Päättymispäivä", "parts_section": "Osat ja tarvikkeet", + "parts_inventory_value": "Varaston arvo", "part_add": "Lisää osa", "part_name": "Nimi", "part_vendor": "Valmistaja", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Pian", "battery_fleet_status_ok": "Kunnossa", "battery_fleet_predicted_on": "Odotettavissa noin {date}", + "battery_fleet_predicted_trend": "Ennustettu tämän pariston purkautumistrendistä: noin {date} ({confidence})", + "battery_fleet_rechargeable": "Ladattava: lataa vaihtamisen sijaan — ei koskaan ostoslistalle", + "battery_fleet_sort_name": "Lajittele nimen mukaan", + "battery_fleet_sort_urgency": "Lajittele kiireellisyyden mukaan", + "battery_fleet_mark_recharged": "Merkitse ladatuksi", + "battery_fleet_sparkline_hint": "Akun varaustaso viimeisten 30 päivän ajalta — pisteviiva: ennuste alarajaan asti", + "battery_fleet_filter_type": "Näytä vain tämä paristotyyppi", + "battery_fleet_record_replacement": "Varaustaso hyppäsi noin {date} — kirjaa tämä vaihto Battery Notesiin", "battery_fleet_total": "{n} paristoa seurannassa", "battery_fleet_setup_button": "Paristokanta", "battery_fleet_setup_done": "Paristokanta määritetty — yksi tehtävä seuraa kaikkia.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/fr.json b/custom_components/maintenance_supporter/frontend-src/locales/fr.json index 69bdcad..8de01c1 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/fr.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/fr.json @@ -716,6 +716,7 @@ "series_end_count_label": "Nombre de fois", "series_end_until_label": "Date de fin", "parts_section": "Pièces & consommables", + "parts_inventory_value": "Valeur du stock", "part_add": "Ajouter une pièce", "part_name": "Nom", "part_vendor": "Fabricant", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Bientôt", "battery_fleet_status_ok": "Bon état", "battery_fleet_predicted_on": "Prévu vers {date}", + "battery_fleet_predicted_trend": "Prédit à partir de la tendance de décharge de cette pile : vers {date} ({confidence})", + "battery_fleet_rechargeable": "Rechargeable : à recharger plutôt qu'à remplacer — jamais sur la liste d'achats", + "battery_fleet_sort_name": "Trier par nom", + "battery_fleet_sort_urgency": "Trier par urgence", + "battery_fleet_mark_recharged": "Marquer comme rechargée", + "battery_fleet_sparkline_hint": "Niveau de batterie des 30 derniers jours — pointillé : projection jusqu'au seuil bas", + "battery_fleet_filter_type": "Afficher uniquement ce type de pile", + "battery_fleet_record_replacement": "Le niveau a bondi vers le {date} — enregistrer ce remplacement dans Battery Notes", "battery_fleet_total": "{n} piles suivies", "battery_fleet_setup_button": "Parc de piles", "battery_fleet_setup_done": "Parc de piles configuré — une tâche suit toutes vos piles.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/hi.json b/custom_components/maintenance_supporter/frontend-src/locales/hi.json index ba1c4b3..50e6045 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/hi.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/hi.json @@ -716,6 +716,7 @@ "series_end_count_label": "कितनी बार", "series_end_until_label": "समाप्ति तिथि", "parts_section": "पुर्ज़े और उपभोग्य", + "parts_inventory_value": "इन्वेंट्री मूल्य", "part_add": "पुर्ज़ा जोड़ें", "part_name": "नाम", "part_vendor": "निर्माता", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "जल्द", "battery_fleet_status_ok": "ठीक", "battery_fleet_predicted_on": "{date} के आसपास अपेक्षित", + "battery_fleet_predicted_trend": "इस बैटरी के डिस्चार्ज रुझान से अनुमानित: लगभग {date} ({confidence})", + "battery_fleet_rechargeable": "रिचार्जेबल: बदलने के बजाय चार्ज करें — खरीदारी सूची में कभी नहीं", + "battery_fleet_sort_name": "नाम के अनुसार क्रमबद्ध करें", + "battery_fleet_sort_urgency": "तात्कालिकता के अनुसार क्रमबद्ध करें", + "battery_fleet_mark_recharged": "रिचार्ज हो गई के रूप में चिह्नित करें", + "battery_fleet_sparkline_hint": "पिछले 30 दिनों का बैटरी स्तर — बिंदीदार: लो थ्रेशोल्ड तक का अनुमान", + "battery_fleet_filter_type": "केवल यही बैटरी प्रकार दिखाएँ", + "battery_fleet_record_replacement": "स्तर लगभग {date} को उछला — इस बदलाव को Battery Notes में दर्ज करें", "battery_fleet_total": "{n} बैटरियाँ ट्रैक की गईं", "battery_fleet_setup_button": "बैटरी फ्लीट", "battery_fleet_setup_done": "बैटरी फ्लीट सेट — एक कार्य सभी बैटरियों को ट्रैक करता है।", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/hu.json b/custom_components/maintenance_supporter/frontend-src/locales/hu.json index 1dc6e6d..7b777c0 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/hu.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/hu.json @@ -722,6 +722,7 @@ "series_end_count_label": "Alkalmak száma", "series_end_until_label": "Befejezés dátuma", "parts_section": "Alkatrészek és fogyóeszközök", + "parts_inventory_value": "Készletérték", "part_add": "Alkatrész hozzáadása", "part_name": "Név", "part_vendor": "Gyártó", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Hamarosan", "battery_fleet_status_ok": "Rendben", "battery_fleet_predicted_on": "Várhatóan {date} körül", + "battery_fleet_predicted_trend": "Az elem merülési trendjéből előrejelezve: kb. {date} ({confidence})", + "battery_fleet_rechargeable": "Újratölthető: cserélés helyett töltse fel — soha nem kerül a bevásárlólistára", + "battery_fleet_sort_name": "Rendezés név szerint", + "battery_fleet_sort_urgency": "Rendezés sürgősség szerint", + "battery_fleet_mark_recharged": "Megjelölés feltöltöttként", + "battery_fleet_sparkline_hint": "Akkumulátorszint az elmúlt 30 napban — pontozott: előrejelzés az alacsony küszöbig", + "battery_fleet_filter_type": "Csak ez az elemtípus megjelenítése", + "battery_fleet_record_replacement": "A szint {date} körül megugrott — rögzítse ezt a cserét a Battery Notes-ban", "battery_fleet_total": "{n} elem követve", "battery_fleet_setup_button": "Elemflotta", "battery_fleet_setup_done": "Elemflotta beállítva — egyetlen feladat követi az összes elemet.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/it.json b/custom_components/maintenance_supporter/frontend-src/locales/it.json index dc0c3ea..df87185 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/it.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/it.json @@ -716,6 +716,7 @@ "series_end_count_label": "Numero di volte", "series_end_until_label": "Data di fine", "parts_section": "Ricambi e consumabili", + "parts_inventory_value": "Valore delle scorte", "part_add": "Aggiungi ricambio", "part_name": "Nome", "part_vendor": "Produttore", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "A breve", "battery_fleet_status_ok": "A posto", "battery_fleet_predicted_on": "Previsto intorno al {date}", + "battery_fleet_predicted_trend": "Previsto dalla tendenza di scarica di questa batteria: intorno al {date} ({confidence})", + "battery_fleet_rechargeable": "Ricaricabile: da ricaricare anziché sostituire — mai nella lista della spesa", + "battery_fleet_sort_name": "Ordina per nome", + "battery_fleet_sort_urgency": "Ordina per urgenza", + "battery_fleet_mark_recharged": "Segna come ricaricata", + "battery_fleet_sparkline_hint": "Livello batteria degli ultimi 30 giorni — tratteggiato: proiezione fino alla soglia di batteria scarica", + "battery_fleet_filter_type": "Mostra solo questo tipo di batteria", + "battery_fleet_record_replacement": "Il livello è balzato intorno al {date} — registra questa sostituzione in Battery Notes", "battery_fleet_total": "{n} batterie monitorate", "battery_fleet_setup_button": "Parco batterie", "battery_fleet_setup_done": "Parco batterie configurato — un'attività monitora tutte.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/ja.json b/custom_components/maintenance_supporter/frontend-src/locales/ja.json index c43e2af..1f1f1f5 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/ja.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/ja.json @@ -716,6 +716,7 @@ "series_end_count_label": "回数", "series_end_until_label": "終了日", "parts_section": "部品・消耗品", + "parts_inventory_value": "在庫金額", "part_add": "部品を追加", "part_name": "名前", "part_vendor": "メーカー", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "まもなく", "battery_fleet_status_ok": "良好", "battery_fleet_predicted_on": "{date} 頃の見込み", + "battery_fleet_predicted_trend": "この電池の放電傾向から予測: {date} 頃 ({confidence})", + "battery_fleet_rechargeable": "充電式:交換ではなく充電——買い物リストには載りません", + "battery_fleet_sort_name": "名前順に並べ替え", + "battery_fleet_sort_urgency": "緊急度順に並べ替え", + "battery_fleet_mark_recharged": "充電済みにする", + "battery_fleet_sparkline_hint": "過去30日間のバッテリー残量——点線:低残量しきい値までの予測", + "battery_fleet_filter_type": "この電池タイプのみ表示", + "battery_fleet_record_replacement": "{date}頃に残量が急上昇——この交換をBattery Notesに記録する", "battery_fleet_total": "{n} 個の電池を監視", "battery_fleet_setup_button": "電池フリート", "battery_fleet_setup_done": "電池フリートを設定 — 1つのタスクで全電池を管理。", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/ko.json b/custom_components/maintenance_supporter/frontend-src/locales/ko.json index 6380498..1c46432 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/ko.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/ko.json @@ -722,6 +722,7 @@ "series_end_count_label": "횟수", "series_end_until_label": "종료일", "parts_section": "부품 및 소모품", + "parts_inventory_value": "재고 가치", "part_add": "부품 추가", "part_name": "이름", "part_vendor": "제조사", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "곧", "battery_fleet_status_ok": "정상", "battery_fleet_predicted_on": "{date}쯤 예상", + "battery_fleet_predicted_trend": "이 배터리의 방전 추세로 예측: 약 {date} ({confidence})", + "battery_fleet_rechargeable": "충전식: 교체 대신 충전 — 쇼핑 목록에 오르지 않습니다", + "battery_fleet_sort_name": "이름순 정렬", + "battery_fleet_sort_urgency": "긴급도순 정렬", + "battery_fleet_mark_recharged": "충전 완료로 표시", + "battery_fleet_sparkline_hint": "지난 30일간의 배터리 잔량 — 점선: 낮음 임계값까지의 예측", + "battery_fleet_filter_type": "이 배터리 유형만 표시", + "battery_fleet_record_replacement": "{date}쯤 잔량이 급상승했습니다 — 이 교체를 Battery Notes에 기록", "battery_fleet_total": "배터리 {n}개 추적 중", "battery_fleet_setup_button": "배터리 플릿", "battery_fleet_setup_done": "배터리 플릿이 설정되었습니다 — 하나의 작업이 모든 배터리를 추적합니다.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/nb.json b/custom_components/maintenance_supporter/frontend-src/locales/nb.json index f91fdad..60f2510 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/nb.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/nb.json @@ -716,6 +716,7 @@ "series_end_count_label": "Antall ganger", "series_end_until_label": "Sluttdato", "parts_section": "Deler & forbruksvarer", + "parts_inventory_value": "Lagerverdi", "part_add": "Legg til del", "part_name": "Navn", "part_vendor": "Produsent", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Snart", "battery_fleet_status_ok": "I orden", "battery_fleet_predicted_on": "Forventes rundt {date}", + "battery_fleet_predicted_trend": "Forutsagt ut fra batteriets utladingstrend: rundt {date} ({confidence})", + "battery_fleet_rechargeable": "Oppladbart: lad i stedet for å bytte — aldri på handlelisten", + "battery_fleet_sort_name": "Sorter etter navn", + "battery_fleet_sort_urgency": "Sorter etter hastegrad", + "battery_fleet_mark_recharged": "Merk som oppladet", + "battery_fleet_sparkline_hint": "Batterinivå de siste 30 dagene — stiplet: fremskrevet ned til lavnivåterskelen", + "battery_fleet_filter_type": "Vis bare denne batteritypen", + "battery_fleet_record_replacement": "Nivået hoppet rundt {date} — registrer dette byttet i Battery Notes", "battery_fleet_total": "{n} batterier spores", "battery_fleet_setup_button": "Batteriflåte", "battery_fleet_setup_done": "Batteriflåte satt opp — én oppgave følger alle batteriene.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/nl.json b/custom_components/maintenance_supporter/frontend-src/locales/nl.json index a97385a..bd5af05 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/nl.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/nl.json @@ -716,6 +716,7 @@ "series_end_count_label": "Aantal keer", "series_end_until_label": "Einddatum", "parts_section": "Onderdelen & verbruiksartikelen", + "parts_inventory_value": "Voorraadwaarde", "part_add": "Onderdeel toevoegen", "part_name": "Naam", "part_vendor": "Fabrikant", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Binnenkort", "battery_fleet_status_ok": "In orde", "battery_fleet_predicted_on": "Verwacht rond {date}", + "battery_fleet_predicted_trend": "Voorspeld uit de ontladingstrend van deze batterij: rond {date} ({confidence})", + "battery_fleet_rechargeable": "Oplaadbaar: opladen in plaats van vervangen — nooit op het boodschappenlijstje", + "battery_fleet_sort_name": "Sorteren op naam", + "battery_fleet_sort_urgency": "Sorteren op urgentie", + "battery_fleet_mark_recharged": "Markeren als opgeladen", + "battery_fleet_sparkline_hint": "Batterijniveau van de afgelopen 30 dagen — gestippeld: prognose tot de lage drempel", + "battery_fleet_filter_type": "Alleen dit batterijtype tonen", + "battery_fleet_record_replacement": "Het niveau maakte rond {date} een sprong — deze vervanging vastleggen in Battery Notes", "battery_fleet_total": "{n} batterijen gevolgd", "battery_fleet_setup_button": "Batterijvloot", "battery_fleet_setup_done": "Batterijvloot ingesteld — één taak volgt al je batterijen.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/pl.json b/custom_components/maintenance_supporter/frontend-src/locales/pl.json index 01a1613..6e20265 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/pl.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/pl.json @@ -716,6 +716,7 @@ "series_end_count_label": "Liczba razy", "series_end_until_label": "Data końcowa", "parts_section": "Części i materiały", + "parts_inventory_value": "Wartość zapasów", "part_add": "Dodaj część", "part_name": "Nazwa", "part_vendor": "Producent", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Wkrótce", "battery_fleet_status_ok": "W porządku", "battery_fleet_predicted_on": "Przewidywane około {date}", + "battery_fleet_predicted_trend": "Prognoza na podstawie trendu rozładowania tej baterii: około {date} ({confidence})", + "battery_fleet_rechargeable": "Akumulator: ładowanie zamiast wymiany — nigdy na liście zakupów", + "battery_fleet_sort_name": "Sortuj według nazwy", + "battery_fleet_sort_urgency": "Sortuj według pilności", + "battery_fleet_mark_recharged": "Oznacz jako naładowaną", + "battery_fleet_sparkline_hint": "Poziom baterii z ostatnich 30 dni — kropkowana linia: prognoza do progu niskiego poziomu", + "battery_fleet_filter_type": "Pokaż tylko ten typ baterii", + "battery_fleet_record_replacement": "Poziom skoczył około {date} — zapisz tę wymianę w Battery Notes", "battery_fleet_total": "Śledzone baterie: {n}", "battery_fleet_setup_button": "Flota baterii", "battery_fleet_setup_done": "Flota baterii skonfigurowana — jedno zadanie śledzi wszystkie.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/pt-br.json b/custom_components/maintenance_supporter/frontend-src/locales/pt-br.json index bfdf93b..ca2fc2d 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/pt-br.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/pt-br.json @@ -722,6 +722,7 @@ "series_end_count_label": "Número de vezes", "series_end_until_label": "Data final", "parts_section": "Peças e consumíveis", + "parts_inventory_value": "Valor do estoque", "part_add": "Adicionar peça", "part_name": "Nome", "part_vendor": "Fabricante", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Em breve", "battery_fleet_status_ok": "Em bom estado", "battery_fleet_predicted_on": "Previsto por volta de {date}", + "battery_fleet_predicted_trend": "Previsto pela tendência de descarga desta bateria: por volta de {date} ({confidence})", + "battery_fleet_rechargeable": "Recarregável: recarregue em vez de substituir — nunca na lista de compras", + "battery_fleet_sort_name": "Ordenar por nome", + "battery_fleet_sort_urgency": "Ordenar por urgência", + "battery_fleet_mark_recharged": "Marcar como recarregada", + "battery_fleet_sparkline_hint": "Nível da bateria dos últimos 30 dias — pontilhado: projeção até o limite baixo", + "battery_fleet_filter_type": "Mostrar apenas este tipo de pilha", + "battery_fleet_record_replacement": "O nível deu um salto por volta de {date} — registrar esta substituição no Battery Notes", "battery_fleet_total": "{n} baterias acompanhadas", "battery_fleet_setup_button": "Frota de baterias", "battery_fleet_setup_done": "Frota de baterias configurada — uma única tarefa acompanha todas as suas baterias.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/pt.json b/custom_components/maintenance_supporter/frontend-src/locales/pt.json index c5e2510..a9f9dae 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/pt.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/pt.json @@ -716,6 +716,7 @@ "series_end_count_label": "Número de vezes", "series_end_until_label": "Data final", "parts_section": "Peças e consumíveis", + "parts_inventory_value": "Valor do stock", "part_add": "Adicionar peça", "part_name": "Nome", "part_vendor": "Fabricante", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Em breve", "battery_fleet_status_ok": "Em bom estado", "battery_fleet_predicted_on": "Previsto por volta de {date}", + "battery_fleet_predicted_trend": "Previsto pela tendência de descarga desta pilha: por volta de {date} ({confidence})", + "battery_fleet_rechargeable": "Recarregável: carrega-se em vez de se substituir — nunca na lista de compras", + "battery_fleet_sort_name": "Ordenar por nome", + "battery_fleet_sort_urgency": "Ordenar por urgência", + "battery_fleet_mark_recharged": "Marcar como recarregada", + "battery_fleet_sparkline_hint": "Nível da bateria dos últimos 30 dias — pontilhado: projeção até ao limiar baixo", + "battery_fleet_filter_type": "Mostrar apenas este tipo de pilha", + "battery_fleet_record_replacement": "O nível deu um salto por volta de {date} — registar esta substituição no Battery Notes", "battery_fleet_total": "{n} baterias monitorizadas", "battery_fleet_setup_button": "Frota de baterias", "battery_fleet_setup_done": "Frota de baterias configurada — uma tarefa acompanha todas.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/ru.json b/custom_components/maintenance_supporter/frontend-src/locales/ru.json index 7efafca..e82f2f2 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/ru.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/ru.json @@ -716,6 +716,7 @@ "series_end_count_label": "Число раз", "series_end_until_label": "Дата окончания", "parts_section": "Детали и расходники", + "parts_inventory_value": "Стоимость запасов", "part_add": "Добавить деталь", "part_name": "Название", "part_vendor": "Производитель", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Скоро", "battery_fleet_status_ok": "В норме", "battery_fleet_predicted_on": "Ожидается примерно {date}", + "battery_fleet_predicted_trend": "Прогноз по тренду разряда этой батареи: примерно {date} ({confidence})", + "battery_fleet_rechargeable": "Аккумулятор: заряжается, а не заменяется — никогда не попадает в список покупок", + "battery_fleet_sort_name": "Сортировать по имени", + "battery_fleet_sort_urgency": "Сортировать по срочности", + "battery_fleet_mark_recharged": "Отметить как заряженную", + "battery_fleet_sparkline_hint": "Уровень заряда за последние 30 дней — пунктир: прогноз до порога разряда", + "battery_fleet_filter_type": "Показать только этот тип батарей", + "battery_fleet_record_replacement": "Уровень резко вырос примерно {date} — записать эту замену в Battery Notes", "battery_fleet_total": "Отслеживается батарей: {n}", "battery_fleet_setup_button": "Парк батарей", "battery_fleet_setup_done": "Парк батарей настроен — одна задача следит за всеми.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/sv.json b/custom_components/maintenance_supporter/frontend-src/locales/sv.json index 1168695..67d03ea 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/sv.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/sv.json @@ -716,6 +716,7 @@ "series_end_count_label": "Antal gånger", "series_end_until_label": "Slutdatum", "parts_section": "Delar & förbrukning", + "parts_inventory_value": "Lagervärde", "part_add": "Lägg till del", "part_name": "Namn", "part_vendor": "Tillverkare", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Snart", "battery_fleet_status_ok": "I ordning", "battery_fleet_predicted_on": "Väntas omkring {date}", + "battery_fleet_predicted_trend": "Förutspått utifrån batteriets urladdningstrend: omkring {date} ({confidence})", + "battery_fleet_rechargeable": "Uppladdningsbart: ladda i stället för att byta — aldrig på inköpslistan", + "battery_fleet_sort_name": "Sortera efter namn", + "battery_fleet_sort_urgency": "Sortera efter angelägenhet", + "battery_fleet_mark_recharged": "Markera som uppladdad", + "battery_fleet_sparkline_hint": "Batterinivå de senaste 30 dagarna — prickad: prognos ner till lågnivåtröskeln", + "battery_fleet_filter_type": "Visa endast denna batterityp", + "battery_fleet_record_replacement": "Nivån hoppade omkring {date} — registrera detta byte i Battery Notes", "battery_fleet_total": "{n} batterier spåras", "battery_fleet_setup_button": "Batteriflotta", "battery_fleet_setup_done": "Batteriflotta konfigurerad — en uppgift följer alla batterier.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/tr.json b/custom_components/maintenance_supporter/frontend-src/locales/tr.json index 24e86ea..b3c0f29 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/tr.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/tr.json @@ -722,6 +722,7 @@ "series_end_count_label": "Tekrar sayısı", "series_end_until_label": "Bitiş tarihi", "parts_section": "Parçalar ve sarf malzemeleri", + "parts_inventory_value": "Stok değeri", "part_add": "Parça ekle", "part_name": "Ad", "part_vendor": "Üretici", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Yakında", "battery_fleet_status_ok": "İyi durumda", "battery_fleet_predicted_on": "Yaklaşık {date} tarihinde bekleniyor", + "battery_fleet_predicted_trend": "Bu pilin deşarj eğiliminden tahmin edildi: yaklaşık {date} ({confidence})", + "battery_fleet_rechargeable": "Şarj edilebilir: değiştirmek yerine şarj edin — alışveriş listesine asla girmez", + "battery_fleet_sort_name": "Ada göre sırala", + "battery_fleet_sort_urgency": "Aciliyete göre sırala", + "battery_fleet_mark_recharged": "Şarj edildi olarak işaretle", + "battery_fleet_sparkline_hint": "Son 30 günün pil seviyesi — noktalı: düşük eşiğe kadar projeksiyon", + "battery_fleet_filter_type": "Yalnızca bu pil türünü göster", + "battery_fleet_record_replacement": "Seviye {date} civarında sıçradı — bu değişimi Battery Notes'a kaydet", "battery_fleet_total": "{n} pil takip ediliyor", "battery_fleet_setup_button": "Pil filosu", "battery_fleet_setup_done": "Pil filosu kuruldu — tek bir görev tüm pillerinizi takip ediyor.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/uk.json b/custom_components/maintenance_supporter/frontend-src/locales/uk.json index 755186d..266b1a4 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/uk.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/uk.json @@ -716,6 +716,7 @@ "series_end_count_label": "Кількість разів", "series_end_until_label": "Дата завершення", "parts_section": "Деталі та витратні матеріали", + "parts_inventory_value": "Вартість запасів", "part_add": "Додати деталь", "part_name": "Назва", "part_vendor": "Виробник", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Незабаром", "battery_fleet_status_ok": "У нормі", "battery_fleet_predicted_on": "Очікується приблизно {date}", + "battery_fleet_predicted_trend": "Прогноз за трендом розряду цієї батареї: приблизно {date} ({confidence})", + "battery_fleet_rechargeable": "Акумулятор: заряджається, а не замінюється — ніколи не потрапляє до списку покупок", + "battery_fleet_sort_name": "Сортувати за назвою", + "battery_fleet_sort_urgency": "Сортувати за терміновістю", + "battery_fleet_mark_recharged": "Позначити як заряджену", + "battery_fleet_sparkline_hint": "Рівень заряду за останні 30 днів — пунктир: прогноз до порогу розряду", + "battery_fleet_filter_type": "Показати лише цей тип батарей", + "battery_fleet_record_replacement": "Рівень різко зріс приблизно {date} — записати цю заміну в Battery Notes", "battery_fleet_total": "Відстежується батарей: {n}", "battery_fleet_setup_button": "Парк батарей", "battery_fleet_setup_done": "Парк батарей налаштовано — одне завдання стежить за всіма.", diff --git a/custom_components/maintenance_supporter/frontend-src/locales/zh.json b/custom_components/maintenance_supporter/frontend-src/locales/zh.json index 00044ee..e463821 100644 --- a/custom_components/maintenance_supporter/frontend-src/locales/zh.json +++ b/custom_components/maintenance_supporter/frontend-src/locales/zh.json @@ -716,6 +716,7 @@ "series_end_count_label": "次数", "series_end_until_label": "结束日期", "parts_section": "配件与耗材", + "parts_inventory_value": "库存价值", "part_add": "添加配件", "part_name": "名称", "part_vendor": "制造商", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "即将", "battery_fleet_status_ok": "正常", "battery_fleet_predicted_on": "预计在 {date} 前后", + "battery_fleet_predicted_trend": "根据此电池的放电趋势预测:约 {date}({confidence})", + "battery_fleet_rechargeable": "可充电电池:充电即可,无需更换——不会出现在购物清单中", + "battery_fleet_sort_name": "按名称排序", + "battery_fleet_sort_urgency": "按紧急程度排序", + "battery_fleet_mark_recharged": "标记为已充电", + "battery_fleet_sparkline_hint": "过去 30 天的电池电量——虚线:外推至低电量阈值", + "battery_fleet_filter_type": "仅显示此电池类型", + "battery_fleet_record_replacement": "电量在 {date} 前后跳升——将此次更换记录到 Battery Notes", "battery_fleet_total": "已跟踪 {n} 个电池", "battery_fleet_setup_button": "电池群", "battery_fleet_setup_done": "电池群已设置 — 一个任务跟踪所有电池。", diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-card.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-card.ts index 72df960..30b35d9 100644 --- a/custom_components/maintenance_supporter/frontend-src/maintenance-card.ts +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-card.ts @@ -1,6 +1,7 @@ /** Maintenance Supporter Lovelace Card. */ import { LitElement, html, css, nothing } from "lit"; +import { mergeSubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge"; import { property, state } from "lit/decorators.js"; import { sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays } from "./styles"; import type { @@ -274,10 +275,14 @@ export class MaintenanceSupporterCard extends LitElement { try { const unsub = await this.hass.connection.subscribeMessage( (msg: unknown) => { - const data = msg as { objects: MaintenanceObjectResponse[] }; - this._objects = data.objects; + const next = mergeSubscriptionEvent( + this._objects, + msg as SubscriptionEvent, + ); + if (next !== null) this._objects = next; }, - { type: "maintenance_supporter/subscribe" } + // deltas: only changed entries arrive — see helpers/subscription-merge. + { type: "maintenance_supporter/subscribe", deltas: true } ); // Detached mid-subscribe → drop the orphaned subscription. if (!this.isConnected) { diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts index 676f5ee..1f1a900 100644 --- a/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts @@ -2,6 +2,7 @@ import { LitElement, html, nothing } from "lit"; import { isSafeHttpUrl } from "./helpers/url"; +import { mergeSubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge"; import { isStaleBundle } from "./helpers/bundle-version"; import { customElement, property, state } from "lit/decorators.js"; import { sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence, setDateTimePrefs } from "./styles"; @@ -29,6 +30,7 @@ import type { StatisticsPoint, SavedView, SavedViewFilters, + ManualDocRef, } from "./types"; import { StatisticsService } from "./statistics-service"; import { UserService } from "./user-service"; @@ -385,10 +387,18 @@ export class MaintenanceSupporterPanel extends LitElement { ]); if (viewsResult) this._savedViews = (viewsResult as { views: SavedView[] }).views || []; if (objResult) this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects; - // Battery Fleet availability (Battery Notes present + not yet set up). + // A data refresh means the open task's history may have grown (complete, + // history edit) — the truncated list payload can't tell, so refetch. + if (this._view === "task" && this._selectedEntryId && this._selectedTaskId) { + this._fetchFullHistory(this._selectedEntryId, this._selectedTaskId); + } + // Battery Fleet availability (batteries present + not yet set up). + // The slim status check, NOT the overview: the full overview runs the + // trend machinery server-side (one recorder regression per healthy + // battery on a cold cache) — far too expensive for hiding a button. this.hass.connection .sendMessagePromise<{ available: boolean; configured: boolean }>({ - type: "maintenance_supporter/battery_fleet/overview", + type: "maintenance_supporter/battery_fleet/status", }) .then((ov) => { this._batteryFleetSetupAvailable = !!ov.available && !ov.configured; @@ -591,10 +601,15 @@ export class MaintenanceSupporterPanel extends LitElement { try { const unsub = await this.hass.connection.subscribeMessage( (msg: unknown) => { - const data = msg as { objects: MaintenanceObjectResponse[] }; - this._objects = data.objects; + const next = mergeSubscriptionEvent( + this._objects, + msg as SubscriptionEvent, + ); + if (next !== null) this._objects = next; }, - { type: "maintenance_supporter/subscribe" } + // deltas: only entries whose rebuilt response actually changed — + // no-op timer waves send nothing, a real change ships one object. + { type: "maintenance_supporter/subscribe", deltas: true } ); // If the element was detached while the subscribe was in flight, drop the // now-orphaned subscription instead of storing it on a dead component. @@ -880,6 +895,9 @@ export class MaintenanceSupporterPanel extends LitElement { this._activeTab = "overview"; this._historyFilter = null; this._scrollContentToTop(); + // Payload diet: list responses carry only the most recent history + // window — the detail's full timeline/charts load here, on demand. + this._fetchFullHistory(entryId, taskId); // Lazy-load statistics for the task's trigger entity const task = this._getTask(entryId, taskId); @@ -1785,6 +1803,52 @@ export class MaintenanceSupporterPanel extends LitElement { } } + /** Open a manual-tagged document from the objects table / object header: + * web-links directly, stored files through a signed path (the same + * Companion-safe recipe as the documents section). */ + private _openManualDoc(doc: ManualDocRef): void { + if (doc.kind !== "file") { + if (isSafeHttpUrl(doc.url)) window.open(doc.url!, "_blank", "noopener"); + return; + } + // Open the tab synchronously (inside the click gesture) so it isn't + // popup-blocked, then point it at the freshly signed URL. + const win = window.open("about:blank", "_blank"); + void this.hass.connection + .sendMessagePromise<{ path: string }>({ + type: "auth/sign_path", + path: `/api/maintenance_supporter/document/${doc.id}`, + expires: 300, + }) + .then((signed) => { + if (win) win.location.href = new URL(signed.path, window.location.origin).href; + }) + .catch(() => win?.close()); + } + + /** #73: persist one checklist tick. Sends the FULL current state (the + * server replaces, not merges — idempotent) and reloads so the progress + * header and any other open surface agree. */ + private async _setChecklistItem(entryId: string, taskId: string, item: string, done: boolean): Promise { + const obj = this._getObject(entryId); + const task = obj?.tasks.find((x) => x.id === taskId); + if (!task) return; + const state: Record = {}; + for (const step of task.checklist || []) { + const current = task.checklist_progress?.[step] ?? false; + state[step] = step === item ? done : current; + } + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/checklist_progress", + entry_id: entryId, task_id: taskId, checklist_state: state, + }); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + private _openCompleteDialog(entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void { const dlg = this.shadowRoot!.querySelector("maintenance-complete-dialog"); if (!dlg) return; @@ -1801,6 +1865,8 @@ export class MaintenanceSupporterPanel extends LitElement { ?.tasks.find((tsk) => tsk.id === taskId); dlg.taskType = tk?.type || ""; dlg.readingUnit = tk?.reading_unit || ""; + // #73: ticks recorded during the cycle prefill the dialog's checklist. + dlg.checklistPrefill = tk?.checklist_progress || {}; dlg.requiredFields = tk?.required_completion_fields || []; // Spare parts: a buy task gets an editable restock-qty field; a consuming // task shows what it will decrement (incl. the storage location). @@ -2765,13 +2831,22 @@ export class MaintenanceSupporterPanel extends LitElement { const area = o.area_id ? (this.hass?.areas?.[o.area_id]?.name || o.area_id) : "—"; return html`${area}`; } - case "documentation_url": + case "documentation_url": { + // Fallback: an UPLOADED manual (category "manual") is the object's + // manual just as much as the legacy URL field — an object with its + // handbook attached must not render "—" here (prod: Easee vs Epson). + const manualDoc = (o.manual_docs || [])[0]; return html`${ isSafeHttpUrl(o.documentation_url) ? html` e.stopPropagation()}>` - : "—" + : manualDoc + ? html` { e.preventDefault(); e.stopPropagation(); this._openManualDoc(manualDoc); }} + >` + : "—" }`; + } case "notes": return html`${o.notes || "—"}`; case "task_count": @@ -3085,7 +3160,16 @@ export class MaintenanceSupporterPanel extends LitElement { ? html`

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

` - : nothing} + : (o.manual_docs || []).length + ? html`

${t("documentation_url_label", L)}: + ${o.manual_docs!.slice(0, 3).map( + (m, i) => html`${i > 0 ? " · " : ""} { e.preventDefault(); this._openManualDoc(m); }}>${m.title}`, + )}${o.manual_docs!.length > 3 + ? html` … +${o.manual_docs!.length - 3}` + : nothing} +

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

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

` : nothing} ${o.warranty_expiry ? this._renderWarrantyMeta(o.warranty_expiry, L) : nothing} ${o.notes @@ -3157,6 +3241,7 @@ export class MaintenanceSupporterPanel extends LitElement { .entryId=${obj.entry_id} .parts=${obj.parts || []} .canWrite=${!isOperator} + .currencySymbol=${this._budget?.currency_symbol || DEFAULT_CURRENCY_SYMBOL} @parts-changed=${() => this._loadData()} >
@@ -3221,7 +3306,16 @@ export class MaintenanceSupporterPanel extends LitElement { const task = this._selectedEntryId && this._selectedTaskId ? this._getObject(this._selectedEntryId)?.tasks.find((tk) => tk.id === this._selectedTaskId) : undefined; - const readings = (task?.history || []) + // Payload diet: the summary's history is truncated to the recent window — + // the reading DELTAS must come from the full record (the oldest visible + // reading would otherwise lose or falsify its delta), which + // _fetchFullHistory loads for the open task. + const fh = this._fullHistory; + const fullHistory = + fh && fh.entryId === this._selectedEntryId && fh.taskId === this._selectedTaskId && fh.entries.length > (task?.history || []).length + ? fh.entries + : task?.history || []; + const readings = fullHistory .filter((h) => h.reading_value != null) .sort((a, b) => a.timestamp.localeCompare(b.timestamp)); return { @@ -3256,6 +3350,9 @@ export class MaintenanceSupporterPanel extends LitElement { taskId, objectName: obj?.object.name || "", objectDocUrl: obj?.object?.documentation_url ?? null, + objectManualDocs: obj?.object?.manual_docs ?? [], + openManualDoc: (doc) => this._openManualDoc(doc), + setChecklistItem: (item, done) => this._setChecklistItem(entryId, taskId, item, done), isOperator: this._isOperator, actionLoading: this._actionLoading, moreMenuOpen: this._moreMenuOpen, @@ -3293,12 +3390,37 @@ export class MaintenanceSupporterPanel extends LitElement { }; } + /** Full history for the OPEN task (list payloads are truncated to the + * most recent window). null until loaded; a failure — e.g. an older + * backend without `task/history` — falls back to the truncated list. */ + @state() private _fullHistory: { entryId: string; taskId: string; entries: HistoryEntry[] } | null = null; + + private async _fetchFullHistory(entryId: string, taskId: string): Promise { + try { + const res = (await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/history", + entry_id: entryId, + task_id: taskId, + })) as { history: HistoryEntry[] }; + if (this._selectedEntryId === entryId && this._selectedTaskId === taskId) { + this._fullHistory = { entryId, taskId, entries: res.history || [] }; + } + } catch { + this._fullHistory = null; + } + } + private _renderTaskDetail() { if (!this._selectedEntryId || !this._selectedTaskId) return nothing; const task = this._getTask(this._selectedEntryId, this._selectedTaskId); if (!task) return html`

Task not found.

`; + const fh = this._fullHistory; + const detailTask = + fh && fh.entryId === this._selectedEntryId && fh.taskId === this._selectedTaskId && fh.entries.length > (task.history || []).length + ? { ...task, history: fh.entries } + : task; return html``; } diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-strategy-shim.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-strategy-shim.ts index 0225090..17f8d44 100644 --- a/custom_components/maintenance_supporter/frontend-src/maintenance-strategy-shim.ts +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-strategy-shim.ts @@ -31,14 +31,25 @@ * (all of which only matter once a dashboard is actually being generated). */ +import { BUNDLE_VERSION } from "./helpers/bundle-version"; + const STRATEGY_TYPE = "maintenance-supporter"; const STRATEGY_TAG = `ll-strategy-dashboard-${STRATEGY_TYPE}`; const EDITOR_TAG = "hui-maintenance-supporter-strategy-editor"; // Absolute URL of the full strategy bundle (served at STRATEGY_URL by the // integration). The shim's only dependency, loaded on demand. +// +// Version-busted (issue #124): the bundle's chunk names are content-hashed +// and change every release, but browsers heuristically cache this entry +// (static serving sends no Cache-Control). A stale cached entry then imports +// chunk names the update deleted → 404 → the strategy dashboard dies until +// a hard refresh. The `?v=` makes the URL change with the release, so a +// fresh entry always pulls its matching chunks. BUNDLE_VERSION is inlined +// by esbuild — the built shim keeps its zero-import guarantee. const BUNDLE_URL = - "/maintenance_supporter_strategy/maintenance-dashboard-strategy.js"; + "/maintenance_supporter_strategy/maintenance-dashboard-strategy.js" + + `?v=${BUNDLE_VERSION}`; let _bundle: Promise | null = null; function loadBundle(): Promise { diff --git a/custom_components/maintenance_supporter/frontend-src/package-lock.json b/custom_components/maintenance_supporter/frontend-src/package-lock.json index d5f3ac9..b6798f2 100644 --- a/custom_components/maintenance_supporter/frontend-src/package-lock.json +++ b/custom_components/maintenance_supporter/frontend-src/package-lock.json @@ -9,10 +9,10 @@ "version": "0.1.0", "dependencies": { "pdfjs-dist": "^4.10.38", - "playwright": "1.61" + "playwright": "1.62" }, "devDependencies": { - "@open-wc/testing": "^4.0.0", + "@open-wc/testing": "^5.0.0", "@web/dev-server-esbuild": "^2.0.0", "@web/test-runner": "^1.0.0", "@web/test-runner-playwright": "^1.0.0", @@ -864,25 +864,25 @@ } }, "node_modules/@open-wc/semantic-dom-diff": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.20.1.tgz", - "integrity": "sha512-mPF/RPT2TU7Dw41LEDdaeP6eyTOWBD4z0+AHP4/d0SbgcfJZVRymlIB6DQmtz0fd2CImIS9kszaMmwMt92HBPA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.21.0.tgz", + "integrity": "sha512-2hrNt9MWhz4kfuIWI6M6zyK+UJXd2ehjaP+nJ1shf9HZAperr+braPToTbRODx1oE6EvoVvTJGQlsCqHv7NKQg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^4.3.1", - "@web/test-runner-commands": "^0.9.0" + "@web/test-runner-commands": "^1.0.0" } }, "node_modules/@open-wc/testing": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@open-wc/testing/-/testing-4.0.0.tgz", - "integrity": "sha512-KI70O0CJEpBWs3jrTju4BFCy7V/d4tFfYWkg8pMzncsDhD7TYNHLw5cy+s1FHXIgVFetnMDhPpwlKIPvtTQW7w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@open-wc/testing/-/testing-5.0.0.tgz", + "integrity": "sha512-2IcM2py1wtz4pyTW5whhltP5L71TVqt0l0IeA2KqHILyxOzI77YnDc9drfN7ezl0+fZbGOXM6qXe4Y4o0R7xpA==", "dev": true, "license": "MIT", "dependencies": { "@esm-bundle/chai": "^4.3.4-fix.0", - "@open-wc/semantic-dom-diff": "^0.20.0", + "@open-wc/semantic-dom-diff": "^0.21.0", "@open-wc/testing-helpers": "^3.0.0", "@types/chai-dom": "^1.11.0", "@types/sinon-chai": "^3.2.3", @@ -2011,16 +2011,16 @@ } }, "node_modules/@web/browser-logs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.1.tgz", - "integrity": "sha512-ypmMG+72ERm+LvP+loj9A64MTXvWMXHUOu773cPO4L1SV/VWg6xA9Pv7vkvkXQX+ItJtCJt+KQ+U6ui2HhSFUw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz", + "integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==", "dev": true, "license": "MIT", "dependencies": { "errorstacks": "^2.4.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/@web/config-loader": { @@ -2064,56 +2064,9 @@ } }, "node_modules/@web/dev-server-core": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.5.tgz", - "integrity": "sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/koa": "^2.11.6", - "@types/ws": "^7.4.0", - "@web/parse5-utils": "^2.1.0", - "chokidar": "^4.0.1", - "clone": "^2.1.2", - "es-module-lexer": "^1.0.0", - "get-stream": "^6.0.0", - "is-stream": "^2.0.0", - "isbinaryfile": "^5.0.0", - "koa": "^2.13.0", - "koa-etag": "^4.0.0", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "lru-cache": "^8.0.4", - "mime-types": "^2.1.27", - "parse5": "^6.0.1", - "picomatch": "^2.2.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@web/dev-server-esbuild": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@web/dev-server-esbuild/-/dev-server-esbuild-2.0.0.tgz", - "integrity": "sha512-D4BPYj3jO3kTDmytKWYB97xIVpR/Mdpy+zOyY1rEpFfFAcM59LCmO76Y8hp78Ssm4+4c2Gvd27gBVLSqWTPN3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mdn/browser-compat-data": "^4.0.0", - "@web/dev-server-core": "^1.0.0", - "esbuild": "^0.28.1", - "parse5": "^6.0.1", - "ua-parser-js": "^1.0.33" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/dev-server-esbuild/node_modules/@web/dev-server-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz", - "integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.1.tgz", + "integrity": "sha512-hwps4+UoNDoAP5oM2wLuwWi7AB9drW6k1ybRx0eLSly966zqj3gRSHCJmtGtC0u77HdnlED4PDFSdJfrq2Oo3A==", "dev": true, "license": "MIT", "dependencies": { @@ -2140,15 +2093,18 @@ "node": ">=22.0.0" } }, - "node_modules/@web/dev-server-esbuild/node_modules/@web/parse5-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz", - "integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==", + "node_modules/@web/dev-server-esbuild": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@web/dev-server-esbuild/-/dev-server-esbuild-2.0.0.tgz", + "integrity": "sha512-D4BPYj3jO3kTDmytKWYB97xIVpR/Mdpy+zOyY1rEpFfFAcM59LCmO76Y8hp78Ssm4+4c2Gvd27gBVLSqWTPN3Q==", "dev": true, "license": "MIT", "dependencies": { - "@types/parse5": "^6.0.1", - "parse5": "^6.0.1" + "@mdn/browser-compat-data": "^4.0.0", + "@web/dev-server-core": "^1.0.0", + "esbuild": "^0.28.1", + "parse5": "^6.0.1", + "ua-parser-js": "^1.0.33" }, "engines": { "node": ">=22.0.0" @@ -2172,98 +2128,10 @@ "node": ">=22.0.0" } }, - "node_modules/@web/dev-server-rollup/node_modules/@web/dev-server-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz", - "integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/koa": "^2.11.6", - "@types/ws": "^7.4.0", - "@web/parse5-utils": "^3.0.0", - "chokidar": "^4.0.1", - "clone": "^2.1.2", - "es-module-lexer": "^1.0.0", - "get-stream": "^6.0.0", - "is-stream": "^2.0.0", - "isbinaryfile": "^5.0.0", - "koa": "^2.16.1", - "koa-etag": "^4.0.0", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "lru-cache": "^8.0.4", - "mime-types": "^2.1.27", - "parse5": "^6.0.1", - "picomatch": "^2.3.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/dev-server-rollup/node_modules/@web/parse5-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz", - "integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse5": "^6.0.1", - "parse5": "^6.0.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/dev-server/node_modules/@web/dev-server-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz", - "integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/koa": "^2.11.6", - "@types/ws": "^7.4.0", - "@web/parse5-utils": "^3.0.0", - "chokidar": "^4.0.1", - "clone": "^2.1.2", - "es-module-lexer": "^1.0.0", - "get-stream": "^6.0.0", - "is-stream": "^2.0.0", - "isbinaryfile": "^5.0.0", - "koa": "^2.16.1", - "koa-etag": "^4.0.0", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "lru-cache": "^8.0.4", - "mime-types": "^2.1.27", - "parse5": "^6.0.1", - "picomatch": "^2.3.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/dev-server/node_modules/@web/parse5-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz", - "integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse5": "^6.0.1", - "parse5": "^6.0.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, "node_modules/@web/parse5-utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.1.tgz", - "integrity": "sha512-7rBVZEMGfrq2iPcAEwJ0KSNSvmA2a6jT2CK8/gyIOHgn4reg7bSSRbzyWIEYWyIkeRoYEukX/aW+nAeCgSSqhQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz", + "integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==", "dev": true, "license": "MIT", "dependencies": { @@ -2271,7 +2139,7 @@ "parse5": "^6.0.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/@web/test-runner": { @@ -2322,64 +2190,21 @@ "node": ">=22.0.0" } }, - "node_modules/@web/test-runner-chrome/node_modules/@web/browser-logs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz", - "integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==", + "node_modules/@web/test-runner-commands": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@web/test-runner-commands/-/test-runner-commands-1.0.1.tgz", + "integrity": "sha512-rvAvHJKlNzh5Tv8L2wGjnNX/hSkPmQx5eyKqeyExEvA7A2pVXiNYFstJzUGCD52TlAJSPF3BtW1yOwpf6vG7Eg==", "dev": true, "license": "MIT", "dependencies": { - "errorstacks": "^2.4.1" + "@web/test-runner-core": "^1.0.0", + "mkdirp": "^1.0.4" }, "engines": { "node": ">=22.0.0" } }, - "node_modules/@web/test-runner-chrome/node_modules/@web/dev-server-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz", - "integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/koa": "^2.11.6", - "@types/ws": "^7.4.0", - "@web/parse5-utils": "^3.0.0", - "chokidar": "^4.0.1", - "clone": "^2.1.2", - "es-module-lexer": "^1.0.0", - "get-stream": "^6.0.0", - "is-stream": "^2.0.0", - "isbinaryfile": "^5.0.0", - "koa": "^2.16.1", - "koa-etag": "^4.0.0", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "lru-cache": "^8.0.4", - "mime-types": "^2.1.27", - "parse5": "^6.0.1", - "picomatch": "^2.3.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner-chrome/node_modules/@web/parse5-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz", - "integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse5": "^6.0.1", - "parse5": "^6.0.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner-chrome/node_modules/@web/test-runner-core": { + "node_modules/@web/test-runner-core": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-1.0.0.tgz", "integrity": "sha512-rn+yYVQGrnr0dW7e/6CjwiefjXAGbQV+ddKJ1PLQ6WOvDAzqPBs7rBYFasy3nkJdgyDRhZStvWtunYGmiR53DA==", @@ -2417,58 +2242,6 @@ "node": ">=22.0.0" } }, - "node_modules/@web/test-runner-commands": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@web/test-runner-commands/-/test-runner-commands-0.9.0.tgz", - "integrity": "sha512-zeLI6QdH0jzzJMDV5O42Pd8WLJtYqovgdt0JdytgHc0d1EpzXDsc7NTCJSImboc2NcayIsWAvvGGeRF69SMMYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@web/test-runner-core": "^0.13.0", - "mkdirp": "^1.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@web/test-runner-core": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.4.tgz", - "integrity": "sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.11", - "@types/babel__code-frame": "^7.0.2", - "@types/co-body": "^6.1.0", - "@types/convert-source-map": "^2.0.0", - "@types/debounce": "^1.2.0", - "@types/istanbul-lib-coverage": "^2.0.3", - "@types/istanbul-reports": "^3.0.0", - "@web/browser-logs": "^0.4.0", - "@web/dev-server-core": "^0.7.3", - "chokidar": "^4.0.1", - "cli-cursor": "^3.1.0", - "co-body": "^6.1.0", - "convert-source-map": "^2.0.0", - "debounce": "^1.2.0", - "dependency-graph": "^0.11.0", - "globby": "^11.0.1", - "internal-ip": "^6.2.0", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.0.2", - "log-update": "^4.0.0", - "nanocolors": "^0.2.1", - "nanoid": "^3.1.25", - "open": "^8.0.2", - "picomatch": "^2.2.2", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@web/test-runner-coverage-v8": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-1.0.0.tgz", @@ -2486,101 +2259,6 @@ "node": ">=22.0.0" } }, - "node_modules/@web/test-runner-coverage-v8/node_modules/@web/browser-logs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz", - "integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "errorstacks": "^2.4.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner-coverage-v8/node_modules/@web/dev-server-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz", - "integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/koa": "^2.11.6", - "@types/ws": "^7.4.0", - "@web/parse5-utils": "^3.0.0", - "chokidar": "^4.0.1", - "clone": "^2.1.2", - "es-module-lexer": "^1.0.0", - "get-stream": "^6.0.0", - "is-stream": "^2.0.0", - "isbinaryfile": "^5.0.0", - "koa": "^2.16.1", - "koa-etag": "^4.0.0", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "lru-cache": "^8.0.4", - "mime-types": "^2.1.27", - "parse5": "^6.0.1", - "picomatch": "^2.3.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner-coverage-v8/node_modules/@web/parse5-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz", - "integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse5": "^6.0.1", - "parse5": "^6.0.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner-coverage-v8/node_modules/@web/test-runner-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-1.0.0.tgz", - "integrity": "sha512-rn+yYVQGrnr0dW7e/6CjwiefjXAGbQV+ddKJ1PLQ6WOvDAzqPBs7rBYFasy3nkJdgyDRhZStvWtunYGmiR53DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.11", - "@types/babel__code-frame": "^7.0.2", - "@types/co-body": "^6.1.0", - "@types/convert-source-map": "^2.0.0", - "@types/debounce": "^1.2.0", - "@types/istanbul-lib-coverage": "^2.0.3", - "@types/istanbul-reports": "^3.0.0", - "@web/browser-logs": "^1.0.0", - "@web/dev-server-core": "^1.0.0", - "chokidar": "^4.0.1", - "cli-cursor": "^3.1.0", - "co-body": "^6.1.0", - "convert-source-map": "^2.0.0", - "debounce": "^1.2.0", - "dependency-graph": "^0.11.0", - "globby": "^11.0.1", - "internal-ip": "^6.2.0", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.0.2", - "log-update": "^4.0.0", - "nanocolors": "^0.2.1", - "nanoid": "^3.1.25", - "open": "^8.0.2", - "picomatch": "^2.3.2", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=22.0.0" - } - }, "node_modules/@web/test-runner-mocha": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@web/test-runner-mocha/-/test-runner-mocha-1.0.0.tgz", @@ -2594,101 +2272,6 @@ "node": ">=22.0.0" } }, - "node_modules/@web/test-runner-mocha/node_modules/@web/browser-logs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz", - "integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "errorstacks": "^2.4.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner-mocha/node_modules/@web/dev-server-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz", - "integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/koa": "^2.11.6", - "@types/ws": "^7.4.0", - "@web/parse5-utils": "^3.0.0", - "chokidar": "^4.0.1", - "clone": "^2.1.2", - "es-module-lexer": "^1.0.0", - "get-stream": "^6.0.0", - "is-stream": "^2.0.0", - "isbinaryfile": "^5.0.0", - "koa": "^2.16.1", - "koa-etag": "^4.0.0", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "lru-cache": "^8.0.4", - "mime-types": "^2.1.27", - "parse5": "^6.0.1", - "picomatch": "^2.3.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner-mocha/node_modules/@web/parse5-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz", - "integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse5": "^6.0.1", - "parse5": "^6.0.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner-mocha/node_modules/@web/test-runner-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-1.0.0.tgz", - "integrity": "sha512-rn+yYVQGrnr0dW7e/6CjwiefjXAGbQV+ddKJ1PLQ6WOvDAzqPBs7rBYFasy3nkJdgyDRhZStvWtunYGmiR53DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.11", - "@types/babel__code-frame": "^7.0.2", - "@types/co-body": "^6.1.0", - "@types/convert-source-map": "^2.0.0", - "@types/debounce": "^1.2.0", - "@types/istanbul-lib-coverage": "^2.0.3", - "@types/istanbul-reports": "^3.0.0", - "@web/browser-logs": "^1.0.0", - "@web/dev-server-core": "^1.0.0", - "chokidar": "^4.0.1", - "cli-cursor": "^3.1.0", - "co-body": "^6.1.0", - "convert-source-map": "^2.0.0", - "debounce": "^1.2.0", - "dependency-graph": "^0.11.0", - "globby": "^11.0.1", - "internal-ip": "^6.2.0", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.0.2", - "log-update": "^4.0.0", - "nanocolors": "^0.2.1", - "nanoid": "^3.1.25", - "open": "^8.0.2", - "picomatch": "^2.3.2", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=22.0.0" - } - }, "node_modules/@web/test-runner-playwright": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@web/test-runner-playwright/-/test-runner-playwright-1.0.0.tgz", @@ -2704,210 +2287,6 @@ "node": ">=22.0.0" } }, - "node_modules/@web/test-runner-playwright/node_modules/@web/browser-logs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz", - "integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "errorstacks": "^2.4.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner-playwright/node_modules/@web/dev-server-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz", - "integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/koa": "^2.11.6", - "@types/ws": "^7.4.0", - "@web/parse5-utils": "^3.0.0", - "chokidar": "^4.0.1", - "clone": "^2.1.2", - "es-module-lexer": "^1.0.0", - "get-stream": "^6.0.0", - "is-stream": "^2.0.0", - "isbinaryfile": "^5.0.0", - "koa": "^2.16.1", - "koa-etag": "^4.0.0", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "lru-cache": "^8.0.4", - "mime-types": "^2.1.27", - "parse5": "^6.0.1", - "picomatch": "^2.3.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner-playwright/node_modules/@web/parse5-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz", - "integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse5": "^6.0.1", - "parse5": "^6.0.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner-playwright/node_modules/@web/test-runner-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-1.0.0.tgz", - "integrity": "sha512-rn+yYVQGrnr0dW7e/6CjwiefjXAGbQV+ddKJ1PLQ6WOvDAzqPBs7rBYFasy3nkJdgyDRhZStvWtunYGmiR53DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.11", - "@types/babel__code-frame": "^7.0.2", - "@types/co-body": "^6.1.0", - "@types/convert-source-map": "^2.0.0", - "@types/debounce": "^1.2.0", - "@types/istanbul-lib-coverage": "^2.0.3", - "@types/istanbul-reports": "^3.0.0", - "@web/browser-logs": "^1.0.0", - "@web/dev-server-core": "^1.0.0", - "chokidar": "^4.0.1", - "cli-cursor": "^3.1.0", - "co-body": "^6.1.0", - "convert-source-map": "^2.0.0", - "debounce": "^1.2.0", - "dependency-graph": "^0.11.0", - "globby": "^11.0.1", - "internal-ip": "^6.2.0", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.0.2", - "log-update": "^4.0.0", - "nanocolors": "^0.2.1", - "nanoid": "^3.1.25", - "open": "^8.0.2", - "picomatch": "^2.3.2", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner/node_modules/@web/browser-logs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-1.0.0.tgz", - "integrity": "sha512-hK/pDqdw8SV29QDtFaI/tjU5xBPUCZ2lfi2AnG8NoWZadRJziC5fMDZDR23yWtvqVHyrxJaogBvfOKgzMubSLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "errorstacks": "^2.4.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner/node_modules/@web/dev-server-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-1.0.0.tgz", - "integrity": "sha512-xe7hE/MMHy0mE8PKSu6lLEYTgk0AKqCKC2Ait80OokQ8RD9n1D/GF4aU3CwFP0KgLZUPkiWj2D4qq6mbgaW6Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/koa": "^2.11.6", - "@types/ws": "^7.4.0", - "@web/parse5-utils": "^3.0.0", - "chokidar": "^4.0.1", - "clone": "^2.1.2", - "es-module-lexer": "^1.0.0", - "get-stream": "^6.0.0", - "is-stream": "^2.0.0", - "isbinaryfile": "^5.0.0", - "koa": "^2.16.1", - "koa-etag": "^4.0.0", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "lru-cache": "^8.0.4", - "mime-types": "^2.1.27", - "parse5": "^6.0.1", - "picomatch": "^2.3.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner/node_modules/@web/parse5-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-3.0.0.tgz", - "integrity": "sha512-NmmZneHvBHjZ5rHpEtD7AnvrUQQL4KUyLhj5YH8CDagIfTC/PTTEl7fXYNwA7+diJuaOPXGfYzpxzhAy829ijg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse5": "^6.0.1", - "parse5": "^6.0.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner/node_modules/@web/test-runner-commands": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/test-runner-commands/-/test-runner-commands-1.0.0.tgz", - "integrity": "sha512-8wXJkwvWWCc6GTPsdjbEN141kz+4MQPFjsId47hoibkY3FhkgeKJUm+cnwMYCIXHuWA0e7V+L2fmH087UPrXXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@web/test-runner-core": "^1.0.0", - "mkdirp": "^1.0.4" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@web/test-runner/node_modules/@web/test-runner-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-1.0.0.tgz", - "integrity": "sha512-rn+yYVQGrnr0dW7e/6CjwiefjXAGbQV+ddKJ1PLQ6WOvDAzqPBs7rBYFasy3nkJdgyDRhZStvWtunYGmiR53DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.11", - "@types/babel__code-frame": "^7.0.2", - "@types/co-body": "^6.1.0", - "@types/convert-source-map": "^2.0.0", - "@types/debounce": "^1.2.0", - "@types/istanbul-lib-coverage": "^2.0.3", - "@types/istanbul-reports": "^3.0.0", - "@web/browser-logs": "^1.0.0", - "@web/dev-server-core": "^1.0.0", - "chokidar": "^4.0.1", - "cli-cursor": "^3.1.0", - "co-body": "^6.1.0", - "convert-source-map": "^2.0.0", - "debounce": "^1.2.0", - "dependency-graph": "^0.11.0", - "globby": "^11.0.1", - "internal-ip": "^6.2.0", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.0.2", - "log-update": "^4.0.0", - "nanocolors": "^0.2.1", - "nanoid": "^3.1.25", - "open": "^8.0.2", - "picomatch": "^2.3.2", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=22.0.0" - } - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -5366,33 +4745,33 @@ } }, "node_modules/playwright": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", - "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.61.1" + "playwright-core": "1.62.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", - "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/portfinder": { diff --git a/custom_components/maintenance_supporter/frontend-src/package.json b/custom_components/maintenance_supporter/frontend-src/package.json index 4c7b2b7..ab2bfc3 100644 --- a/custom_components/maintenance_supporter/frontend-src/package.json +++ b/custom_components/maintenance_supporter/frontend-src/package.json @@ -9,7 +9,7 @@ "test:watch": "web-test-runner --config web-test-runner.config.mjs --watch" }, "devDependencies": { - "@open-wc/testing": "^4.0.0", + "@open-wc/testing": "^5.0.0", "@web/dev-server-esbuild": "^2.0.0", "@web/test-runner": "^1.0.0", "@web/test-runner-playwright": "^1.0.0", @@ -19,7 +19,7 @@ }, "dependencies": { "pdfjs-dist": "^4.10.38", - "playwright": "1.61" + "playwright": "1.62" }, "overrides": { "esbuild": "$esbuild", diff --git a/custom_components/maintenance_supporter/frontend-src/panel-styles.ts b/custom_components/maintenance_supporter/frontend-src/panel-styles.ts index 694b84a..2a09804 100644 --- a/custom_components/maintenance_supporter/frontend-src/panel-styles.ts +++ b/custom_components/maintenance_supporter/frontend-src/panel-styles.ts @@ -81,7 +81,10 @@ export const panelStyles = css` flex-wrap: wrap; gap: 8px; padding: 4px 0 8px; - justify-content: flex-end; + /* Left-aligned on purpose: flex-end mimicked the pre-v2.37 look (buttons + trailing the filter row), but on wide desktops it strands the primary + "new task" action at the far right of an otherwise empty row. */ + justify-content: flex-start; } :host([narrow]) .actions-bar { @@ -1009,6 +1012,21 @@ export const panelStyles = css` .checklist-preview-list li { padding: 1px 0; } + /* #73: interactive in-cycle ticks. */ + .checklist-preview-list label { + display: inline-flex; + gap: 8px; + align-items: baseline; + cursor: pointer; + } + .checklist-preview-list input[type="checkbox"] { + accent-color: var(--primary-color); + cursor: pointer; + } + .checklist-preview-list li.checked label span { + text-decoration: line-through; + opacity: 0.6; + } /* Recommendation Card */ .recommendation-card { 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 32e3215..25f654e 100644 --- a/custom_components/maintenance_supporter/frontend-src/renderers/task-detail.ts +++ b/custom_components/maintenance_supporter/frontend-src/renderers/task-detail.ts @@ -12,7 +12,7 @@ import { html, nothing } from "lit"; import { isSafeHttpUrl } from "../helpers/url"; import { t, formatDate, formatDateTime, formatRecurrence } from "../styles"; -import type { AdvancedFeatures, HomeAssistant, MaintenanceTask } from "../types"; +import type { AdvancedFeatures, HomeAssistant, MaintenanceTask, ManualDocRef } from "../types"; import { renderTriggerSection, type SparklineContext } from "./sparkline"; import { renderPredictionSection } from "./prediction"; import { renderWeibullSection } from "./weibull"; @@ -32,6 +32,13 @@ export interface TaskDetailContext { objectName: string; /** Parent object's documentation_url (raw; sanitised here). */ objectDocUrl: string | null | undefined; + /** Parent object's manual-tagged documents — the fallback for the manual + * row when documentation_url is empty (same rule as the objects table). */ + objectManualDocs: ManualDocRef[]; + /** Opens a manual document (signed path / weblink) — panel-owned. */ + openManualDoc: (doc: ManualDocRef) => void; + /** #73: persist one checklist tick (panel sends the full state via WS). */ + setChecklistItem: (item: string, done: boolean) => void; isOperator: boolean; actionLoading: boolean; moreMenuOpen: boolean; @@ -178,22 +185,38 @@ function collapsible(key: string, titleKey: string, body: unknown, ctx: TaskDeta `; } -/** Read-only preview of the configured checklist steps so users can see - * the steps without having to open the Edit or Complete dialog. Only - * rendered when the Checklists feature is enabled and steps are set. */ +/** Interactive checklist (#73): steps can be ticked off DURING the cycle + * without completing the task — progress persists server-side (survives + * reloads, prefills the complete dialog) and resets when the task is + * completed or skipped. Only rendered when the Checklists feature is + * enabled and steps are set. */ function renderChecklistCard(task: MaintenanceTask, ctx: TaskDetailContext) { if (!ctx.features.checklists) return nothing; const items = task.checklist || []; if (items.length === 0) return nothing; const L = ctx.lang; + const progress = task.checklist_progress || {}; + const done = items.filter((item) => progress[item]).length; return html`
- ${t("checklist", L)} (${items.length}) + ${t("checklist", L)} (${done}/${items.length})
    - ${items.map((item) => html`
  1. ${item}
  2. `)} + ${items.map((item) => html` +
  3. + +
  4. + `)}
`; @@ -206,7 +229,10 @@ function renderTaskMeta(task: MaintenanceTask, ctx: TaskDetailContext) { const safeTaskUrl = isSafeHttpUrl(task.documentation_url) ? task.documentation_url : null; const safeObjUrl = isSafeHttpUrl(ctx.objectDocUrl) ? ctx.objectDocUrl : null; - if (!task.notes && !safeTaskUrl && !safeObjUrl) return nothing; + // Same fallback rule as the objects table: an UPLOADED manual (category + // "manual") stands in when the object's URL field is empty. + const manualDoc = safeObjUrl ? null : (ctx.objectManualDocs || [])[0]; + if (!task.notes && !safeTaskUrl && !safeObjUrl && !manualDoc) return nothing; const L = ctx.lang; return html`
@@ -227,6 +253,13 @@ function renderTaskMeta(task: MaintenanceTask, ctx: TaskDetailContext) { ${t("documentation_url_label", L)} (${ctx.objectName})
+ ` : manualDoc ? html` + ` : nothing}
`; diff --git a/custom_components/maintenance_supporter/frontend-src/types.ts b/custom_components/maintenance_supporter/frontend-src/types.ts index b2ee111..a6c55b5 100644 --- a/custom_components/maintenance_supporter/frontend-src/types.ts +++ b/custom_components/maintenance_supporter/frontend-src/types.ts @@ -31,6 +31,17 @@ export interface MaintenanceObject { /** (roadmap P2) number of attached documents (files + web-links); drives the * objects-table paperclip badge. Computed server-side, not persisted. */ document_count?: number; + /** Attached documents tagged as manuals — the fallback for the "manual" + * column/header when documentation_url is unset. Computed server-side. */ + manual_docs?: ManualDocRef[]; +} + +/** Slim reference to a manual-tagged document (subset of MaintenanceDocument). */ +export interface ManualDocRef { + id: string; + title: string; + kind: string; // "file" | "weblink" + url?: string | null; } export interface TriggerConfig { @@ -161,6 +172,9 @@ export interface MaintenanceTask { notes?: string | null; documentation_url?: string | null; checklist?: string[]; + /** #73: in-cycle ticks ({item text: bool}); persists server-side, resets on + * complete/skip. */ + checklist_progress?: Record; labels?: string[]; assignee_pool?: string[]; rotation_strategy?: string | null; @@ -184,7 +198,11 @@ export interface MaintenanceTask { trigger_entity_infos?: TriggerEntityInfo[] | null; /** Battery Fleet: the single aggregate task renders the battery section. */ battery_fleet_task?: boolean; + /** LIST payloads carry only the most recent window (payload diet) — the + * task detail fetches the full record via `task/history`. */ history: HistoryEntry[]; + /** Total entries that exist, including those beyond the list window. */ + history_count?: number; // Computed status: string; // "ok" | "due_soon" | "overdue" | "triggered" | "archived" /** True for a one-time task that has been completed (done; never re-arms). */ diff --git a/custom_components/maintenance_supporter/frontend/__init__.py b/custom_components/maintenance_supporter/frontend/__init__.py index e451b1a..180585e 100644 --- a/custom_components/maintenance_supporter/frontend/__init__.py +++ b/custom_components/maintenance_supporter/frontend/__init__.py @@ -20,6 +20,7 @@ from ..const import ( STRATEGY_URL, VENDOR_URL, ) +from ..panel import _async_file_hash _LOGGER = logging.getLogger(__name__) @@ -93,10 +94,24 @@ async def async_register_card(hass: HomeAssistant) -> None: # whenDefined race even under heavy HACS plugin load (the bundle is loaded # lazily by the shim). The heavy bundle's static path stays mounted above # so the shim's import() can fetch it on demand. + # + # Every URL carries a content-hash query (issue #124): these module + # scripts are served without Cache-Control, so browsers cache them + # heuristically. After an update a stale cached strategy entry kept + # importing chunk names the update had deleted → 404 → the strategy + # dashboard rendered nothing until a hard refresh (and a rollback + # "fixed" it by putting the old chunks back). A hash-busted URL changes + # with the file, so each release starts from a fresh entry point; the + # chunk layer below is content-hashed already. Same trick the panel has + # used since #112. extra = hass.data.setdefault(DATA_EXTRA_MODULE_URL, set()) - extra.add(CARD_URL) - extra.add(STRATEGY_SHIM_URL) - extra.add(CALENDAR_CARD_URL) + for url, filename in ( + (CARD_URL, "maintenance-card.js"), + (STRATEGY_SHIM_URL, "maintenance-strategy-shim.js"), + (CALENDAR_CARD_URL, "maintenance-calendar-card.js"), + ): + digest = await _async_file_hash(hass, frontend_dir / filename) + extra.add(f"{url}?v={digest}") hass.data[_CARD_REGISTERED_KEY] = True _LOGGER.debug( diff --git a/custom_components/maintenance_supporter/frontend/locales/cs.json b/custom_components/maintenance_supporter/frontend/locales/cs.json index 5d3e9a4..9d54d86 100644 --- a/custom_components/maintenance_supporter/frontend/locales/cs.json +++ b/custom_components/maintenance_supporter/frontend/locales/cs.json @@ -716,6 +716,7 @@ "series_end_count_label": "Počet opakování", "series_end_until_label": "Datum konce", "parts_section": "Díly a spotřební materiál", + "parts_inventory_value": "Hodnota zásob", "part_add": "Přidat díl", "part_name": "Název", "part_vendor": "Výrobce", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Brzy", "battery_fleet_status_ok": "V pořádku", "battery_fleet_predicted_on": "Očekáváno kolem {date}", + "battery_fleet_predicted_trend": "Předpověď z trendu vybíjení této baterie: přibližně {date} ({confidence})", + "battery_fleet_rechargeable": "Akumulátor: nabíjí se místo výměny — nikdy na nákupním seznamu", + "battery_fleet_sort_name": "Řadit podle názvu", + "battery_fleet_sort_urgency": "Řadit podle naléhavosti", + "battery_fleet_mark_recharged": "Označit jako nabitou", + "battery_fleet_sparkline_hint": "Stav baterie za posledních 30 dní — tečkovaně: projekce k prahu vybití", + "battery_fleet_filter_type": "Zobrazit pouze tento typ baterie", + "battery_fleet_record_replacement": "Stav poskočil kolem {date} — zaznamenat tuto výměnu do Battery Notes", "battery_fleet_total": "Sledováno baterií: {n}", "battery_fleet_setup_button": "Flotila baterií", "battery_fleet_setup_done": "Flotila baterií nastavena — jeden úkol sleduje všechny.", diff --git a/custom_components/maintenance_supporter/frontend/locales/da.json b/custom_components/maintenance_supporter/frontend/locales/da.json index 631681f..ce3c16f 100644 --- a/custom_components/maintenance_supporter/frontend/locales/da.json +++ b/custom_components/maintenance_supporter/frontend/locales/da.json @@ -716,6 +716,7 @@ "series_end_count_label": "Antal gange", "series_end_until_label": "Slutdato", "parts_section": "Dele & forbrugsvarer", + "parts_inventory_value": "Lagerværdi", "part_add": "Tilføj del", "part_name": "Navn", "part_vendor": "Producent", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Snart", "battery_fleet_status_ok": "I orden", "battery_fleet_predicted_on": "Forventes omkring {date}", + "battery_fleet_predicted_trend": "Forudsagt ud fra batteriets afladningstendens: omkring {date} ({confidence})", + "battery_fleet_rechargeable": "Genopladeligt: oplad i stedet for at udskifte — aldrig på indkøbslisten", + "battery_fleet_sort_name": "Sortér efter navn", + "battery_fleet_sort_urgency": "Sortér efter hastende grad", + "battery_fleet_mark_recharged": "Markér som genopladet", + "battery_fleet_sparkline_hint": "Batteriniveau de seneste 30 dage — stiplet: fremskrivning ned til lav-tærsklen", + "battery_fleet_filter_type": "Vis kun denne batteritype", + "battery_fleet_record_replacement": "Niveauet sprang omkring {date} — registrér denne udskiftning i Battery Notes", "battery_fleet_total": "{n} batterier overvåges", "battery_fleet_setup_button": "Batteriflåde", "battery_fleet_setup_done": "Batteriflåde opsat — én opgave følger alle batterier.", diff --git a/custom_components/maintenance_supporter/frontend/locales/de.json b/custom_components/maintenance_supporter/frontend/locales/de.json index fcf3ebb..70991d2 100644 --- a/custom_components/maintenance_supporter/frontend/locales/de.json +++ b/custom_components/maintenance_supporter/frontend/locales/de.json @@ -716,6 +716,7 @@ "series_end_count_label": "Anzahl", "series_end_until_label": "Enddatum", "parts_section": "Teile & Verbrauchsmaterial", + "parts_inventory_value": "Lagerwert", "part_add": "Teil hinzufügen", "part_name": "Name", "part_vendor": "Hersteller", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Bald", "battery_fleet_status_ok": "In Ordnung", "battery_fleet_predicted_on": "Voraussichtlich um {date}", + "battery_fleet_predicted_trend": "Aus dem Entladetrend dieser Batterie vorhergesagt: etwa {date} ({confidence})", + "battery_fleet_rechargeable": "Wiederaufladbar: laden statt ersetzen — nie auf der Einkaufsliste", + "battery_fleet_sort_name": "Nach Name sortieren", + "battery_fleet_sort_urgency": "Nach Dringlichkeit sortieren", + "battery_fleet_mark_recharged": "Als aufgeladen markieren", + "battery_fleet_sparkline_hint": "Batteriestand der letzten 30 Tage — gepunktet: Prognose bis zur Low-Schwelle", + "battery_fleet_filter_type": "Nur diesen Batterietyp anzeigen", + "battery_fleet_record_replacement": "Der Batteriestand ist um den {date} gesprungen — diesen Wechsel in Battery Notes nachtragen", "battery_fleet_total": "{n} Batterien überwacht", "battery_fleet_setup_button": "Batterie-Flotte", "battery_fleet_setup_done": "Batterie-Flotte eingerichtet — ein Task überwacht alle Batterien.", diff --git a/custom_components/maintenance_supporter/frontend/locales/en.json b/custom_components/maintenance_supporter/frontend/locales/en.json index 556a52b..ca508fc 100644 --- a/custom_components/maintenance_supporter/frontend/locales/en.json +++ b/custom_components/maintenance_supporter/frontend/locales/en.json @@ -722,6 +722,7 @@ "series_end_count_label": "Number of times", "series_end_until_label": "End date", "parts_section": "Parts & consumables", + "parts_inventory_value": "Inventory value", "part_add": "Add part", "part_name": "Name", "part_vendor": "Manufacturer", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Soon", "battery_fleet_status_ok": "Healthy", "battery_fleet_predicted_on": "Expected around {date}", + "battery_fleet_predicted_trend": "Predicted from this battery's discharge trend: around {date} ({confidence})", + "battery_fleet_rechargeable": "Rechargeable: charge instead of replacing — never on the shopping list", + "battery_fleet_sort_name": "Sort by name", + "battery_fleet_sort_urgency": "Sort by urgency", + "battery_fleet_mark_recharged": "Mark as recharged", + "battery_fleet_sparkline_hint": "Battery level over the last 30 days — dotted: projected until the low threshold", + "battery_fleet_filter_type": "Show only this battery type", + "battery_fleet_record_replacement": "The level jumped around {date} — record this replacement in Battery Notes", "battery_fleet_total": "{n} batteries tracked", "battery_fleet_setup_button": "Battery fleet", "battery_fleet_setup_done": "Battery fleet set up — one task tracks all your batteries.", diff --git a/custom_components/maintenance_supporter/frontend/locales/es.json b/custom_components/maintenance_supporter/frontend/locales/es.json index 49be220..5743df8 100644 --- a/custom_components/maintenance_supporter/frontend/locales/es.json +++ b/custom_components/maintenance_supporter/frontend/locales/es.json @@ -716,6 +716,7 @@ "series_end_count_label": "Número de veces", "series_end_until_label": "Fecha de fin", "parts_section": "Piezas y consumibles", + "parts_inventory_value": "Valor del inventario", "part_add": "Añadir pieza", "part_name": "Nombre", "part_vendor": "Fabricante", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Pronto", "battery_fleet_status_ok": "Correcta", "battery_fleet_predicted_on": "Previsto hacia {date}", + "battery_fleet_predicted_trend": "Predicción según la tendencia de descarga de esta pila: hacia {date} ({confidence})", + "battery_fleet_rechargeable": "Recargable: se recarga en lugar de sustituirse — nunca en la lista de compra", + "battery_fleet_sort_name": "Ordenar por nombre", + "battery_fleet_sort_urgency": "Ordenar por urgencia", + "battery_fleet_mark_recharged": "Marcar como recargada", + "battery_fleet_sparkline_hint": "Nivel de batería de los últimos 30 días — punteado: proyección hasta el umbral bajo", + "battery_fleet_filter_type": "Mostrar solo este tipo de pila", + "battery_fleet_record_replacement": "El nivel dio un salto hacia el {date} — registrar esta sustitución en Battery Notes", "battery_fleet_total": "{n} baterías monitorizadas", "battery_fleet_setup_button": "Flota de baterías", "battery_fleet_setup_done": "Flota de baterías configurada — una tarea controla todas.", diff --git a/custom_components/maintenance_supporter/frontend/locales/fi.json b/custom_components/maintenance_supporter/frontend/locales/fi.json index 91051ee..6d01eb0 100644 --- a/custom_components/maintenance_supporter/frontend/locales/fi.json +++ b/custom_components/maintenance_supporter/frontend/locales/fi.json @@ -716,6 +716,7 @@ "series_end_count_label": "Kertojen määrä", "series_end_until_label": "Päättymispäivä", "parts_section": "Osat ja tarvikkeet", + "parts_inventory_value": "Varaston arvo", "part_add": "Lisää osa", "part_name": "Nimi", "part_vendor": "Valmistaja", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Pian", "battery_fleet_status_ok": "Kunnossa", "battery_fleet_predicted_on": "Odotettavissa noin {date}", + "battery_fleet_predicted_trend": "Ennustettu tämän pariston purkautumistrendistä: noin {date} ({confidence})", + "battery_fleet_rechargeable": "Ladattava: lataa vaihtamisen sijaan — ei koskaan ostoslistalle", + "battery_fleet_sort_name": "Lajittele nimen mukaan", + "battery_fleet_sort_urgency": "Lajittele kiireellisyyden mukaan", + "battery_fleet_mark_recharged": "Merkitse ladatuksi", + "battery_fleet_sparkline_hint": "Akun varaustaso viimeisten 30 päivän ajalta — pisteviiva: ennuste alarajaan asti", + "battery_fleet_filter_type": "Näytä vain tämä paristotyyppi", + "battery_fleet_record_replacement": "Varaustaso hyppäsi noin {date} — kirjaa tämä vaihto Battery Notesiin", "battery_fleet_total": "{n} paristoa seurannassa", "battery_fleet_setup_button": "Paristokanta", "battery_fleet_setup_done": "Paristokanta määritetty — yksi tehtävä seuraa kaikkia.", diff --git a/custom_components/maintenance_supporter/frontend/locales/fr.json b/custom_components/maintenance_supporter/frontend/locales/fr.json index 69bdcad..8de01c1 100644 --- a/custom_components/maintenance_supporter/frontend/locales/fr.json +++ b/custom_components/maintenance_supporter/frontend/locales/fr.json @@ -716,6 +716,7 @@ "series_end_count_label": "Nombre de fois", "series_end_until_label": "Date de fin", "parts_section": "Pièces & consommables", + "parts_inventory_value": "Valeur du stock", "part_add": "Ajouter une pièce", "part_name": "Nom", "part_vendor": "Fabricant", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Bientôt", "battery_fleet_status_ok": "Bon état", "battery_fleet_predicted_on": "Prévu vers {date}", + "battery_fleet_predicted_trend": "Prédit à partir de la tendance de décharge de cette pile : vers {date} ({confidence})", + "battery_fleet_rechargeable": "Rechargeable : à recharger plutôt qu'à remplacer — jamais sur la liste d'achats", + "battery_fleet_sort_name": "Trier par nom", + "battery_fleet_sort_urgency": "Trier par urgence", + "battery_fleet_mark_recharged": "Marquer comme rechargée", + "battery_fleet_sparkline_hint": "Niveau de batterie des 30 derniers jours — pointillé : projection jusqu'au seuil bas", + "battery_fleet_filter_type": "Afficher uniquement ce type de pile", + "battery_fleet_record_replacement": "Le niveau a bondi vers le {date} — enregistrer ce remplacement dans Battery Notes", "battery_fleet_total": "{n} piles suivies", "battery_fleet_setup_button": "Parc de piles", "battery_fleet_setup_done": "Parc de piles configuré — une tâche suit toutes vos piles.", diff --git a/custom_components/maintenance_supporter/frontend/locales/hi.json b/custom_components/maintenance_supporter/frontend/locales/hi.json index ba1c4b3..50e6045 100644 --- a/custom_components/maintenance_supporter/frontend/locales/hi.json +++ b/custom_components/maintenance_supporter/frontend/locales/hi.json @@ -716,6 +716,7 @@ "series_end_count_label": "कितनी बार", "series_end_until_label": "समाप्ति तिथि", "parts_section": "पुर्ज़े और उपभोग्य", + "parts_inventory_value": "इन्वेंट्री मूल्य", "part_add": "पुर्ज़ा जोड़ें", "part_name": "नाम", "part_vendor": "निर्माता", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "जल्द", "battery_fleet_status_ok": "ठीक", "battery_fleet_predicted_on": "{date} के आसपास अपेक्षित", + "battery_fleet_predicted_trend": "इस बैटरी के डिस्चार्ज रुझान से अनुमानित: लगभग {date} ({confidence})", + "battery_fleet_rechargeable": "रिचार्जेबल: बदलने के बजाय चार्ज करें — खरीदारी सूची में कभी नहीं", + "battery_fleet_sort_name": "नाम के अनुसार क्रमबद्ध करें", + "battery_fleet_sort_urgency": "तात्कालिकता के अनुसार क्रमबद्ध करें", + "battery_fleet_mark_recharged": "रिचार्ज हो गई के रूप में चिह्नित करें", + "battery_fleet_sparkline_hint": "पिछले 30 दिनों का बैटरी स्तर — बिंदीदार: लो थ्रेशोल्ड तक का अनुमान", + "battery_fleet_filter_type": "केवल यही बैटरी प्रकार दिखाएँ", + "battery_fleet_record_replacement": "स्तर लगभग {date} को उछला — इस बदलाव को Battery Notes में दर्ज करें", "battery_fleet_total": "{n} बैटरियाँ ट्रैक की गईं", "battery_fleet_setup_button": "बैटरी फ्लीट", "battery_fleet_setup_done": "बैटरी फ्लीट सेट — एक कार्य सभी बैटरियों को ट्रैक करता है।", diff --git a/custom_components/maintenance_supporter/frontend/locales/hu.json b/custom_components/maintenance_supporter/frontend/locales/hu.json index 1dc6e6d..7b777c0 100644 --- a/custom_components/maintenance_supporter/frontend/locales/hu.json +++ b/custom_components/maintenance_supporter/frontend/locales/hu.json @@ -722,6 +722,7 @@ "series_end_count_label": "Alkalmak száma", "series_end_until_label": "Befejezés dátuma", "parts_section": "Alkatrészek és fogyóeszközök", + "parts_inventory_value": "Készletérték", "part_add": "Alkatrész hozzáadása", "part_name": "Név", "part_vendor": "Gyártó", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Hamarosan", "battery_fleet_status_ok": "Rendben", "battery_fleet_predicted_on": "Várhatóan {date} körül", + "battery_fleet_predicted_trend": "Az elem merülési trendjéből előrejelezve: kb. {date} ({confidence})", + "battery_fleet_rechargeable": "Újratölthető: cserélés helyett töltse fel — soha nem kerül a bevásárlólistára", + "battery_fleet_sort_name": "Rendezés név szerint", + "battery_fleet_sort_urgency": "Rendezés sürgősség szerint", + "battery_fleet_mark_recharged": "Megjelölés feltöltöttként", + "battery_fleet_sparkline_hint": "Akkumulátorszint az elmúlt 30 napban — pontozott: előrejelzés az alacsony küszöbig", + "battery_fleet_filter_type": "Csak ez az elemtípus megjelenítése", + "battery_fleet_record_replacement": "A szint {date} körül megugrott — rögzítse ezt a cserét a Battery Notes-ban", "battery_fleet_total": "{n} elem követve", "battery_fleet_setup_button": "Elemflotta", "battery_fleet_setup_done": "Elemflotta beállítva — egyetlen feladat követi az összes elemet.", diff --git a/custom_components/maintenance_supporter/frontend/locales/it.json b/custom_components/maintenance_supporter/frontend/locales/it.json index dc0c3ea..df87185 100644 --- a/custom_components/maintenance_supporter/frontend/locales/it.json +++ b/custom_components/maintenance_supporter/frontend/locales/it.json @@ -716,6 +716,7 @@ "series_end_count_label": "Numero di volte", "series_end_until_label": "Data di fine", "parts_section": "Ricambi e consumabili", + "parts_inventory_value": "Valore delle scorte", "part_add": "Aggiungi ricambio", "part_name": "Nome", "part_vendor": "Produttore", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "A breve", "battery_fleet_status_ok": "A posto", "battery_fleet_predicted_on": "Previsto intorno al {date}", + "battery_fleet_predicted_trend": "Previsto dalla tendenza di scarica di questa batteria: intorno al {date} ({confidence})", + "battery_fleet_rechargeable": "Ricaricabile: da ricaricare anziché sostituire — mai nella lista della spesa", + "battery_fleet_sort_name": "Ordina per nome", + "battery_fleet_sort_urgency": "Ordina per urgenza", + "battery_fleet_mark_recharged": "Segna come ricaricata", + "battery_fleet_sparkline_hint": "Livello batteria degli ultimi 30 giorni — tratteggiato: proiezione fino alla soglia di batteria scarica", + "battery_fleet_filter_type": "Mostra solo questo tipo di batteria", + "battery_fleet_record_replacement": "Il livello è balzato intorno al {date} — registra questa sostituzione in Battery Notes", "battery_fleet_total": "{n} batterie monitorate", "battery_fleet_setup_button": "Parco batterie", "battery_fleet_setup_done": "Parco batterie configurato — un'attività monitora tutte.", diff --git a/custom_components/maintenance_supporter/frontend/locales/ja.json b/custom_components/maintenance_supporter/frontend/locales/ja.json index c43e2af..1f1f1f5 100644 --- a/custom_components/maintenance_supporter/frontend/locales/ja.json +++ b/custom_components/maintenance_supporter/frontend/locales/ja.json @@ -716,6 +716,7 @@ "series_end_count_label": "回数", "series_end_until_label": "終了日", "parts_section": "部品・消耗品", + "parts_inventory_value": "在庫金額", "part_add": "部品を追加", "part_name": "名前", "part_vendor": "メーカー", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "まもなく", "battery_fleet_status_ok": "良好", "battery_fleet_predicted_on": "{date} 頃の見込み", + "battery_fleet_predicted_trend": "この電池の放電傾向から予測: {date} 頃 ({confidence})", + "battery_fleet_rechargeable": "充電式:交換ではなく充電——買い物リストには載りません", + "battery_fleet_sort_name": "名前順に並べ替え", + "battery_fleet_sort_urgency": "緊急度順に並べ替え", + "battery_fleet_mark_recharged": "充電済みにする", + "battery_fleet_sparkline_hint": "過去30日間のバッテリー残量——点線:低残量しきい値までの予測", + "battery_fleet_filter_type": "この電池タイプのみ表示", + "battery_fleet_record_replacement": "{date}頃に残量が急上昇——この交換をBattery Notesに記録する", "battery_fleet_total": "{n} 個の電池を監視", "battery_fleet_setup_button": "電池フリート", "battery_fleet_setup_done": "電池フリートを設定 — 1つのタスクで全電池を管理。", diff --git a/custom_components/maintenance_supporter/frontend/locales/ko.json b/custom_components/maintenance_supporter/frontend/locales/ko.json index 6380498..1c46432 100644 --- a/custom_components/maintenance_supporter/frontend/locales/ko.json +++ b/custom_components/maintenance_supporter/frontend/locales/ko.json @@ -722,6 +722,7 @@ "series_end_count_label": "횟수", "series_end_until_label": "종료일", "parts_section": "부품 및 소모품", + "parts_inventory_value": "재고 가치", "part_add": "부품 추가", "part_name": "이름", "part_vendor": "제조사", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "곧", "battery_fleet_status_ok": "정상", "battery_fleet_predicted_on": "{date}쯤 예상", + "battery_fleet_predicted_trend": "이 배터리의 방전 추세로 예측: 약 {date} ({confidence})", + "battery_fleet_rechargeable": "충전식: 교체 대신 충전 — 쇼핑 목록에 오르지 않습니다", + "battery_fleet_sort_name": "이름순 정렬", + "battery_fleet_sort_urgency": "긴급도순 정렬", + "battery_fleet_mark_recharged": "충전 완료로 표시", + "battery_fleet_sparkline_hint": "지난 30일간의 배터리 잔량 — 점선: 낮음 임계값까지의 예측", + "battery_fleet_filter_type": "이 배터리 유형만 표시", + "battery_fleet_record_replacement": "{date}쯤 잔량이 급상승했습니다 — 이 교체를 Battery Notes에 기록", "battery_fleet_total": "배터리 {n}개 추적 중", "battery_fleet_setup_button": "배터리 플릿", "battery_fleet_setup_done": "배터리 플릿이 설정되었습니다 — 하나의 작업이 모든 배터리를 추적합니다.", diff --git a/custom_components/maintenance_supporter/frontend/locales/nb.json b/custom_components/maintenance_supporter/frontend/locales/nb.json index f91fdad..60f2510 100644 --- a/custom_components/maintenance_supporter/frontend/locales/nb.json +++ b/custom_components/maintenance_supporter/frontend/locales/nb.json @@ -716,6 +716,7 @@ "series_end_count_label": "Antall ganger", "series_end_until_label": "Sluttdato", "parts_section": "Deler & forbruksvarer", + "parts_inventory_value": "Lagerverdi", "part_add": "Legg til del", "part_name": "Navn", "part_vendor": "Produsent", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Snart", "battery_fleet_status_ok": "I orden", "battery_fleet_predicted_on": "Forventes rundt {date}", + "battery_fleet_predicted_trend": "Forutsagt ut fra batteriets utladingstrend: rundt {date} ({confidence})", + "battery_fleet_rechargeable": "Oppladbart: lad i stedet for å bytte — aldri på handlelisten", + "battery_fleet_sort_name": "Sorter etter navn", + "battery_fleet_sort_urgency": "Sorter etter hastegrad", + "battery_fleet_mark_recharged": "Merk som oppladet", + "battery_fleet_sparkline_hint": "Batterinivå de siste 30 dagene — stiplet: fremskrevet ned til lavnivåterskelen", + "battery_fleet_filter_type": "Vis bare denne batteritypen", + "battery_fleet_record_replacement": "Nivået hoppet rundt {date} — registrer dette byttet i Battery Notes", "battery_fleet_total": "{n} batterier spores", "battery_fleet_setup_button": "Batteriflåte", "battery_fleet_setup_done": "Batteriflåte satt opp — én oppgave følger alle batteriene.", diff --git a/custom_components/maintenance_supporter/frontend/locales/nl.json b/custom_components/maintenance_supporter/frontend/locales/nl.json index a97385a..bd5af05 100644 --- a/custom_components/maintenance_supporter/frontend/locales/nl.json +++ b/custom_components/maintenance_supporter/frontend/locales/nl.json @@ -716,6 +716,7 @@ "series_end_count_label": "Aantal keer", "series_end_until_label": "Einddatum", "parts_section": "Onderdelen & verbruiksartikelen", + "parts_inventory_value": "Voorraadwaarde", "part_add": "Onderdeel toevoegen", "part_name": "Naam", "part_vendor": "Fabrikant", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Binnenkort", "battery_fleet_status_ok": "In orde", "battery_fleet_predicted_on": "Verwacht rond {date}", + "battery_fleet_predicted_trend": "Voorspeld uit de ontladingstrend van deze batterij: rond {date} ({confidence})", + "battery_fleet_rechargeable": "Oplaadbaar: opladen in plaats van vervangen — nooit op het boodschappenlijstje", + "battery_fleet_sort_name": "Sorteren op naam", + "battery_fleet_sort_urgency": "Sorteren op urgentie", + "battery_fleet_mark_recharged": "Markeren als opgeladen", + "battery_fleet_sparkline_hint": "Batterijniveau van de afgelopen 30 dagen — gestippeld: prognose tot de lage drempel", + "battery_fleet_filter_type": "Alleen dit batterijtype tonen", + "battery_fleet_record_replacement": "Het niveau maakte rond {date} een sprong — deze vervanging vastleggen in Battery Notes", "battery_fleet_total": "{n} batterijen gevolgd", "battery_fleet_setup_button": "Batterijvloot", "battery_fleet_setup_done": "Batterijvloot ingesteld — één taak volgt al je batterijen.", diff --git a/custom_components/maintenance_supporter/frontend/locales/pl.json b/custom_components/maintenance_supporter/frontend/locales/pl.json index 01a1613..6e20265 100644 --- a/custom_components/maintenance_supporter/frontend/locales/pl.json +++ b/custom_components/maintenance_supporter/frontend/locales/pl.json @@ -716,6 +716,7 @@ "series_end_count_label": "Liczba razy", "series_end_until_label": "Data końcowa", "parts_section": "Części i materiały", + "parts_inventory_value": "Wartość zapasów", "part_add": "Dodaj część", "part_name": "Nazwa", "part_vendor": "Producent", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Wkrótce", "battery_fleet_status_ok": "W porządku", "battery_fleet_predicted_on": "Przewidywane około {date}", + "battery_fleet_predicted_trend": "Prognoza na podstawie trendu rozładowania tej baterii: około {date} ({confidence})", + "battery_fleet_rechargeable": "Akumulator: ładowanie zamiast wymiany — nigdy na liście zakupów", + "battery_fleet_sort_name": "Sortuj według nazwy", + "battery_fleet_sort_urgency": "Sortuj według pilności", + "battery_fleet_mark_recharged": "Oznacz jako naładowaną", + "battery_fleet_sparkline_hint": "Poziom baterii z ostatnich 30 dni — kropkowana linia: prognoza do progu niskiego poziomu", + "battery_fleet_filter_type": "Pokaż tylko ten typ baterii", + "battery_fleet_record_replacement": "Poziom skoczył około {date} — zapisz tę wymianę w Battery Notes", "battery_fleet_total": "Śledzone baterie: {n}", "battery_fleet_setup_button": "Flota baterii", "battery_fleet_setup_done": "Flota baterii skonfigurowana — jedno zadanie śledzi wszystkie.", diff --git a/custom_components/maintenance_supporter/frontend/locales/pt-br.json b/custom_components/maintenance_supporter/frontend/locales/pt-br.json index bfdf93b..ca2fc2d 100644 --- a/custom_components/maintenance_supporter/frontend/locales/pt-br.json +++ b/custom_components/maintenance_supporter/frontend/locales/pt-br.json @@ -722,6 +722,7 @@ "series_end_count_label": "Número de vezes", "series_end_until_label": "Data final", "parts_section": "Peças e consumíveis", + "parts_inventory_value": "Valor do estoque", "part_add": "Adicionar peça", "part_name": "Nome", "part_vendor": "Fabricante", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Em breve", "battery_fleet_status_ok": "Em bom estado", "battery_fleet_predicted_on": "Previsto por volta de {date}", + "battery_fleet_predicted_trend": "Previsto pela tendência de descarga desta bateria: por volta de {date} ({confidence})", + "battery_fleet_rechargeable": "Recarregável: recarregue em vez de substituir — nunca na lista de compras", + "battery_fleet_sort_name": "Ordenar por nome", + "battery_fleet_sort_urgency": "Ordenar por urgência", + "battery_fleet_mark_recharged": "Marcar como recarregada", + "battery_fleet_sparkline_hint": "Nível da bateria dos últimos 30 dias — pontilhado: projeção até o limite baixo", + "battery_fleet_filter_type": "Mostrar apenas este tipo de pilha", + "battery_fleet_record_replacement": "O nível deu um salto por volta de {date} — registrar esta substituição no Battery Notes", "battery_fleet_total": "{n} baterias acompanhadas", "battery_fleet_setup_button": "Frota de baterias", "battery_fleet_setup_done": "Frota de baterias configurada — uma única tarefa acompanha todas as suas baterias.", diff --git a/custom_components/maintenance_supporter/frontend/locales/pt.json b/custom_components/maintenance_supporter/frontend/locales/pt.json index c5e2510..a9f9dae 100644 --- a/custom_components/maintenance_supporter/frontend/locales/pt.json +++ b/custom_components/maintenance_supporter/frontend/locales/pt.json @@ -716,6 +716,7 @@ "series_end_count_label": "Número de vezes", "series_end_until_label": "Data final", "parts_section": "Peças e consumíveis", + "parts_inventory_value": "Valor do stock", "part_add": "Adicionar peça", "part_name": "Nome", "part_vendor": "Fabricante", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Em breve", "battery_fleet_status_ok": "Em bom estado", "battery_fleet_predicted_on": "Previsto por volta de {date}", + "battery_fleet_predicted_trend": "Previsto pela tendência de descarga desta pilha: por volta de {date} ({confidence})", + "battery_fleet_rechargeable": "Recarregável: carrega-se em vez de se substituir — nunca na lista de compras", + "battery_fleet_sort_name": "Ordenar por nome", + "battery_fleet_sort_urgency": "Ordenar por urgência", + "battery_fleet_mark_recharged": "Marcar como recarregada", + "battery_fleet_sparkline_hint": "Nível da bateria dos últimos 30 dias — pontilhado: projeção até ao limiar baixo", + "battery_fleet_filter_type": "Mostrar apenas este tipo de pilha", + "battery_fleet_record_replacement": "O nível deu um salto por volta de {date} — registar esta substituição no Battery Notes", "battery_fleet_total": "{n} baterias monitorizadas", "battery_fleet_setup_button": "Frota de baterias", "battery_fleet_setup_done": "Frota de baterias configurada — uma tarefa acompanha todas.", diff --git a/custom_components/maintenance_supporter/frontend/locales/ru.json b/custom_components/maintenance_supporter/frontend/locales/ru.json index 7efafca..e82f2f2 100644 --- a/custom_components/maintenance_supporter/frontend/locales/ru.json +++ b/custom_components/maintenance_supporter/frontend/locales/ru.json @@ -716,6 +716,7 @@ "series_end_count_label": "Число раз", "series_end_until_label": "Дата окончания", "parts_section": "Детали и расходники", + "parts_inventory_value": "Стоимость запасов", "part_add": "Добавить деталь", "part_name": "Название", "part_vendor": "Производитель", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Скоро", "battery_fleet_status_ok": "В норме", "battery_fleet_predicted_on": "Ожидается примерно {date}", + "battery_fleet_predicted_trend": "Прогноз по тренду разряда этой батареи: примерно {date} ({confidence})", + "battery_fleet_rechargeable": "Аккумулятор: заряжается, а не заменяется — никогда не попадает в список покупок", + "battery_fleet_sort_name": "Сортировать по имени", + "battery_fleet_sort_urgency": "Сортировать по срочности", + "battery_fleet_mark_recharged": "Отметить как заряженную", + "battery_fleet_sparkline_hint": "Уровень заряда за последние 30 дней — пунктир: прогноз до порога разряда", + "battery_fleet_filter_type": "Показать только этот тип батарей", + "battery_fleet_record_replacement": "Уровень резко вырос примерно {date} — записать эту замену в Battery Notes", "battery_fleet_total": "Отслеживается батарей: {n}", "battery_fleet_setup_button": "Парк батарей", "battery_fleet_setup_done": "Парк батарей настроен — одна задача следит за всеми.", diff --git a/custom_components/maintenance_supporter/frontend/locales/sv.json b/custom_components/maintenance_supporter/frontend/locales/sv.json index 1168695..67d03ea 100644 --- a/custom_components/maintenance_supporter/frontend/locales/sv.json +++ b/custom_components/maintenance_supporter/frontend/locales/sv.json @@ -716,6 +716,7 @@ "series_end_count_label": "Antal gånger", "series_end_until_label": "Slutdatum", "parts_section": "Delar & förbrukning", + "parts_inventory_value": "Lagervärde", "part_add": "Lägg till del", "part_name": "Namn", "part_vendor": "Tillverkare", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Snart", "battery_fleet_status_ok": "I ordning", "battery_fleet_predicted_on": "Väntas omkring {date}", + "battery_fleet_predicted_trend": "Förutspått utifrån batteriets urladdningstrend: omkring {date} ({confidence})", + "battery_fleet_rechargeable": "Uppladdningsbart: ladda i stället för att byta — aldrig på inköpslistan", + "battery_fleet_sort_name": "Sortera efter namn", + "battery_fleet_sort_urgency": "Sortera efter angelägenhet", + "battery_fleet_mark_recharged": "Markera som uppladdad", + "battery_fleet_sparkline_hint": "Batterinivå de senaste 30 dagarna — prickad: prognos ner till lågnivåtröskeln", + "battery_fleet_filter_type": "Visa endast denna batterityp", + "battery_fleet_record_replacement": "Nivån hoppade omkring {date} — registrera detta byte i Battery Notes", "battery_fleet_total": "{n} batterier spåras", "battery_fleet_setup_button": "Batteriflotta", "battery_fleet_setup_done": "Batteriflotta konfigurerad — en uppgift följer alla batterier.", diff --git a/custom_components/maintenance_supporter/frontend/locales/tr.json b/custom_components/maintenance_supporter/frontend/locales/tr.json index 24e86ea..b3c0f29 100644 --- a/custom_components/maintenance_supporter/frontend/locales/tr.json +++ b/custom_components/maintenance_supporter/frontend/locales/tr.json @@ -722,6 +722,7 @@ "series_end_count_label": "Tekrar sayısı", "series_end_until_label": "Bitiş tarihi", "parts_section": "Parçalar ve sarf malzemeleri", + "parts_inventory_value": "Stok değeri", "part_add": "Parça ekle", "part_name": "Ad", "part_vendor": "Üretici", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Yakında", "battery_fleet_status_ok": "İyi durumda", "battery_fleet_predicted_on": "Yaklaşık {date} tarihinde bekleniyor", + "battery_fleet_predicted_trend": "Bu pilin deşarj eğiliminden tahmin edildi: yaklaşık {date} ({confidence})", + "battery_fleet_rechargeable": "Şarj edilebilir: değiştirmek yerine şarj edin — alışveriş listesine asla girmez", + "battery_fleet_sort_name": "Ada göre sırala", + "battery_fleet_sort_urgency": "Aciliyete göre sırala", + "battery_fleet_mark_recharged": "Şarj edildi olarak işaretle", + "battery_fleet_sparkline_hint": "Son 30 günün pil seviyesi — noktalı: düşük eşiğe kadar projeksiyon", + "battery_fleet_filter_type": "Yalnızca bu pil türünü göster", + "battery_fleet_record_replacement": "Seviye {date} civarında sıçradı — bu değişimi Battery Notes'a kaydet", "battery_fleet_total": "{n} pil takip ediliyor", "battery_fleet_setup_button": "Pil filosu", "battery_fleet_setup_done": "Pil filosu kuruldu — tek bir görev tüm pillerinizi takip ediyor.", diff --git a/custom_components/maintenance_supporter/frontend/locales/uk.json b/custom_components/maintenance_supporter/frontend/locales/uk.json index 755186d..266b1a4 100644 --- a/custom_components/maintenance_supporter/frontend/locales/uk.json +++ b/custom_components/maintenance_supporter/frontend/locales/uk.json @@ -716,6 +716,7 @@ "series_end_count_label": "Кількість разів", "series_end_until_label": "Дата завершення", "parts_section": "Деталі та витратні матеріали", + "parts_inventory_value": "Вартість запасів", "part_add": "Додати деталь", "part_name": "Назва", "part_vendor": "Виробник", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "Незабаром", "battery_fleet_status_ok": "У нормі", "battery_fleet_predicted_on": "Очікується приблизно {date}", + "battery_fleet_predicted_trend": "Прогноз за трендом розряду цієї батареї: приблизно {date} ({confidence})", + "battery_fleet_rechargeable": "Акумулятор: заряджається, а не замінюється — ніколи не потрапляє до списку покупок", + "battery_fleet_sort_name": "Сортувати за назвою", + "battery_fleet_sort_urgency": "Сортувати за терміновістю", + "battery_fleet_mark_recharged": "Позначити як заряджену", + "battery_fleet_sparkline_hint": "Рівень заряду за останні 30 днів — пунктир: прогноз до порогу розряду", + "battery_fleet_filter_type": "Показати лише цей тип батарей", + "battery_fleet_record_replacement": "Рівень різко зріс приблизно {date} — записати цю заміну в Battery Notes", "battery_fleet_total": "Відстежується батарей: {n}", "battery_fleet_setup_button": "Парк батарей", "battery_fleet_setup_done": "Парк батарей налаштовано — одне завдання стежить за всіма.", diff --git a/custom_components/maintenance_supporter/frontend/locales/zh.json b/custom_components/maintenance_supporter/frontend/locales/zh.json index 00044ee..e463821 100644 --- a/custom_components/maintenance_supporter/frontend/locales/zh.json +++ b/custom_components/maintenance_supporter/frontend/locales/zh.json @@ -716,6 +716,7 @@ "series_end_count_label": "次数", "series_end_until_label": "结束日期", "parts_section": "配件与耗材", + "parts_inventory_value": "库存价值", "part_add": "添加配件", "part_name": "名称", "part_vendor": "制造商", @@ -820,6 +821,14 @@ "battery_fleet_status_soon": "即将", "battery_fleet_status_ok": "正常", "battery_fleet_predicted_on": "预计在 {date} 前后", + "battery_fleet_predicted_trend": "根据此电池的放电趋势预测:约 {date}({confidence})", + "battery_fleet_rechargeable": "可充电电池:充电即可,无需更换——不会出现在购物清单中", + "battery_fleet_sort_name": "按名称排序", + "battery_fleet_sort_urgency": "按紧急程度排序", + "battery_fleet_mark_recharged": "标记为已充电", + "battery_fleet_sparkline_hint": "过去 30 天的电池电量——虚线:外推至低电量阈值", + "battery_fleet_filter_type": "仅显示此电池类型", + "battery_fleet_record_replacement": "电量在 {date} 前后跳升——将此次更换记录到 Battery Notes", "battery_fleet_total": "已跟踪 {n} 个电池", "battery_fleet_setup_button": "电池群", "battery_fleet_setup_done": "电池群已设置 — 一个任务跟踪所有电池。", diff --git a/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js b/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js index 1c5c438..b167782 100644 --- a/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js +++ b/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js @@ -1,4 +1,4 @@ -/*! maintenance_supporter frontend 2.46.0 */ +/*! maintenance_supporter frontend 2.52.0 */ var it=Object.defineProperty;var lt=Object.getOwnPropertyDescriptor;var x=(a,e,t,o)=>{for(var r=o>1?void 0:o?lt(e,t):e,n=a.length-1,s;n>=0;n--)(s=a[n])&&(r=(o?s(e,t,r):s(r))||r);return o&&r&&it(e,t,r),r};var ee=globalThis,te=ee.ShadowRoot&&(ee.ShadyCSS===void 0||ee.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,de=Symbol(),Se=new WeakMap,I=class{constructor(e,t,o){if(this._$cssResult$=!0,o!==de)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o,t=this.t;if(te&&e===void 0){let o=t!==void 0&&t.length===1;o&&(e=Se.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),o&&Se.set(t,e))}return e}toString(){return this.cssText}},$e=a=>new I(typeof a=="string"?a:a+"",void 0,de),k=(a,...e)=>{let t=a.length===1?a[0]:e.reduce((o,r,n)=>o+(s=>{if(s._$cssResult$===!0)return s.cssText;if(typeof s=="number")return s;throw Error("Value passed to 'css' function must be a 'css' function result: "+s+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(r)+a[n+1],a[0]);return new I(t,a,de)},Ae=(a,e)=>{if(te)a.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(let t of e){let o=document.createElement("style"),r=ee.litNonce;r!==void 0&&o.setAttribute("nonce",r),o.textContent=t.cssText,a.appendChild(o)}},pe=te?a=>a:a=>a instanceof CSSStyleSheet?(e=>{let t="";for(let o of e.cssRules)t+=o.cssText;return $e(t)})(a):a;var{is:ct,defineProperty:dt,getOwnPropertyDescriptor:pt,getOwnPropertyNames:ut,getOwnPropertySymbols:_t,getPrototypeOf:ht}=Object,oe=globalThis,je=oe.trustedTypes,gt=je?je.emptyScript:"",ft=oe.reactiveElementPolyfillSupport,F=(a,e)=>a,B={toAttribute(a,e){switch(e){case Boolean:a=a?gt:null;break;case Object:case Array:a=a==null?a:JSON.stringify(a)}return a},fromAttribute(a,e){let t=a;switch(e){case Boolean:t=a!==null;break;case Number:t=a===null?null:Number(a);break;case Object:case Array:try{t=JSON.parse(a)}catch{t=null}}return t}},re=(a,e)=>!ct(a,e),Ce={attribute:!0,type:String,converter:B,reflect:!1,useDefault:!1,hasChanged:re};Symbol.metadata??=Symbol("metadata"),oe.litPropertyMetadata??=new WeakMap;var S=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=Ce){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){let o=Symbol(),r=this.getPropertyDescriptor(e,o,t);r!==void 0&&dt(this.prototype,e,r)}}static getPropertyDescriptor(e,t,o){let{get:r,set:n}=pt(this.prototype,e)??{get(){return this[t]},set(s){this[t]=s}};return{get:r,set(s){let l=r?.call(this);n?.call(this,s),this.requestUpdate(e,l,o)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??Ce}static _$Ei(){if(this.hasOwnProperty(F("elementProperties")))return;let e=ht(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(F("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(F("properties"))){let t=this.properties,o=[...ut(t),..._t(t)];for(let r of o)this.createProperty(r,t[r])}let e=this[Symbol.metadata];if(e!==null){let t=litPropertyMetadata.get(e);if(t!==void 0)for(let[o,r]of t)this.elementProperties.set(o,r)}this._$Eh=new Map;for(let[t,o]of this.elementProperties){let r=this._$Eu(t,o);r!==void 0&&this._$Eh.set(r,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){let t=[];if(Array.isArray(e)){let o=new Set(e.flat(1/0).reverse());for(let r of o)t.unshift(pe(r))}else e!==void 0&&t.push(pe(e));return t}static _$Eu(e,t){let o=t.attribute;return o===!1?void 0:typeof o=="string"?o:typeof e=="string"?e.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(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),this.renderRoot!==void 0&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){let e=new Map,t=this.constructor.elementProperties;for(let o of t.keys())this.hasOwnProperty(o)&&(e.set(o,this[o]),delete this[o]);e.size>0&&(this._$Ep=e)}createRenderRoot(){let e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return Ae(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,o){this._$AK(e,o)}_$ET(e,t){let o=this.constructor.elementProperties.get(e),r=this.constructor._$Eu(e,o);if(r!==void 0&&o.reflect===!0){let n=(o.converter?.toAttribute!==void 0?o.converter:B).toAttribute(t,o.type);this._$Em=e,n==null?this.removeAttribute(r):this.setAttribute(r,n),this._$Em=null}}_$AK(e,t){let o=this.constructor,r=o._$Eh.get(e);if(r!==void 0&&this._$Em!==r){let n=o.getPropertyOptions(r),s=typeof n.converter=="function"?{fromAttribute:n.converter}:n.converter?.fromAttribute!==void 0?n.converter:B;this._$Em=r;let l=s.fromAttribute(t,n.type);this[r]=l??this._$Ej?.get(r)??l,this._$Em=null}}requestUpdate(e,t,o,r=!1,n){if(e!==void 0){let s=this.constructor;if(r===!1&&(n=this[e]),o??=s.getPropertyOptions(e),!((o.hasChanged??re)(n,t)||o.useDefault&&o.reflect&&n===this._$Ej?.get(e)&&!this.hasAttribute(s._$Eu(e,o))))return;this.C(e,t,o)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(e,t,{useDefault:o,reflect:r,wrapped:n},s){o&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,s??t??this[e]),n!==!0||s!==void 0)||(this._$AL.has(e)||(this.hasUpdated||o||(t=void 0),this._$AL.set(e,t)),r===!0&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}let e=this.scheduleUpdate();return e!=null&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[r,n]of this._$Ep)this[r]=n;this._$Ep=void 0}let o=this.constructor.elementProperties;if(o.size>0)for(let[r,n]of o){let{wrapped:s}=n,l=this[r];s!==!0||this._$AL.has(r)||l===void 0||this.C(r,void 0,n,l)}}let e=!1,t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(o=>o.hostUpdate?.()),this.update(t)):this._$EM()}catch(o){throw e=!1,this._$EM(),o}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(e){}firstUpdated(e){}};S.elementStyles=[],S.shadowRootOptions={mode:"open"},S[F("elementProperties")]=new Map,S[F("finalized")]=new Map,ft?.({ReactiveElement:S}),(oe.reactiveElementVersions??=[]).push("2.1.2");var be=globalThis,Ee=a=>a,ae=be.trustedTypes,Te=ae?ae.createPolicy("lit-html",{createHTML:a=>a}):void 0,Oe="$lit$",A=`lit$${Math.random().toFixed(9).slice(2)}$`,Le="?"+A,mt=`<${Le}>`,T=document,Y=()=>T.createComment(""),G=a=>a===null||typeof a!="object"&&typeof a!="function",ye=Array.isArray,bt=a=>ye(a)||typeof a?.[Symbol.iterator]=="function",ue=`[ \f\r]`,W=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,De=/-->/g,Ne=/>/g,C=RegExp(`>|${ue}(?:([^\\s"'>=/]+)(${ue}*=${ue}*(?:[^ \f\r"'\`<>=]|("|')|))|$)`,"g"),Re=/'/g,Pe=/"/g,Me=/^(?:script|style|textarea|title)$/i,ve=a=>(e,...t)=>({_$litType$:a,strings:e,values:t}),m=ve(1),qt=ve(2),It=ve(3),D=Symbol.for("lit-noChange"),g=Symbol.for("lit-nothing"),ze=new WeakMap,E=T.createTreeWalker(T,129);function Ue(a,e){if(!ye(a)||!a.hasOwnProperty("raw"))throw Error("invalid template strings array");return Te!==void 0?Te.createHTML(e):e}var yt=(a,e)=>{let t=a.length-1,o=[],r,n=e===2?"":e===3?"":"",s=W;for(let l=0;l"?(s=r??W,c=-1):u[1]===void 0?c=-2:(c=s.lastIndex-u[2].length,p=u[1],s=u[3]===void 0?C:u[3]==='"'?Pe:Re):s===Pe||s===Re?s=C:s===De||s===Ne?s=W:(s=C,r=void 0);let h=s===C&&a[l+1].startsWith("/>")?" ":"";n+=s===W?d+mt:c>=0?(o.push(p),d.slice(0,c)+Oe+d.slice(c)+A+h):d+A+(c===-2?l:h)}return[Ue(a,n+(a[t]||"")+(e===2?"":e===3?"":"")),o]},V=class a{constructor({strings:e,_$litType$:t},o){let r;this.parts=[];let n=0,s=0,l=e.length-1,d=this.parts,[p,u]=yt(e,t);if(this.el=a.createElement(p,o),E.currentNode=this.el.content,t===2||t===3){let c=this.el.content.firstChild;c.replaceWith(...c.childNodes)}for(;(r=E.nextNode())!==null&&d.length0){r.textContent=ae?ae.emptyScript:"";for(let h=0;h<_;h++)r.append(c[h],Y()),E.nextNode(),d.push({type:2,index:++n});r.append(c[_],Y())}}}else if(r.nodeType===8)if(r.data===Le)d.push({type:2,index:n});else{let c=-1;for(;(c=r.data.indexOf(A,c+1))!==-1;)d.push({type:7,index:n}),c+=A.length-1}n++}}static createElement(e,t){let o=T.createElement("template");return o.innerHTML=e,o}};function z(a,e,t=a,o){if(e===D)return e;let r=o!==void 0?t._$Co?.[o]:t._$Cl,n=G(e)?void 0:e._$litDirective$;return r?.constructor!==n&&(r?._$AO?.(!1),n===void 0?r=void 0:(r=new n(a),r._$AT(a,t,o)),o!==void 0?(t._$Co??=[])[o]=r:t._$Cl=r),r!==void 0&&(e=z(a,r._$AS(a,e.values),r,o)),e}var _e=class{constructor(e,t){this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=t}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(e){let{el:{content:t},parts:o}=this._$AD,r=(e?.creationScope??T).importNode(t,!0);E.currentNode=r;let n=E.nextNode(),s=0,l=0,d=o[0];for(;d!==void 0;){if(s===d.index){let p;d.type===2?p=new K(n,n.nextSibling,this,e):d.type===1?p=new d.ctor(n,d.name,d.strings,this,e):d.type===6&&(p=new me(n,this,e)),this._$AV.push(p),d=o[++l]}s!==d?.index&&(n=E.nextNode(),s++)}return E.currentNode=T,r}p(e){let t=0;for(let o of this._$AV)o!==void 0&&(o.strings!==void 0?(o._$AI(e,o,t),t+=o.strings.length-2):o._$AI(e[t])),t++}},K=class a{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,o,r){this.type=2,this._$AH=g,this._$AN=void 0,this._$AA=e,this._$AB=t,this._$AM=o,this.options=r,this._$Cv=r?.isConnected??!0}get parentNode(){let e=this._$AA.parentNode,t=this._$AM;return t!==void 0&&e?.nodeType===11&&(e=t.parentNode),e}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(e,t=this){e=z(this,e,t),G(e)?e===g||e==null||e===""?(this._$AH!==g&&this._$AR(),this._$AH=g):e!==this._$AH&&e!==D&&this._(e):e._$litType$!==void 0?this.$(e):e.nodeType!==void 0?this.T(e):bt(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==g&&G(this._$AH)?this._$AA.nextSibling.data=e:this.T(T.createTextNode(e)),this._$AH=e}$(e){let{values:t,_$litType$:o}=e,r=typeof o=="number"?this._$AC(e):(o.el===void 0&&(o.el=V.createElement(Ue(o.h,o.h[0]),this.options)),o);if(this._$AH?._$AD===r)this._$AH.p(t);else{let n=new _e(r,this),s=n.u(this.options);n.p(t),this.T(s),this._$AH=n}}_$AC(e){let t=ze.get(e.strings);return t===void 0&&ze.set(e.strings,t=new V(e)),t}k(e){ye(this._$AH)||(this._$AH=[],this._$AR());let t=this._$AH,o,r=0;for(let n of e)r===t.length?t.push(o=new a(this.O(Y()),this.O(Y()),this,this.options)):o=t[r],o._$AI(n),r++;r2||o[0]!==""||o[1]!==""?(this._$AH=Array(o.length-1).fill(new String),this.strings=o):this._$AH=g}_$AI(e,t=this,o,r){let n=this.strings,s=!1;if(n===void 0)e=z(this,e,t,0),s=!G(e)||e!==this._$AH&&e!==D,s&&(this._$AH=e);else{let l=e,d,p;for(e=n[0],d=0;d{let o=t?.renderBefore??e,r=o._$litPart$;if(r===void 0){let n=t?.renderBefore??null;o._$litPart$=r=new K(e.insertBefore(Y(),n),n,void 0,t??{})}return r._$AI(a),r};var xe=globalThis,$=class extends S{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){let t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=He(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return D}};$._$litElement$=!0,$.finalized=!0,xe.litElementHydrateSupport?.({LitElement:$});var xt=xe.litElementPolyfillSupport;xt?.({LitElement:$});(xe.litElementVersions??=[]).push("4.2.2");var wt={attribute:!0,type:String,converter:B,reflect:!1,hasChanged:re},kt=(a=wt,e,t)=>{let{kind:o,metadata:r}=t,n=globalThis.litPropertyMetadata.get(r);if(n===void 0&&globalThis.litPropertyMetadata.set(r,n=new Map),o==="setter"&&((a=Object.create(a)).wrapped=!0),n.set(t.name,a),o==="accessor"){let{name:s}=t;return{set(l){let d=e.get.call(this);e.set.call(this,l),this.requestUpdate(s,d,a,!0,l)},init(l){return l!==void 0&&this.C(s,void 0,a,l),l}}}if(o==="setter"){let{name:s}=t;return function(l){let d=this[s];e.call(this,l),this.requestUpdate(s,d,a,!0,l)}}throw Error("Unsupported decorator location: "+o)};function Q(a){return(e,t)=>typeof t=="object"?kt(a,e,t):((o,r,n)=>{let s=r.hasOwnProperty(n);return r.constructor.createProperty(n,o),s?Object.getOwnPropertyDescriptor(r,n):void 0})(a,e,t)}function w(a){return Q({...a,state:!0,attribute:!1})}var St={days:1,weeks:7,months:30.4368,years:365.25};function qe(a,e){return!a||a<=0?0:a*(St[e||"days"]??1)}var Ie=5;function J(a){let e=a.getFullYear(),t=String(a.getMonth()+1).padStart(2,"0"),o=String(a.getDate()).padStart(2,"0");return`${e}-${t}-${o}`}function $t(a,e){let t=[];for(let o=0;ot.cost).filter(t=>typeof t=="number");return e.length===0?null:e.reduce((t,o)=>t+o,0)/e.length}function jt(a){let{windowStart:e,windowEnd:t,task:o,entryId:r,objectName:n}=a,s=[],l=(c,_)=>({date:c,entry_id:r,task_id:o.id,task_name:o.name,object_name:n,status:_&&(o.status==="overdue"||o.status==="triggered")?"ok":o.status,days_until_due:_?null:o.days_until_due??null,projected:_,schedule_type:o.schedule_type,interval_days:o.interval_days??null,interval_unit:o.interval_unit??null,responsible_user_id:o.responsible_user_id??null,avg_cost:At(o.history),adaptive_enabled:!!o.adaptive_config?.enabled,prediction_confidence:o.threshold_prediction_confidence??null}),d=Math.max(1,Math.round(qe(o.interval_days,o.interval_unit)));if(o.status==="overdue"||o.status==="triggered"){if(s.push(l(e,!1)),o.schedule_type==="time_based"&&o.interval_days&&o.interval_days>0){let c=se(e,d),_=1;for(;c<=t&&_=e&&u<=t)s.push(l(u,!1));else if(u>t)return s;if(o.schedule_type==="time_based"&&o.interval_days&&o.interval_days>0){let c=se(u,d),_=s.length;for(;c<=t&&_=e&&(s.push(l(c,!0)),_++),c=se(c,d)}return s}var Fe={overdue:0,triggered:1,due_soon:2,ok:3};function Be(a,e,t,o=null){let r=$t(e,t),n=r[0],s=r[r.length-1],l=[];for(let p of a){let u=p.object?.name||"",c=p.entry_id,_=p.tasks||[];for(let h of _){if(o&&h.responsible_user_id!==o||h.enabled===!1)continue;let b=jt({windowStart:n,windowEnd:s,task:h,entryId:c,objectName:u});l.push(...b)}}let d=new Map;for(let p of r)d.set(p,[]);for(let p of l){let u=d.get(p.date);u&&u.push(p)}for(let[,p]of d)p.sort((u,c)=>{let _=Fe[u.status]??99,h=Fe[c.status]??99;if(_!==h)return _-h;if(u.projected!==c.projected)return u.projected?1:-1;let b=u.object_name.localeCompare(c.object_name);return b!==0?b:u.task_name.localeCompare(c.task_name)});return r.map(p=>({date:p,events:d.get(p)??[]}))}var Ct={completed:"ok",reset:"ok",skipped:"due_soon",triggered:"triggered",trigger_replaced:"triggered",trigger_removed:"ok"};function Et(a,e){let t=[];for(let o=e-1;o>=0;o--){let r=new Date(a);r.setDate(r.getDate()-o),r.setHours(0,0,0,0),t.push(J(r))}return t}function We(a,e,t,o=null){let r=Et(e,t),n=r[0],s=r[r.length-1],l=new Map;for(let p of r)l.set(p,[]);for(let p of a){let u=p.object?.name||"",c=p.entry_id,_=p.tasks||[];for(let h of _){if(o&&h.responsible_user_id!==o)continue;let b=h.history||[];for(let y of b){if(typeof y?.timestamp!="string")continue;let R=y.timestamp.slice(0,10);if(Rs)continue;let M=l.get(R);if(!M)continue;let U=y.type??"completed";M.push({date:R,entry_id:c,task_id:h.id,task_name:h.name,object_name:u,status:Ct[U]??"ok",days_until_due:null,projected:!1,schedule_type:h.schedule_type,interval_days:h.interval_days??null,responsible_user_id:h.responsible_user_id??null,avg_cost:typeof y.cost=="number"?y.cost:null,adaptive_enabled:!!h.adaptive_config?.enabled,prediction_confidence:null,history_timestamp:y.timestamp,history_type:U,history_cost:typeof y.cost=="number"?y.cost:null,history_notes:typeof y.notes=="string"?y.notes:null,history_duration:typeof y.duration=="number"?y.duration:null})}}}let d={completed:0,reset:1,skipped:2,triggered:3,trigger_replaced:4};for(let[,p]of l)p.sort((u,c)=>{let _=d[u.history_type??""]??99,h=d[c.history_type??""]??99;if(_!==h)return _-h;let b=u.object_name.localeCompare(c.object_name);return b!==0?b:u.task_name.localeCompare(c.task_name)});return r.map(p=>({date:p,events:l.get(p)??[]}))}var Ye=k` @@ -200,7 +200,7 @@ var it=Object.defineProperty;var lt=Object.getOwnPropertyDescriptor;var x=(a,e,t } `;var Ge={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",require_on_completion:"Require on completion",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_areas:"Filter by areas",card_filter_areas_help:"Empty = show all areas.",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",settings_install_assist_sentences:"Install Assist sentences",settings_install_assist_sentences_hint:"Copies the voice sentences into your configuration so the classic Assist agent recognises them. A file you edited yourself is never overwritten.",test_notification:"Test notification",send_test:"Send test",testing:"Sending\u2026",test_notification_success:"Test notification sent",test_notification_failed:"Test notification failed",notify_per_person:"Per-person delivery",notify_no_own_device:"No own device \u2014 uses the household service",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",settings_export_selection:"Limit to selected objects (optional)",settings_docs_archive:"Documents archive (with files)",settings_docs_archive_hint:"The JSON/YAML/CSV exports carry settings only. This ZIP includes the uploaded file contents so a restore is complete.",settings_docs_export_btn:"Download documents ZIP",settings_docs_import_btn:"Restore documents ZIP",settings_docs_import_success:"Restored: {blobs} files, {docs} documents",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",photo_label:"Photo",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",parts_section:"Parts & consumables",part_add:"Add part",part_name:"Name",part_vendor:"Manufacturer",part_storage_location:"Storage location",part_product_url:"Product URL",part_unit:"Unit",part_cost:"Unit price",part_stock:"Stock",part_reorder_threshold:"Reorder at",part_restock_quantity:"Restock quantity",part_auto_buy:"Auto-create buy task when low",part_restock:"Adjust stock",restock_quantity_label:"Quantity bought",consumes_parts_label:"Consumes parts",shared_parts_other_objects:"Parts from other objects",shared_parts_help:"Several objects can share one stock. Completing this task takes from the owning object.",shared_part_unknown:"Unknown part",parts_load_failed:"Couldn't load this object's parts \u2014 the consumes-parts options are unavailable right now.",adopt_problem_button:"Adopt problem sensors",adopt_problem_title:"Adopt problem sensors",adopt_problem_hint:"Turn HA problem sensors (printer errors, filter warnings, low battery) into maintenance tasks that trigger while the problem is active and clear themselves when it resolves.",adopt_problem_none:"No problem sensors found that aren't already tracked.",adopt_problem_active:"active",adopt_problem_ok:"ok",adopt_problem_new_object:"(new)",adopt_problem_adopt:"Adopt selected",adopt_problem_done:"Adopted {tasks} problem sensor(s)",views_label:"Views",views_none:"\u2014 No view \u2014",views_manage:"Save / manage views",views_dialog_title:"Saved views",views_dialog_hint:"Save the current filters as a named view everyone can reuse.",views_name_placeholder:"View name",views_save_current:"Save current filters",views_none_yet:"No saved views yet.",close:"Close",trigger_hint_now:"The sensor reads {value} right now.",trigger_hint_above:"The task triggers once it rises above {target}.",trigger_hint_below:"It triggers once it falls below {target}.",trigger_hint_counter_delta:"Counts from the current reading ({value}): due at {due} (+{target}), and the count restarts after each completion.",trigger_hint_counter_delta_edit:"Counts usage since the last completion: due after +{target}; the count restarts after each completion.",trigger_hint_counter_abs:"The task becomes due once the sensor reaches {target}.",trigger_hint_runtime:"The task becomes due after {hours} h of accumulated on-time; the counter restarts after each completion.",trigger_hint_state_change:"The task becomes due after {count} state change(s).",trigger_hint_state_change_to:"The task becomes due after {count} change(s) to \u201C{state}\u201D.",trigger_hint_state_now:"Current state: {value}.",adopt_problem_part:"Uses part: {name}",label_filter:"Label",all_labels:"All labels",settings_notify_scope:"Notify only for view",settings_notify_scope_all:"All tasks",settings_notify_scope_hint:"Only tasks matching the selected saved view's label/user filters send reminders. Status, sorting and grouping of the view are ignored here.",card_saved_view:"Saved view",card_saved_view_none:"None",card_saved_view_help:"Applies the view's status, user and label filters on top of the filters above. The view's sorting and grouping are panel display settings and are not applied on the card.",doc_part_none:"No documents linked to this part.",settings_templates_toggle_group:"Enable or disable all templates in this group",setups_button:"Suggested setups",setups_title:"Suggested setups (Beta)",setups_hint:"Devices of supported integrations whose consumable sensors can drive maintenance tasks. Adopting creates the object and wires each task to its sensor \u2014 it triggers when the consumable runs low and resolves itself after replacement.",setups_none:"No supported devices with unwired consumable sensors found.",setups_adopt:"Set up selected",setups_done:"{tasks} sensor-wired tasks created.",complete_parts_used:"Parts used this time",part_delete_confirm:"Delete part '{name}'? Its stock tracking, task links and any open buy reminder will be removed.",baseline_start_value:"Start reading (optional)",baseline_start_help:"Counting starts from this reading. Leave empty to count from the current value; enter the reading at the last service so usage since then already counts.",setups_baseline_hint:"reading at last service (optional)",baseline_start_help_edit:"Leave empty to keep the existing counting. Entering a value re-anchors the counting (e.g. the reading at the last service).",baseline_current_effective:"Currently effective start value: {value}",runtime_on_states:"Active states (comma-separated)",runtime_on_states_help:"States that count as running \u2014 default: on. E.g. mowing, cleaning, printing. With an attribute selected, its values are matched instead.",setups_target_new:"Create new: {name}",schedule_preview_title:"Next dates",schedule_preview_ontime:"Assuming on-time completion.",schedule_preview_ends:"(series ends)",adopt_problem_responsible:"Responsible user for all adopted tasks (optional)",adopt_problem_configure:"Configure",history_auto:"Automatic",battery_fleet_title:"Battery fleet",battery_fleet_none_low:"All batteries OK \u2014 nothing to replace.",battery_fleet_buy_now:"Buy now",battery_fleet_soon:"Needed soon",battery_fleet_soon_hint:"Predicted from the last replacement date \u2014 order ahead.",battery_fleet_mark_all:"Mark all replaced",battery_fleet_mark_one:"Mark this battery replaced",battery_fleet_offline:"offline",battery_fleet_trigger_lost:"This task's sensor trigger was lost \u2014 it will not fire or auto-complete.",battery_fleet_repair:"Repair",battery_fleet_exclude:"Exclude from the fleet",battery_fleet_excluded:"Excluded",battery_fleet_include:"Track again",battery_fleet_all:"All tracked batteries",battery_fleet_all_hint:"Exclude a device here to drop it from the fleet before it ever reports low \u2014 a vacuum that recharges itself, or a phone that warns you on its own.",battery_fleet_status_low:"Low",battery_fleet_status_soon:"Soon",battery_fleet_status_ok:"Healthy",battery_fleet_predicted_on:"Expected around {date}",battery_fleet_total:"{n} batteries tracked",battery_fleet_setup_button:"Battery fleet",battery_fleet_setup_done:"Battery fleet set up \u2014 one task tracks all your batteries.",update_banner:"A newer version of Maintenance Supporter is on the server \u2014 reload to update the panel.",update_reload:"Reload"};var Ke="\u20AC",we="en",Qe=(()=>{let a=window;return a.__msLocales||(a.__msLocales={store:{},inflight:{}}),a.__msLocales})(),N=Qe.store;N.en||(N.en=Ge);var Dt=new Set(["de","nl","fr","it","es","pt","pt-br","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi","hu","ko","tr"]),Nt="/maintenance_supporter_locales",Z=Qe.inflight;function ke(a){let e=(a||we).toLowerCase();return e.startsWith("pt")&&e.endsWith("br")?"pt-br":e.substring(0,2)}function f(a,e){let t=ke(e);return N[t]?.[a]??N.en[a]??a}function Je(a){let e=ke(a);return e===we||e in N}function Ze(a){let e=ke(a);return e===we||e in N||!Dt.has(e)?Promise.resolve():(e in Z||(Z[e]=fetch(`${Nt}/${e}.json`).then(t=>t.ok?t.json():null).then(t=>{t?N[e]=t:delete Z[e]}).catch(()=>{delete Z[e]})),Z[e])}var Rt=window,Ve=Rt.__msDateTimePrefs??={};function Xe(a){a&&(Ve.date=a.date_format,Ve.time=a.time_format)}function et(a,e){if(a==null)return"\u2014";let t=e||"en";return a<0?`${Math.abs(a)} ${f("d_overdue",t)}`:a===0?f("today",t):`${a} ${f(a===1?"day":"days",t)}`}var Ho=k` +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_areas:"Filter by areas",card_filter_areas_help:"Empty = show all areas.",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",settings_install_assist_sentences:"Install Assist sentences",settings_install_assist_sentences_hint:"Copies the voice sentences into your configuration so the classic Assist agent recognises them. A file you edited yourself is never overwritten.",test_notification:"Test notification",send_test:"Send test",testing:"Sending\u2026",test_notification_success:"Test notification sent",test_notification_failed:"Test notification failed",notify_per_person:"Per-person delivery",notify_no_own_device:"No own device \u2014 uses the household service",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",settings_export_selection:"Limit to selected objects (optional)",settings_docs_archive:"Documents archive (with files)",settings_docs_archive_hint:"The JSON/YAML/CSV exports carry settings only. This ZIP includes the uploaded file contents so a restore is complete.",settings_docs_export_btn:"Download documents ZIP",settings_docs_import_btn:"Restore documents ZIP",settings_docs_import_success:"Restored: {blobs} files, {docs} documents",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",photo_label:"Photo",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",parts_section:"Parts & consumables",parts_inventory_value:"Inventory value",part_add:"Add part",part_name:"Name",part_vendor:"Manufacturer",part_storage_location:"Storage location",part_product_url:"Product URL",part_unit:"Unit",part_cost:"Unit price",part_stock:"Stock",part_reorder_threshold:"Reorder at",part_restock_quantity:"Restock quantity",part_auto_buy:"Auto-create buy task when low",part_restock:"Adjust stock",restock_quantity_label:"Quantity bought",consumes_parts_label:"Consumes parts",shared_parts_other_objects:"Parts from other objects",shared_parts_help:"Several objects can share one stock. Completing this task takes from the owning object.",shared_part_unknown:"Unknown part",parts_load_failed:"Couldn't load this object's parts \u2014 the consumes-parts options are unavailable right now.",adopt_problem_button:"Adopt problem sensors",adopt_problem_title:"Adopt problem sensors",adopt_problem_hint:"Turn HA problem sensors (printer errors, filter warnings, low battery) into maintenance tasks that trigger while the problem is active and clear themselves when it resolves.",adopt_problem_none:"No problem sensors found that aren't already tracked.",adopt_problem_active:"active",adopt_problem_ok:"ok",adopt_problem_new_object:"(new)",adopt_problem_adopt:"Adopt selected",adopt_problem_done:"Adopted {tasks} problem sensor(s)",views_label:"Views",views_none:"\u2014 No view \u2014",views_manage:"Save / manage views",views_dialog_title:"Saved views",views_dialog_hint:"Save the current filters as a named view everyone can reuse.",views_name_placeholder:"View name",views_save_current:"Save current filters",views_none_yet:"No saved views yet.",close:"Close",trigger_hint_now:"The sensor reads {value} right now.",trigger_hint_above:"The task triggers once it rises above {target}.",trigger_hint_below:"It triggers once it falls below {target}.",trigger_hint_counter_delta:"Counts from the current reading ({value}): due at {due} (+{target}), and the count restarts after each completion.",trigger_hint_counter_delta_edit:"Counts usage since the last completion: due after +{target}; the count restarts after each completion.",trigger_hint_counter_abs:"The task becomes due once the sensor reaches {target}.",trigger_hint_runtime:"The task becomes due after {hours} h of accumulated on-time; the counter restarts after each completion.",trigger_hint_state_change:"The task becomes due after {count} state change(s).",trigger_hint_state_change_to:"The task becomes due after {count} change(s) to \u201C{state}\u201D.",trigger_hint_state_now:"Current state: {value}.",adopt_problem_part:"Uses part: {name}",label_filter:"Label",all_labels:"All labels",settings_notify_scope:"Notify only for view",settings_notify_scope_all:"All tasks",settings_notify_scope_hint:"Only tasks matching the selected saved view's label/user filters send reminders. Status, sorting and grouping of the view are ignored here.",card_saved_view:"Saved view",card_saved_view_none:"None",card_saved_view_help:"Applies the view's status, user and label filters on top of the filters above. The view's sorting and grouping are panel display settings and are not applied on the card.",doc_part_none:"No documents linked to this part.",settings_templates_toggle_group:"Enable or disable all templates in this group",setups_button:"Suggested setups",setups_title:"Suggested setups (Beta)",setups_hint:"Devices of supported integrations whose consumable sensors can drive maintenance tasks. Adopting creates the object and wires each task to its sensor \u2014 it triggers when the consumable runs low and resolves itself after replacement.",setups_none:"No supported devices with unwired consumable sensors found.",setups_adopt:"Set up selected",setups_done:"{tasks} sensor-wired tasks created.",complete_parts_used:"Parts used this time",part_delete_confirm:"Delete part '{name}'? Its stock tracking, task links and any open buy reminder will be removed.",baseline_start_value:"Start reading (optional)",baseline_start_help:"Counting starts from this reading. Leave empty to count from the current value; enter the reading at the last service so usage since then already counts.",setups_baseline_hint:"reading at last service (optional)",baseline_start_help_edit:"Leave empty to keep the existing counting. Entering a value re-anchors the counting (e.g. the reading at the last service).",baseline_current_effective:"Currently effective start value: {value}",runtime_on_states:"Active states (comma-separated)",runtime_on_states_help:"States that count as running \u2014 default: on. E.g. mowing, cleaning, printing. With an attribute selected, its values are matched instead.",setups_target_new:"Create new: {name}",schedule_preview_title:"Next dates",schedule_preview_ontime:"Assuming on-time completion.",schedule_preview_ends:"(series ends)",adopt_problem_responsible:"Responsible user for all adopted tasks (optional)",adopt_problem_configure:"Configure",history_auto:"Automatic",battery_fleet_title:"Battery fleet",battery_fleet_none_low:"All batteries OK \u2014 nothing to replace.",battery_fleet_buy_now:"Buy now",battery_fleet_soon:"Needed soon",battery_fleet_soon_hint:"Predicted from the last replacement date \u2014 order ahead.",battery_fleet_mark_all:"Mark all replaced",battery_fleet_mark_one:"Mark this battery replaced",battery_fleet_offline:"offline",battery_fleet_trigger_lost:"This task's sensor trigger was lost \u2014 it will not fire or auto-complete.",battery_fleet_repair:"Repair",battery_fleet_exclude:"Exclude from the fleet",battery_fleet_excluded:"Excluded",battery_fleet_include:"Track again",battery_fleet_all:"All tracked batteries",battery_fleet_all_hint:"Exclude a device here to drop it from the fleet before it ever reports low \u2014 a vacuum that recharges itself, or a phone that warns you on its own.",battery_fleet_status_low:"Low",battery_fleet_status_soon:"Soon",battery_fleet_status_ok:"Healthy",battery_fleet_predicted_on:"Expected around {date}",battery_fleet_predicted_trend:"Predicted from this battery's discharge trend: around {date} ({confidence})",battery_fleet_rechargeable:"Rechargeable: charge instead of replacing \u2014 never on the shopping list",battery_fleet_sort_name:"Sort by name",battery_fleet_sort_urgency:"Sort by urgency",battery_fleet_mark_recharged:"Mark as recharged",battery_fleet_sparkline_hint:"Battery level over the last 30 days \u2014 dotted: projected until the low threshold",battery_fleet_filter_type:"Show only this battery type",battery_fleet_record_replacement:"The level jumped around {date} \u2014 record this replacement in Battery Notes",battery_fleet_total:"{n} batteries tracked",battery_fleet_setup_button:"Battery fleet",battery_fleet_setup_done:"Battery fleet set up \u2014 one task tracks all your batteries.",update_banner:"A newer version of Maintenance Supporter is on the server \u2014 reload to update the panel.",update_reload:"Reload"};var Ke="\u20AC",we="en",Qe=(()=>{let a=window;return a.__msLocales||(a.__msLocales={store:{},inflight:{}}),a.__msLocales})(),N=Qe.store;N.en||(N.en=Ge);var Dt=new Set(["de","nl","fr","it","es","pt","pt-br","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi","hu","ko","tr"]),Nt="/maintenance_supporter_locales",Z=Qe.inflight;function ke(a){let e=(a||we).toLowerCase();return e.startsWith("pt")&&e.endsWith("br")?"pt-br":e.substring(0,2)}function f(a,e){let t=ke(e);return N[t]?.[a]??N.en[a]??a}function Je(a){let e=ke(a);return e===we||e in N}function Ze(a){let e=ke(a);return e===we||e in N||!Dt.has(e)?Promise.resolve():(e in Z||(Z[e]=fetch(`${Nt}/${e}.json`).then(t=>t.ok?t.json():null).then(t=>{t?N[e]=t:delete Z[e]}).catch(()=>{delete Z[e]})),Z[e])}var Rt=window,Ve=Rt.__msDateTimePrefs??={};function Xe(a){a&&(Ve.date=a.date_format,Ve.time=a.time_format)}function et(a,e){if(a==null)return"\u2014";let t=e||"en";return a<0?`${Math.abs(a)} ${f("d_overdue",t)}`:a===0?f("today",t):`${a} ${f(a===1?"day":"days",t)}`}var Ho=k` .field { display: flex; flex-direction: column; gap: 4px; } .field-label { font-size: 12px; color: var(--secondary-text-color); } .field-input { diff --git a/custom_components/maintenance_supporter/frontend/maintenance-card.js b/custom_components/maintenance_supporter/frontend/maintenance-card.js index 37c4f35..5f6c6ba 100644 --- a/custom_components/maintenance_supporter/frontend/maintenance-card.js +++ b/custom_components/maintenance_supporter/frontend/maintenance-card.js @@ -1,9 +1,9 @@ -/*! maintenance_supporter frontend 2.46.0 */ -var _t=Object.defineProperty;var Ci=Object.getOwnPropertyDescriptor;var w=(a,s,e)=>()=>{if(e)throw e[0];try{return a&&(s=a(a=0)),s}catch(t){throw e=[t],t}};var Li=(a,s)=>{for(var e in s)_t(a,e,{get:s[e],enumerable:!0})};var l=(a,s,e,t)=>{for(var i=t>1?void 0:t?Ci(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&&_t(s,e,i),i};var Ee,Se,Fe,gt,pe,mt,E,ft,Ue,Ve=w(()=>{Ee=globalThis,Se=Ee.ShadowRoot&&(Ee.ShadyCSS===void 0||Ee.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Fe=Symbol(),gt=new WeakMap,pe=class{constructor(s,e,t){if(this._$cssResult$=!0,t!==Fe)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(Se&&s===void 0){let t=e!==void 0&&e.length===1;t&&(s=gt.get(e)),s===void 0&&((this.o=s=new CSSStyleSheet).replaceSync(this.cssText),t&>.set(e,s))}return s}toString(){return this.cssText}},mt=a=>new pe(typeof a=="string"?a:a+"",void 0,Fe),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 pe(e,a,Fe)},ft=(a,s)=>{if(Se)a.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of s){let t=document.createElement("style"),i=Ee.litNonce;i!==void 0&&t.setAttribute("nonce",i),t.textContent=e.cssText,a.appendChild(t)}},Ue=Se?a=>a:a=>a instanceof CSSStyleSheet?(s=>{let e="";for(let t of s.cssRules)e+=t.cssText;return mt(e)})(a):a});var Ii,Pi,Ri,ji,Hi,Ni,Ae,vt,qi,zi,he,ue,Te,bt,V,_e=w(()=>{Ve();Ve();({is:Ii,defineProperty:Pi,getOwnPropertyDescriptor:Ri,getOwnPropertyNames:ji,getOwnPropertySymbols:Hi,getPrototypeOf:Ni}=Object),Ae=globalThis,vt=Ae.trustedTypes,qi=vt?vt.emptyScript:"",zi=Ae.reactiveElementPolyfillSupport,he=(a,s)=>a,ue={toAttribute(a,s){switch(s){case Boolean:a=a?qi: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}},Te=(a,s)=>!Ii(a,s),bt={attribute:!0,type:String,converter:ue,reflect:!1,useDefault:!1,hasChanged:Te};Symbol.metadata??=Symbol("metadata"),Ae.litPropertyMetadata??=new WeakMap;V=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=bt){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&&Pi(this.prototype,s,i)}}static getPropertyDescriptor(s,e,t){let{get:i,set:n}=Ri(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)??bt}static _$Ei(){if(this.hasOwnProperty(he("elementProperties")))return;let s=Ni(this);s.finalize(),s.l!==void 0&&(this.l=[...s.l]),this.elementProperties=new Map(s.elementProperties)}static finalize(){if(this.hasOwnProperty(he("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(he("properties"))){let e=this.properties,t=[...ji(e),...Hi(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(Ue(i))}else s!==void 0&&e.push(Ue(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 ft(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:ue).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:ue;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??Te)(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){}};V.elementStyles=[],V.shadowRootOptions={mode:"open"},V[he("elementProperties")]=new Map,V[he("finalized")]=new Map,zi?.({ReactiveElement:V}),(Ae.reactiveElementVersions??=[]).push("2.1.2")});function Lt(a,s){if(!Ze(a)||!a.hasOwnProperty("raw"))throw Error("invalid template strings array");return xt!==void 0?xt.createHTML(s):s}function ae(a,s,e=a,t){if(s===te)return s;let i=t!==void 0?e._$Co?.[t]:e._$Cl,n=fe(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=ae(a,i._$AS(a,s.values),i,t)),s}var Qe,yt,Ce,xt,At,J,Tt,Mi,ee,me,fe,Ze,Oi,Be,ge,wt,$t,Z,kt,Et,Ct,Xe,o,oe,js,te,h,St,X,Di,ve,We,be,ne,Ge,Ke,Ye,Je,Fi,It,Le=w(()=>{Qe=globalThis,yt=a=>a,Ce=Qe.trustedTypes,xt=Ce?Ce.createPolicy("lit-html",{createHTML:a=>a}):void 0,At="$lit$",J=`lit$${Math.random().toFixed(9).slice(2)}$`,Tt="?"+J,Mi=`<${Tt}>`,ee=document,me=()=>ee.createComment(""),fe=a=>a===null||typeof a!="object"&&typeof a!="function",Ze=Array.isArray,Oi=a=>Ze(a)||typeof a?.[Symbol.iterator]=="function",Be=`[ +/*! maintenance_supporter frontend 2.52.0 */ +var _t=Object.defineProperty;var Li=Object.getOwnPropertyDescriptor;var w=(a,s,e)=>()=>{if(e)throw e[0];try{return a&&(s=a(a=0)),s}catch(t){throw e=[t],t}};var Ii=(a,s)=>{for(var e in s)_t(a,e,{get:s[e],enumerable:!0})};var l=(a,s,e,t)=>{for(var i=t>1?void 0:t?Li(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&&_t(s,e,i),i};var Ee,Se,Fe,gt,pe,mt,E,ft,Ue,Ve=w(()=>{Ee=globalThis,Se=Ee.ShadowRoot&&(Ee.ShadyCSS===void 0||Ee.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Fe=Symbol(),gt=new WeakMap,pe=class{constructor(s,e,t){if(this._$cssResult$=!0,t!==Fe)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(Se&&s===void 0){let t=e!==void 0&&e.length===1;t&&(s=gt.get(e)),s===void 0&&((this.o=s=new CSSStyleSheet).replaceSync(this.cssText),t&>.set(e,s))}return s}toString(){return this.cssText}},mt=a=>new pe(typeof a=="string"?a:a+"",void 0,Fe),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 pe(e,a,Fe)},ft=(a,s)=>{if(Se)a.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of s){let t=document.createElement("style"),i=Ee.litNonce;i!==void 0&&t.setAttribute("nonce",i),t.textContent=e.cssText,a.appendChild(t)}},Ue=Se?a=>a:a=>a instanceof CSSStyleSheet?(s=>{let e="";for(let t of s.cssRules)e+=t.cssText;return mt(e)})(a):a});var Pi,Ri,ji,Hi,Ni,qi,Ae,vt,Mi,zi,he,ue,Te,bt,V,_e=w(()=>{Ve();Ve();({is:Pi,defineProperty:Ri,getOwnPropertyDescriptor:ji,getOwnPropertyNames:Hi,getOwnPropertySymbols:Ni,getPrototypeOf:qi}=Object),Ae=globalThis,vt=Ae.trustedTypes,Mi=vt?vt.emptyScript:"",zi=Ae.reactiveElementPolyfillSupport,he=(a,s)=>a,ue={toAttribute(a,s){switch(s){case Boolean:a=a?Mi: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}},Te=(a,s)=>!Pi(a,s),bt={attribute:!0,type:String,converter:ue,reflect:!1,useDefault:!1,hasChanged:Te};Symbol.metadata??=Symbol("metadata"),Ae.litPropertyMetadata??=new WeakMap;V=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=bt){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&&Ri(this.prototype,s,i)}}static getPropertyDescriptor(s,e,t){let{get:i,set:n}=ji(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)??bt}static _$Ei(){if(this.hasOwnProperty(he("elementProperties")))return;let s=qi(this);s.finalize(),s.l!==void 0&&(this.l=[...s.l]),this.elementProperties=new Map(s.elementProperties)}static finalize(){if(this.hasOwnProperty(he("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(he("properties"))){let e=this.properties,t=[...Hi(e),...Ni(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(Ue(i))}else s!==void 0&&e.push(Ue(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 ft(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:ue).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:ue;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??Te)(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){}};V.elementStyles=[],V.shadowRootOptions={mode:"open"},V[he("elementProperties")]=new Map,V[he("finalized")]=new Map,zi?.({ReactiveElement:V}),(Ae.reactiveElementVersions??=[]).push("2.1.2")});function Lt(a,s){if(!Ze(a)||!a.hasOwnProperty("raw"))throw Error("invalid template strings array");return xt!==void 0?xt.createHTML(s):s}function ae(a,s,e=a,t){if(s===te)return s;let i=t!==void 0?e._$Co?.[t]:e._$Cl,n=fe(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=ae(a,i._$AS(a,s.values),i,t)),s}var Qe,yt,Ce,xt,At,J,Tt,Oi,ee,me,fe,Ze,Di,Be,ge,wt,$t,Z,kt,Et,Ct,Xe,o,oe,Hs,te,h,St,X,Fi,ve,We,be,ne,Ge,Ke,Ye,Je,Ui,It,Le=w(()=>{Qe=globalThis,yt=a=>a,Ce=Qe.trustedTypes,xt=Ce?Ce.createPolicy("lit-html",{createHTML:a=>a}):void 0,At="$lit$",J=`lit$${Math.random().toFixed(9).slice(2)}$`,Tt="?"+J,Oi=`<${Tt}>`,ee=document,me=()=>ee.createComment(""),fe=a=>a===null||typeof a!="object"&&typeof a!="function",Ze=Array.isArray,Di=a=>Ze(a)||typeof a?.[Symbol.iterator]=="function",Be=`[ \f\r]`,ge=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,wt=/-->/g,$t=/>/g,Z=RegExp(`>|${Be}(?:([^\\s"'>=/]+)(${Be}*=${Be}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`,"g"),kt=/'/g,Et=/"/g,Ct=/^(?:script|style|textarea|title)$/i,Xe=a=>(s,...e)=>({_$litType$:a,strings:s,values:e}),o=Xe(1),oe=Xe(2),js=Xe(3),te=Symbol.for("lit-noChange"),h=Symbol.for("lit-nothing"),St=new WeakMap,X=ee.createTreeWalker(ee,129);Di=(a,s)=>{let e=a.length-1,t=[],i,n=s===2?"":s===3?"":"",c=ge;for(let d=0;d"?(c=i??ge,f=-1):v[1]===void 0?f=-2:(f=c.lastIndex-v[2].length,_=v[1],c=v[3]===void 0?Z:v[3]==='"'?Et:kt):c===Et||c===kt?c=Z:c===wt||c===$t?c=ge:(c=Z,i=void 0);let g=c===Z&&a[d+1].startsWith("/>")?" ":"";n+=c===ge?u+Mi:f>=0?(t.push(_),u.slice(0,f)+At+u.slice(f)+J+g):u+J+(f===-2?d:g)}return[Lt(a,n+(a[e]||"")+(s===2?"":s===3?"":"")),t]},ve=class a{constructor({strings:s,_$litType$:e},t){let i;this.parts=[];let n=0,c=0,d=s.length-1,u=this.parts,[_,v]=Di(s,e);if(this.el=a.createElement(_,t),X.currentNode=this.el.content,e===2||e===3){let f=this.el.content.firstChild;f.replaceWith(...f.childNodes)}for(;(i=X.nextNode())!==null&&u.length0){i.textContent=Ce?Ce.emptyScript:"";for(let g=0;g2||t[0]!==""||t[1]!==""?(this._$AH=Array(t.length-1).fill(new String),this.strings=t):this._$AH=h}_$AI(s,e=this,t,i){let n=this.strings,c=!1;if(n===void 0)s=ae(this,s,e,0),c=!fe(s)||s!==this._$AH&&s!==te,c&&(this._$AH=s);else{let d=s,u,_;for(s=n[0],u=0;u{let t=e?.renderBefore??s,i=t._$litPart$;if(i===void 0){let n=e?.renderBefore??null;t._$litPart$=i=new be(s.insertBefore(me(),n),n,void 0,e??{})}return i._$AI(a),i}});var et,k,Ui,Pt=w(()=>{_e();_e();Le();Le();et=globalThis,k=class extends V{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=It(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return te}};k._$litElement$=!0,k.finalized=!0,et.litElementHydrateSupport?.({LitElement:k});Ui=et.litElementPolyfillSupport;Ui?.({LitElement:k});(et.litElementVersions??=[]).push("4.2.2")});var Rt=w(()=>{});var I=w(()=>{_e();Le();Pt();Rt()});var jt=w(()=>{});function y(a){return(s,e)=>typeof e=="object"?Bi(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 Vi,Bi,tt=w(()=>{_e();Vi={attribute:!0,type:String,converter:ue,reflect:!1,hasChanged:Te},Bi=(a=Vi,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 u=s.get.call(this);s.set.call(this,d),this.requestUpdate(c,u,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 u=this[c];s.call(this,d),this.requestUpdate(c,u,a,!0,d)}}throw Error("Unsupported decorator location: "+t)}});function p(a){return y({...a,state:!0,attribute:!1})}var Ht=w(()=>{tt();});var Nt=w(()=>{});var le=w(()=>{});var qt=w(()=>{le();});var zt=w(()=>{le();});var Mt=w(()=>{le();});var Ot=w(()=>{le();});var Dt=w(()=>{le();});var M=w(()=>{jt();tt();Ht();Nt();qt();zt();Mt();Ot();Dt()});var Ut,Ft=w(()=>{Ut={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",require_on_completion:"Require on completion",checklist_steps_optional:"Checklist steps (optional)",checklist_placeholder:`Clean filter +\f\r"'\`<>=]|("|')|))|$)`,"g"),kt=/'/g,Et=/"/g,Ct=/^(?:script|style|textarea|title)$/i,Xe=a=>(s,...e)=>({_$litType$:a,strings:s,values:e}),o=Xe(1),oe=Xe(2),Hs=Xe(3),te=Symbol.for("lit-noChange"),h=Symbol.for("lit-nothing"),St=new WeakMap,X=ee.createTreeWalker(ee,129);Fi=(a,s)=>{let e=a.length-1,t=[],i,n=s===2?"":s===3?"":"",c=ge;for(let d=0;d"?(c=i??ge,f=-1):v[1]===void 0?f=-2:(f=c.lastIndex-v[2].length,_=v[1],c=v[3]===void 0?Z:v[3]==='"'?Et:kt):c===Et||c===kt?c=Z:c===wt||c===$t?c=ge:(c=Z,i=void 0);let g=c===Z&&a[d+1].startsWith("/>")?" ":"";n+=c===ge?u+Oi:f>=0?(t.push(_),u.slice(0,f)+At+u.slice(f)+J+g):u+J+(f===-2?d:g)}return[Lt(a,n+(a[e]||"")+(s===2?"":s===3?"":"")),t]},ve=class a{constructor({strings:s,_$litType$:e},t){let i;this.parts=[];let n=0,c=0,d=s.length-1,u=this.parts,[_,v]=Fi(s,e);if(this.el=a.createElement(_,t),X.currentNode=this.el.content,e===2||e===3){let f=this.el.content.firstChild;f.replaceWith(...f.childNodes)}for(;(i=X.nextNode())!==null&&u.length0){i.textContent=Ce?Ce.emptyScript:"";for(let g=0;g2||t[0]!==""||t[1]!==""?(this._$AH=Array(t.length-1).fill(new String),this.strings=t):this._$AH=h}_$AI(s,e=this,t,i){let n=this.strings,c=!1;if(n===void 0)s=ae(this,s,e,0),c=!fe(s)||s!==this._$AH&&s!==te,c&&(this._$AH=s);else{let d=s,u,_;for(s=n[0],u=0;u{let t=e?.renderBefore??s,i=t._$litPart$;if(i===void 0){let n=e?.renderBefore??null;t._$litPart$=i=new be(s.insertBefore(me(),n),n,void 0,e??{})}return i._$AI(a),i}});var et,k,Vi,Pt=w(()=>{_e();_e();Le();Le();et=globalThis,k=class extends V{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=It(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return te}};k._$litElement$=!0,k.finalized=!0,et.litElementHydrateSupport?.({LitElement:k});Vi=et.litElementPolyfillSupport;Vi?.({LitElement:k});(et.litElementVersions??=[]).push("4.2.2")});var Rt=w(()=>{});var I=w(()=>{_e();Le();Pt();Rt()});var Ht=w(()=>{});function b(a){return(s,e)=>typeof e=="object"?Wi(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 Bi,Wi,tt=w(()=>{_e();Bi={attribute:!0,type:String,converter:ue,reflect:!1,hasChanged:Te},Wi=(a=Bi,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 u=s.get.call(this);s.set.call(this,d),this.requestUpdate(c,u,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 u=this[c];s.call(this,d),this.requestUpdate(c,u,a,!0,d)}}throw Error("Unsupported decorator location: "+t)}});function p(a){return b({...a,state:!0,attribute:!1})}var Nt=w(()=>{tt();});var qt=w(()=>{});var le=w(()=>{});var Mt=w(()=>{le();});var zt=w(()=>{le();});var Ot=w(()=>{le();});var Dt=w(()=>{le();});var Ft=w(()=>{le();});var z=w(()=>{Ht();tt();Nt();qt();Mt();zt();Ot();Dt();Ft()});var Vt,Ut=w(()=>{Vt={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",require_on_completion:"Require on completion",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_areas:"Filter by areas",card_filter_areas_help:"Empty = show all areas.",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",settings_install_assist_sentences:"Install Assist sentences",settings_install_assist_sentences_hint:"Copies the voice sentences into your configuration so the classic Assist agent recognises them. A file you edited yourself is never overwritten.",test_notification:"Test notification",send_test:"Send test",testing:"Sending\u2026",test_notification_success:"Test notification sent",test_notification_failed:"Test notification failed",notify_per_person:"Per-person delivery",notify_no_own_device:"No own device \u2014 uses the household service",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",settings_export_selection:"Limit to selected objects (optional)",settings_docs_archive:"Documents archive (with files)",settings_docs_archive_hint:"The JSON/YAML/CSV exports carry settings only. This ZIP includes the uploaded file contents so a restore is complete.",settings_docs_export_btn:"Download documents ZIP",settings_docs_import_btn:"Restore documents ZIP",settings_docs_import_success:"Restored: {blobs} files, {docs} documents",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",photo_label:"Photo",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",parts_section:"Parts & consumables",part_add:"Add part",part_name:"Name",part_vendor:"Manufacturer",part_storage_location:"Storage location",part_product_url:"Product URL",part_unit:"Unit",part_cost:"Unit price",part_stock:"Stock",part_reorder_threshold:"Reorder at",part_restock_quantity:"Restock quantity",part_auto_buy:"Auto-create buy task when low",part_restock:"Adjust stock",restock_quantity_label:"Quantity bought",consumes_parts_label:"Consumes parts",shared_parts_other_objects:"Parts from other objects",shared_parts_help:"Several objects can share one stock. Completing this task takes from the owning object.",shared_part_unknown:"Unknown part",parts_load_failed:"Couldn't load this object's parts \u2014 the consumes-parts options are unavailable right now.",adopt_problem_button:"Adopt problem sensors",adopt_problem_title:"Adopt problem sensors",adopt_problem_hint:"Turn HA problem sensors (printer errors, filter warnings, low battery) into maintenance tasks that trigger while the problem is active and clear themselves when it resolves.",adopt_problem_none:"No problem sensors found that aren't already tracked.",adopt_problem_active:"active",adopt_problem_ok:"ok",adopt_problem_new_object:"(new)",adopt_problem_adopt:"Adopt selected",adopt_problem_done:"Adopted {tasks} problem sensor(s)",views_label:"Views",views_none:"\u2014 No view \u2014",views_manage:"Save / manage views",views_dialog_title:"Saved views",views_dialog_hint:"Save the current filters as a named view everyone can reuse.",views_name_placeholder:"View name",views_save_current:"Save current filters",views_none_yet:"No saved views yet.",close:"Close",trigger_hint_now:"The sensor reads {value} right now.",trigger_hint_above:"The task triggers once it rises above {target}.",trigger_hint_below:"It triggers once it falls below {target}.",trigger_hint_counter_delta:"Counts from the current reading ({value}): due at {due} (+{target}), and the count restarts after each completion.",trigger_hint_counter_delta_edit:"Counts usage since the last completion: due after +{target}; the count restarts after each completion.",trigger_hint_counter_abs:"The task becomes due once the sensor reaches {target}.",trigger_hint_runtime:"The task becomes due after {hours} h of accumulated on-time; the counter restarts after each completion.",trigger_hint_state_change:"The task becomes due after {count} state change(s).",trigger_hint_state_change_to:"The task becomes due after {count} change(s) to \u201C{state}\u201D.",trigger_hint_state_now:"Current state: {value}.",adopt_problem_part:"Uses part: {name}",label_filter:"Label",all_labels:"All labels",settings_notify_scope:"Notify only for view",settings_notify_scope_all:"All tasks",settings_notify_scope_hint:"Only tasks matching the selected saved view's label/user filters send reminders. Status, sorting and grouping of the view are ignored here.",card_saved_view:"Saved view",card_saved_view_none:"None",card_saved_view_help:"Applies the view's status, user and label filters on top of the filters above. The view's sorting and grouping are panel display settings and are not applied on the card.",doc_part_none:"No documents linked to this part.",settings_templates_toggle_group:"Enable or disable all templates in this group",setups_button:"Suggested setups",setups_title:"Suggested setups (Beta)",setups_hint:"Devices of supported integrations whose consumable sensors can drive maintenance tasks. Adopting creates the object and wires each task to its sensor \u2014 it triggers when the consumable runs low and resolves itself after replacement.",setups_none:"No supported devices with unwired consumable sensors found.",setups_adopt:"Set up selected",setups_done:"{tasks} sensor-wired tasks created.",complete_parts_used:"Parts used this time",part_delete_confirm:"Delete part '{name}'? Its stock tracking, task links and any open buy reminder will be removed.",baseline_start_value:"Start reading (optional)",baseline_start_help:"Counting starts from this reading. Leave empty to count from the current value; enter the reading at the last service so usage since then already counts.",setups_baseline_hint:"reading at last service (optional)",baseline_start_help_edit:"Leave empty to keep the existing counting. Entering a value re-anchors the counting (e.g. the reading at the last service).",baseline_current_effective:"Currently effective start value: {value}",runtime_on_states:"Active states (comma-separated)",runtime_on_states_help:"States that count as running \u2014 default: on. E.g. mowing, cleaning, printing. With an attribute selected, its values are matched instead.",setups_target_new:"Create new: {name}",schedule_preview_title:"Next dates",schedule_preview_ontime:"Assuming on-time completion.",schedule_preview_ends:"(series ends)",adopt_problem_responsible:"Responsible user for all adopted tasks (optional)",adopt_problem_configure:"Configure",history_auto:"Automatic",battery_fleet_title:"Battery fleet",battery_fleet_none_low:"All batteries OK \u2014 nothing to replace.",battery_fleet_buy_now:"Buy now",battery_fleet_soon:"Needed soon",battery_fleet_soon_hint:"Predicted from the last replacement date \u2014 order ahead.",battery_fleet_mark_all:"Mark all replaced",battery_fleet_mark_one:"Mark this battery replaced",battery_fleet_offline:"offline",battery_fleet_trigger_lost:"This task's sensor trigger was lost \u2014 it will not fire or auto-complete.",battery_fleet_repair:"Repair",battery_fleet_exclude:"Exclude from the fleet",battery_fleet_excluded:"Excluded",battery_fleet_include:"Track again",battery_fleet_all:"All tracked batteries",battery_fleet_all_hint:"Exclude a device here to drop it from the fleet before it ever reports low \u2014 a vacuum that recharges itself, or a phone that warns you on its own.",battery_fleet_status_low:"Low",battery_fleet_status_soon:"Soon",battery_fleet_status_ok:"Healthy",battery_fleet_predicted_on:"Expected around {date}",battery_fleet_total:"{n} batteries tracked",battery_fleet_setup_button:"Battery fleet",battery_fleet_setup_done:"Battery fleet set up \u2014 one task tracks all your batteries.",update_banner:"A newer version of Maintenance Supporter is on the server \u2014 reload to update the panel.",update_reload:"Reload"}});var ie,Vt=w(()=>{"use strict";ie={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)"}});function je(a){let s=(a||it).toLowerCase();return s.startsWith("pt")&&s.endsWith("br")?"pt-br":s.substring(0,2)}function r(a,s){let e=je(s);return se[e]?.[a]??se.en[a]??a}function He(a){let s=je(a);return s===it||s in se}function Ne(a){let s=je(a);return s===it||s in se||!Gi.has(s)?Promise.resolve():(s in ye||(ye[s]=fetch(`${Ki}/${s}.json`).then(e=>e.ok?e.json():null).then(e=>{e?se[s]=e:delete ye[s]}).catch(()=>{delete ye[s]})),ye[s])}function xe(a){let s=je(a);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",pl:"pl-PL",cs:"cs-CZ",sv:"sv-SE","pt-br":"pt-BR",hu:"hu-HU",ko:"ko-KR",tr:"tr-TR"}[s]??"en-US"}function Wt(a){a&&(Pe.date=a.date_format,Pe.time=a.time_format)}function Gt(a,s){let e=String(a.getDate()).padStart(2,"0"),t=String(a.getMonth()+1).padStart(2,"0"),i=String(a.getFullYear());switch(Pe.date){case"DMY":return`${e}/${t}/${i}`;case"MDY":return`${t}/${e}/${i}`;case"YMD":return`${i}-${t}-${e}`;case"system":return a.toLocaleDateString(void 0,{day:"2-digit",month:"2-digit",year:"numeric"});default:return a.toLocaleDateString(xe(s),{day:"2-digit",month:"2-digit",year:"numeric"})}}function Ji(a,s){switch(Pe.time){case"12":return a.toLocaleTimeString(xe(s),{hour:"2-digit",minute:"2-digit",hour12:!0});case"24":return a.toLocaleTimeString(xe(s),{hour:"2-digit",minute:"2-digit",hour12:!1});case"system":return a.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"});default:return a.toLocaleTimeString(xe(s),{hour:"2-digit",minute:"2-digit"})}}function B(a,s){if(!a)return"\u2014";try{let e=a.includes("T")?a:a+"T00:00:00";return Gt(new Date(e),s)}catch{return a}}function Kt(a,s){if(!a)return"\u2014";try{let e=new Date(a);return Gt(e,s)+" "+Ji(e,s)}catch{return a}}function st(a,s){if(a==null)return"\u2014";let e=s||"en";return a<0?`${Math.abs(a)} ${r("d_overdue",e)}`:a===0?r("today",e):`${a} ${r(a===1?"day":"days",e)}`}function Re(a,s,e){return a==null?"\u2014":`${a} ${r("unit_"+(s||"days"),e)}`}function we(a,s,e="long"){return new Date(Date.UTC(2024,0,1+a)).toLocaleDateString(xe(s),{weekday:e,timeZone:"UTC"})}function Yt(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=>we(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)} ${we(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?B(a.due_date,s):r("one_time",s);case"manual":return r("manual",s);case"interval":return Re(e.every,e.unit,s)}return a.schedule_type==="one_time"?a.due_date?B(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?Re(a.interval_days,a.interval_unit,s):"\u2014"}function Jt(a,s){a.currentTarget.dispatchEvent(new CustomEvent("hass-more-info",{detail:{entityId:s},bubbles:!0,composed:!0}))}var it,Bt,se,Gi,Ki,ye,Yi,Pe,Qt,qe,L=w(()=>{"use strict";I();Ft();Vt();it="en",Bt=(()=>{let a=window;return a.__msLocales||(a.__msLocales={store:{},inflight:{}}),a.__msLocales})(),se=Bt.store;se.en||(se.en=Ut);Gi=new Set(["de","nl","fr","it","es","pt","pt-br","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi","hu","ko","tr"]),Ki="/maintenance_supporter_locales",ye=Bt.inflight;Yi=window,Pe=Yi.__msDateTimePrefs??={};Qt=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_areas:"Filter by areas",card_filter_areas_help:"Empty = show all areas.",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",settings_install_assist_sentences:"Install Assist sentences",settings_install_assist_sentences_hint:"Copies the voice sentences into your configuration so the classic Assist agent recognises them. A file you edited yourself is never overwritten.",test_notification:"Test notification",send_test:"Send test",testing:"Sending\u2026",test_notification_success:"Test notification sent",test_notification_failed:"Test notification failed",notify_per_person:"Per-person delivery",notify_no_own_device:"No own device \u2014 uses the household service",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",settings_export_selection:"Limit to selected objects (optional)",settings_docs_archive:"Documents archive (with files)",settings_docs_archive_hint:"The JSON/YAML/CSV exports carry settings only. This ZIP includes the uploaded file contents so a restore is complete.",settings_docs_export_btn:"Download documents ZIP",settings_docs_import_btn:"Restore documents ZIP",settings_docs_import_success:"Restored: {blobs} files, {docs} documents",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",photo_label:"Photo",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",parts_section:"Parts & consumables",parts_inventory_value:"Inventory value",part_add:"Add part",part_name:"Name",part_vendor:"Manufacturer",part_storage_location:"Storage location",part_product_url:"Product URL",part_unit:"Unit",part_cost:"Unit price",part_stock:"Stock",part_reorder_threshold:"Reorder at",part_restock_quantity:"Restock quantity",part_auto_buy:"Auto-create buy task when low",part_restock:"Adjust stock",restock_quantity_label:"Quantity bought",consumes_parts_label:"Consumes parts",shared_parts_other_objects:"Parts from other objects",shared_parts_help:"Several objects can share one stock. Completing this task takes from the owning object.",shared_part_unknown:"Unknown part",parts_load_failed:"Couldn't load this object's parts \u2014 the consumes-parts options are unavailable right now.",adopt_problem_button:"Adopt problem sensors",adopt_problem_title:"Adopt problem sensors",adopt_problem_hint:"Turn HA problem sensors (printer errors, filter warnings, low battery) into maintenance tasks that trigger while the problem is active and clear themselves when it resolves.",adopt_problem_none:"No problem sensors found that aren't already tracked.",adopt_problem_active:"active",adopt_problem_ok:"ok",adopt_problem_new_object:"(new)",adopt_problem_adopt:"Adopt selected",adopt_problem_done:"Adopted {tasks} problem sensor(s)",views_label:"Views",views_none:"\u2014 No view \u2014",views_manage:"Save / manage views",views_dialog_title:"Saved views",views_dialog_hint:"Save the current filters as a named view everyone can reuse.",views_name_placeholder:"View name",views_save_current:"Save current filters",views_none_yet:"No saved views yet.",close:"Close",trigger_hint_now:"The sensor reads {value} right now.",trigger_hint_above:"The task triggers once it rises above {target}.",trigger_hint_below:"It triggers once it falls below {target}.",trigger_hint_counter_delta:"Counts from the current reading ({value}): due at {due} (+{target}), and the count restarts after each completion.",trigger_hint_counter_delta_edit:"Counts usage since the last completion: due after +{target}; the count restarts after each completion.",trigger_hint_counter_abs:"The task becomes due once the sensor reaches {target}.",trigger_hint_runtime:"The task becomes due after {hours} h of accumulated on-time; the counter restarts after each completion.",trigger_hint_state_change:"The task becomes due after {count} state change(s).",trigger_hint_state_change_to:"The task becomes due after {count} change(s) to \u201C{state}\u201D.",trigger_hint_state_now:"Current state: {value}.",adopt_problem_part:"Uses part: {name}",label_filter:"Label",all_labels:"All labels",settings_notify_scope:"Notify only for view",settings_notify_scope_all:"All tasks",settings_notify_scope_hint:"Only tasks matching the selected saved view's label/user filters send reminders. Status, sorting and grouping of the view are ignored here.",card_saved_view:"Saved view",card_saved_view_none:"None",card_saved_view_help:"Applies the view's status, user and label filters on top of the filters above. The view's sorting and grouping are panel display settings and are not applied on the card.",doc_part_none:"No documents linked to this part.",settings_templates_toggle_group:"Enable or disable all templates in this group",setups_button:"Suggested setups",setups_title:"Suggested setups (Beta)",setups_hint:"Devices of supported integrations whose consumable sensors can drive maintenance tasks. Adopting creates the object and wires each task to its sensor \u2014 it triggers when the consumable runs low and resolves itself after replacement.",setups_none:"No supported devices with unwired consumable sensors found.",setups_adopt:"Set up selected",setups_done:"{tasks} sensor-wired tasks created.",complete_parts_used:"Parts used this time",part_delete_confirm:"Delete part '{name}'? Its stock tracking, task links and any open buy reminder will be removed.",baseline_start_value:"Start reading (optional)",baseline_start_help:"Counting starts from this reading. Leave empty to count from the current value; enter the reading at the last service so usage since then already counts.",setups_baseline_hint:"reading at last service (optional)",baseline_start_help_edit:"Leave empty to keep the existing counting. Entering a value re-anchors the counting (e.g. the reading at the last service).",baseline_current_effective:"Currently effective start value: {value}",runtime_on_states:"Active states (comma-separated)",runtime_on_states_help:"States that count as running \u2014 default: on. E.g. mowing, cleaning, printing. With an attribute selected, its values are matched instead.",setups_target_new:"Create new: {name}",schedule_preview_title:"Next dates",schedule_preview_ontime:"Assuming on-time completion.",schedule_preview_ends:"(series ends)",adopt_problem_responsible:"Responsible user for all adopted tasks (optional)",adopt_problem_configure:"Configure",history_auto:"Automatic",battery_fleet_title:"Battery fleet",battery_fleet_none_low:"All batteries OK \u2014 nothing to replace.",battery_fleet_buy_now:"Buy now",battery_fleet_soon:"Needed soon",battery_fleet_soon_hint:"Predicted from the last replacement date \u2014 order ahead.",battery_fleet_mark_all:"Mark all replaced",battery_fleet_mark_one:"Mark this battery replaced",battery_fleet_offline:"offline",battery_fleet_trigger_lost:"This task's sensor trigger was lost \u2014 it will not fire or auto-complete.",battery_fleet_repair:"Repair",battery_fleet_exclude:"Exclude from the fleet",battery_fleet_excluded:"Excluded",battery_fleet_include:"Track again",battery_fleet_all:"All tracked batteries",battery_fleet_all_hint:"Exclude a device here to drop it from the fleet before it ever reports low \u2014 a vacuum that recharges itself, or a phone that warns you on its own.",battery_fleet_status_low:"Low",battery_fleet_status_soon:"Soon",battery_fleet_status_ok:"Healthy",battery_fleet_predicted_on:"Expected around {date}",battery_fleet_predicted_trend:"Predicted from this battery's discharge trend: around {date} ({confidence})",battery_fleet_rechargeable:"Rechargeable: charge instead of replacing \u2014 never on the shopping list",battery_fleet_sort_name:"Sort by name",battery_fleet_sort_urgency:"Sort by urgency",battery_fleet_mark_recharged:"Mark as recharged",battery_fleet_sparkline_hint:"Battery level over the last 30 days \u2014 dotted: projected until the low threshold",battery_fleet_filter_type:"Show only this battery type",battery_fleet_record_replacement:"The level jumped around {date} \u2014 record this replacement in Battery Notes",battery_fleet_total:"{n} batteries tracked",battery_fleet_setup_button:"Battery fleet",battery_fleet_setup_done:"Battery fleet set up \u2014 one task tracks all your batteries.",update_banner:"A newer version of Maintenance Supporter is on the server \u2014 reload to update the panel.",update_reload:"Reload"}});var ie,Bt=w(()=>{"use strict";ie={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)"}});function je(a){let s=(a||it).toLowerCase();return s.startsWith("pt")&&s.endsWith("br")?"pt-br":s.substring(0,2)}function r(a,s){let e=je(s);return se[e]?.[a]??se.en[a]??a}function He(a){let s=je(a);return s===it||s in se}function Ne(a){let s=je(a);return s===it||s in se||!Ki.has(s)?Promise.resolve():(s in ye||(ye[s]=fetch(`${Yi}/${s}.json`).then(e=>e.ok?e.json():null).then(e=>{e?se[s]=e:delete ye[s]}).catch(()=>{delete ye[s]})),ye[s])}function xe(a){let s=je(a);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",pl:"pl-PL",cs:"cs-CZ",sv:"sv-SE","pt-br":"pt-BR",hu:"hu-HU",ko:"ko-KR",tr:"tr-TR"}[s]??"en-US"}function Gt(a){a&&(Pe.date=a.date_format,Pe.time=a.time_format)}function Kt(a,s){let e=String(a.getDate()).padStart(2,"0"),t=String(a.getMonth()+1).padStart(2,"0"),i=String(a.getFullYear());switch(Pe.date){case"DMY":return`${e}/${t}/${i}`;case"MDY":return`${t}/${e}/${i}`;case"YMD":return`${i}-${t}-${e}`;case"system":return a.toLocaleDateString(void 0,{day:"2-digit",month:"2-digit",year:"numeric"});default:return a.toLocaleDateString(xe(s),{day:"2-digit",month:"2-digit",year:"numeric"})}}function Qi(a,s){switch(Pe.time){case"12":return a.toLocaleTimeString(xe(s),{hour:"2-digit",minute:"2-digit",hour12:!0});case"24":return a.toLocaleTimeString(xe(s),{hour:"2-digit",minute:"2-digit",hour12:!1});case"system":return a.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"});default:return a.toLocaleTimeString(xe(s),{hour:"2-digit",minute:"2-digit"})}}function B(a,s){if(!a)return"\u2014";try{let e=a.includes("T")?a:a+"T00:00:00";return Kt(new Date(e),s)}catch{return a}}function Yt(a,s){if(!a)return"\u2014";try{let e=new Date(a);return Kt(e,s)+" "+Qi(e,s)}catch{return a}}function st(a,s){if(a==null)return"\u2014";let e=s||"en";return a<0?`${Math.abs(a)} ${r("d_overdue",e)}`:a===0?r("today",e):`${a} ${r(a===1?"day":"days",e)}`}function Re(a,s,e){return a==null?"\u2014":`${a} ${r("unit_"+(s||"days"),e)}`}function we(a,s,e="long"){return new Date(Date.UTC(2024,0,1+a)).toLocaleDateString(xe(s),{weekday:e,timeZone:"UTC"})}function Jt(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=>we(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)} ${we(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?B(a.due_date,s):r("one_time",s);case"manual":return r("manual",s);case"interval":return Re(e.every,e.unit,s)}return a.schedule_type==="one_time"?a.due_date?B(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?Re(a.interval_days,a.interval_unit,s):"\u2014"}function Qt(a,s){a.currentTarget.dispatchEvent(new CustomEvent("hass-more-info",{detail:{entityId:s},bubbles:!0,composed:!0}))}var it,Wt,se,Ki,Yi,ye,Ji,Pe,Zt,qe,L=w(()=>{"use strict";I();Ut();Bt();it="en",Wt=(()=>{let a=window;return a.__msLocales||(a.__msLocales={store:{},inflight:{}}),a.__msLocales})(),se=Wt.store;se.en||(se.en=Vt);Ki=new Set(["de","nl","fr","it","es","pt","pt-br","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi","hu","ko","tr"]),Yi="/maintenance_supporter_locales",ye=Wt.inflight;Ji=window,Pe=Ji.__msDateTimePrefs??={};Zt=E` .field { display: flex; flex-direction: column; gap: 4px; } .field-label { font-size: 12px; color: var(--secondary-text-color); } .field-input { @@ -1179,7 +1179,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:" .stat-item .stat-label { font-size: 11px; white-space: normal; text-align: center; line-height: 1.2; } .stat-value { font-size: 20px; } } -`});var ce,rt=w(()=>{"use strict";ce=class{constructor(s){this.usersCache=null;this.cacheTimestamp=0;this.CACHE_TTL_MS=6e4;this.hass=s}updateHass(s){this.hass=s}async getUsers(s=!1){let e=Date.now();if(!s&&this.usersCache&&e-this.cacheTimestampt.id===s)?.name||null}getUser(s){return!s||!this.usersCache?null:this.usersCache.find(e=>e.id===s)||null}getCurrentUserId(){return this.hass.user?.id||null}isCurrentUser(s){return s?s===this.getCurrentUserId():!1}clearCache(){this.usersCache=null,this.cacheTimestamp=0}}});function Q(a){return`${a.entry_id??""}\0${a.part_id}`}function Qi(a,s,e,t){let i=!!a.entry_id&&a.entry_id!==s,n=i?a.entry_id:s,c=e.find(v=>v.entry_id===n),d=(c?.parts||[]).find(v=>v.id===a.part_id)||null,u=i&&c?.object?.name||"",_=d?.name||r("shared_part_unknown",t);return{part:d,foreign:i,ownerName:u,label:u?`${_} (${u})`:_}}function Zt(a,s,e,t){let n=(e.find(d=>d.entry_id===s)?.parts||[]).map(d=>({...d})),c=new Set(n.map(d=>Q({part_id:d.id})));for(let d of a?.consumes_parts||[]){if(!d.entry_id||d.entry_id===s)continue;let u=Q(d);if(c.has(u))continue;c.add(u);let{part:_,ownerName:v}=Qi(d,s,e,t);n.push({id:d.part_id,name:_?.name||r("shared_part_unknown",t),unit:_?.unit,stock:_?.stock??null,storage_location:_?.storage_location,entry_id:d.entry_id,owner_name:v})}return n}var ze=w(()=>{"use strict";L()});function es(a,s){let e=Xi[a];if(!e)return a;let t=r(e,s);return t&&t!==e?t:a}function ts(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 P(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=ts(i),c=n.field?es(n.field,s):"",d=u=>r(u,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 Xi,re=w(()=>{"use strict";L();Xi={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 Xt,Me,at=w(()=>{"use strict";Xt=["notes","cost","duration","photo","user"],Me={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"}});var $,nt=w(()=>{"use strict";I();M();L();re();ze();at();$=class extends k{constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this.restockDefault=null;this.parts=[];this.consumesParts=[];this.consumesInfo=[];this.requiredFields=[];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="";this._restockQty="";this._usedParts={}}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="",this._restockQty=this.restockDefault!==null?String(this.restockDefault):"",this._usedParts=Object.fromEntries(this.consumesParts.map(e=>[Q(e),{...e}])))}_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)}if(this.restockDefault!==null&&this._restockQty!==""){let t=parseFloat(this._restockQty);!isNaN(t)&&t>=1&&(e.restock_quantity=t)}this.parts.length>0&&(e.used_parts=Object.values(this._usedParts).filter(t=>Number.isFinite(t.quantity)&&t.quantity>0).map(t=>t.entry_id?{part_id:t.part_id,quantity:t.quantity,entry_id:t.entry_id}:{part_id:t.part_id,quantity:t.quantity})),await this.hass.connection.sendMessagePromise(e),this._open=!1,this.dispatchEvent(new CustomEvent("task-completed"))}catch(e){this._error=P(e,this.lang,r("save_error",this.lang))}finally{this._loading=!1}}get _missingRequired(){let e={notes:this._notes.trim()!=="",cost:this._cost.trim()!=="",duration:this._duration.trim()!=="",photo:this._photoDocId!=="",user:!!this.hass?.user};return this.requiredFields.filter(t=>!e[t])}_req(e){return this.requiredFields.includes(e)?o``:h}_close(){this._open=!1}render(){if(!this._open)return o``;let e=this.lang||this.hass?.language||"en";return o` +`});var ce,rt=w(()=>{"use strict";ce=class{constructor(s){this.usersCache=null;this.cacheTimestamp=0;this.CACHE_TTL_MS=6e4;this.hass=s}updateHass(s){this.hass=s}async getUsers(s=!1){let e=Date.now();if(!s&&this.usersCache&&e-this.cacheTimestampt.id===s)?.name||null}getUser(s){return!s||!this.usersCache?null:this.usersCache.find(e=>e.id===s)||null}getCurrentUserId(){return this.hass.user?.id||null}isCurrentUser(s){return s?s===this.getCurrentUserId():!1}clearCache(){this.usersCache=null,this.cacheTimestamp=0}}});function Q(a){return`${a.entry_id??""}\0${a.part_id}`}function Zi(a,s,e,t){let i=!!a.entry_id&&a.entry_id!==s,n=i?a.entry_id:s,c=e.find(v=>v.entry_id===n),d=(c?.parts||[]).find(v=>v.id===a.part_id)||null,u=i&&c?.object?.name||"",_=d?.name||r("shared_part_unknown",t);return{part:d,foreign:i,ownerName:u,label:u?`${_} (${u})`:_}}function Xt(a,s,e,t){let n=(e.find(d=>d.entry_id===s)?.parts||[]).map(d=>({...d})),c=new Set(n.map(d=>Q({part_id:d.id})));for(let d of a?.consumes_parts||[]){if(!d.entry_id||d.entry_id===s)continue;let u=Q(d);if(c.has(u))continue;c.add(u);let{part:_,ownerName:v}=Zi(d,s,e,t);n.push({id:d.part_id,name:_?.name||r("shared_part_unknown",t),unit:_?.unit,stock:_?.stock??null,storage_location:_?.storage_location,entry_id:d.entry_id,owner_name:v})}return n}var Me=w(()=>{"use strict";L()});function ts(a,s){let e=es[a];if(!e)return a;let t=r(e,s);return t&&t!==e?t:a}function is(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 P(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=is(i),c=n.field?ts(n.field,s):"",d=u=>r(u,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 es,re=w(()=>{"use strict";L();es={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 ei,ze,at=w(()=>{"use strict";ei=["notes","cost","duration","photo","user"],ze={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"}});var $,nt=w(()=>{"use strict";I();z();L();re();Me();at();$=class extends k{constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this.restockDefault=null;this.parts=[];this.consumesParts=[];this.consumesInfo=[];this.requiredFields=[];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="";this._restockQty="";this._usedParts={};this.checklistPrefill={}}open(){this._open||(this._open=!0,this._notes="",this._cost="",this._duration="",this._error="",this._checklistState=Object.fromEntries(this.checklist.map((e,t)=>[String(t),!!this.checklistPrefill[e]]).filter(([,e])=>e)),this._feedback="needed",this._photoDocId="",this._photoPreview="",this._photoUploading=!1,this._readingValue="",this._restockQty=this.restockDefault!==null?String(this.restockDefault):"",this._usedParts=Object.fromEntries(this.consumesParts.map(e=>[Q(e),{...e}])))}_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)}if(this.restockDefault!==null&&this._restockQty!==""){let t=parseFloat(this._restockQty);!isNaN(t)&&t>=1&&(e.restock_quantity=t)}this.parts.length>0&&(e.used_parts=Object.values(this._usedParts).filter(t=>Number.isFinite(t.quantity)&&t.quantity>0).map(t=>t.entry_id?{part_id:t.part_id,quantity:t.quantity,entry_id:t.entry_id}:{part_id:t.part_id,quantity:t.quantity})),await this.hass.connection.sendMessagePromise(e),this._open=!1,this.dispatchEvent(new CustomEvent("task-completed"))}catch(e){this._error=P(e,this.lang,r("save_error",this.lang))}finally{this._loading=!1}}get _missingRequired(){let e={notes:this._notes.trim()!=="",cost:this._cost.trim()!=="",duration:this._duration.trim()!=="",photo:this._photoDocId!=="",user:!!this.hass?.user};return this.requiredFields.filter(t=>!e[t])}_req(e){return this.requiredFields.includes(e)?o``:h}_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}
@@ -1293,13 +1293,13 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:" 0} - title=${this._missingRequired.length?this._missingRequired.map(t=>r("err_required",e).replace("{field}",r(Me[t]??t,e))).join(" \xB7 "):""} + title=${this._missingRequired.length?this._missingRequired.map(t=>r("err_required",e).replace("{field}",r(ze[t]??t,e))).join(" \xB7 "):""} > ${this._loading?r("completing",e):r("complete",e)}
- `}};$.styles=[Qt,E` + `}};$.styles=[Zt,E` .req-mark { color: var(--error-color, #f44336); margin-left: 2px; @@ -1451,7 +1451,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:" color: var(--text-primary-color, #fff); border-color: var(--primary-color); } - `],l([y({attribute:!1})],$.prototype,"hass",2),l([y()],$.prototype,"entryId",2),l([y()],$.prototype,"taskId",2),l([y()],$.prototype,"taskName",2),l([y()],$.prototype,"lang",2),l([y({type:Array})],$.prototype,"checklist",2),l([y({type:Boolean})],$.prototype,"adaptiveEnabled",2),l([y()],$.prototype,"taskType",2),l([y()],$.prototype,"readingUnit",2),l([y({attribute:!1})],$.prototype,"restockDefault",2),l([y({attribute:!1})],$.prototype,"parts",2),l([y({attribute:!1})],$.prototype,"consumesParts",2),l([y({type:Array})],$.prototype,"consumesInfo",2),l([y({type:Array})],$.prototype,"requiredFields",2),l([p()],$.prototype,"_open",2),l([p()],$.prototype,"_notes",2),l([p()],$.prototype,"_cost",2),l([p()],$.prototype,"_duration",2),l([p()],$.prototype,"_loading",2),l([p()],$.prototype,"_error",2),l([p()],$.prototype,"_checklistState",2),l([p()],$.prototype,"_feedback",2),l([p()],$.prototype,"_photoDocId",2),l([p()],$.prototype,"_photoPreview",2),l([p()],$.prototype,"_photoUploading",2),l([p()],$.prototype,"_readingValue",2),l([p()],$.prototype,"_restockQty",2),l([p()],$.prototype,"_usedParts",2);customElements.get("maintenance-complete-dialog")||customElements.define("maintenance-complete-dialog",$)});var R,ot=w(()=>{"use strict";I();M();R=class extends k{constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1}_onInput(e){let t=e.target.value;this.value=t,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:t}}))}render(){return o` + `],l([b({attribute:!1})],$.prototype,"hass",2),l([b()],$.prototype,"entryId",2),l([b()],$.prototype,"taskId",2),l([b()],$.prototype,"taskName",2),l([b()],$.prototype,"lang",2),l([b({type:Array})],$.prototype,"checklist",2),l([b({type:Boolean})],$.prototype,"adaptiveEnabled",2),l([b()],$.prototype,"taskType",2),l([b()],$.prototype,"readingUnit",2),l([b({attribute:!1})],$.prototype,"restockDefault",2),l([b({attribute:!1})],$.prototype,"parts",2),l([b({attribute:!1})],$.prototype,"consumesParts",2),l([b({type:Array})],$.prototype,"consumesInfo",2),l([b({type:Array})],$.prototype,"requiredFields",2),l([p()],$.prototype,"_open",2),l([p()],$.prototype,"_notes",2),l([p()],$.prototype,"_cost",2),l([p()],$.prototype,"_duration",2),l([p()],$.prototype,"_loading",2),l([p()],$.prototype,"_error",2),l([p()],$.prototype,"_checklistState",2),l([p()],$.prototype,"_feedback",2),l([p()],$.prototype,"_photoDocId",2),l([p()],$.prototype,"_photoPreview",2),l([p()],$.prototype,"_photoUploading",2),l([p()],$.prototype,"_readingValue",2),l([p()],$.prototype,"_restockQty",2),l([p()],$.prototype,"_usedParts",2),l([b({attribute:!1})],$.prototype,"checklistPrefill",2);customElements.get("maintenance-complete-dialog")||customElements.define("maintenance-complete-dialog",$)});var R,ot=w(()=>{"use strict";I();z();R=class extends k{constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1}_onInput(e){let t=e.target.value;this.value=t,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:t}}))}render(){return o`