diff --git a/custom_components/maintenance_supporter/__init__.py b/custom_components/maintenance_supporter/__init__.py
index f05aa56f..5c3f801f 100644
--- a/custom_components/maintenance_supporter/__init__.py
+++ b/custom_components/maintenance_supporter/__init__.py
@@ -3,7 +3,9 @@
from __future__ import annotations
import logging
+from collections.abc import Awaitable, Callable
from dataclasses import dataclass
+from datetime import date as date_cls
from datetime import timedelta
from typing import Any
@@ -18,7 +20,7 @@ from homeassistant.core import (
SupportsResponse,
callback,
)
-from homeassistant.exceptions import ServiceValidationError
+from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
)
@@ -34,6 +36,7 @@ from homeassistant.helpers.event import (
)
from homeassistant.helpers.start import async_at_started
from homeassistant.helpers.typing import ConfigType
+from homeassistant.util import dt as dt_util
from .const import (
BATTERY_FLEET_OBJECT_FLAG,
@@ -131,7 +134,7 @@ type MaintenanceSupporterConfigEntry = ConfigEntry[MaintenanceSupporterData]
# --- Service Schemas ---
SERVICE_COMPLETE_SCHEMA = vol.Schema(
{
- vol.Required(ATTR_ENTITY_ID): cv.entity_id,
+ vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional("notes"): vol.All(cv.string, vol.Length(max=2000)),
vol.Optional("cost"): vol.All(vol.Coerce(float), vol.Range(min=0, max=MAX_COST)),
vol.Optional("duration"): vol.All(vol.Coerce(int), vol.Range(min=0, max=MAX_DURATION_MINUTES)),
@@ -149,14 +152,14 @@ SERVICE_COMPLETE_SCHEMA = vol.Schema(
SERVICE_RESET_SCHEMA = vol.Schema(
{
- vol.Required(ATTR_ENTITY_ID): cv.entity_id,
+ vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional("date"): cv.date,
}
)
SERVICE_SKIP_SCHEMA = vol.Schema(
{
- vol.Required(ATTR_ENTITY_ID): cv.entity_id,
+ vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional("reason"): vol.All(cv.string, vol.Length(max=2000)),
}
)
@@ -451,9 +454,8 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
)
return str(user_id)
- async def _handle_complete(call: ServiceCall) -> None:
- """Handle the complete service call."""
- entity_id = call.data[ATTR_ENTITY_ID]
+ def _resolve_task_target(entity_id: str) -> tuple[MaintenanceCoordinator, str]:
+ """entity_id -> (coordinator, task_id), with the classic errors."""
coordinator = _get_coordinator_for_entity(hass, entity_id)
if coordinator is None:
raise ServiceValidationError(
@@ -468,68 +470,94 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
translation_key="no_task_for_entity",
translation_placeholders={"entity_id": entity_id},
)
+ return coordinator, task_id
+
+ async def _run_per_task(
+ action: Callable[[MaintenanceCoordinator, str], Awaitable[None]],
+ entity_ids: list[str],
+ label: str,
+ ) -> None:
+ """#158: one or many task sensors. A single entity keeps today's
+ exact error shape; a list runs sequentially best-effort — the
+ choke-point refusals (too_early, tag_scan_required,
+ completion_details_required, the skip lock, ...) hit per task, are
+ collected, and never block the other tasks. Partial success stands
+ and the summary error names what was refused."""
+ ids = list(dict.fromkeys(entity_ids))
+ if len(ids) == 1:
+ coordinator, task_id = _resolve_task_target(ids[0])
+ await action(coordinator, task_id)
+ return
+ done = 0
+ errors: list[str] = []
+ for eid in ids:
+ try:
+ coordinator, task_id = _resolve_task_target(eid)
+ await action(coordinator, task_id)
+ done += 1
+ except HomeAssistantError as err:
+ errors.append(f"{eid}: {err}")
+ if errors:
+ raise ServiceValidationError(
+ f"{label}: {done} of {len(ids)} succeeded; " + " | ".join(errors)
+ )
+
+ async def _handle_complete(call: ServiceCall) -> None:
+ """Handle the complete service call (one or many task sensors, #158)."""
+ entity_ids = call.data[ATTR_ENTITY_ID]
+ if len(dict.fromkeys(entity_ids)) > 1:
+ # Per-task values must not fan out: a reading belongs to ONE
+ # meter, cost/duration would multiply into the budget and the
+ # averages, and one physical scan proves exactly one tag.
+ for field in ("reading_value", "cost", "duration", "via_tag_scan"):
+ if call.data.get(field) is not None:
+ raise ServiceValidationError(
+ f"'{field}' can only be used with a single entity_id"
+ )
# #128: explicit person beats the call context; the context covers the
# common case for free (a dashboard tap propagates the tapping user).
if call.data.get("completed_by"):
completed_by: str | None = _resolve_person_user_id(call.data["completed_by"])
else:
completed_by = call.context.user_id if call.context else None
- await coordinator.complete_maintenance(
- task_id=task_id,
- notes=call.data.get("notes"),
- cost=call.data.get("cost"),
- duration=call.data.get("duration"),
- reading_value=call.data.get("reading_value"),
- completed_by=completed_by,
- completed_at=call.data.get("completed_at"),
- # Proof of presence: an automation that reacted to the physical
- # scan itself passes via_tag_scan to satisfy require_tag_scan.
- tag_verified=bool(call.data.get("via_tag_scan")),
- )
+
+ async def _one(coordinator: MaintenanceCoordinator, task_id: str) -> None:
+ await coordinator.complete_maintenance(
+ task_id=task_id,
+ notes=call.data.get("notes"),
+ cost=call.data.get("cost"),
+ duration=call.data.get("duration"),
+ reading_value=call.data.get("reading_value"),
+ completed_by=completed_by,
+ completed_at=call.data.get("completed_at"),
+ # Proof of presence: an automation that reacted to the physical
+ # scan itself passes via_tag_scan to satisfy require_tag_scan.
+ tag_verified=bool(call.data.get("via_tag_scan")),
+ )
+
+ await _run_per_task(_one, entity_ids, "complete")
async def _handle_reset(call: ServiceCall) -> None:
- """Handle the reset service call."""
- entity_id = call.data[ATTR_ENTITY_ID]
- coordinator = _get_coordinator_for_entity(hass, entity_id)
- if coordinator is None:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="no_coordinator_for_entity",
- translation_placeholders={"entity_id": entity_id},
+ """Handle the reset service call (one or many task sensors, #158)."""
+
+ async def _one(coordinator: MaintenanceCoordinator, task_id: str) -> None:
+ await coordinator.reset_maintenance(
+ task_id=task_id,
+ date=call.data.get("date"),
)
- task_id = _get_task_id_for_entity(hass, entity_id)
- if task_id is None:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="no_task_for_entity",
- translation_placeholders={"entity_id": entity_id},
- )
- await coordinator.reset_maintenance(
- task_id=task_id,
- date=call.data.get("date"),
- )
+
+ await _run_per_task(_one, call.data[ATTR_ENTITY_ID], "reset")
async def _handle_skip(call: ServiceCall) -> None:
- """Handle the skip service call."""
- entity_id = call.data[ATTR_ENTITY_ID]
- coordinator = _get_coordinator_for_entity(hass, entity_id)
- if coordinator is None:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="no_coordinator_for_entity",
- translation_placeholders={"entity_id": entity_id},
+ """Handle the skip service call (one or many task sensors, #158)."""
+
+ async def _one(coordinator: MaintenanceCoordinator, task_id: str) -> None:
+ await coordinator.skip_maintenance(
+ task_id=task_id,
+ reason=call.data.get("reason"),
)
- task_id = _get_task_id_for_entity(hass, entity_id)
- if task_id is None:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="no_task_for_entity",
- translation_placeholders={"entity_id": entity_id},
- )
- await coordinator.skip_maintenance(
- task_id=task_id,
- reason=call.data.get("reason"),
- )
+
+ await _run_per_task(_one, call.data[ATTR_ENTITY_ID], "skip")
async def _handle_export(call: ServiceCall) -> None:
"""Handle the export_data service call."""
@@ -689,6 +717,21 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
continue
if wanted_status and status != wanted_status:
continue
+ # #151: last completion + live trigger reading, so
+ # notification automations don't need extra lookups.
+ lp = task.get("last_performed")
+ days_since: int | None = None
+ if lp:
+ try:
+ days_since = (dt_util.now().date() - date_cls.fromisoformat(str(lp)[:10])).days
+ except ValueError:
+ days_since = None
+ tc = task.get("trigger_config") or {}
+ trigger_unit: str | None = None
+ if tc.get("entity_id"):
+ st = hass.states.get(tc["entity_id"])
+ if st:
+ trigger_unit = st.attributes.get("unit_of_measurement")
tasks.append(
{
"entry_id": ce.entry_id,
@@ -700,6 +743,10 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
"days_until_due": task.get("_days_until_due"),
"type": task.get("type"),
"priority": task.get("priority", "normal"),
+ "last_performed": lp,
+ "days_since_last_completed": days_since,
+ "trigger_current_value": task.get("_trigger_current_value"),
+ "trigger_unit": trigger_unit,
}
)
return {"tasks": tasks, "count": len(tasks)}
@@ -1484,12 +1531,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
except Exception:
_LOGGER.exception("Battery-fleet part reconcile at start failed")
return
- if result["added"] or result["pruned"]:
+ if result["added"] or result["pruned"] or result["orphans_removed"]:
_LOGGER.info(
- "Battery-fleet parts reconciled at start: added %s, pruned legacy UNKNOWN: %s",
+ "Battery-fleet parts reconciled at start: added %s, pruned legacy UNKNOWN: %s, orphaned stock sensors removed: %s",
result["added"] or "none",
result["pruned"],
+ result["orphans_removed"],
)
+ if result.get("trigger_healed"):
+ # #156: the recovery flag only reaches the LIVE trigger
+ # after a reload — do it once; the next start finds it set.
+ _LOGGER.info("Battery-fleet trigger healed (auto_complete_on_recovery) — reloading %s", entry.title)
+ hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
entry.async_on_unload(async_at_started(hass, _fleet_parts_at_start))
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/cs.json b/custom_components/maintenance_supporter/assist_sentences/responses/cs.json
index 1c39d922..3a006b28 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/cs.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/cs.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Řekni mi, o kolik ho mám odložit — třeba o pět dní.",
"postpone_past": "Tím by '{task}' spadl do minulosti.",
"skipped": "Tento cyklus úkolu '{task}' u zařízení {object} je přeskočený. Další termín je {date}.",
+ "skip_disabled": "Přeskakování je pro '{task}' vypnuto.",
"not_found": "Nenašel jsem úkol údržby, který by odpovídal '{name}'.",
"ambiguous": "Tomu odpovídá víc úkolů: {candidates}. Upřesni to, prosím.",
"too_early": "Úkol '{task}' se dá dokončit až blíž k termínu.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/da.json b/custom_components/maintenance_supporter/assist_sentences/responses/da.json
index 88c4d3f2..64f87c5c 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/da.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/da.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Sig, hvor langt jeg skal udskyde den — for eksempel fem dage.",
"postpone_past": "Så ville '{task}' ligge i fortiden.",
"skipped": "Jeg har sprunget denne omgang af '{task}' på {object} over. Næste omgang skal laves den {date}.",
+ "skip_disabled": "Spring over er slået fra for '{task}'.",
"not_found": "Jeg kunne ikke finde en vedligeholdelsesopgave, der passer på '{name}'.",
"ambiguous": "Det passer på flere opgaver: {candidates}. Vær lige lidt mere præcis.",
"too_early": "'{task}' kan først markeres som udført tættere på forfaldsdatoen.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/de.json b/custom_components/maintenance_supporter/assist_sentences/responses/de.json
index 49317fd3..f27a3e63 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/de.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/de.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Sag mir, um wie lange ich verschieben soll — zum Beispiel um fünf Tage.",
"postpone_past": "Damit läge '{task}' in der Vergangenheit.",
"skipped": "Dieser Durchgang von '{task}' an {object} wurde übersprungen. Der nächste ist am {date} fällig.",
+ "skip_disabled": "Überspringen ist für '{task}' ausgeschaltet.",
"not_found": "Ich habe keine Wartungsaufgabe zu '{name}' gefunden.",
"ambiguous": "Das passt auf mehrere Aufgaben: {candidates}. Bitte formuliere genauer.",
"too_early": "'{task}' kann erst näher am Fälligkeitstermin erledigt werden.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/en.json b/custom_components/maintenance_supporter/assist_sentences/responses/en.json
index df20fdb1..795bcea0 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/en.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/en.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Tell me how far to postpone it — for example, by five days.",
"postpone_past": "That would put '{task}' in the past.",
"skipped": "Skipped this cycle of '{task}' on {object}. The next one is due {date}.",
+ "skip_disabled": "Skipping is turned off for '{task}'.",
"not_found": "I couldn't find a maintenance task matching '{name}'.",
"ambiguous": "That matches several tasks: {candidates}. Please be more specific.",
"too_early": "'{task}' can only be completed closer to its due date.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/es.json b/custom_components/maintenance_supporter/assist_sentences/responses/es.json
index 9cbe0ddf..e786acc2 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/es.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/es.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Dime cuánto quieres aplazarlo — por ejemplo, cinco días.",
"postpone_past": "Eso dejaría '{task}' en el pasado.",
"skipped": "Me he saltado este ciclo de '{task}' en {object}. El siguiente toca el {date}.",
+ "skip_disabled": "Omitir está desactivado para '{task}'.",
"not_found": "No he encontrado ninguna tarea de mantenimiento que coincida con '{name}'.",
"ambiguous": "Eso coincide con varias tareas: {candidates}. Concreta un poco más, por favor.",
"too_early": "'{task}' solo se puede completar cuando se acerque su fecha prevista.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/fi.json b/custom_components/maintenance_supporter/assist_sentences/responses/fi.json
index 7552f227..4933b203 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/fi.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/fi.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Kerro, kuinka paljon siirrän sitä — esimerkiksi viisi päivää.",
"postpone_past": "Silloin tehtävä '{task}' menisi menneisyyteen.",
"skipped": "Ohitin tehtävän '{task}' tämän kerran kohteessa {object}. Seuraava ajankohta: {date}.",
+ "skip_disabled": "Ohittaminen on poistettu käytöstä kohteelle '{task}'.",
"not_found": "En löytänyt huoltotehtävää, joka vastaisi hakua '{name}'.",
"ambiguous": "Tämä vastaa useaa tehtävää: {candidates}. Tarkenna vielä hieman.",
"too_early": "Tehtävän '{task}' voi kuitata tehdyksi vasta lähempänä sen ajankohtaa.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/fr.json b/custom_components/maintenance_supporter/assist_sentences/responses/fr.json
index 0dfceaa6..d26b11c3 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/fr.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/fr.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Dis-moi de combien je dois le reporter — par exemple, de cinq jours.",
"postpone_past": "Ça mettrait '{task}' dans le passé.",
"skipped": "J'ai sauté ce cycle de '{task}' sur {object}. Le prochain est à faire le {date}.",
+ "skip_disabled": "Ignorer est désactivé pour '{task}'.",
"not_found": "Je n'ai pas trouvé de tâche d'entretien qui corresponde à '{name}'.",
"ambiguous": "Ça correspond à plusieurs tâches : {candidates}. Sois un peu plus précis, s'il te plaît.",
"too_early": "Tu ne pourras valider '{task}' que plus près de sa date d'échéance.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/hi.json b/custom_components/maintenance_supporter/assist_sentences/responses/hi.json
index abfb8ddd..ece55eda 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/hi.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/hi.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "बताइए कितना टालना है — जैसे पाँच दिन।",
"postpone_past": "इससे '{task}' की तारीख़ पिछली हो जाएगी।",
"skipped": "{object} पर '{task}' का यह चक्र छोड़ दिया है। अगली बार {date} को करना है।",
+ "skip_disabled": "'{task}' के लिए छोड़ना बंद है।",
"not_found": "'{name}' से मिलता-जुलता कोई मेंटेनेंस काम नहीं मिला।",
"ambiguous": "इससे कई काम मेल खाते हैं: {candidates}। थोड़ा और साफ़ बताइए।",
"too_early": "'{task}' को तय तारीख़ के क़रीब आने पर ही पूरा किया जा सकता है।",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/hu.json b/custom_components/maintenance_supporter/assist_sentences/responses/hu.json
index 93ea125b..9146c3ec 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/hu.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/hu.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Mondd meg, mennyivel halasszam el — például öt nappal.",
"postpone_past": "'{task}': ezzel a feladat a múltba kerülne.",
"skipped": "Kihagytam ezt a fordulót: '{task}' – {object}. A következő időpont: {date}.",
+ "skip_disabled": "A kihagyás ki van kapcsolva ehhez: '{task}'.",
"not_found": "Nem találtam ehhez illő karbantartási feladatot: '{name}'.",
"ambiguous": "Több feladatra is illik: {candidates}. Fogalmazz pontosabban.",
"too_early": "'{task}': ezt a feladatot csak az esedékesség közelében lehet elvégezni.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/it.json b/custom_components/maintenance_supporter/assist_sentences/responses/it.json
index a0ff3cb3..8bdf624b 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/it.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/it.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Dimmi di quanto rimandarla — per esempio, di cinque giorni.",
"postpone_past": "Così '{task}' finirebbe nel passato.",
"skipped": "Ho saltato questo ciclo di '{task}' su {object}. Il prossimo è previsto per il {date}.",
+ "skip_disabled": "Saltare è disattivato per '{task}'.",
"not_found": "Non ho trovato nessuna attività di manutenzione che corrisponda a '{name}'.",
"ambiguous": "Corrisponde a più attività: {candidates}. Puoi essere un po' più preciso?",
"too_early": "Puoi completare '{task}' solo quando si avvicina la scadenza.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/ja.json b/custom_components/maintenance_supporter/assist_sentences/responses/ja.json
index 239ae8e5..8b4dd523 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/ja.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/ja.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "どのくらい先に延ばすか教えてください。たとえば「5日」のように言ってください。",
"postpone_past": "それだと「{task}」の日付が過去になってしまいます。",
"skipped": "{object}の「{task}」の今回分をスキップしました。次回は{date}が期限です。",
+ "skip_disabled": "'{task}'のスキップは無効になっています。",
"not_found": "「{name}」に一致するメンテナンスタスクは見つかりませんでした。",
"ambiguous": "いくつかのタスクが当てはまります:{candidates}。もう少し詳しく言ってください。",
"too_early": "「{task}」は期限が近づいてからでないと完了にできません。",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/ko.json b/custom_components/maintenance_supporter/assist_sentences/responses/ko.json
index 691442d0..0252226b 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/ko.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/ko.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "얼마나 미룰지 알려주세요. 예를 들어 '닷새 뒤'처럼요.",
"postpone_past": "그러면 '{task}' 날짜가 과거가 돼요.",
"skipped": "{object}의 '{task}', 이번 차례는 건너뛰었어요. 다음은 {date}에 예정돼 있어요.",
+ "skip_disabled": "'{task}'의 건너뛰기가 꺼져 있습니다.",
"not_found": "'{name}'에 맞는 정비 작업을 찾지 못했어요.",
"ambiguous": "여러 작업이 해당돼요: {candidates}. 조금 더 구체적으로 말씀해 주세요.",
"too_early": "'{task}' 작업은 예정일이 가까워져야 완료할 수 있어요.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/nb.json b/custom_components/maintenance_supporter/assist_sentences/responses/nb.json
index 15dec171..78392c34 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/nb.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/nb.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Si hvor langt jeg skal utsette den — for eksempel fem dager.",
"postpone_past": "Da ville '{task}' havne i fortiden.",
"skipped": "Jeg hoppet over denne runden av '{task}' på {object}. Neste runde skal gjøres {date}.",
+ "skip_disabled": "Hopp over er slått av for '{task}'.",
"not_found": "Jeg fant ingen vedlikeholdsoppgave som passer med '{name}'.",
"ambiguous": "Det passer med flere oppgaver: {candidates}. Vær litt mer presis.",
"too_early": "'{task}' kan først krysses av nærmere forfallsdatoen.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/nl.json b/custom_components/maintenance_supporter/assist_sentences/responses/nl.json
index 8049d1df..7c30e945 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/nl.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/nl.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Zeg even hoe lang ik het moet uitstellen — bijvoorbeeld vijf dagen.",
"postpone_past": "Dan zou '{task}' in het verleden liggen.",
"skipped": "Ik heb deze ronde van '{task}' bij {object} overgeslagen. De volgende staat op {date}.",
+ "skip_disabled": "Overslaan is uitgeschakeld voor '{task}'.",
"not_found": "Ik kon geen onderhoudstaak vinden die past bij '{name}'.",
"ambiguous": "Dat past bij meerdere taken: {candidates}. Kun je het iets preciezer zeggen?",
"too_early": "'{task}' kun je pas afvinken als de datum dichterbij is.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/pl.json b/custom_components/maintenance_supporter/assist_sentences/responses/pl.json
index e64f6188..13606fba 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/pl.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/pl.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Powiedz mi, o ile mam przełożyć — na przykład o pięć dni.",
"postpone_past": "Wtedy '{task}' wypadłoby w przeszłości.",
"skipped": "Ten cykl zadania '{task}' na urządzeniu {object} został pominięty. Następny termin to {date}.",
+ "skip_disabled": "Pomijanie jest wyłączone dla '{task}'.",
"not_found": "Nie znalazłem zadania konserwacyjnego pasującego do '{name}'.",
"ambiguous": "To pasuje do kilku zadań: {candidates}. Doprecyzuj, proszę.",
"too_early": "Zadanie '{task}' można wykonać dopiero bliżej terminu.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json b/custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json
index 4b183452..7b4f2df4 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/pt-br.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Me diga por quanto tempo adiar — por exemplo, cinco dias.",
"postpone_past": "Isso colocaria '{task}' no passado.",
"skipped": "Pulei este ciclo de '{task}' em {object}. O próximo vence em {date}.",
+ "skip_disabled": "Pular está desativado para '{task}'.",
"not_found": "Não encontrei nenhuma tarefa de manutenção que corresponda a '{name}'.",
"ambiguous": "Isso corresponde a várias tarefas: {candidates}. Seja mais específico, por favor.",
"too_early": "'{task}' só pode ser concluída mais perto da data de vencimento.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/pt.json b/custom_components/maintenance_supporter/assist_sentences/responses/pt.json
index 1a6855cf..6f5d491f 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/pt.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/pt.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Diz-me quanto tempo queres adiar — por exemplo, cinco dias.",
"postpone_past": "Assim '{task}' ficaria no passado.",
"skipped": "Saltei este ciclo de '{task}' em {object}. O próximo está previsto para {date}.",
+ "skip_disabled": "Ignorar está desativado para '{task}'.",
"not_found": "Não encontrei nenhuma tarefa de manutenção que corresponda a '{name}'.",
"ambiguous": "Isso corresponde a várias tarefas: {candidates}. Podes ser um pouco mais específico?",
"too_early": "Só podes concluir '{task}' mais perto da data prevista.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/ru.json b/custom_components/maintenance_supporter/assist_sentences/responses/ru.json
index ddbcceb5..6bcedd82 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/ru.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/ru.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Скажи, на сколько перенести — например, на пять дней.",
"postpone_past": "Тогда '{task}' окажется в прошлом.",
"skipped": "Этот цикл задачи '{task}' у устройства {object} пропущен. Следующий срок — {date}.",
+ "skip_disabled": "Пропуск отключён для '{task}'.",
"not_found": "Я не нашёл задачу обслуживания, подходящую под '{name}'.",
"ambiguous": "Под это подходит несколько задач: {candidates}. Уточни, пожалуйста.",
"too_early": "Задачу '{task}' можно выполнить только ближе к сроку.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/sv.json b/custom_components/maintenance_supporter/assist_sentences/responses/sv.json
index f52f8d8f..e6aa21f3 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/sv.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/sv.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Säg hur långt jag ska skjuta upp den — till exempel fem dagar.",
"postpone_past": "Då skulle '{task}' hamna i det förflutna.",
"skipped": "Jag har hoppat över den här omgången av '{task}' på {object}. Nästa omgång ska göras den {date}.",
+ "skip_disabled": "Hoppa över är avstängt för '{task}'.",
"not_found": "Jag hittade ingen underhållsuppgift som matchar '{name}'.",
"ambiguous": "Det matchar flera uppgifter: {candidates}. Var lite mer specifik.",
"too_early": "'{task}' går att bocka av först närmare förfallodatumet.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/tr.json b/custom_components/maintenance_supporter/assist_sentences/responses/tr.json
index 0830e77e..67ab1a8a 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/tr.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/tr.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Ne kadar erteleyeceğimi söyle — örneğin beş gün.",
"postpone_past": "Bu durumda '{task}' görevi geçmişte kalır.",
"skipped": "{object} için '{task}' görevinin bu turu atlandı. Sıradaki {date} tarihinde.",
+ "skip_disabled": "'{task}' için atlama kapalı.",
"not_found": "'{name}' ile eşleşen bir bakım görevi bulamadım.",
"ambiguous": "Birden fazla göreve uyuyor: {candidates}. Biraz daha açık söyler misin?",
"too_early": "'{task}' görevi ancak son tarihine yaklaşınca tamamlanabilir.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/uk.json b/custom_components/maintenance_supporter/assist_sentences/responses/uk.json
index 944520f3..aefb2af9 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/uk.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/uk.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "Скажи, на скільки перенести — наприклад, на п’ять днів.",
"postpone_past": "Тоді '{task}' опиниться в минулому.",
"skipped": "Цей цикл завдання '{task}' на пристрої {object} пропущено. Наступний термін — {date}.",
+ "skip_disabled": "Пропуск вимкнено для '{task}'.",
"not_found": "Я не знайшов завдання з обслуговування, яке відповідає '{name}'.",
"ambiguous": "Цьому відповідає кілька завдань: {candidates}. Уточни, будь ласка.",
"too_early": "Завдання '{task}' можна виконати лише ближче до терміну.",
diff --git a/custom_components/maintenance_supporter/assist_sentences/responses/zh.json b/custom_components/maintenance_supporter/assist_sentences/responses/zh.json
index ad2a4906..fbf3fe53 100644
--- a/custom_components/maintenance_supporter/assist_sentences/responses/zh.json
+++ b/custom_components/maintenance_supporter/assist_sentences/responses/zh.json
@@ -11,6 +11,7 @@
"postpone_needs_when": "告诉我要推迟多久,比如推迟五天。",
"postpone_past": "那样的话,“{task}”的日期就会落在过去了。",
"skipped": "已跳过{object}的“{task}”这一次。下一次在{date}到期。",
+ "skip_disabled": "已为'{task}'关闭跳过。",
"not_found": "我没有找到和“{name}”对得上的维护任务。",
"ambiguous": "有好几项任务都对得上:{candidates}。请说得更具体一些。",
"too_early": "“{task}”要等到接近到期日才能完成。",
diff --git a/custom_components/maintenance_supporter/config_flow_trigger.py b/custom_components/maintenance_supporter/config_flow_trigger.py
index 9d508ed8..c35e77e6 100644
--- a/custom_components/maintenance_supporter/config_flow_trigger.py
+++ b/custom_components/maintenance_supporter/config_flow_trigger.py
@@ -353,6 +353,14 @@ class TriggerConfigMixin:
if "trigger_baseline_value" in prev_tc
else {}
),
+ # #149: the session cap is panel-managed (the flow grew no
+ # field) — carry it so an options-flow trigger edit doesn't
+ # silently drop it.
+ **(
+ {"trigger_runtime_max_session_seconds": prev_tc["trigger_runtime_max_session_seconds"]}
+ if "trigger_runtime_max_session_seconds" in prev_tc
+ else {}
+ ),
}
return await next_step()
diff --git a/custom_components/maintenance_supporter/coordinator.py b/custom_components/maintenance_supporter/coordinator.py
index a4716d4a..3ca58e67 100644
--- a/custom_components/maintenance_supporter/coordinator.py
+++ b/custom_components/maintenance_supporter/coordinator.py
@@ -1436,6 +1436,13 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
_LOGGER.error("Task %s not found in entry %s", task_id, self.entry.title)
return
+ # #150: the per-task skip lock is enforced HERE — the choke point
+ # every surface funnels through (WS, voice, vacation preview).
+ if merged[task_id].get("allow_skip") is False:
+ raise ServiceValidationError(
+ f"Skipping is disabled for task {merged[task_id].get('name', task_id)!r}"
+ )
+
task = MaintenanceTask.from_dict(merged[task_id])
# Skipping an already-overdue task means it lapsed → record it as MISSED
# (not a deliberate skip). An explicit as_missed=True always wins.
diff --git a/custom_components/maintenance_supporter/entity/triggers/__init__.py b/custom_components/maintenance_supporter/entity/triggers/__init__.py
index 0f256742..2439792e 100644
--- a/custom_components/maintenance_supporter/entity/triggers/__init__.py
+++ b/custom_components/maintenance_supporter/entity/triggers/__init__.py
@@ -100,6 +100,10 @@ def _inject_per_entity_state(config: dict[str, Any], entity_state: dict[str, Any
config["trigger_accumulated_seconds"] = entity_state["accumulated_seconds"]
if "on_since" in entity_state:
config["trigger_on_since"] = entity_state["on_since"]
+ # #149: what the current session already booked — the per-session cap
+ # must survive a restart, or a stuck sensor books the cap again.
+ if "session_booked_seconds" in entity_state:
+ config["trigger_session_booked_seconds"] = entity_state["session_booked_seconds"]
elif trigger_type == TriggerType.STATE_CHANGE:
if "change_count" in entity_state:
config["trigger_change_count"] = entity_state["change_count"]
diff --git a/custom_components/maintenance_supporter/entity/triggers/runtime.py b/custom_components/maintenance_supporter/entity/triggers/runtime.py
index ef21dbb1..037c6072 100644
--- a/custom_components/maintenance_supporter/entity/triggers/runtime.py
+++ b/custom_components/maintenance_supporter/entity/triggers/runtime.py
@@ -59,6 +59,18 @@ class RuntimeTrigger(BaseTrigger):
self._target_hours: float = trigger_config.get("trigger_runtime_hours", 100.0)
self._accumulated_seconds: float = trigger_config.get("trigger_accumulated_seconds", 0.0)
+ # #149: optional per-session cap (seconds). A session that exceeds it
+ # contributes exactly the cap — protects against a sensor stuck ON
+ # (lost connection, restart while "running").
+ raw_cap = trigger_config.get("trigger_runtime_max_session_seconds")
+ self._max_session: float | None = (
+ float(raw_cap) if isinstance(raw_cap, (int, float)) and not isinstance(raw_cap, bool) and raw_cap > 0 else None
+ )
+ # Seconds the CURRENT session already booked. Persisted, because the
+ # 5-minute periodic persist resets the on_since window — the cap is
+ # per SESSION, not per window.
+ self._session_booked: float = float(trigger_config.get("trigger_session_booked_seconds", 0.0) or 0.0)
+
# Restore on_since timestamp for restart recovery. parse_persisted_utc
# coerces a naive legacy payload to UTC — dt_util.parse_datetime kept
# it naive here, and `utcnow() - naive` raises TypeError in the
@@ -107,6 +119,7 @@ class RuntimeTrigger(BaseTrigger):
now = dt_util.utcnow()
self._on_since_dt = now
self._on_since = now.isoformat()
+ self._session_booked = 0.0
await self._persist_runtime()
_LOGGER.debug(
"Runtime trigger: entity %s is ON, started tracking from now",
@@ -119,6 +132,7 @@ class RuntimeTrigger(BaseTrigger):
self._accumulate_elapsed()
self._on_since_dt = None
self._on_since = None
+ self._session_booked = 0.0
await self._persist_runtime()
# Register state change listener
@@ -184,6 +198,7 @@ class RuntimeTrigger(BaseTrigger):
now = dt_util.utcnow()
self._on_since_dt = now
self._on_since = now.isoformat()
+ self._session_booked = 0.0
self.hass.async_create_task(self._persist_runtime())
elif not self._is_on(new_val) and self._on_since_dt is not None:
# Restored anchor but the device APPEARS off (deferred setup
@@ -195,6 +210,7 @@ class RuntimeTrigger(BaseTrigger):
self._accumulate_elapsed()
self._on_since_dt = None
self._on_since = None
+ self._session_booked = 0.0
self.hass.async_create_task(self._persist_runtime())
self._update_evaluation()
return
@@ -207,6 +223,7 @@ class RuntimeTrigger(BaseTrigger):
self._accumulate_elapsed()
self._on_since_dt = None
self._on_since = None
+ self._session_booked = 0.0
self.hass.async_create_task(self._persist_runtime())
if not self._logged_unavailable:
_LOGGER.warning(
@@ -234,6 +251,7 @@ class RuntimeTrigger(BaseTrigger):
self._accumulate_elapsed()
self._on_since_dt = None
self._on_since = None
+ self._session_booked = 0.0
self.hass.async_create_task(self._persist_runtime())
_LOGGER.debug(
"Runtime trigger: %s turned OFF (accumulated=%.2fh)",
@@ -245,6 +263,7 @@ class RuntimeTrigger(BaseTrigger):
now = dt_util.utcnow()
self._on_since_dt = now
self._on_since = now.isoformat()
+ self._session_booked = 0.0
self.hass.async_create_task(self._persist_runtime())
_LOGGER.debug(
"Runtime trigger: %s turned ON (tracking started)",
@@ -258,6 +277,7 @@ class RuntimeTrigger(BaseTrigger):
self._accumulate_elapsed()
self._on_since_dt = None
self._on_since = None
+ self._session_booked = 0.0
self.hass.async_create_task(self._persist_runtime())
self._update_evaluation()
@@ -279,12 +299,23 @@ class RuntimeTrigger(BaseTrigger):
return str(state.state)
def _accumulate_elapsed(self, now: datetime | None = None) -> None:
- """Add elapsed time since _on_since to accumulated total."""
+ """Add elapsed time since _on_since to accumulated total.
+
+ With a per-session cap (#149) the whole session contributes at most
+ the cap: ``_session_booked`` remembers what this session booked across
+ the 5-minute persist windows, so a stuck-ON sensor cannot book capped
+ slices window after window.
+ """
if self._on_since_dt is None:
return
now = now or dt_util.utcnow()
elapsed = (now - self._on_since_dt).total_seconds()
+ if elapsed <= 0:
+ return
+ if self._max_session is not None:
+ elapsed = min(elapsed, max(0.0, self._max_session - self._session_booked))
if elapsed > 0:
+ self._session_booked += elapsed
self._accumulated_seconds += elapsed
def _get_current_runtime_hours(self) -> float:
@@ -293,6 +324,8 @@ class RuntimeTrigger(BaseTrigger):
if self._on_since_dt is not None:
elapsed = (dt_util.utcnow() - self._on_since_dt).total_seconds()
if elapsed > 0:
+ if self._max_session is not None:
+ elapsed = min(elapsed, max(0.0, self._max_session - self._session_booked))
total_seconds += elapsed
return total_seconds / 3600.0
@@ -326,6 +359,7 @@ class RuntimeTrigger(BaseTrigger):
data: dict[str, Any] = {
"accumulated_seconds": self._accumulated_seconds,
"on_since": self._on_since,
+ "session_booked_seconds": self._session_booked,
}
await self._coordinator.async_persist_trigger_runtime(
self._task_id,
diff --git a/custom_components/maintenance_supporter/export.py b/custom_components/maintenance_supporter/export.py
index 06f887a8..7a50e2b3 100644
--- a/custom_components/maintenance_supporter/export.py
+++ b/custom_components/maintenance_supporter/export.py
@@ -117,6 +117,8 @@ def _build_export_object(
"custom_icon": tdata.get("custom_icon"),
"nfc_tag_id": tdata.get("nfc_tag_id"),
"require_tag_scan": tdata.get("require_tag_scan"),
+ # #150: per-task skip lock.
+ "allow_skip": tdata.get("allow_skip"),
"responsible_user_id": tdata.get("responsible_user_id"),
"entity_slug": tdata.get("entity_slug"),
"adaptive_config": tdata.get("adaptive_config"),
diff --git a/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts b/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts
index 83cb7c1a..680820c6 100644
--- a/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts
+++ b/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts
@@ -227,6 +227,7 @@ export class MaintenanceTaskDialog extends LitElement {
@state() private _triggerToState = "";
@state() private _triggerTargetChanges = "";
@state() private _triggerRuntimeHours = "";
+ @state() private _triggerRuntimeMaxSession = "";
// Comma-separated "running" states for the runtime trigger (#103) —
// empty = the backend default ["on"]. Must roundtrip on edit: adopted
// tasks ship e.g. ["mowing"], and dropping it on save silently stops
@@ -245,6 +246,7 @@ export class MaintenanceTaskDialog extends LitElement {
@state() private _lastPerformed = "";
@state() private _nfcTagId = "";
@state() private _requireTagScan = false;
+ @state() private _allowSkip = true;
// v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...)
@state() private _readingUnit = "";
/** The picked links, keyed by `partLinkKey` — the (entry_id, part_id) pair,
@@ -400,6 +402,7 @@ export class MaintenanceTaskDialog extends LitElement {
this._lastPerformed = task.last_performed || "";
this._nfcTagId = task.nfc_tag_id || "";
this._requireTagScan = !!task.require_tag_scan;
+ this._allowSkip = task.allow_skip !== false;
this._readingUnit = task.reading_unit || "";
// Whole link, entry_id included — hydrating only part_id would turn every
// shared-pool link into an own-part link on the next save (#111).
@@ -500,6 +503,7 @@ export class MaintenanceTaskDialog extends LitElement {
this._triggerToState = tc.trigger_to_state || "";
this._triggerTargetChanges = tc.trigger_target_changes?.toString() || "";
this._triggerRuntimeHours = tc.trigger_runtime_hours?.toString() || "";
+ this._triggerRuntimeMaxSession = tc.trigger_runtime_max_session_seconds?.toString() || "";
this._triggerOnStates = (tc.trigger_on_states || []).join(", ");
if (tc.type === "compound") {
this._compoundLogic = tc.compound_logic === "OR" ? "OR" : "AND";
@@ -551,6 +555,7 @@ export class MaintenanceTaskDialog extends LitElement {
this._lastPerformed = "";
this._nfcTagId = "";
this._requireTagScan = false;
+ this._allowSkip = true;
this._readingUnit = "";
this._consumesParts = {};
this._responsibleUserId = null;
@@ -610,6 +615,7 @@ export class MaintenanceTaskDialog extends LitElement {
this._triggerToState = "";
this._triggerTargetChanges = "";
this._triggerRuntimeHours = "";
+ this._triggerRuntimeMaxSession = "";
this._triggerOnStates = "";
this._compoundLogic = "AND";
this._compoundConditions = [];
@@ -1253,6 +1259,7 @@ export class MaintenanceTaskDialog extends LitElement {
data.last_performed = this._lastPerformed || null;
data.nfc_tag_id = this._nfcTagId || null;
data.require_tag_scan = this._requireTagScan;
+ data.allow_skip = this._allowSkip;
data.reading_unit = this._readingUnit.trim() || null;
// Task phases (#139): always sent — null clears a removed cycle.
{
@@ -1366,6 +1373,8 @@ export class MaintenanceTaskDialog extends LitElement {
if (this._triggerForMinutes) { const v = parseInt(this._triggerForMinutes, 10); if (!isNaN(v)) triggerConfig.trigger_for_minutes = v; }
} else if (this._triggerType === "runtime") {
if (this._triggerRuntimeHours) { const v = parseFloat(this._triggerRuntimeHours); if (!isNaN(v)) triggerConfig.trigger_runtime_hours = v; }
+ // #149: per-session cap (seconds) against sensors stuck ON.
+ if (this._triggerRuntimeMaxSession) { const v = parseInt(this._triggerRuntimeMaxSession, 10); if (!isNaN(v) && v > 0) triggerConfig.trigger_runtime_max_session_seconds = v; }
const onStates = this._triggerOnStates.split(",").map((s) => s.trim()).filter(Boolean);
if (onStates.length > 0) triggerConfig.trigger_on_states = onStates;
}
@@ -2480,6 +2489,14 @@ export class MaintenanceTaskDialog extends LitElement {
.value=${this._triggerRuntimeHours}
@input=${(e: Event) => (this._triggerRuntimeHours = (e.target as HTMLInputElement).value)}
>
+ (this._triggerRuntimeMaxSession = (e.target as HTMLInputElement).value)}
+ >
+
ctx.toggleMoreMenu()}>
${ctx.moreMenuOpen ? html`
diff --git a/custom_components/maintenance_supporter/frontend-src/styles.ts b/custom_components/maintenance_supporter/frontend-src/styles.ts
index bac45067..3ecc6122 100644
--- a/custom_components/maintenance_supporter/frontend-src/styles.ts
+++ b/custom_components/maintenance_supporter/frontend-src/styles.ts
@@ -908,8 +908,15 @@ export const sharedStyles = css`
display: flex;
flex-direction: column;
gap: 2px;
+ /* #150 follow-up (2026-08-31, maisun's mobile screenshot): the box used
+ to carry min-width: 90px — in the narrow grid the due column is
+ fit-content(100px) and right-aligned, so on cells narrower than 90px
+ the box overhung LEFT across the object name. It now tracks the cell
+ with no floor, and the label ellipsizes inside. */
+ align-self: stretch;
+ max-width: 100%;
+ min-width: 0;
width: 100%;
- min-width: 90px;
}
.trigger-progress-bar {
@@ -930,8 +937,25 @@ export const sharedStyles = css`
font-size: 12px;
color: var(--secondary-text-color);
text-align: right;
+ max-width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
+ /* #150 follow-up: 3-state trend arrow — replaces the sparkline on
+ narrow/tight rows (panel-styles toggles visibility per regime). */
+ .trend-arrow {
+ display: none;
+ font-style: normal;
+ font-weight: 700;
+ margin-left: 2px;
+ }
+ .trend-approaching { color: var(--warning-color, #ff9800); }
+ .trend-stable { color: var(--secondary-text-color); }
+ .trend-easing { color: var(--success-color, #4caf50); }
+
+
/* Days progress bar (detail view) */
.days-progress {
margin: 8px 0 16px;
@@ -974,6 +998,11 @@ export const sharedStyles = css`
/* Mini-sparkline in overview rows */
.mini-sparkline {
width: 60px;
+ /* Same class of bug as the trigger-progress floor (#150 follow-up): in
+ due cells narrower than the fixed width the right-aligned SVG used to
+ overhang LEFT (phone-360 sweep). preserveAspectRatio="none" lets it
+ just squeeze. */
+ max-width: 100%;
height: 20px;
display: block;
margin-top: 2px;
diff --git a/custom_components/maintenance_supporter/frontend-src/types.ts b/custom_components/maintenance_supporter/frontend-src/types.ts
index 90f277f9..5b40e72c 100644
--- a/custom_components/maintenance_supporter/frontend-src/types.ts
+++ b/custom_components/maintenance_supporter/frontend-src/types.ts
@@ -68,6 +68,8 @@ export interface TriggerConfig {
trigger_to_state?: string | null;
trigger_target_changes?: number;
trigger_runtime_hours?: number;
+ /** #149: a single session books at most this many seconds. */
+ trigger_runtime_max_session_seconds?: number;
/** States (or attribute values) that count as "running" — default ["on"]. */
trigger_on_states?: string[];
compound_logic?: "AND" | "OR";
@@ -173,6 +175,8 @@ export interface MaintenanceTask {
name: string;
type: string; // "cleaning" | "inspection" | "replacement" | "calibration" | "service" | "reading" | "custom"
enabled: boolean;
+ /** #150: false = the skip lock — Skip hidden everywhere, server refuses. */
+ allow_skip?: boolean;
schedule_type: string; // "time_based" | "sensor_based" | "one_time" | "manual" | calendar kind
interval_days?: number | null;
interval_unit?: string; // "days" | "weeks" | "months" | "years"
@@ -509,6 +513,8 @@ export interface TaskRow {
archived: boolean;
history: HistoryEntry[];
enabled: boolean;
+ /** #150: false = skip locked for this task. */
+ allow_skip: boolean;
nfc_tag_id: string | null;
priority: string;
labels: string[];
diff --git a/custom_components/maintenance_supporter/frontend/locales/cs.json b/custom_components/maintenance_supporter/frontend/locales/cs.json
index 37ae4ba0..c624b81b 100644
--- a/custom_components/maintenance_supporter/frontend/locales/cs.json
+++ b/custom_components/maintenance_supporter/frontend/locales/cs.json
@@ -528,6 +528,7 @@
"groupby_area": "Podle oblasti",
"groupby_group": "Podle skupiny",
"groupby_user": "Podle uživatele",
+ "groupby_object": "Podle objektu",
"filter_label": "Filtr",
"user_label": "Uživatel",
"photo_label": "Fotografie",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Aktuálně platná počáteční hodnota: {value}",
"runtime_on_states": "Aktivní stavy",
"runtime_on_states_help": "Stavy počítané jako doba běhu — výchozí: on. Např. mowing, cleaning, printing. Při zvoleném atributu se porovnávají jeho hodnoty.",
+ "runtime_max_session": "Max. doba běhu relace (sekundy)",
+ "runtime_max_session_help": "Jeden běh se počítá nejvýše na tolik sekund — chrání před senzorem zaseknutým na ZAP (ztráta spojení, restart). Prázdné = bez limitu.",
+ "trend_approaching": "Blíží se k prahu",
+ "trend_easing": "Vzdaluje se od prahu",
+ "split_select_hint": "Vyberte úkol a zde se zobrazí podrobnosti",
"setups_target_new": "Vytvořit nový: {name}",
"schedule_preview_title": "Nejbližší termíny",
"schedule_preview_ontime": "Za předpokladu včasného dokončení.",
@@ -927,5 +933,7 @@
"history_add_past": "Přidat dřívější dokončení",
"require_tag_scan": "Dokončit pouze naskenováním štítku",
"require_tag_scan_help": "Důkaz přítomnosti: „Hotovo“ je všude blokováno, dokud nenaskenujete NFC štítek nebo QR kód na zařízení. Automatizace mohou službě complete předat 'via_tag_scan'.",
+ "disallow_skip": "Nepovolit přeskočení",
+ "disallow_skip_help": "Akce Přeskočit zmizí ze všech ploch a server odmítne přeskočení i z automatizací a hlasu.",
"require_tag_scan_hint": "Tento úkol lze dokončit pouze naskenováním jeho NFC štítku nebo QR kódu na zařízení — uložení zde bude odmítnuto."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/da.json b/custom_components/maintenance_supporter/frontend/locales/da.json
index 08522fd4..ac127415 100644
--- a/custom_components/maintenance_supporter/frontend/locales/da.json
+++ b/custom_components/maintenance_supporter/frontend/locales/da.json
@@ -530,6 +530,7 @@
"groupby_area": "Efter område",
"groupby_group": "Efter gruppe",
"groupby_user": "Efter bruger",
+ "groupby_object": "Efter objekt",
"filter_label": "Filter",
"user_label": "Bruger",
"photo_label": "Foto",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Aktuelt gældende startværdi: {value}",
"runtime_on_states": "Aktive tilstande",
"runtime_on_states_help": "Tilstande der tæller som driftstid — standard: on. F.eks. mowing, cleaning, printing. Med en valgt attribut sammenlignes dens værdier i stedet.",
+ "runtime_max_session": "Maks. sessionstid (sekunder)",
+ "runtime_max_session_help": "En enkelt kørsel tæller højst så mange sekunder — beskytter mod en sensor, der hænger på TÆNDT (tabt forbindelse, genstart). Tom = ingen grænse.",
+ "trend_approaching": "Nærmer sig tærsklen",
+ "trend_easing": "Bevæger sig væk fra tærsklen",
+ "split_select_hint": "Vælg en opgave for at se detaljerne her",
"setups_target_new": "Opret ny: {name}",
"schedule_preview_title": "Kommende datoer",
"schedule_preview_ontime": "Forudsat rettidig fuldførelse.",
@@ -927,5 +933,7 @@
"history_add_past": "Tilføj tidligere udførelse",
"require_tag_scan": "Kan kun udføres ved tag-scanning",
"require_tag_scan_help": "Tilstedeværelsesbevis: „Udført“ er blokeret overalt, indtil NFC-tagget eller QR-koden på selve tingen er scannet. Automatiseringer kan give 'via_tag_scan' til complete-tjenesten.",
+ "disallow_skip": "Tillad ikke at springe over",
+ "disallow_skip_help": "Handlingen Spring over forsvinder fra alle flader, og serveren afviser også spring fra automatiseringer og stemme.",
"require_tag_scan_hint": "Denne opgave udføres kun ved at scanne dens NFC-tag eller QR-kode på selve tingen — at gemme her vil blive afvist."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/de.json b/custom_components/maintenance_supporter/frontend/locales/de.json
index 65f6b5ed..04f6d679 100644
--- a/custom_components/maintenance_supporter/frontend/locales/de.json
+++ b/custom_components/maintenance_supporter/frontend/locales/de.json
@@ -530,6 +530,7 @@
"groupby_area": "Nach Bereich",
"groupby_group": "Nach Gruppe",
"groupby_user": "Nach Verantwortlichem",
+ "groupby_object": "Nach Objekt",
"filter_label": "Filter",
"user_label": "Benutzer",
"photo_label": "Foto",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Aktuell wirksamer Startwert: {value}",
"runtime_on_states": "Aktive Zustände",
"runtime_on_states_help": "Zustände, die als Laufzeit zählen — Standard: on. Z. B. mowing, cleaning, printing. Bei gewähltem Attribut werden dessen Werte verglichen.",
+ "runtime_max_session": "Maximale Sitzungslaufzeit (Sekunden)",
+ "runtime_max_session_help": "Ein einzelner Lauf zählt höchstens so viele Sekunden — schützt vor Sensoren, die auf AN hängen bleiben (Verbindungsabriss, Neustart). Leer = keine Begrenzung.",
+ "trend_approaching": "Läuft auf die Schwelle zu",
+ "trend_easing": "Entfernt sich von der Schwelle",
+ "split_select_hint": "Wähle eine Aufgabe, um hier die Details zu sehen",
"setups_target_new": "Neu anlegen: {name}",
"schedule_preview_title": "Nächste Termine",
"schedule_preview_ontime": "Bei pünktlichem Abschluss.",
@@ -927,5 +933,7 @@
"history_add_past": "Vergangene Erledigung nachtragen",
"require_tag_scan": "Nur per Tag-Scan abschließbar",
"require_tag_scan_help": "Anwesenheitsnachweis: „Erledigt“ ist überall blockiert, bis das NFC-Tag oder der QR-Code am Gerät gescannt wurde. Automationen können 'via_tag_scan' an den complete-Dienst übergeben.",
+ "disallow_skip": "Überspringen nicht erlauben",
+ "disallow_skip_help": "Die Aktion Überspringen verschwindet von allen Oberflächen; auch Automationen und Sprachbefehle werden abgewiesen.",
"require_tag_scan_hint": "Diese Aufgabe kann nur per Scan des NFC-Tags oder QR-Codes am Gerät erledigt werden — Speichern hier wird abgelehnt."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/en.json b/custom_components/maintenance_supporter/frontend/locales/en.json
index 6832e2b7..5cc20d56 100644
--- a/custom_components/maintenance_supporter/frontend/locales/en.json
+++ b/custom_components/maintenance_supporter/frontend/locales/en.json
@@ -536,6 +536,7 @@
"groupby_area": "By area",
"groupby_group": "By group",
"groupby_user": "By user",
+ "groupby_object": "By object",
"filter_label": "Filter",
"user_label": "User",
"photo_label": "Photo",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Currently effective start value: {value}",
"runtime_on_states": "Active states",
"runtime_on_states_help": "States that count as running — default: on. E.g. mowing, cleaning, printing. With an attribute selected, its values are matched instead.",
+ "runtime_max_session": "Max session runtime (seconds)",
+ "runtime_max_session_help": "A single run counts at most this many seconds — protects against a sensor stuck ON (lost connection, restart). Empty = no cap.",
+ "trend_approaching": "Heading toward the threshold",
+ "trend_easing": "Easing away from the threshold",
+ "split_select_hint": "Select a task to see its details here",
"setups_target_new": "Create new: {name}",
"schedule_preview_title": "Next dates",
"schedule_preview_ontime": "Assuming on-time completion.",
@@ -927,5 +933,7 @@
"history_add_past": "Add past completion",
"require_tag_scan": "Only complete by scanning the tag",
"require_tag_scan_help": "Proof of presence: Done is blocked on every surface until the NFC tag or QR code on the thing itself is scanned. Automations can pass 'via_tag_scan' to the complete service.",
+ "disallow_skip": "Don't allow skipping",
+ "disallow_skip_help": "The Skip action disappears from every surface, and the server refuses a skip from automations and voice too.",
"require_tag_scan_hint": "This task completes only by scanning its NFC tag or QR code on the thing itself — saving here will be refused."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/es.json b/custom_components/maintenance_supporter/frontend/locales/es.json
index 7dff280d..0f6ccff7 100644
--- a/custom_components/maintenance_supporter/frontend/locales/es.json
+++ b/custom_components/maintenance_supporter/frontend/locales/es.json
@@ -528,6 +528,7 @@
"groupby_area": "Por área",
"groupby_group": "Por grupo",
"groupby_user": "Por usuario",
+ "groupby_object": "Por objeto",
"filter_label": "Filtro",
"user_label": "Usuario",
"photo_label": "Foto",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Valor inicial vigente: {value}",
"runtime_on_states": "Estados activos",
"runtime_on_states_help": "Estados que cuentan como en funcionamiento — predeterminado: on. P. ej. mowing, cleaning, printing. Con un atributo seleccionado se comparan sus valores.",
+ "runtime_max_session": "Duración máxima por sesión (segundos)",
+ "runtime_max_session_help": "Una sola ejecución cuenta como máximo estos segundos — protege contra un sensor atascado en ON (conexión perdida, reinicio). Vacío = sin límite.",
+ "trend_approaching": "Se acerca al umbral",
+ "trend_easing": "Se aleja del umbral",
+ "split_select_hint": "Selecciona una tarea para ver sus detalles aquí",
"setups_target_new": "Crear nuevo: {name}",
"schedule_preview_title": "Próximas fechas",
"schedule_preview_ontime": "Suponiendo finalización puntual.",
@@ -927,5 +933,7 @@
"history_add_past": "Añadir una finalización pasada",
"require_tag_scan": "Completar solo escaneando la etiqueta",
"require_tag_scan_help": "Prueba de presencia: «Hecho» queda bloqueado en todas partes hasta escanear la etiqueta NFC o el código QR del propio objeto. Las automatizaciones pueden pasar 'via_tag_scan' al servicio complete.",
+ "disallow_skip": "No permitir omitir",
+ "disallow_skip_help": "La acción Omitir desaparece de todas las superficies y el servidor también rechaza omisiones desde automatizaciones y voz.",
"require_tag_scan_hint": "Esta tarea solo se completa escaneando su etiqueta NFC o código QR en el propio objeto; guardar aquí será rechazado."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/fi.json b/custom_components/maintenance_supporter/frontend/locales/fi.json
index 94e36e90..f6367804 100644
--- a/custom_components/maintenance_supporter/frontend/locales/fi.json
+++ b/custom_components/maintenance_supporter/frontend/locales/fi.json
@@ -530,6 +530,7 @@
"groupby_area": "Alueen mukaan",
"groupby_group": "Ryhmän mukaan",
"groupby_user": "Käyttäjän mukaan",
+ "groupby_object": "Kohteen mukaan",
"filter_label": "Suodata",
"user_label": "Käyttäjä",
"photo_label": "Valokuva",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Nyt voimassa oleva aloitusarvo: {value}",
"runtime_on_states": "Aktiiviset tilat",
"runtime_on_states_help": "Tilat, jotka lasketaan käyntiajaksi — oletus: on. Esim. mowing, cleaning, printing. Jos attribuutti on valittu, verrataan sen arvoja.",
+ "runtime_max_session": "Istunnon enimmäiskesto (sekuntia)",
+ "runtime_max_session_help": "Yksi ajo lasketaan enintään näin moneksi sekunniksi — suojaa PÄÄLLÄ-tilaan jumittuneelta anturilta (katkennut yhteys, uudelleenkäynnistys). Tyhjä = ei rajaa.",
+ "trend_approaching": "Lähestyy raja-arvoa",
+ "trend_easing": "Loittonee raja-arvosta",
+ "split_select_hint": "Valitse tehtävä nähdäksesi sen tiedot tässä",
"setups_target_new": "Luo uusi: {name}",
"schedule_preview_title": "Seuraavat päivämäärät",
"schedule_preview_ontime": "Olettaen, että valmistuminen on ajallaan.",
@@ -927,5 +933,7 @@
"history_add_past": "Lisää aiempi suoritus",
"require_tag_scan": "Suoritus vain tunnisteen skannauksella",
"require_tag_scan_help": "Läsnäolotodistus: ”Valmis” on estetty kaikkialla, kunnes laitteen NFC-tunniste tai QR-koodi on skannattu. Automaatiot voivat välittää 'via_tag_scan' complete-palvelulle.",
+ "disallow_skip": "Estä ohittaminen",
+ "disallow_skip_help": "Ohita-toiminto katoaa kaikista näkymistä, ja palvelin hylkää ohituksen myös automaatioista ja puheesta.",
"require_tag_scan_hint": "Tämä tehtävä valmistuu vain skannaamalla sen NFC-tunniste tai QR-koodi itse laitteesta — tallennus tässä estetään."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/fr.json b/custom_components/maintenance_supporter/frontend/locales/fr.json
index 961f9ce5..d59629ae 100644
--- a/custom_components/maintenance_supporter/frontend/locales/fr.json
+++ b/custom_components/maintenance_supporter/frontend/locales/fr.json
@@ -528,6 +528,7 @@
"groupby_area": "Par zone",
"groupby_group": "Par groupe",
"groupby_user": "Par utilisateur",
+ "groupby_object": "Par objet",
"filter_label": "Filtre",
"user_label": "Utilisateur",
"photo_label": "Photo",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Valeur de départ actuellement en vigueur : {value}",
"runtime_on_states": "États actifs",
"runtime_on_states_help": "États comptés comme en fonctionnement — par défaut : on. P. ex. mowing, cleaning, printing. Si un attribut est sélectionné, ce sont ses valeurs qui sont comparées.",
+ "runtime_max_session": "Durée max. par session (secondes)",
+ "runtime_max_session_help": "Une seule exécution compte au plus ce nombre de secondes — protège contre un capteur bloqué sur ON (connexion perdue, redémarrage). Vide = pas de limite.",
+ "trend_approaching": "Se rapproche du seuil",
+ "trend_easing": "S'éloigne du seuil",
+ "split_select_hint": "Sélectionnez une tâche pour voir ses détails ici",
"setups_target_new": "Créer : {name}",
"schedule_preview_title": "Prochaines échéances",
"schedule_preview_ontime": "En supposant un achèvement ponctuel.",
@@ -927,5 +933,7 @@
"history_add_past": "Ajouter une réalisation passée",
"require_tag_scan": "Terminer uniquement en scannant le tag",
"require_tag_scan_help": "Preuve de présence : « Terminé » est bloqué partout tant que le tag NFC ou le QR code sur l'objet n'a pas été scanné. Les automatisations peuvent passer 'via_tag_scan' au service complete.",
+ "disallow_skip": "Ne pas autoriser à ignorer",
+ "disallow_skip_help": "L'action Ignorer disparaît de toutes les surfaces et le serveur refuse aussi un saut via les automatisations et la voix.",
"require_tag_scan_hint": "Cette tâche ne se termine qu'en scannant son tag NFC ou son QR code sur l'objet — l'enregistrement ici sera refusé."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/hi.json b/custom_components/maintenance_supporter/frontend/locales/hi.json
index 9c429624..64784c4b 100644
--- a/custom_components/maintenance_supporter/frontend/locales/hi.json
+++ b/custom_components/maintenance_supporter/frontend/locales/hi.json
@@ -530,6 +530,7 @@
"groupby_area": "क्षेत्र के अनुसार",
"groupby_group": "समूह के अनुसार",
"groupby_user": "उपयोगकर्ता के अनुसार",
+ "groupby_object": "वस्तु के अनुसार",
"filter_label": "फ़िल्टर",
"user_label": "उपयोगकर्ता",
"photo_label": "फ़ोटो",
@@ -834,6 +835,11 @@
"baseline_current_effective": "वर्तमान में प्रभावी प्रारंभिक मान: {value}",
"runtime_on_states": "सक्रिय अवस्थाएँ",
"runtime_on_states_help": "वे अवस्थाएँ जो चालू समय में गिनी जाती हैं — डिफ़ॉल्ट: on। जैसे mowing, cleaning, printing। विशेषता चुनी होने पर उसकी मानों की तुलना होती है।",
+ "runtime_max_session": "प्रति सत्र अधिकतम रनटाइम (सेकंड)",
+ "runtime_max_session_help": "एक रन अधिकतम इतने सेकंड गिना जाता है — ON पर अटके सेंसर (कनेक्शन टूटना, रीस्टार्ट) से बचाता है। खाली = कोई सीमा नहीं।",
+ "trend_approaching": "सीमा की ओर बढ़ रहा है",
+ "trend_easing": "सीमा से दूर जा रहा है",
+ "split_select_hint": "विवरण यहाँ देखने के लिए कोई कार्य चुनें",
"setups_target_new": "नया बनाएँ: {name}",
"schedule_preview_title": "अगली तिथियाँ",
"schedule_preview_ontime": "समय पर पूर्णता मानते हुए।",
@@ -927,5 +933,7 @@
"history_add_past": "पिछली पूर्णता जोड़ें",
"require_tag_scan": "केवल टैग स्कैन से पूर्ण करें",
"require_tag_scan_help": "उपस्थिति प्रमाण: जब तक वस्तु पर लगा NFC टैग या QR कोड स्कैन न हो, हर जगह “पूर्ण” अवरुद्ध रहता है। ऑटोमेशन complete सेवा को 'via_tag_scan' दे सकते हैं।",
+ "disallow_skip": "छोड़ने की अनुमति न दें",
+ "disallow_skip_help": "छोड़ें क्रिया सभी सतहों से हट जाती है, और सर्वर ऑटोमेशन व वॉयस से छोड़ने को भी अस्वीकार करता है।",
"require_tag_scan_hint": "यह कार्य केवल वस्तु पर लगे NFC टैग या QR कोड को स्कैन करके ही पूर्ण होता है — यहाँ सहेजना अस्वीकार होगा।"
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/hu.json b/custom_components/maintenance_supporter/frontend/locales/hu.json
index 0107af75..4994d4c8 100644
--- a/custom_components/maintenance_supporter/frontend/locales/hu.json
+++ b/custom_components/maintenance_supporter/frontend/locales/hu.json
@@ -536,6 +536,7 @@
"groupby_area": "Terület szerint",
"groupby_group": "Csoport szerint",
"groupby_user": "Felhasználó szerint",
+ "groupby_object": "Objektum szerint",
"filter_label": "Szűrő",
"user_label": "Felhasználó",
"photo_label": "Fénykép",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Jelenleg érvényes kezdőérték: {value}",
"runtime_on_states": "Aktív állapotok",
"runtime_on_states_help": "Működésként számító állapotok — alapértelmezés: on. Pl. mowing, cleaning, printing. Attribútum kiválasztásakor annak értékei számítanak.",
+ "runtime_max_session": "Max. munkamenet-futásidő (másodperc)",
+ "runtime_max_session_help": "Egy futás legfeljebb ennyi másodpercet számít — véd a BE állapotban beragadt érzékelő ellen (kapcsolatvesztés, újraindítás). Üres = nincs korlát.",
+ "trend_approaching": "Közeledik a küszöbhöz",
+ "trend_easing": "Távolodik a küszöbtől",
+ "split_select_hint": "Válassz egy feladatot, és itt jelennek meg a részletei",
"setups_target_new": "Új létrehozása: {name}",
"schedule_preview_title": "Következő dátumok",
"schedule_preview_ontime": "Időben történő elvégzést feltételezve.",
@@ -927,5 +933,7 @@
"history_add_past": "Korábbi elvégzés rögzítése",
"require_tag_scan": "Csak címke beolvasásával fejezhető be",
"require_tag_scan_help": "Jelenlét-igazolás: a „Kész” mindenhol blokkolva van, amíg be nem olvasod az eszközön lévő NFC-címkét vagy QR-kódot. Az automatizálások 'via_tag_scan' értéket adhatnak át a complete szolgáltatásnak.",
+ "disallow_skip": "Kihagyás tiltása",
+ "disallow_skip_help": "A Kihagyás művelet minden felületről eltűnik, és a szerver az automatizálásokból és hangból érkező kihagyást is elutasítja.",
"require_tag_scan_hint": "Ez a feladat csak az eszközön lévő NFC-címke vagy QR-kód beolvasásával fejezhető be — az itteni mentés el lesz utasítva."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/it.json b/custom_components/maintenance_supporter/frontend/locales/it.json
index 66cf4a74..c7272cb2 100644
--- a/custom_components/maintenance_supporter/frontend/locales/it.json
+++ b/custom_components/maintenance_supporter/frontend/locales/it.json
@@ -528,6 +528,7 @@
"groupby_area": "Per area",
"groupby_group": "Per gruppo",
"groupby_user": "Per utente",
+ "groupby_object": "Per oggetto",
"filter_label": "Filtro",
"user_label": "Utente",
"photo_label": "Foto",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Valore iniziale attualmente in vigore: {value}",
"runtime_on_states": "Stati attivi",
"runtime_on_states_help": "Stati che contano come in funzione — predefinito: on. Ad es. mowing, cleaning, printing. Con un attributo selezionato vengono confrontati i suoi valori.",
+ "runtime_max_session": "Durata massima per sessione (secondi)",
+ "runtime_max_session_help": "Una singola esecuzione conta al massimo questi secondi — protegge da un sensore bloccato su ON (connessione persa, riavvio). Vuoto = nessun limite.",
+ "trend_approaching": "Si avvicina alla soglia",
+ "trend_easing": "Si allontana dalla soglia",
+ "split_select_hint": "Seleziona un'attività per vederne i dettagli qui",
"setups_target_new": "Crea nuovo: {name}",
"schedule_preview_title": "Prossime date",
"schedule_preview_ontime": "Presumendo un completamento puntuale.",
@@ -927,5 +933,7 @@
"history_add_past": "Aggiungi un completamento passato",
"require_tag_scan": "Completa solo scansionando il tag",
"require_tag_scan_help": "Prova di presenza: «Fatto» è bloccato ovunque finché non viene scansionato il tag NFC o il codice QR sull'oggetto. Le automazioni possono passare 'via_tag_scan' al servizio complete.",
+ "disallow_skip": "Non consentire di saltare",
+ "disallow_skip_help": "L'azione Salta scompare da tutte le superfici e il server rifiuta anche i salti da automazioni e voce.",
"require_tag_scan_hint": "Questa attività si completa solo scansionando il suo tag NFC o codice QR sull'oggetto — il salvataggio qui verrà rifiutato."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/ja.json b/custom_components/maintenance_supporter/frontend/locales/ja.json
index 8c1d3130..183defe4 100644
--- a/custom_components/maintenance_supporter/frontend/locales/ja.json
+++ b/custom_components/maintenance_supporter/frontend/locales/ja.json
@@ -530,6 +530,7 @@
"groupby_area": "エリア別",
"groupby_group": "グループ別",
"groupby_user": "ユーザー別",
+ "groupby_object": "対象別",
"filter_label": "絞り込み",
"user_label": "ユーザー",
"photo_label": "写真",
@@ -834,6 +835,11 @@
"baseline_current_effective": "現在有効な開始値:{value}",
"runtime_on_states": "稼働中とみなす状態",
"runtime_on_states_help": "稼働時間として数える状態 — 既定値:on。例:mowing, cleaning, printing。属性を選択している場合はその値が比較されます。",
+ "runtime_max_session": "1回の最大稼働時間(秒)",
+ "runtime_max_session_help": "1回の稼働は最大この秒数までカウント——ONのまま固まったセンサー(接続切れ・再起動)への保護です。空欄=制限なし。",
+ "trend_approaching": "しきい値に近づいています",
+ "trend_easing": "しきい値から遠ざかっています",
+ "split_select_hint": "タスクを選択すると、ここに詳細が表示されます",
"setups_target_new": "新規作成:{name}",
"schedule_preview_title": "次回の予定日",
"schedule_preview_ontime": "期日どおりに完了した場合。",
@@ -927,5 +933,7 @@
"history_add_past": "過去の完了を追加",
"require_tag_scan": "タグのスキャンでのみ完了可能",
"require_tag_scan_help": "在席証明:対象物のNFCタグまたはQRコードをスキャンするまで、どの画面でも「完了」はブロックされます。オートメーションはcompleteサービスに 'via_tag_scan' を渡せます。",
+ "disallow_skip": "スキップを許可しない",
+ "disallow_skip_help": "スキップ操作はすべての画面から消え、オートメーションや音声からのスキップもサーバーが拒否します。",
"require_tag_scan_hint": "このタスクは対象物のNFCタグまたはQRコードをスキャンしないと完了できません。ここでの保存は拒否されます。"
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/ko.json b/custom_components/maintenance_supporter/frontend/locales/ko.json
index 2048aac5..f923edd2 100644
--- a/custom_components/maintenance_supporter/frontend/locales/ko.json
+++ b/custom_components/maintenance_supporter/frontend/locales/ko.json
@@ -536,6 +536,7 @@
"groupby_area": "구역별",
"groupby_group": "그룹별",
"groupby_user": "사용자별",
+ "groupby_object": "대상별",
"filter_label": "필터",
"user_label": "사용자",
"photo_label": "사진",
@@ -834,6 +835,11 @@
"baseline_current_effective": "현재 적용 중인 시작 값: {value}",
"runtime_on_states": "활성 상태",
"runtime_on_states_help": "가동 중으로 계산할 상태입니다 — 기본값: on. 예: mowing, cleaning, printing. 속성을 선택한 경우 해당 속성 값과 비교합니다.",
+ "runtime_max_session": "세션당 최대 가동 시간(초)",
+ "runtime_max_session_help": "한 번의 가동은 최대 이 초까지만 집계됩니다 — ON에 멈춘 센서(연결 끊김, 재시작)로부터 보호합니다. 비워두면 제한 없음.",
+ "trend_approaching": "임계값에 가까워지는 중",
+ "trend_easing": "임계값에서 멀어지는 중",
+ "split_select_hint": "작업을 선택하면 여기에 세부 정보가 표시됩니다",
"setups_target_new": "새로 만들기: {name}",
"schedule_preview_title": "다음 예정일",
"schedule_preview_ontime": "제때 완료한다고 가정한 일정입니다.",
@@ -927,5 +933,7 @@
"history_add_past": "지난 완료 추가",
"require_tag_scan": "태그 스캔으로만 완료 가능",
"require_tag_scan_help": "현장 확인: 기기의 NFC 태그나 QR 코드를 스캔할 때까지 모든 화면에서 '완료'가 차단됩니다. 자동화는 complete 서비스에 'via_tag_scan'을 전달할 수 있습니다.",
+ "disallow_skip": "건너뛰기 허용 안 함",
+ "disallow_skip_help": "건너뛰기 동작이 모든 화면에서 사라지며, 서버는 자동화와 음성의 건너뛰기도 거부합니다.",
"require_tag_scan_hint": "이 작업은 기기의 NFC 태그나 QR 코드를 스캔해야만 완료됩니다. 여기서 저장하면 거부됩니다."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/nb.json b/custom_components/maintenance_supporter/frontend/locales/nb.json
index 794ea7b4..13ca7f0b 100644
--- a/custom_components/maintenance_supporter/frontend/locales/nb.json
+++ b/custom_components/maintenance_supporter/frontend/locales/nb.json
@@ -530,6 +530,7 @@
"groupby_area": "Etter område",
"groupby_group": "Etter gruppe",
"groupby_user": "Etter bruker",
+ "groupby_object": "Etter objekt",
"filter_label": "Filter",
"user_label": "Bruker",
"photo_label": "Bilde",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Gjeldende startverdi: {value}",
"runtime_on_states": "Aktive tilstander",
"runtime_on_states_help": "Tilstander som teller som driftstid — standard: on. F.eks. mowing, cleaning, printing. Med et valgt attributt sammenlignes verdiene dets i stedet.",
+ "runtime_max_session": "Maks økt-varighet (sekunder)",
+ "runtime_max_session_help": "En enkelt kjøring teller høyst så mange sekunder — beskytter mot en sensor som henger på PÅ (mistet forbindelse, omstart). Tom = ingen grense.",
+ "trend_approaching": "Nærmer seg terskelen",
+ "trend_easing": "Beveger seg bort fra terskelen",
+ "split_select_hint": "Velg en oppgave for å se detaljene her",
"setups_target_new": "Opprett ny: {name}",
"schedule_preview_title": "Kommende datoer",
"schedule_preview_ontime": "Forutsatt fullføring i tide.",
@@ -927,5 +933,7 @@
"history_add_past": "Legg til tidligere fullføring",
"require_tag_scan": "Kan bare fullføres ved tag-skanning",
"require_tag_scan_help": "Tilstedeværelsesbevis: «Fullført» er blokkert overalt til NFC-taggen eller QR-koden på selve tingen er skannet. Automatiseringer kan sende 'via_tag_scan' til complete-tjenesten.",
+ "disallow_skip": "Ikke tillat å hoppe over",
+ "disallow_skip_help": "Handlingen Hopp over forsvinner fra alle flater, og serveren avviser også hopp fra automatiseringer og tale.",
"require_tag_scan_hint": "Denne oppgaven fullføres bare ved å skanne NFC-taggen eller QR-koden på selve tingen — lagring her vil bli avvist."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/nl.json b/custom_components/maintenance_supporter/frontend/locales/nl.json
index 5f578e71..8214df62 100644
--- a/custom_components/maintenance_supporter/frontend/locales/nl.json
+++ b/custom_components/maintenance_supporter/frontend/locales/nl.json
@@ -528,6 +528,7 @@
"groupby_area": "Per gebied",
"groupby_group": "Per groep",
"groupby_user": "Per gebruiker",
+ "groupby_object": "Per object",
"filter_label": "Filter",
"user_label": "Gebruiker",
"photo_label": "Foto",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Momenteel geldende startwaarde: {value}",
"runtime_on_states": "Actieve statussen",
"runtime_on_states_help": "Statussen die als draaitijd tellen — standaard: on. Bijv. mowing, cleaning, printing. Met een geselecteerd attribuut worden de waarden daarvan vergeleken.",
+ "runtime_max_session": "Maximale sessieduur (seconden)",
+ "runtime_max_session_help": "Eén run telt maximaal dit aantal seconden — beschermt tegen sensoren die op AAN blijven hangen (verbroken verbinding, herstart). Leeg = geen limiet.",
+ "trend_approaching": "Beweegt richting de drempel",
+ "trend_easing": "Beweegt weg van de drempel",
+ "split_select_hint": "Selecteer een taak om hier de details te zien",
"setups_target_new": "Nieuw aanmaken: {name}",
"schedule_preview_title": "Volgende data",
"schedule_preview_ontime": "Uitgaande van tijdige afronding.",
@@ -927,5 +933,7 @@
"history_add_past": "Eerdere afronding toevoegen",
"require_tag_scan": "Alleen af te ronden via tag-scan",
"require_tag_scan_help": "Aanwezigheidsbewijs: „Klaar” is overal geblokkeerd totdat de NFC-tag of QR-code op het apparaat zelf is gescand. Automatiseringen kunnen 'via_tag_scan' aan de complete-service meegeven.",
+ "disallow_skip": "Overslaan niet toestaan",
+ "disallow_skip_help": "De actie Overslaan verdwijnt overal, en de server weigert ook een skip via automatiseringen en spraak.",
"require_tag_scan_hint": "Deze taak wordt alleen afgerond door de NFC-tag of QR-code op het apparaat te scannen — opslaan hier wordt geweigerd."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/pl.json b/custom_components/maintenance_supporter/frontend/locales/pl.json
index f995872f..0105ad01 100644
--- a/custom_components/maintenance_supporter/frontend/locales/pl.json
+++ b/custom_components/maintenance_supporter/frontend/locales/pl.json
@@ -528,6 +528,7 @@
"groupby_area": "Wg obszaru",
"groupby_group": "Wg grupy",
"groupby_user": "Wg użytkownika",
+ "groupby_object": "Według obiektu",
"filter_label": "Filtr",
"user_label": "Użytkownik",
"photo_label": "Zdjęcie",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Obecnie obowiązująca wartość początkowa: {value}",
"runtime_on_states": "Stany aktywne",
"runtime_on_states_help": "Stany liczone jako czas pracy — domyślnie: on. Np. mowing, cleaning, printing. Przy wybranym atrybucie porównywane są jego wartości.",
+ "runtime_max_session": "Maks. czas sesji (sekundy)",
+ "runtime_max_session_help": "Pojedynczy przebieg liczy się najwyżej tyle sekund — chroni przed czujnikiem zawieszonym na WŁ. (utrata połączenia, restart). Puste = bez limitu.",
+ "trend_approaching": "Zbliża się do progu",
+ "trend_easing": "Oddala się od progu",
+ "split_select_hint": "Wybierz zadanie, aby zobaczyć tutaj szczegóły",
"setups_target_new": "Utwórz nowy: {name}",
"schedule_preview_title": "Najbliższe terminy",
"schedule_preview_ontime": "Przy założeniu terminowego ukończenia.",
@@ -927,5 +933,7 @@
"history_add_past": "Dodaj wcześniejsze ukończenie",
"require_tag_scan": "Ukończenie tylko przez skan tagu",
"require_tag_scan_help": "Dowód obecności: „Gotowe” jest wszędzie zablokowane, dopóki nie zeskanujesz tagu NFC lub kodu QR na urządzeniu. Automatyzacje mogą przekazać 'via_tag_scan' do usługi complete.",
+ "disallow_skip": "Nie zezwalaj na pomijanie",
+ "disallow_skip_help": "Akcja Pomiń znika ze wszystkich widoków, a serwer odrzuca pominięcie także z automatyzacji i głosu.",
"require_tag_scan_hint": "To zadanie można ukończyć tylko skanując jego tag NFC lub kod QR na urządzeniu — zapis tutaj zostanie odrzucony."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/pt-br.json b/custom_components/maintenance_supporter/frontend/locales/pt-br.json
index aa9668a0..f363ffd0 100644
--- a/custom_components/maintenance_supporter/frontend/locales/pt-br.json
+++ b/custom_components/maintenance_supporter/frontend/locales/pt-br.json
@@ -536,6 +536,7 @@
"groupby_area": "Por área",
"groupby_group": "Por grupo",
"groupby_user": "Por usuário",
+ "groupby_object": "Por objeto",
"filter_label": "Filtro",
"user_label": "Usuário",
"photo_label": "Foto",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Valor inicial efetivo atual: {value}",
"runtime_on_states": "Estados ativos",
"runtime_on_states_help": "Estados que contam como em funcionamento — padrão: on. Ex.: mowing, cleaning, printing. Com um atributo selecionado, os valores dele são comparados.",
+ "runtime_max_session": "Duração máxima por sessão (segundos)",
+ "runtime_max_session_help": "Uma única execução conta no máximo esses segundos — protege contra um sensor travado em ON (conexão perdida, reinício). Vazio = sem limite.",
+ "trend_approaching": "Aproximando-se do limite",
+ "trend_easing": "Afastando-se do limite",
+ "split_select_hint": "Selecione uma tarefa para ver os detalhes aqui",
"setups_target_new": "Criar novo: {name}",
"schedule_preview_title": "Próximas datas",
"schedule_preview_ontime": "Supondo conclusão em dia.",
@@ -927,5 +933,7 @@
"history_add_past": "Adicionar conclusão passada",
"require_tag_scan": "Concluir apenas escaneando a etiqueta",
"require_tag_scan_help": "Prova de presença: “Concluído” fica bloqueado em toda parte até escanear a etiqueta NFC ou o código QR no próprio objeto. Automações podem passar 'via_tag_scan' ao serviço complete.",
+ "disallow_skip": "Não permitir pular",
+ "disallow_skip_help": "A ação Pular desaparece de todas as telas e o servidor também recusa pular via automações e voz.",
"require_tag_scan_hint": "Esta tarefa só é concluída escaneando sua etiqueta NFC ou código QR no próprio objeto — salvar aqui será recusado."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/pt.json b/custom_components/maintenance_supporter/frontend/locales/pt.json
index 19f178a3..b1631d99 100644
--- a/custom_components/maintenance_supporter/frontend/locales/pt.json
+++ b/custom_components/maintenance_supporter/frontend/locales/pt.json
@@ -573,6 +573,7 @@
"groupby_area": "Por área",
"groupby_group": "Por grupo",
"groupby_user": "Por usuário",
+ "groupby_object": "Por objeto",
"unassigned": "Não atribuído",
"no_area": "Sem área",
"has_overdue": "Tarefas em atraso",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Valor inicial atualmente em vigor: {value}",
"runtime_on_states": "Estados ativos",
"runtime_on_states_help": "Estados que contam como em funcionamento — padrão: on. P. ex. mowing, cleaning, printing. Com um atributo selecionado, são comparados os valores dele.",
+ "runtime_max_session": "Duração máxima por sessão (segundos)",
+ "runtime_max_session_help": "Uma única execução conta no máximo estes segundos — protege contra um sensor preso em ON (ligação perdida, reinício). Vazio = sem limite.",
+ "trend_approaching": "A aproximar-se do limiar",
+ "trend_easing": "A afastar-se do limiar",
+ "split_select_hint": "Selecione uma tarefa para ver os detalhes aqui",
"setups_target_new": "Criar novo: {name}",
"schedule_preview_title": "Próximas datas",
"schedule_preview_ontime": "Assumindo conclusão pontual.",
@@ -927,5 +933,7 @@
"history_add_past": "Adicionar conclusão passada",
"require_tag_scan": "Concluir apenas lendo a etiqueta",
"require_tag_scan_help": "Prova de presença: «Concluído» fica bloqueado em todo o lado até ler a etiqueta NFC ou o código QR no próprio objeto. As automações podem passar 'via_tag_scan' ao serviço complete.",
+ "disallow_skip": "Não permitir ignorar",
+ "disallow_skip_help": "A ação Ignorar desaparece de todas as superfícies e o servidor também recusa ignorar via automações e voz.",
"require_tag_scan_hint": "Esta tarefa só é concluída lendo a sua etiqueta NFC ou código QR no próprio objeto — guardar aqui será recusado."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/ru.json b/custom_components/maintenance_supporter/frontend/locales/ru.json
index 86314351..465f6820 100644
--- a/custom_components/maintenance_supporter/frontend/locales/ru.json
+++ b/custom_components/maintenance_supporter/frontend/locales/ru.json
@@ -528,6 +528,7 @@
"groupby_area": "По области",
"groupby_group": "По группе",
"groupby_user": "По пользователю",
+ "groupby_object": "По объекту",
"filter_label": "Фильтр",
"user_label": "Пользователь",
"photo_label": "Фото",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Действующее начальное значение: {value}",
"runtime_on_states": "Активные состояния",
"runtime_on_states_help": "Состояния, засчитываемые как работа — по умолчанию: on. Напр. mowing, cleaning, printing. Если выбран атрибут, сравниваются его значения.",
+ "runtime_max_session": "Макс. время сеанса (секунды)",
+ "runtime_max_session_help": "Один запуск засчитывается не более чем на столько секунд — защита от датчика, зависшего во включённом состоянии (обрыв связи, перезапуск). Пусто = без ограничения.",
+ "trend_approaching": "Приближается к порогу",
+ "trend_easing": "Отдаляется от порога",
+ "split_select_hint": "Выберите задачу, чтобы увидеть здесь её детали",
"setups_target_new": "Создать новый: {name}",
"schedule_preview_title": "Ближайшие даты",
"schedule_preview_ontime": "При условии своевременного выполнения.",
@@ -927,5 +933,7 @@
"history_add_past": "Добавить прошлое выполнение",
"require_tag_scan": "Завершение только по скану метки",
"require_tag_scan_help": "Подтверждение присутствия: «Готово» заблокировано везде, пока не отсканирована NFC-метка или QR-код на самом устройстве. Автоматизации могут передавать 'via_tag_scan' в службу complete.",
+ "disallow_skip": "Запретить пропуск",
+ "disallow_skip_help": "Действие «Пропустить» исчезает со всех экранов, а сервер отклоняет пропуск из автоматизаций и голосовых команд.",
"require_tag_scan_hint": "Эта задача завершается только сканированием её NFC-метки или QR-кода на самом устройстве — сохранение здесь будет отклонено."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/sv.json b/custom_components/maintenance_supporter/frontend/locales/sv.json
index 62a08fc5..d0337d09 100644
--- a/custom_components/maintenance_supporter/frontend/locales/sv.json
+++ b/custom_components/maintenance_supporter/frontend/locales/sv.json
@@ -528,6 +528,7 @@
"groupby_area": "Per område",
"groupby_group": "Per grupp",
"groupby_user": "Per användare",
+ "groupby_object": "Efter objekt",
"filter_label": "Filter",
"user_label": "Användare",
"photo_label": "Foto",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Nu gällande startvärde: {value}",
"runtime_on_states": "Aktiva tillstånd",
"runtime_on_states_help": "Tillstånd som räknas som drifttid — standard: on. T.ex. mowing, cleaning, printing. Med ett valt attribut jämförs dess värden i stället.",
+ "runtime_max_session": "Max körtid per session (sekunder)",
+ "runtime_max_session_help": "En enskild körning räknas som högst så här många sekunder — skyddar mot en sensor som fastnat på PÅ (tappad anslutning, omstart). Tomt = ingen gräns.",
+ "trend_approaching": "Närmar sig tröskeln",
+ "trend_easing": "Rör sig bort från tröskeln",
+ "split_select_hint": "Välj en uppgift för att se detaljerna här",
"setups_target_new": "Skapa ny: {name}",
"schedule_preview_title": "Kommande datum",
"schedule_preview_ontime": "Förutsatt slutförande i tid.",
@@ -927,5 +933,7 @@
"history_add_past": "Lägg till tidigare slutförande",
"require_tag_scan": "Kan endast slutföras via tagg-skanning",
"require_tag_scan_help": "Närvarobevis: ”Klar” är blockerad överallt tills NFC-taggen eller QR-koden på själva saken har skannats. Automationer kan skicka 'via_tag_scan' till complete-tjänsten.",
+ "disallow_skip": "Tillåt inte att hoppa över",
+ "disallow_skip_help": "Åtgärden Hoppa över försvinner från alla ytor och servern nekar även hopp från automationer och röst.",
"require_tag_scan_hint": "Den här uppgiften slutförs endast genom att skanna dess NFC-tagg eller QR-kod på själva saken — att spara här kommer att nekas."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/tr.json b/custom_components/maintenance_supporter/frontend/locales/tr.json
index 08502906..cce813d1 100644
--- a/custom_components/maintenance_supporter/frontend/locales/tr.json
+++ b/custom_components/maintenance_supporter/frontend/locales/tr.json
@@ -536,6 +536,7 @@
"groupby_area": "Alana göre",
"groupby_group": "Gruba göre",
"groupby_user": "Kullanıcıya göre",
+ "groupby_object": "Nesneye göre",
"filter_label": "Filtre",
"user_label": "Kullanıcı",
"photo_label": "Fotoğraf",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Şu anda geçerli başlangıç değeri: {value}",
"runtime_on_states": "Etkin durumlar",
"runtime_on_states_help": "Çalışıyor sayılan durumlar — varsayılan: on. Örn. mowing, cleaning, printing. Bir öznitelik seçiliyse onun değerleri eşleştirilir.",
+ "runtime_max_session": "Oturum başına azami çalışma süresi (saniye)",
+ "runtime_max_session_help": "Tek bir çalışma en fazla bu kadar saniye sayılır — AÇIK durumda takılı kalan sensöre (bağlantı kopması, yeniden başlatma) karşı korur. Boş = sınır yok.",
+ "trend_approaching": "Eşiğe yaklaşıyor",
+ "trend_easing": "Eşikten uzaklaşıyor",
+ "split_select_hint": "Ayrıntıları burada görmek için bir görev seçin",
"setups_target_new": "Yeni oluştur: {name}",
"schedule_preview_title": "Sonraki tarihler",
"schedule_preview_ontime": "Zamanında tamamlama varsayılarak.",
@@ -927,5 +933,7 @@
"history_add_past": "Geçmiş tamamlama ekle",
"require_tag_scan": "Yalnızca etiket taranarak tamamlanır",
"require_tag_scan_help": "Yerinde olma kanıtı: Cihazın üzerindeki NFC etiketi veya QR kodu taranana kadar 'Tamamlandı' her yerde engellenir. Otomasyonlar complete servisine 'via_tag_scan' iletebilir.",
+ "disallow_skip": "Atlamaya izin verme",
+ "disallow_skip_help": "Atla eylemi tüm yüzeylerden kaybolur; sunucu otomasyonlardan ve sesten gelen atlamayı da reddeder.",
"require_tag_scan_hint": "Bu görev yalnızca cihazın üzerindeki NFC etiketi veya QR kodu taranarak tamamlanır — buradan kaydetme reddedilecektir."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/uk.json b/custom_components/maintenance_supporter/frontend/locales/uk.json
index 9789afa2..4f251edb 100644
--- a/custom_components/maintenance_supporter/frontend/locales/uk.json
+++ b/custom_components/maintenance_supporter/frontend/locales/uk.json
@@ -528,6 +528,7 @@
"groupby_area": "За зоною",
"groupby_group": "За групою",
"groupby_user": "За користувачем",
+ "groupby_object": "За об'єктом",
"filter_label": "Фільтр",
"user_label": "Користувач",
"photo_label": "Фото",
@@ -834,6 +835,11 @@
"baseline_current_effective": "Чинне початкове значення: {value}",
"runtime_on_states": "Активні стани",
"runtime_on_states_help": "Стани, що зараховуються як робота — типово: on. Напр. mowing, cleaning, printing. Якщо вибрано атрибут, порівнюються його значення.",
+ "runtime_max_session": "Макс. час сеансу (секунди)",
+ "runtime_max_session_help": "Один запуск зараховується щонайбільше на стільки секунд — захист від датчика, що завис в увімкненому стані (втрата зв'язку, перезапуск). Порожньо = без обмеження.",
+ "trend_approaching": "Наближається до порога",
+ "trend_easing": "Віддаляється від порога",
+ "split_select_hint": "Виберіть завдання, щоб побачити тут його деталі",
"setups_target_new": "Створити новий: {name}",
"schedule_preview_title": "Найближчі дати",
"schedule_preview_ontime": "За умови вчасного виконання.",
@@ -927,5 +933,7 @@
"history_add_past": "Додати минуле виконання",
"require_tag_scan": "Завершення лише скануванням мітки",
"require_tag_scan_help": "Підтвердження присутності: «Готово» заблоковано всюди, доки не відскановано NFC-мітку або QR-код на самому пристрої. Автоматизації можуть передати 'via_tag_scan' службі complete.",
+ "disallow_skip": "Заборонити пропуск",
+ "disallow_skip_help": "Дія «Пропустити» зникає з усіх поверхонь, а сервер відхиляє пропуск з автоматизацій і голосу.",
"require_tag_scan_hint": "Це завдання завершується лише скануванням його NFC-мітки або QR-коду на самому пристрої — збереження тут буде відхилено."
}
diff --git a/custom_components/maintenance_supporter/frontend/locales/zh.json b/custom_components/maintenance_supporter/frontend/locales/zh.json
index 38de2aa1..3b07be94 100644
--- a/custom_components/maintenance_supporter/frontend/locales/zh.json
+++ b/custom_components/maintenance_supporter/frontend/locales/zh.json
@@ -530,6 +530,7 @@
"groupby_area": "按区域分组",
"groupby_group": "按分组",
"groupby_user": "按负责人",
+ "groupby_object": "按对象",
"filter_label": "过滤器",
"user_label": "用户",
"photo_label": "照片",
@@ -834,6 +835,11 @@
"baseline_current_effective": "当前生效的起始读数:{value}",
"runtime_on_states": "活动状态",
"runtime_on_states_help": "计入运行时间的状态 — 默认:on。例如 mowing、cleaning、printing。选择了属性时将比较该属性的值。",
+ "runtime_max_session": "单次运行最长时间(秒)",
+ "runtime_max_session_help": "单次运行最多计入这么多秒——防止传感器卡在开启状态(连接丢失、重启)。留空 = 不限制。",
+ "trend_approaching": "正接近阈值",
+ "trend_easing": "正远离阈值",
+ "split_select_hint": "选择一个任务,在此查看详情",
"setups_target_new": "新建:{name}",
"schedule_preview_title": "下次日期",
"schedule_preview_ontime": "假设按时完成。",
@@ -927,5 +933,7 @@
"history_add_past": "补录过去的完成记录",
"require_tag_scan": "仅可通过扫描标签完成",
"require_tag_scan_help": "在场证明:在扫描设备上的 NFC 标签或二维码之前,所有界面的“完成”均被阻止。自动化可向 complete 服务传递 'via_tag_scan'。",
+ "disallow_skip": "不允许跳过",
+ "disallow_skip_help": "跳过操作将从所有界面消失,服务器也会拒绝来自自动化和语音的跳过。",
"require_tag_scan_hint": "此任务只能通过扫描设备上的 NFC 标签或二维码来完成——在此保存将被拒绝。"
}
diff --git a/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js b/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js
index fb573f92..a349356a 100644
--- a/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js
+++ b/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js
@@ -1,9 +1,9 @@
-/*! maintenance_supporter frontend 2.70.0 */
-var Tt=Object.defineProperty;var cs=Object.getOwnPropertyDescriptor;var w=(r,s,e)=>()=>{if(e)throw e[0];try{return r&&(s=r(r=0)),s}catch(t){throw e=[t],t}};var ds=(r,s)=>{for(var e in s)Tt(r,e,{get:s[e],enumerable:!0})};var c=(r,s,e,t)=>{for(var i=t>1?void 0:t?cs(s,e):s,n=r.length-1,o;n>=0;n--)(o=r[n])&&(i=(t?o(s,e,i):o(i))||i);return t&&i&&Tt(s,e,i),i};var Me,Oe,Xe,Ct,xe,It,S,Pt,et,tt=w(()=>{Me=globalThis,Oe=Me.ShadowRoot&&(Me.ShadyCSS===void 0||Me.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Xe=Symbol(),Ct=new WeakMap,xe=class{constructor(s,e,t){if(this._$cssResult$=!0,t!==Xe)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(Oe&&s===void 0){let t=e!==void 0&&e.length===1;t&&(s=Ct.get(e)),s===void 0&&((this.o=s=new CSSStyleSheet).replaceSync(this.cssText),t&&Ct.set(e,s))}return s}toString(){return this.cssText}},It=r=>new xe(typeof r=="string"?r:r+"",void 0,Xe),S=(r,...s)=>{let e=r.length===1?r[0]:s.reduce((t,i,n)=>t+(o=>{if(o._$cssResult$===!0)return o.cssText;if(typeof o=="number")return o;throw Error("Value passed to 'css' function must be a 'css' function result: "+o+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+r[n+1],r[0]);return new xe(e,r,Xe)},Pt=(r,s)=>{if(Oe)r.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of s){let t=document.createElement("style"),i=Me.litNonce;i!==void 0&&t.setAttribute("nonce",i),t.textContent=e.cssText,r.appendChild(t)}},et=Oe?r=>r:r=>r instanceof CSSStyleSheet?(s=>{let e="";for(let t of s.cssRules)e+=t.cssText;return It(e)})(r):r});var ps,us,hs,_s,gs,ms,ze,Lt,fs,vs,we,$e,Fe,qt,Y,ke=w(()=>{tt();tt();({is:ps,defineProperty:us,getOwnPropertyDescriptor:hs,getOwnPropertyNames:_s,getOwnPropertySymbols:gs,getPrototypeOf:ms}=Object),ze=globalThis,Lt=ze.trustedTypes,fs=Lt?Lt.emptyScript:"",vs=ze.reactiveElementPolyfillSupport,we=(r,s)=>r,$e={toAttribute(r,s){switch(s){case Boolean:r=r?fs:null;break;case Object:case Array:r=r==null?r:JSON.stringify(r)}return r},fromAttribute(r,s){let e=r;switch(s){case Boolean:e=r!==null;break;case Number:e=r===null?null:Number(r);break;case Object:case Array:try{e=JSON.parse(r)}catch{e=null}}return e}},Fe=(r,s)=>!ps(r,s),qt={attribute:!0,type:String,converter:$e,reflect:!1,useDefault:!1,hasChanged:Fe};Symbol.metadata??=Symbol("metadata"),ze.litPropertyMetadata??=new WeakMap;Y=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=qt){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&&us(this.prototype,s,i)}}static getPropertyDescriptor(s,e,t){let{get:i,set:n}=hs(this.prototype,s)??{get(){return this[e]},set(o){this[e]=o}};return{get:i,set(o){let p=i?.call(this);n?.call(this,o),this.requestUpdate(s,p,t)},configurable:!0,enumerable:!0}}static getPropertyOptions(s){return this.elementProperties.get(s)??qt}static _$Ei(){if(this.hasOwnProperty(we("elementProperties")))return;let s=ms(this);s.finalize(),s.l!==void 0&&(this.l=[...s.l]),this.elementProperties=new Map(s.elementProperties)}static finalize(){if(this.hasOwnProperty(we("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(we("properties"))){let e=this.properties,t=[..._s(e),...gs(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(et(i))}else s!==void 0&&e.push(et(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 Pt(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:$e).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),o=typeof n.converter=="function"?{fromAttribute:n.converter}:n.converter?.fromAttribute!==void 0?n.converter:$e;this._$Em=i;let p=o.fromAttribute(e,n.type);this[i]=p??this._$Ej?.get(i)??p,this._$Em=null}}requestUpdate(s,e,t,i=!1,n){if(s!==void 0){let o=this.constructor;if(i===!1&&(n=this[s]),t??=o.getPropertyOptions(s),!((t.hasChanged??Fe)(n,e)||t.useDefault&&t.reflect&&n===this._$Ej?.get(s)&&!this.hasAttribute(o._$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},o){t&&!(this._$Ej??=new Map).has(s)&&(this._$Ej.set(s,o??e??this[s]),n!==!0||o!==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:o}=n,p=this[i];o!==!0||this._$AL.has(i)||p===void 0||this.C(i,void 0,n,p)}}let s=!1,e=this._$AL;try{s=this.shouldUpdate(e),s?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(t){throw s=!1,this._$EM(),t}s&&this._$AE(e)}willUpdate(s){}_$AE(s){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(s)),this.updated(s)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(s){return!0}update(s){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(s){}firstUpdated(s){}};Y.elementStyles=[],Y.shadowRootOptions={mode:"open"},Y[we("elementProperties")]=new Map,Y[we("finalized")]=new Map,vs?.({ReactiveElement:Y}),(ze.reactiveElementVersions??=[]).push("2.1.2")});function Bt(r,s){if(!ct(r)||!r.hasOwnProperty("raw"))throw Error("invalid template strings array");return Nt!==void 0?Nt.createHTML(s):s}function ue(r,s,e=r,t){if(s===oe)return s;let i=t!==void 0?e._$Co?.[t]:e._$Cl,n=Ae(s)?void 0:s._$litDirective$;return i?.constructor!==n&&(i?._$AO?.(!1),n===void 0?i=void 0:(i=new n(r),i._$AT(r,e,t)),t!==void 0?(e._$Co??=[])[t]=i:e._$Cl=i),i!==void 0&&(s=ue(r,i._$AS(r,s.values),i,t)),s}var lt,Rt,Ue,Nt,zt,ie,Ft,ys,ne,Se,Ae,ct,bs,it,Ee,jt,Ht,re,Dt,Mt,Ut,dt,l,_e,Ar,oe,_,Ot,ae,xs,Te,st,Ce,he,rt,at,nt,ot,ws,Wt,Be=w(()=>{lt=globalThis,Rt=r=>r,Ue=lt.trustedTypes,Nt=Ue?Ue.createPolicy("lit-html",{createHTML:r=>r}):void 0,zt="$lit$",ie=`lit$${Math.random().toFixed(9).slice(2)}$`,Ft="?"+ie,ys=`<${Ft}>`,ne=document,Se=()=>ne.createComment(""),Ae=r=>r===null||typeof r!="object"&&typeof r!="function",ct=Array.isArray,bs=r=>ct(r)||typeof r?.[Symbol.iterator]=="function",it=`[
-\f\r]`,Ee=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,jt=/-->/g,Ht=/>/g,re=RegExp(`>|${it}(?:([^\\s"'>=/]+)(${it}*=${it}*(?:[^
-\f\r"'\`<>=]|("|')|))|$)`,"g"),Dt=/'/g,Mt=/"/g,Ut=/^(?:script|style|textarea|title)$/i,dt=r=>(s,...e)=>({_$litType$:r,strings:s,values:e}),l=dt(1),_e=dt(2),Ar=dt(3),oe=Symbol.for("lit-noChange"),_=Symbol.for("lit-nothing"),Ot=new WeakMap,ae=ne.createTreeWalker(ne,129);xs=(r,s)=>{let e=r.length-1,t=[],i,n=s===2?"":s===3?"":"")),t]},Te=class r{constructor({strings:s,_$litType$:e},t){let i;this.parts=[];let n=0,o=0,p=s.length-1,d=this.parts,[u,m]=xs(s,e);if(this.el=r.createElement(u,t),ae.currentNode=this.el.content,e===2||e===3){let f=this.el.content.firstChild;f.replaceWith(...f.childNodes)}for(;(i=ae.nextNode())!==null&&d.length
0){i.textContent=Ue?Ue.emptyScript:"";for(let y=0;y2||t[0]!==""||t[1]!==""?(this._$AH=Array(t.length-1).fill(new String),this.strings=t):this._$AH=_}_$AI(s,e=this,t,i){let n=this.strings,o=!1;if(n===void 0)s=ue(this,s,e,0),o=!Ae(s)||s!==this._$AH&&s!==oe,o&&(this._$AH=s);else{let p=s,d,u;for(s=n[0],d=0;d{let t=e?.renderBefore??s,i=t._$litPart$;if(i===void 0){let n=e?.renderBefore??null;t._$litPart$=i=new Ce(s.insertBefore(Se(),n),n,void 0,e??{})}return i._$AI(r),i}});var pt,A,$s,Vt=w(()=>{ke();ke();Be();Be();pt=globalThis,A=class extends Y{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=Wt(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return oe}};A._$litElement$=!0,A.finalized=!0,pt.litElementHydrateSupport?.({LitElement:A});$s=pt.litElementPolyfillSupport;$s?.({LitElement:A});(pt.litElementVersions??=[]).push("4.2.2")});var Kt=w(()=>{});var P=w(()=>{ke();Be();Vt();Kt()});var Gt=w(()=>{});function x(r){return(s,e)=>typeof e=="object"?Es(r,s,e):((t,i,n)=>{let o=i.hasOwnProperty(n);return i.constructor.createProperty(n,t),o?Object.getOwnPropertyDescriptor(i,n):void 0})(r,s,e)}var ks,Es,ut=w(()=>{ke();ks={attribute:!0,type:String,converter:$e,reflect:!1,hasChanged:Fe},Es=(r=ks,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"&&((r=Object.create(r)).wrapped=!0),n.set(e.name,r),t==="accessor"){let{name:o}=e;return{set(p){let d=s.get.call(this);s.set.call(this,p),this.requestUpdate(o,d,r,!0,p)},init(p){return p!==void 0&&this.C(o,void 0,r,p),p}}}if(t==="setter"){let{name:o}=e;return function(p){let d=this[o];s.call(this,p),this.requestUpdate(o,d,r,!0,p)}}throw Error("Unsupported decorator location: "+t)}});function h(r){return x({...r,state:!0,attribute:!1})}var Yt=w(()=>{ut();});var Qt=w(()=>{});var ge=w(()=>{});var Jt=w(()=>{ge();});var Zt=w(()=>{ge();});var Xt=w(()=>{ge();});var ei=w(()=>{ge();});var ti=w(()=>{ge();});var W=w(()=>{Gt();ut();Yt();Qt();Jt();Zt();Xt();ei();ti()});function ii(r,s){return!r||r<=0?0:r*(Ss[s||"days"]??1)}var Ss,si=w(()=>{"use strict";Ss={days:1,weeks:7,months:30.4368,years:365.25}});function le(r){let s=r.getFullYear(),e=String(r.getMonth()+1).padStart(2,"0"),t=String(r.getDate()).padStart(2,"0");return`${s}-${e}-${t}`}function As(r,s){let e=[];for(let t=0;te.cost).filter(e=>typeof e=="number");return s.length===0?null:s.reduce((e,t)=>e+t,0)/s.length}function Cs(r){let{windowStart:s,windowEnd:e,task:t,entryId:i,objectName:n}=r,o=[],p=(f,v)=>({date:f,entry_id:i,task_id:t.id,task_name:t.name,object_name:n,status:v&&(t.status==="overdue"||t.status==="triggered")?"ok":t.status,days_until_due:v?null:t.days_until_due??null,projected:v,schedule_type:t.schedule_type,interval_days:t.interval_days??null,interval_unit:t.interval_unit??null,responsible_user_id:t.responsible_user_id??null,avg_cost:Ts(t.history),adaptive_enabled:!!t.adaptive_config?.enabled,prediction_confidence:t.threshold_prediction_confidence??null}),d=Math.max(1,Math.round(ii(t.interval_days,t.interval_unit)));if(t.status==="overdue"||t.status==="triggered"){if(o.push(p(s,!1)),t.schedule_type==="time_based"&&t.interval_days&&t.interval_days>0){let f=Ve(s,d),v=1;for(;f<=e&&v=s&&m<=e)o.push(p(m,!1));else if(m>e)return o;if(t.schedule_type==="time_based"&&t.interval_days&&t.interval_days>0){let f=Ve(m,d),v=o.length;for(;f<=e&&v=s&&(o.push(p(f,!0)),v++),f=Ve(f,d)}return o}function ni(r,s,e,t=null){let i=As(s,e),n=i[0],o=i[i.length-1],p=[];for(let u of r){let m=u.object?.name||"",f=u.entry_id,v=u.tasks||[];for(let y of v){if(t&&y.responsible_user_id!==t||y.enabled===!1)continue;let k=Cs({windowStart:n,windowEnd:o,task:y,entryId:f,objectName:m});p.push(...k)}}let d=new Map;for(let u of i)d.set(u,[]);for(let u of p){let m=d.get(u.date);m&&m.push(u)}for(let[,u]of d)u.sort((m,f)=>{let v=ai[m.status]??99,y=ai[f.status]??99;if(v!==y)return v-y;if(m.projected!==f.projected)return m.projected?1:-1;let k=m.object_name.localeCompare(f.object_name);return k!==0?k:m.task_name.localeCompare(f.task_name)});return i.map(u=>({date:u,events:d.get(u)??[]}))}function Ps(r,s){let e=[];for(let t=s-1;t>=0;t--){let i=new Date(r);i.setDate(i.getDate()-t),i.setHours(0,0,0,0),e.push(le(i))}return e}function oi(r,s,e,t=null){let i=Ps(s,e),n=i[0],o=i[i.length-1],p=new Map;for(let u of i)p.set(u,[]);for(let u of r){let m=u.object?.name||"",f=u.entry_id,v=u.tasks||[];for(let y of v){if(t&&y.responsible_user_id!==t)continue;let k=y.history||[];for(let E of k){if(typeof E?.timestamp!="string")continue;let H=E.timestamp.slice(0,10);if(Ho)continue;let M=p.get(H);if(!M)continue;let q=E.type??"completed";M.push({date:H,entry_id:f,task_id:y.id,task_name:y.name,object_name:m,status:Is[q]??"ok",days_until_due:null,projected:!1,schedule_type:y.schedule_type,interval_days:y.interval_days??null,responsible_user_id:y.responsible_user_id??null,avg_cost:typeof E.cost=="number"?E.cost:null,adaptive_enabled:!!y.adaptive_config?.enabled,prediction_confidence:null,history_timestamp:E.timestamp,history_type:q,history_cost:typeof E.cost=="number"?E.cost:null,history_notes:typeof E.notes=="string"?E.notes:null,history_duration:typeof E.duration=="number"?E.duration:null})}}}let d={completed:0,reset:1,skipped:2,triggered:3,trigger_replaced:4};for(let[,u]of p)u.sort((m,f)=>{let v=d[m.history_type??""]??99,y=d[f.history_type??""]??99;if(v!==y)return v-y;let k=m.object_name.localeCompare(f.object_name);return k!==0?k:m.task_name.localeCompare(f.task_name)});return i.map(u=>({date:u,events:p.get(u)??[]}))}var ri,ai,Is,ht=w(()=>{"use strict";si();ri=5;ai={overdue:0,triggered:1,due_soon:2,ok:3};Is={completed:"ok",reset:"ok",skipped:"due_soon",missed:"overdue",triggered:"triggered",trigger_replaced:"triggered",trigger_removed:"ok"}});var di,ci=w(()=>{di={maintenance:"Maintenance",objects:"Objects",tasks:"Tasks",overdue:"Overdue",due_soon:"Due Soon",triggered:"Triggered",trigger_replaced:"Trigger replaced",trigger_removed:"Trigger removed",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
+/*! maintenance_supporter frontend 2.71.0 */
+var Tt=Object.defineProperty;var cs=Object.getOwnPropertyDescriptor;var w=(r,s,e)=>()=>{if(e)throw e[0];try{return r&&(s=r(r=0)),s}catch(t){throw e=[t],t}};var ds=(r,s)=>{for(var e in s)Tt(r,e,{get:s[e],enumerable:!0})};var c=(r,s,e,t)=>{for(var i=t>1?void 0:t?cs(s,e):s,n=r.length-1,o;n>=0;n--)(o=r[n])&&(i=(t?o(s,e,i):o(i))||i);return t&&i&&Tt(s,e,i),i};var Me,Oe,Xe,It,xe,Ct,S,Pt,et,tt=w(()=>{Me=globalThis,Oe=Me.ShadowRoot&&(Me.ShadyCSS===void 0||Me.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Xe=Symbol(),It=new WeakMap,xe=class{constructor(s,e,t){if(this._$cssResult$=!0,t!==Xe)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(Oe&&s===void 0){let t=e!==void 0&&e.length===1;t&&(s=It.get(e)),s===void 0&&((this.o=s=new CSSStyleSheet).replaceSync(this.cssText),t&&It.set(e,s))}return s}toString(){return this.cssText}},Ct=r=>new xe(typeof r=="string"?r:r+"",void 0,Xe),S=(r,...s)=>{let e=r.length===1?r[0]:s.reduce((t,i,n)=>t+(o=>{if(o._$cssResult$===!0)return o.cssText;if(typeof o=="number")return o;throw Error("Value passed to 'css' function must be a 'css' function result: "+o+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+r[n+1],r[0]);return new xe(e,r,Xe)},Pt=(r,s)=>{if(Oe)r.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of s){let t=document.createElement("style"),i=Me.litNonce;i!==void 0&&t.setAttribute("nonce",i),t.textContent=e.cssText,r.appendChild(t)}},et=Oe?r=>r:r=>r instanceof CSSStyleSheet?(s=>{let e="";for(let t of s.cssRules)e+=t.cssText;return Ct(e)})(r):r});var ps,us,hs,_s,gs,ms,ze,Lt,fs,vs,we,$e,Fe,qt,Y,ke=w(()=>{tt();tt();({is:ps,defineProperty:us,getOwnPropertyDescriptor:hs,getOwnPropertyNames:_s,getOwnPropertySymbols:gs,getPrototypeOf:ms}=Object),ze=globalThis,Lt=ze.trustedTypes,fs=Lt?Lt.emptyScript:"",vs=ze.reactiveElementPolyfillSupport,we=(r,s)=>r,$e={toAttribute(r,s){switch(s){case Boolean:r=r?fs:null;break;case Object:case Array:r=r==null?r:JSON.stringify(r)}return r},fromAttribute(r,s){let e=r;switch(s){case Boolean:e=r!==null;break;case Number:e=r===null?null:Number(r);break;case Object:case Array:try{e=JSON.parse(r)}catch{e=null}}return e}},Fe=(r,s)=>!ps(r,s),qt={attribute:!0,type:String,converter:$e,reflect:!1,useDefault:!1,hasChanged:Fe};Symbol.metadata??=Symbol("metadata"),ze.litPropertyMetadata??=new WeakMap;Y=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=qt){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&&us(this.prototype,s,i)}}static getPropertyDescriptor(s,e,t){let{get:i,set:n}=hs(this.prototype,s)??{get(){return this[e]},set(o){this[e]=o}};return{get:i,set(o){let p=i?.call(this);n?.call(this,o),this.requestUpdate(s,p,t)},configurable:!0,enumerable:!0}}static getPropertyOptions(s){return this.elementProperties.get(s)??qt}static _$Ei(){if(this.hasOwnProperty(we("elementProperties")))return;let s=ms(this);s.finalize(),s.l!==void 0&&(this.l=[...s.l]),this.elementProperties=new Map(s.elementProperties)}static finalize(){if(this.hasOwnProperty(we("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(we("properties"))){let e=this.properties,t=[..._s(e),...gs(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(et(i))}else s!==void 0&&e.push(et(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 Pt(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:$e).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),o=typeof n.converter=="function"?{fromAttribute:n.converter}:n.converter?.fromAttribute!==void 0?n.converter:$e;this._$Em=i;let p=o.fromAttribute(e,n.type);this[i]=p??this._$Ej?.get(i)??p,this._$Em=null}}requestUpdate(s,e,t,i=!1,n){if(s!==void 0){let o=this.constructor;if(i===!1&&(n=this[s]),t??=o.getPropertyOptions(s),!((t.hasChanged??Fe)(n,e)||t.useDefault&&t.reflect&&n===this._$Ej?.get(s)&&!this.hasAttribute(o._$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},o){t&&!(this._$Ej??=new Map).has(s)&&(this._$Ej.set(s,o??e??this[s]),n!==!0||o!==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:o}=n,p=this[i];o!==!0||this._$AL.has(i)||p===void 0||this.C(i,void 0,n,p)}}let s=!1,e=this._$AL;try{s=this.shouldUpdate(e),s?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(t){throw s=!1,this._$EM(),t}s&&this._$AE(e)}willUpdate(s){}_$AE(s){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(s)),this.updated(s)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(s){return!0}update(s){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(s){}firstUpdated(s){}};Y.elementStyles=[],Y.shadowRootOptions={mode:"open"},Y[we("elementProperties")]=new Map,Y[we("finalized")]=new Map,vs?.({ReactiveElement:Y}),(ze.reactiveElementVersions??=[]).push("2.1.2")});function Bt(r,s){if(!ct(r)||!r.hasOwnProperty("raw"))throw Error("invalid template strings array");return jt!==void 0?jt.createHTML(s):s}function ue(r,s,e=r,t){if(s===oe)return s;let i=t!==void 0?e._$Co?.[t]:e._$Cl,n=Ae(s)?void 0:s._$litDirective$;return i?.constructor!==n&&(i?._$AO?.(!1),n===void 0?i=void 0:(i=new n(r),i._$AT(r,e,t)),t!==void 0?(e._$Co??=[])[t]=i:e._$Cl=i),i!==void 0&&(s=ue(r,i._$AS(r,s.values),i,t)),s}var lt,Rt,Ue,jt,zt,ie,Ft,ys,ne,Se,Ae,ct,bs,it,Ee,Nt,Ht,re,Dt,Mt,Ut,dt,l,_e,Ar,oe,_,Ot,ae,xs,Te,st,Ie,he,rt,at,nt,ot,ws,Wt,Be=w(()=>{lt=globalThis,Rt=r=>r,Ue=lt.trustedTypes,jt=Ue?Ue.createPolicy("lit-html",{createHTML:r=>r}):void 0,zt="$lit$",ie=`lit$${Math.random().toFixed(9).slice(2)}$`,Ft="?"+ie,ys=`<${Ft}>`,ne=document,Se=()=>ne.createComment(""),Ae=r=>r===null||typeof r!="object"&&typeof r!="function",ct=Array.isArray,bs=r=>ct(r)||typeof r?.[Symbol.iterator]=="function",it=`[
+\f\r]`,Ee=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Nt=/-->/g,Ht=/>/g,re=RegExp(`>|${it}(?:([^\\s"'>=/]+)(${it}*=${it}*(?:[^
+\f\r"'\`<>=]|("|')|))|$)`,"g"),Dt=/'/g,Mt=/"/g,Ut=/^(?:script|style|textarea|title)$/i,dt=r=>(s,...e)=>({_$litType$:r,strings:s,values:e}),l=dt(1),_e=dt(2),Ar=dt(3),oe=Symbol.for("lit-noChange"),_=Symbol.for("lit-nothing"),Ot=new WeakMap,ae=ne.createTreeWalker(ne,129);xs=(r,s)=>{let e=r.length-1,t=[],i,n=s===2?"":s===3?"":"")),t]},Te=class r{constructor({strings:s,_$litType$:e},t){let i;this.parts=[];let n=0,o=0,p=s.length-1,d=this.parts,[u,m]=xs(s,e);if(this.el=r.createElement(u,t),ae.currentNode=this.el.content,e===2||e===3){let f=this.el.content.firstChild;f.replaceWith(...f.childNodes)}for(;(i=ae.nextNode())!==null&&d.length
0){i.textContent=Ue?Ue.emptyScript:"";for(let y=0;y2||t[0]!==""||t[1]!==""?(this._$AH=Array(t.length-1).fill(new String),this.strings=t):this._$AH=_}_$AI(s,e=this,t,i){let n=this.strings,o=!1;if(n===void 0)s=ue(this,s,e,0),o=!Ae(s)||s!==this._$AH&&s!==oe,o&&(this._$AH=s);else{let p=s,d,u;for(s=n[0],d=0;d{let t=e?.renderBefore??s,i=t._$litPart$;if(i===void 0){let n=e?.renderBefore??null;t._$litPart$=i=new Ie(s.insertBefore(Se(),n),n,void 0,e??{})}return i._$AI(r),i}});var pt,A,$s,Vt=w(()=>{ke();ke();Be();Be();pt=globalThis,A=class extends Y{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=Wt(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return oe}};A._$litElement$=!0,A.finalized=!0,pt.litElementHydrateSupport?.({LitElement:A});$s=pt.litElementPolyfillSupport;$s?.({LitElement:A});(pt.litElementVersions??=[]).push("4.2.2")});var Kt=w(()=>{});var P=w(()=>{ke();Be();Vt();Kt()});var Gt=w(()=>{});function x(r){return(s,e)=>typeof e=="object"?Es(r,s,e):((t,i,n)=>{let o=i.hasOwnProperty(n);return i.constructor.createProperty(n,t),o?Object.getOwnPropertyDescriptor(i,n):void 0})(r,s,e)}var ks,Es,ut=w(()=>{ke();ks={attribute:!0,type:String,converter:$e,reflect:!1,hasChanged:Fe},Es=(r=ks,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"&&((r=Object.create(r)).wrapped=!0),n.set(e.name,r),t==="accessor"){let{name:o}=e;return{set(p){let d=s.get.call(this);s.set.call(this,p),this.requestUpdate(o,d,r,!0,p)},init(p){return p!==void 0&&this.C(o,void 0,r,p),p}}}if(t==="setter"){let{name:o}=e;return function(p){let d=this[o];s.call(this,p),this.requestUpdate(o,d,r,!0,p)}}throw Error("Unsupported decorator location: "+t)}});function h(r){return x({...r,state:!0,attribute:!1})}var Yt=w(()=>{ut();});var Qt=w(()=>{});var ge=w(()=>{});var Jt=w(()=>{ge();});var Zt=w(()=>{ge();});var Xt=w(()=>{ge();});var ei=w(()=>{ge();});var ti=w(()=>{ge();});var W=w(()=>{Gt();ut();Yt();Qt();Jt();Zt();Xt();ei();ti()});function ii(r,s){return!r||r<=0?0:r*(Ss[s||"days"]??1)}var Ss,si=w(()=>{"use strict";Ss={days:1,weeks:7,months:30.4368,years:365.25}});function le(r){let s=r.getFullYear(),e=String(r.getMonth()+1).padStart(2,"0"),t=String(r.getDate()).padStart(2,"0");return`${s}-${e}-${t}`}function As(r,s){let e=[];for(let t=0;te.cost).filter(e=>typeof e=="number");return s.length===0?null:s.reduce((e,t)=>e+t,0)/s.length}function Is(r){let{windowStart:s,windowEnd:e,task:t,entryId:i,objectName:n}=r,o=[],p=(f,v)=>({date:f,entry_id:i,task_id:t.id,task_name:t.name,object_name:n,status:v&&(t.status==="overdue"||t.status==="triggered")?"ok":t.status,days_until_due:v?null:t.days_until_due??null,projected:v,schedule_type:t.schedule_type,interval_days:t.interval_days??null,interval_unit:t.interval_unit??null,responsible_user_id:t.responsible_user_id??null,avg_cost:Ts(t.history),adaptive_enabled:!!t.adaptive_config?.enabled,prediction_confidence:t.threshold_prediction_confidence??null}),d=Math.max(1,Math.round(ii(t.interval_days,t.interval_unit)));if(t.status==="overdue"||t.status==="triggered"){if(o.push(p(s,!1)),t.schedule_type==="time_based"&&t.interval_days&&t.interval_days>0){let f=Ve(s,d),v=1;for(;f<=e&&v=s&&m<=e)o.push(p(m,!1));else if(m>e)return o;if(t.schedule_type==="time_based"&&t.interval_days&&t.interval_days>0){let f=Ve(m,d),v=o.length;for(;f<=e&&v=s&&(o.push(p(f,!0)),v++),f=Ve(f,d)}return o}function ni(r,s,e,t=null){let i=As(s,e),n=i[0],o=i[i.length-1],p=[];for(let u of r){let m=u.object?.name||"",f=u.entry_id,v=u.tasks||[];for(let y of v){if(t&&y.responsible_user_id!==t||y.enabled===!1)continue;let k=Is({windowStart:n,windowEnd:o,task:y,entryId:f,objectName:m});p.push(...k)}}let d=new Map;for(let u of i)d.set(u,[]);for(let u of p){let m=d.get(u.date);m&&m.push(u)}for(let[,u]of d)u.sort((m,f)=>{let v=ai[m.status]??99,y=ai[f.status]??99;if(v!==y)return v-y;if(m.projected!==f.projected)return m.projected?1:-1;let k=m.object_name.localeCompare(f.object_name);return k!==0?k:m.task_name.localeCompare(f.task_name)});return i.map(u=>({date:u,events:d.get(u)??[]}))}function Ps(r,s){let e=[];for(let t=s-1;t>=0;t--){let i=new Date(r);i.setDate(i.getDate()-t),i.setHours(0,0,0,0),e.push(le(i))}return e}function oi(r,s,e,t=null){let i=Ps(s,e),n=i[0],o=i[i.length-1],p=new Map;for(let u of i)p.set(u,[]);for(let u of r){let m=u.object?.name||"",f=u.entry_id,v=u.tasks||[];for(let y of v){if(t&&y.responsible_user_id!==t)continue;let k=y.history||[];for(let E of k){if(typeof E?.timestamp!="string")continue;let H=E.timestamp.slice(0,10);if(Ho)continue;let M=p.get(H);if(!M)continue;let q=E.type??"completed";M.push({date:H,entry_id:f,task_id:y.id,task_name:y.name,object_name:m,status:Cs[q]??"ok",days_until_due:null,projected:!1,schedule_type:y.schedule_type,interval_days:y.interval_days??null,responsible_user_id:y.responsible_user_id??null,avg_cost:typeof E.cost=="number"?E.cost:null,adaptive_enabled:!!y.adaptive_config?.enabled,prediction_confidence:null,history_timestamp:E.timestamp,history_type:q,history_cost:typeof E.cost=="number"?E.cost:null,history_notes:typeof E.notes=="string"?E.notes:null,history_duration:typeof E.duration=="number"?E.duration:null})}}}let d={completed:0,reset:1,skipped:2,triggered:3,trigger_replaced:4};for(let[,u]of p)u.sort((m,f)=>{let v=d[m.history_type??""]??99,y=d[f.history_type??""]??99;if(v!==y)return v-y;let k=m.object_name.localeCompare(f.object_name);return k!==0?k:m.task_name.localeCompare(f.task_name)});return i.map(u=>({date:u,events:p.get(u)??[]}))}var ri,ai,Cs,ht=w(()=>{"use strict";si();ri=5;ai={overdue:0,triggered:1,due_soon:2,ok:3};Cs={completed:"ok",reset:"ok",skipped:"due_soon",missed:"overdue",triggered:"triggered",trigger_replaced:"triggered",trigger_removed:"ok"}});var di,ci=w(()=>{di={maintenance:"Maintenance",objects:"Objects",tasks:"Tasks",overdue:"Overdue",due_soon:"Due Soon",triggered:"Triggered",trigger_replaced:"Trigger replaced",trigger_removed:"Trigger removed",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)",notes_markdown_hint:"Markdown is supported \u2014 **bold**, lists, [links](\u2026)",cost_optional:"Cost (optional)",duration_minutes:"Duration in minutes (optional)",completed_at_optional:"Completed at (optional, empty = now)",completed_at_future_error:"The completion date cannot be in the future.",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",all_priorities:"All priorities",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",all_parts:"All parts",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",trigger_equals:"Trigger when equal to (=)",trigger_not_equals:"Trigger when different from (\u2260)",for_at_least_minutes:"For at least (minutes)",safety_interval_days:"Safety interval (days, optional)",safety_interval:"Safety interval (optional)",trigger_combinator:"Combine trigger and interval",trigger_combinator_any:"Trigger or interval (whichever first)",trigger_combinator_all:"Trigger and interval (both required)",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",adaptive_prediction_enabled:"Enable sensor-driven predictions",adaptive_seasonal_enabled:"Enable seasonal awareness",adaptive_max_interval:"Maximum interval (days)",adaptive_min_interval:"Minimum interval (days)",adaptive_ewa_alpha:"Learning rate (alpha)",adaptive_enabled:"Enable adaptive scheduling",adaptive_section_title:"Adaptive Scheduling",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_action_style:"Complete button style",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_priority_help:"Empty = show all priorities. Tasks without an explicit priority count as Normal.",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_consumable_threshold:"Consumable low threshold (%)",settings_battery_low_percent:"Battery low threshold (%)",settings_thresholds_hint:"Defaults for new Suggested setups (percent-remaining consumables) and for fleet batteries without their own Battery Notes threshold. Existing tasks keep their value.",bn_summary:"{name}: {pct} % for {n} batteries",bn_floor_decides:"Your {floor} % floor decides for all of them; the higher of the two thresholds counts per battery.",bn_above_floor:"That is above your {floor} % floor, so {name} decides for all noted batteries.",bn_overrides:"{n} devices with their own threshold:",bn_more:"+ {n} more",settings_row_actions:"Task row actions",row_actions_buttons_compact:"Buttons (icons only on phones)",row_actions_buttons:"Buttons with text",row_actions_icons:"Icons (classic)",row_actions_follow:"Follow the household setting",settings_panel_enabled:"Sidebar panel",settings_panel_title:"Sidebar panel title",settings_notifications:"Notifications",settings_notify_service:"Notification service",settings_shopping_list:"Shopping list (buy tasks)",settings_shopping_list_help:"Low-part buy reminders appear in this to-do list; checking one off restocks the part.",shopping_list_none:"Off \u2014 no shopping list",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_export_settings:"Export settings (JSON)",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).",for_minutes_state_help:"0 counts every change immediately. Set minutes and the new state must hold that long first \u2014 brief flickers then neither trigger nor count.",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",parts_used_by:"Used by",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",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_for_minutes_hint:"Only trigger once the problem has persisted this long \u2014 0 reacts to the first flicker.",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_add:"Add a battery",battery_fleet_add_hint:"Pick a battery sensor the automatic discovery missed \u2014 it joins the roster immediately.",battery_fleet_track_self:"Track self-charging batteries",battery_fleet_track_self_hint:"Phones, vacuums and other devices that recharge themselves appear as rechargeables \u2014 a low one asks for a charge, never for new cells.",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",row_actions_banner:"Complete and Skip in task rows are now buttons. Prefer the old icons?",row_actions_keep:"Keep buttons",row_actions_back:"Back to icons",battery_fleet_forecast_overdue:"Predicted date passed \u2014 the battery still reports healthy. If you swapped it, record the replacement; otherwise the forecast was off.",cost_from_parts:"Use \u2248 {amount} from parts",dismiss:"Dismiss",gs_label:"Getting started \u2014 these hints retire as your setup grows",gs_setups_chip:"Suggested setups found {n} devices with pre-wired triggers",gs_adopt_chip:"{n} problem sensors can become maintenance tasks",gs_fleet_chip:"One click sets up the battery fleet",cal_editor_window:"Default window",cal_editor_window_week:"Week (7 days)",cal_editor_window_fortnight:"Fortnight (14 days)",cal_editor_window_month:"Month (30 days, default)",cal_editor_window_year:"Year (365 days, empty days collapsed)",cal_editor_show_chips:"Show window chips inside the card",cal_editor_chips_hint:"Hide the chips when the card is embedded in a strategy view that already serves as the window selector.",cal_editor_show_user_filter:"Show user filter dropdown",cal_editor_default_user:"Default user filter",cal_editor_my_tasks:"My tasks (current user)",cal_editor_show_object_filter:"Show object filter dropdown",cal_editor_object_hint:'Pre-select one object via YAML: object_filter: "
`:_}render(){if(!this._open)return l``;let e=this._lang,t=this._taskId?a("edit_task",e):a("new_task",e);return l`
@@ -2166,7 +2203,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
.checked=${this._requiredCompletion.includes(i)}
@change=${n=>this._toggleRequired(i,n.target.checked)}
/>
- ${a(je[i],e)}
+ ${a(Ne[i],e)}
`)}
@@ -2225,7 +2262,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
a("environmental_entity_optional",e)}
.computeHelper=${()=>a("environmental_entity_helper",e)}
@@ -2290,6 +2327,15 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
${a("require_tag_scan",e)}
${this._requireTagScan?l`